Watch Vercel deploys for [REPLACE: VERCEL_PROJECT] — alert on [REPLACE: ALERT_ON] in the last [REPLACE: LOOKBACK_HOURS] hours
日本語の概要は準備中です。原文の説明を表示しています。
One lifecycle poll over everything vuln-scanner produces - PR and advisory status, PVR triage transitions, and pending-disclosure aging, with a stars-secured impact headline and one action queue.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
${var} — Scope selector for the lifecycle poll:
- empty → full lifecycle poll: PR/advisory status + PVR triage + disclosure-queue aging (default).
prs(alsopr/tracker) → Arm A only — PR/advisory status audit + stars-secured dashboard.pvr→ Arm B only — PVR triage-state poll on submitted advisories.queue(alsodisclosures/backlog) → Arm C only — pending-disclosure queue aging + escalation.- a bare
GHSA-xxxx-xxxx-xxxx→ Arm B, single-advisory mode — check just that one advisory's triage state on demand.
Today is ${today}. This skill is the daily read/poll arm of the vuln pipeline: vuln-scanner opens PRs, submits PVRs, and queues disclosure drafts, then moves on. This skill polls everything it produced and surfaces what the operator must look at — it does not open PRs or submit advisories itself (those are vuln-scanner's write actions).
If soul/SOUL.md and soul/STYLE.md are populated, read them and match the operator's voice in every notification. If empty or absent, use a clear, direct, neutral tone.
This skill runs mode: write deliberately. It is a read/poll arm, but three of its capabilities cannot run under read-only:
gh api repos/$REPO/security-advisories/$GHSA. Draft/triage advisories are visible only to the repo maintainers and the reporter, so the read is intrinsically authenticated; read-only strips gh, and a bare $SECRET on the command line is refused by the Bash permission layer (so a hand-rolled authenticated curl isn't an option either) — the read needs gh api, which handles auth internally.state:/last_checked:/resolved_at: frontmatter in memory/pending-disclosures/*.md in place and moves resolved files to memory/pending-disclosures/resolved/ (Edit/git mv — both stripped in read-only).gh api for the PVR-state endpoint (repos/$REPO/private-vulnerability-reporting) and repo/advisory reads.No sibling writes a repo file outside memory/, so there was nothing to relocate — the only outside-memory/ writes are the ephemeral .pending-notify-temp/ notify-staging files. Keeping write preserves every absorbed capability.
memory/MEMORY.md for context.memory/logs/ and drop anything already reported — don't re-surface the same signal twice.soul/SOUL.md + soul/STYLE.md if populated (voice).${var} → scope (deterministic; trim + lowercase, except a GHSA- value which is compared case-insensitively but preserved verbatim):
scope = full (run Arm A, then B, then C).^GHSA- (case-insensitive) → scope = pvr, single_advisory = <the GHSA value> (Arm B filtered to one advisory).prs / pr / tracker → scope = prs (Arm A only).pvr → scope = pvr (Arm B only, all advisories).queue / disclosures / backlog → scope = queue (Arm C only).VULN_TRACKER_BAD_VAR: unrecognized scope '<var>', send no notification, exit.mkdir -p .pending-notify-temp and start an empty combined-notification buffer at .pending-notify-temp/vuln-tracker-${today}.md. Each arm that has signal appends its section to this buffer; at the very end (step "Notify") the skill sends the buffer once if it is non-empty. This keeps notifications tight — a full poll with signal in two arms is one message, not two.Then run the arm(s) selected by scope, and finish with the shared Notify and Log steps.
gh api / gh search / gh pr view. gh handles auth internally via GH_TOKEN (and Arm B's private-advisory reads need the elevated GH_GLOBAL PAT). No env-var-authenticated curl from bash — a bare $SECRET on the command line is refused by the Bash permission layer, so gh api (auth handled internally) is the reliable path; no postprocess scripts needed. Arm B keeps a documented curl fallback for the advisory endpoint — see Arm B step B2 — but gh api is preferred.memory/pending-disclosures/, memory/issues/, memory/topics/pr-status.md). No outbound network or auth required.full or prs)Audit the lifecycle status of every disclosure vuln-scanner has produced. Without a follow-up loop, three things rot silently:
channel: "skipped" (no-safe-channel) vanish into vuln-scanned.json with no recurring re-probe.This arm cross-references memory/vuln-scanned.json against live GitHub state and surfaces anything the operator should look at.
memory/vuln-scanned.json may be a flat top-level array of scan rows, not a {scans: [...]} object - vuln-scanner writes the flat shape. Read it in a way that accepts both:
# Accepts both the flat `[ {repo,scanned_at,findings,channel}, ... ]` shape
# and the legacy `{scans: [...]}` object.
jq -c 'if type=="array" then .[] else (.scans[]? // empty) end' memory/vuln-scanned.json 2>/dev/null
A bare jq -c '.scans[]' errors on a flat-array file (Cannot index array with string "scans") and, with 2>/dev/null swallowing the error, silently yields zero rows - so the arm reads an empty scan history and reports only what other passes happen to surface. The if type=="array" guard is the fix; it keeps the legacy object shape working too.
If memory/vuln-scanned.json doesn't exist or the parsed row count is 0, log VULN_TRACKER_SKIP: no scan history for this arm and skip Arm A (no notification section - first runs of vuln-scanner haven't happened yet). In a full poll, continue to Arm B/C.
Each scan entry has at minimum: repo, scanned_at, findings, channel, severity. Public-PR entries also have pr (URL). Pending-disclosure entries have draft_at and patch_branch. Skipped entries have reason.
Retro-active coverage: if the JSON was written after vuln-scanner started running, some PRs won't be in the JSON. Pull all bot-authored security PRs from GitHub directly (next step) to fill the gap.
vuln-scanner opens security-fix PRs on a security/ branch. The title varies: dependency-bump PRs use fix(deps): (the A5a template is fix(deps): bump <pkg> to patch <CVE>), and older or hand-written ones use fix(security): or a bare security:. Match all three title prefixes, and treat the security/ branch prefix as the real invariant - the branch name is mandated by A5a, the title wording is not.
Do not filter on
fix(security):alone. vuln-scanner's dominant output isfix(deps):, so afix(security):-only filter silently drops most in-flight security PRs - that is howalibaba/open-code-review#541(conflicted, lint-failing, one unaddressed review) was missed by a poll while other PRs were tracked. Also excludedocs(security):- those aresecurity-syncwebsite PRs, not disclosures.
gh search prs --json does not expose headRefName - that field is only available via GraphQL. So run both passes below every time and union them on repository + number; neither alone is complete.
The bot author is whoever the workflow uses (typically github-actions[bot] or a dedicated account configured in the workflow). Determine the author from aeon.yml or the workflow file; default to whatever account opened the most recent security-fix PR (any of the prefixes above) you can find.
Pass 1 (title prefixes, works everywhere):
BOT_AUTHOR="<resolved bot author>"
gh search prs --author "$BOT_AUTHOR" --json number,title,url,state,createdAt,closedAt,repository --limit 200 \
| jq '[.[] | select(
(.title | startswith("fix(deps):")) or
(.title | startswith("fix(security):")) or
(.title | startswith("security:"))
)]'
Pass 2 (GraphQL, required - picks up every security/ branch whatever the title says):
gh api graphql -f query='
{
search(query: "author:'"$BOT_AUTHOR"' is:pr sort:created-desc", type: ISSUE, first: 100) {
nodes { ... on PullRequest {
number title url state createdAt closedAt mergedAt
repository { nameWithOwner }
headRefName
}}
}
}' | jq '[.data.search.nodes[]
| select((.headRefName // "") | startswith("security/"))]'
Union the two result sets, dedup by repository + number (equivalently, by URL).
Cross-reference with vuln-scanned.json:
severity / cwe / note for the row.pre-history; fill severity from the PR title if obvious.channel != "public-pr" → no PR to fetch; goes in the "queued" / "skipped" sections.For each open PR (state from step A2):
gh pr view "$REPO/$NUM" --json state,merged,closedAt,createdAt,reviews,comments,reviewDecision,author
Per-PR signals:
author.login != $BOT_AUTHOR posted after the most recent comment by $BOT_AUTHOR (or after PR creation if the bot hasn't commented). Also reviewDecision == "CHANGES_REQUESTED" always counts.state == "OPEN" AND no review AND no maintainer comment AND createdAt > 7d ago.state == "OPEN" AND any maintainer activity AND open > 14d.If a scan entry has advisory_ids (one or more GHSA IDs), check each one's published state:
gh api "repos/$ORIGIN_REPO/security-advisories/$GHSA_ID" --jq '.state // "not found"'
state == "published" → public advisory visible. 404 → osv-scanner referenced it but the upstream repo never published its own advisory.
For every unique repo across the union from step A2 (JSON history + bot-authored security PRs):
gh api "repos/$REPO" --jq '{stars: .stargazers_count, archived: .archived}' 2>/dev/null
Refetch every run. Do NOT carry star counts forward from the previous memory/topics/vuln-followup.md — per-repo counts drift between runs and the secured-stars headline is the operator's load-bearing metric. Cache only within a single run, keyed by nameWithOwner, so a repo with multiple PRs is fetched once.
Repo-state handling:
.stargazers_count (raw integer).archived: true: still use the star count, but suffix the repo cell with (archived) so the operator knows the maintainer isn't responsive.null and render as repo-deleted. Exclude from total_stars_* aggregates entirely so dead repos don't quietly zero out the totals.null and render ★?. Flag in the run log for operator follow-up.These per-repo counts power both the Stars Secured aggregate (step A6) and the Stars column on every per-repo table in step A6.
channel: "skipped" and channel: "pending-disclosure" reposFor each historical entry where the disclosure couldn't ship, re-check whether the situation changed:
channel: "skipped" with reason containing "no PVR":
PVR_NOW=$(gh api "repos/$REPO/private-vulnerability-reporting" --jq .enabled 2>/dev/null || echo "false")
If PVR_NOW=true and the original was false, surface as newly-actionable.channel: "skipped" with reason containing "no SECURITY.md" — re-check gh api repos/$REPO/contents/SECURITY.md and .github/SECURITY.md. If now present, surface as newly-actionable.channel: "pending-disclosure" — cross-reference with memory/pending-disclosures/ to see if the draft is still on disk. If the file is gone but the JSON entry says pending-disclosure, mark as lost-draft so it stops being escalated forever.| Status | Meaning |
|---|---|
merged | PR merged. One-time celebration — drop from notifications after 30d. |
open-clean | PR open, no maintainer activity yet, < 7d old. Wait. |
needs-answer | Maintainer commented or requested changes. Operator action. |
stale-no-review | Open > 7d, zero maintainer activity. Consider polite ping or close. |
aging-engaged | Open > 14d with engagement. Operator should triage. |
closed-no-merge | PR closed without merging. Capture the reason for review. |
queued | pending-disclosure draft on disk, not yet shipped. |
skipped-rechecked | Channel was "skipped" originally; re-probe still shows no channel. |
newly-actionable | Skipped originally; PVR or SECURITY.md now present. Operator action. |
lost-draft | JSON says pending-disclosure but draft file is gone. Display once, then suppress. |
pre-history | PR found via search but predates vuln-scanned.json. Fill what we can. |
Then rewrite memory/topics/vuln-followup.md (rewrite — don't append; this file is a living dashboard, not a log).
The Stars Secured block goes at the top so the operator sees aggregate impact before drilling into rows. total_stars_secured = sum of stargazers across every unique repo where vuln-scanner has landed at least one merged PR. total_stars_in_flight = sum across repos with an open PR. total_stars_tracked = sum across the full union. Track all three because celebration uses secured, prioritization uses in_flight, and historical review uses tracked.
Round star counts to abbreviated form for the headline (12.4k, 1.8k, 940). Keep raw integers in the per-repo tables so sort/diff stays exact.
# Vuln Tracker Status
*Last updated: ${today}*
## Stars Secured
- **Merged-PR repos (secured):** ★ <total_stars_secured> across <secured_repo_count> repos
- **Open-PR repos (in flight):** ★ <total_stars_in_flight> across <in_flight_repo_count> repos
- **All tracked repos:** ★ <total_stars_tracked> across <total_repo_count> repos
### Secured leaderboard — every merged PR ranked by repo stars
| Rank | Repo | Stars | PR | Merged | Severity | Title |
|------|------|-------|----|--------|----------|-------|
### Per-repo breakdown — secured (sorted by stars desc)
| Repo | Stars | Merged PRs | First merge | Latest merge | Severities landed |
|------|-------|------------|-------------|--------------|-------------------|
### Per-repo breakdown — in flight (sorted by stars desc)
| Repo | Stars | Open PRs | Oldest open | Severities open |
|------|-------|----------|-------------|-----------------|
### Per-repo breakdown — queued / skipped / closed (sorted by stars desc)
| Repo | Stars | Status | Severity | Note |
|------|-------|--------|----------|------|
## Operator-action queue
### Needs answer (<count>)
| Repo | Stars | PR | Title | Last activity | Latest commenter |
|------|-------|----|----|--------------|------------------|
### Newly actionable — channel opened up since the original scan (<count>)
| Repo | Stars | Original date | Original blocker | Now |
|------|-------|---------------|------------------|-----|
### Stale or aging
| Repo | Stars | PR | Age | Status | Suggested action |
|------|-------|----|----|-----|------------------|
## Recently merged (last 30d, <count>)
| Date merged | Repo | Stars | PR | Severity | Title |
|-------------|------|-------|----|----------|-------|
## Open / clean (no operator action — wait, < 7d) (<count>)
| Repo | Stars | PR | Severity | Opened | Age |
|------|-------|----|----------|--------|-----|
## Closed without merge (last 30d, <count>)
| Date | Repo | Stars | PR | Severity | Title | Likely reason |
|------|------|-------|----|----------|-------|---------------|
## Queued (no PR yet) (<count>)
| Severity | Repo | Stars | Original channel | Original blocker | Days queued |
|----------|------|-------|------------------|------------------|-------------|
## Lost-draft ghosts (suppressed from notifications)
| Date | Repo | Stars | Severity |
No signal (contribute nothing to the notification) if all are true:
needs-answernewly-actionablemerged since the last runclosed-no-merge since the last runstale-no-review or aging-engaged since the last runTo detect "since last run," diff today's categorization against the previous memory/topics/vuln-followup.md. If the file doesn't exist (first run), treat all entries as new and surface the full backlog.
Append this section to the combined buffer (.pending-notify-temp/vuln-tracker-${today}.md). Keep the whole message under 4000 chars; if tight, drop the merged/opened/stale section bodies and keep counts only.
*PR & advisory status*
★ secured: <total_stars_secured> across <secured_repo_count> repos (in flight: <total_stars_in_flight> / <in_flight_repo_count>)
needs answer: <N>
<repo> (★<stars>) #<num> — <latest_commenter>: "<comment_excerpt_first_120_chars>"
newly actionable: <N>
<repo> (★<stars>, queued <days>d) — <what_changed>
merged this week: <N> <list_with_stars>
opened, waiting: <N>
stale: <N>
queued: <N> (<critical>C / <high>H / <other>M+L)
leaderboard top-3 (PRs by ★): #1 <repo1> ★<s1> (PR #<pr1>) — #2 <repo2> ★<s2> (PR #<pr2>) — #3 <repo3> ★<s3> (PR #<pr3>)
dashboard: memory/topics/vuln-followup.md (full leaderboard inside)
Record Arm A status VULN_TRACKER_OK for the log.
full, pvr, or a bare GHSA-…)vuln-scanner's re-submit arm (var=resubmit, the former pvr-watchlist) monitors repos waiting to open PVR. This arm monitors PVRs that have already been submitted and tracks their lifecycle: triage → draft (accepted) → published (public) or withdrawn (rejected). Without this, submitted advisories sit unmonitored until manually recalled from memory.
Source of truth: memory/pending-disclosures/*.md files with channel: pvr frontmatter. Each such file must have ghsa, repo, state, submitted_at fields (full schema below).
Configuration — optional tracking issue. The arm can reference an optional tracking issue in the operator's own repo — useful for cross-linking advisory state with an internal issue board. Resolve from (priority order):
aeon.yml top-level key pvr_triage.tracking_issue: (e.g. pvr_triage: { tracking_issue: "owner/repo#123" })AEON_PVR_TRACKING_ISSUEIf a tracking issue is configured, mention its URL in the notification and the per-advisory write-up so the operator can navigate to the canonical tracker.
Scan memory/pending-disclosures/ for all .md files. Parse the YAML frontmatter. Keep only those with channel: pvr.
If single_advisory is set (a bare GHSA-… was passed), filter to just the matching ghsa value (one-off mode).
If no PVR files found:
PVRT_SKIP: no submitted PVRs on disk
Record that Arm B status and skip Arm B (no notification section). In a full poll, continue to Arm C.
For each entry, determine repo and ghsa from frontmatter.
REPO="owner/repo"
GHSA="GHSA-xxxx-xxxx-xxxx"
gh api "repos/${REPO}/security-advisories/${GHSA}" \
--jq '{state: .state, cve_id: .cve_id, published_at: .published_at}' 2>&1
Expected outcomes:
| Response | Meaning |
|---|---|
{state: "triage", ...} | Maintainer hasn't reviewed yet |
{state: "draft", ...} | Accepted — maintainer is working on it |
{state: "published", ...} | Published — fully resolved |
{state: "withdrawn", ...} | Rejected or withdrawn by reporter |
| HTTP 403 | Private advisory, we don't have read access — state unknown, treat as still triage |
| HTTP 404 | Advisory deleted / repo private / GHSA invalid — flag as not-found |
Fallback: gh api uses GH_TOKEN internally (workflow wires GH_GLOBAL for the elevated advisory read). If gh is somehow unavailable, fall back to ./secretcurl with the {GH_TOKEN} placeholder (same token; GH_GLOBAL itself is not a substitutable name):
./secretcurl -s -H "Authorization: Bearer {GH_TOKEN}" \
"https://api.github.com/repos/${REPO}/security-advisories/${GHSA}" \
| grep -o '"state":"[a-z]*"'
(Never write a raw curl with $GH_GLOBAL / $GH_TOKEN: a bare $SECRET on the command line is refused by the Bash permission layer. gh api stays the primary path.)
Compare the probed state to the state in the frontmatter.
Also flag:
state=triage AND (today − submitted_at) > 30 days → escalate. Most maintainers respond within 30 days; silence past that is actionable.patch_branch frontmatter field — maintainer may want a PR instead of a private advisory.For each file with a state change, rewrite just the state field in the YAML frontmatter. Also update a last_checked field (add it if absent).
Do NOT modify the body of the advisory file — only update frontmatter.
Example frontmatter update:
state: draft # was: triage
last_checked: 2026-05-21
For published or withdrawn entries, add:
resolved_at: 2026-05-21
triage, no changes, none aged: no notification section. Record status and continue.Append to the combined buffer:
*PVR triage*
pvr triage: {total} advisories in flight. {changed_count} changed.
CHANGED:
- {repo} {ghsa} — {old_state} → {new_state}
{action_item}
AGED (>30d no response):
- {repo} {ghsa} — {days}d in triage. {severity}. escalate or close.
patch: {patch_branch}
STILL TRIAGE:
{n} advisories waiting. oldest: {repo} ({days}d).
{if tracking_issue configured}
tracker: {tracking_issue_url}
{end}
Action items by transition:
triage → draft → "maintainer accepted — offer to PR the patch branch: {patch_branch}"triage → published → "published as {cve_id}. remove from tracking."triage → withdrawn → "rejected. remove from tracking and note in vuln-scanned.json."For entries where state=published or state=withdrawn AND resolved_at is set: move the file from memory/pending-disclosures/ to memory/pending-disclosures/resolved/ (create the directory if needed; use git mv).
Do NOT delete — keep as a historical record.
Record Arm B status PVRT_OK for the log.
full or queue)Monitor the pending vulnerability disclosure backlog. vuln-scanner queues draft advisories to memory/pending-disclosures/ when PVR auto-submission fails or the disclosure path is email-only. Without daily visibility, CRITICAL/HIGH advisories silently age past responsible-disclosure windows. This arm surfaces the queue state and escalates when findings sit too long. (Arm B tracks the lifecycle of the channel: pvr subset; Arm C ages the whole queue by severity and disclosure status.)
ls memory/pending-disclosures/ 2>/dev/null
If the directory doesn't exist or is empty: log DISCLOSURE_TRACKER_SKIP: no pending advisories for this arm and skip Arm C (no notification section).
For each .md file in memory/pending-disclosures/ (ignore the resolved/ subdir):
From the filename (pattern: {repo-slug}-{YYYY-MM-DD}.md or {repo-slug}-{YYYY-MM-DD}-{ampm}.md):
From the YAML frontmatter (if present) parse:
repo: — overrides the filename slug when present (canonical target)severity: — CRITICAL / HIGH / MEDIUM / LOWstatus: — see step C2.5 for the controlled vocabularyFrom the file content (fallback for files without frontmatter), look for these near the top of the file:
Severity: or **Severity:** — one of CRITICAL / HIGH / MEDIUM / LOWCVE/CWE: or similar identifierIf severity is not parseable, treat as MEDIUM. Compute age: today - filed date in days.
Before counting any draft as a past-threshold escalation, decide whether the draft is genuinely pending or already covered. A draft can be in one of these states:
escalate — pending, no canonical PR found, past the severity-tier thresholdpending — pending, no canonical PR found, within the threshold windowoperator-todo — needs operator-only action (email send, PVR enable nudge); not an agent failurecovered-by-pr — a canonical disclosure PR has already been filed against the target repo and is OPEN or recently mergedsuperseded-upstream — the bypass / vuln is fixed in upstream already, draft is dead-weightsubmitted — already submitted via PVR / GHSA; awaiting maintainer responseResolution rules:
Check frontmatter status: first — map the literal value to a state:
superseded-upstream → superseded-upstreamsubmitted, submitted-via-pvr, disclosed-via-pr-{N}, email-sent → submitted or covered-by-prpending-operator-send with auto_send: true → pending (vuln-scanner's disclose arm, var=disclose, sends it autonomously - not a human task)pending-operator-send (without auto_send: true), queued for operator manual send, email-failed, any string mentioning "operator" → operator-todopending, blank, or missing → fall through to rule 2Cross-reference memory/topics/pr-status.md (if present) — grep for the {repo} slug (frontmatter repo: or filename) in the Open section and Recent Merges section. If a row exists with a fix(security) or chore(security) title against that repo, opened on or after the draft's detected_at / reconstructed_at / filed-date, classify as covered-by-pr and capture the PR number / title for the summary. If memory/topics/pr-status.md doesn't exist, skip this lookup and fall to rule 3.
Fall through — if no status hint and no canonical PR found, classify as pending. Then check age vs the severity-tier threshold (CRITICAL 3d / HIGH 7d / MED-LOW 14d) — if past, promote to escalate.
This is the load-bearing step. Without cross-referencing already-merged fix PRs the tracker generates false-positive escalations for drafts that have already been resolved.
Group advisories by state first, then by severity within each state. The three buckets are:
escalate state only (truly stuck, past threshold, no canonical PR)operator-todo state (email-only sends, PVR-enable nudges, anything awaiting human action)covered-by-pr, submitted, superseded-upstream (draft files that can be removed from memory/pending-disclosures/)Severity tiers and thresholds (only apply to escalate and pending states):
For each advisory, produce one line:
- {repo-slug} | {severity} | {age}d | {short title}{state-suffix}
where {state-suffix} is empty for escalate / pending, [operator-todo: {reason}] for operator-todo, and [covered: PR #{N}] / [superseded-upstream] / [submitted] for cleanup candidates.
Count totals. Identify advisories in the escalate state — those are the only ones that drive the urgent notification path.
Look in memory/issues/INDEX.md for any open issues tagged with pvr, repository_advisories, or missing-secret that explain why advisories are stuck. If such an issue exists, note:
If no such issue exists, treat the queue as routine and skip the "blocked by" line in the notification.
Compute counts from step C3:
escalate_count — drafts in the escalate statepending_count — drafts in pending (in-window) stateoperator_todo_count — drafts awaiting operator actioncleanup_count — drafts in covered-by-pr / submitted / superseded-upstreamDecision:
DISCLOSURE_TRACKER_SKIP: queue empty, no section.escalate_count > 0: emit the urgent section.escalate_count == 0 but cleanup_count > 0: emit the daily digest section (includes the cleanup-candidate list so the operator can prune memory/pending-disclosures/).pending / operator-todo, nothing past threshold, no cleanup candidates: emit the daily digest section.Coverage from covered-by-pr / submitted / superseded-upstream is never counted as an escalation — informational only. Operator-todo is never an escalation either; it's surfaced separately so the operator knows their inbox.
Append to the combined buffer.
Urgent format (escalate_count > 0):
*Disclosure queue*
disclosure queue: {escalate_count} past threshold (of {total} drafts).
ESCALATE:
- {repo} — {severity}, {age}d old (threshold: {N}d)
[... others in `escalate` state ...]
operator-todo ({operator_todo_count}):
- {repo} — {severity}, {age}d — {operator-reason}
cleanup candidates ({cleanup_count}):
- {repo} — [covered: PR #{N} / superseded-upstream / submitted] — safe to delete from memory/pending-disclosures/
{IF blocking issue tracked in memory/issues/INDEX.md}
blocked by {ISS-ID} — {short reason}
fix: {fix estimate} unblocks {N} of {escalate_count + pending_count}
{end}
Daily digest format (no escalation):
*Disclosure queue*
disclosure queue: {total} drafts. {critical_count} CRITICAL, {high_count} HIGH, {other_count} MED/LOW.
{pending_count} in-window, {operator_todo_count} operator-todo, {cleanup_count} cleanup candidates.
oldest in-window: {repo} ({age}d).
{cleanup section if cleanup_count > 0}
{IF blocking issue tracked in memory/issues/INDEX.md}
blocked by {ISS-ID} — {short reason}.
{end}
Record Arm C status DISCLOSURE_TRACKER_OK for the log.
If the combined buffer .pending-notify-temp/vuln-tracker-${today}.md has at least one arm section, prepend the header line *Vuln Tracker — ${today}* and send once:
./notify -f .pending-notify-temp/vuln-tracker-${today}.md
If the buffer is empty (every arm that ran was silent), send nothing — a clean poll is silent. Keep the whole message under 4000 chars; if over, trim Arm A's body first (keep counts), then Arm C's cleanup list, keeping every needs-answer / newly-actionable / ESCALATE line.
Append to memory/logs/${today}.md under ONE heading (the health loop parses ### <skill-name>), with a discriminator line naming the scope/arms that ran:
### vuln-tracker
- scope: <full | prs | pvr | queue | single-advisory:GHSA-…>
- Arm A (PR/advisory): <status> — <total_in_json> JSON + <pre_history_prs> pre-history = <total> tracked; states: <merged> merged / <open_clean> open-clean / <needs_answer> needs-answer / <stale> stale / <aging> aging / <closed> closed / <queued> queued / <skipped> skipped-rechecked / <newly_actionable> newly-actionable
- Stars: ★<total_stars_secured> secured (<secured_repo_count> repos) / ★<total_stars_in_flight> in flight (<in_flight_repo_count> repos) / ★<total_stars_tracked> tracked (<total_repo_count> repos)
- Arm B (PVR triage): <status> — checked <total>; changed <changed_count> (<list>); aged>30d <aged_count>; still-triage <waiting_count>; tracking issue <url|none>
- Arm C (disclosure queue): <status> — <total> drafts (<critical_count> CRITICAL / <high_count> HIGH / <other_count> MED/LOW); <escalate_count> escalate / <pending_count> in-window / <operator_todo_count> operator-todo / <cleanup_count> cleanup; oldest in-window <repo> (<age>d); blocking issue <ISS-ID|none>
- Operator queue this run: <needs_answer + newly_actionable + aged PVRs + escalate_count>
- Notification: <sent | skipped (no movement)>
- VULN_TRACKER_OK
Only include the Arm B / Arm C lines for arms that actually ran under the current scope. Use each arm's own status token (VULN_TRACKER_OK / VULN_TRACKER_SKIP, PVRT_OK / PVRT_SKIP, DISCLOSURE_TRACKER_OK / DISCLOSURE_TRACKER_SKIP) inline, and close with VULN_TRACKER_OK for the hub run (or VULN_TRACKER_BAD_VAR if the var was unrecognized).
GH_TOKEN / GITHUB_TOKEN — GitHub read token for Arm A's PR/comment/repo/advisory reads and the PVR-state endpoint. repo scope is enough. Present by default in GitHub Actions.GH_GLOBAL — GitHub PAT with public_repo + repository_advisories:write scope, used by Arm B to read the triage state of private/unpublished advisories you submitted. Same token vuln-scanner uses (all arms). Without it, Arm B advisory reads may 403 (treated as still triage).AEON_PVR_TRACKING_ISSUE (optional) — internal tracking-issue reference for Arm B cross-linking (else resolved from aeon.yml pvr_triage.tracking_issue:, else skipped).vuln-scanner.memory/pending-disclosures/*.md files. Newer drafts use YAML frontmatter:
---
repo: owner/repo
ghsa: GHSA-xxxx-xxxx-xxxx
ghsa_url: https://github.com/owner/repo/security/advisories/GHSA-xxxx-xxxx-xxxx
channel: pvr # Arm B tracks only channel: pvr entries
state: triage # triage | draft | published | withdrawn
status: pending-operator-send # optional; Arm C vocabulary below
submitted_at: 2026-05-12T19:54:42Z
last_checked: 2026-05-15 # added/updated by Arm B
severity: HIGH
cwe: [CWE-xxx] # may be a string or an array — handle both
patch_branch: https://github.com/<fork-owner>/repo/tree/security/branch-name
patch_commit: abc1234
submit_url: https://github.com/owner/repo/security/advisories/new
---
# {Repo}: {Title}
...
repo, ghsa, channel: pvr, state, submitted_at. Optional: patch_branch, patch_commit, cwe, ghsa_url.**Severity:** lines and no frontmatter — parse defensively: grep for severity: and Severity: case-insensitively; if unparseable, default to MEDIUM.status: controlled vocabulary (Arm C, step C2.5)Set by vuln-scanner / operator / cleanup chores:
pending — same as blankpending-operator-send with auto_send: true - queued for autonomous send by vuln-scanner's disclose arm (var=disclose); classify as pending (NOT operator-todo). It flips to email-sent on the next disclose run, which only happens when vuln-scanner is dispatched with var=disclose (the default schedule runs the scan arm). If it lingers armed-but-unsent across several days, either no disclose run was dispatched or RESEND_API_KEY isn't configured - surface it once so the operator schedules or runs the disclose arm and wires up Resend.pending-operator-send with auto_send: false/absent / queued for operator manual send — operator-todo (human must send; e.g. non-email contact or AI-report ban)email-sent - sent by vuln-scanner's disclose arm via Resend; awaiting maintainer reply. Treat like submitted (informational cleanup candidate, never an escalation).email-failed - the disclose arm gave up after repeated send failures (bad address / Resend error). operator-todo — the contact needs fixing or a manual send.submitted / submitted-via-pvr — submitted, awaiting maintainerdisclosed-via-pr-<N> — covered-by-pr, draft can be archivedsuperseded-upstream — bypass is already fixed in upstream; draft is deadoperator — operator-todoWhen a canonical PR lands but the draft's status: was never set, Arm C falls through to step C2.5 rule 2 (cross-ref pr-status.md) and classifies as covered-by-pr automatically. The status: shortcut just bypasses the grep.
When to delete a draft: cleanup candidates in the notification can be removed by the operator with rm memory/pending-disclosures/<file>.md. Safe once the canonical PR is open — the patch branch on the fork remains the authoritative artifact.
vuln-scanned.json schema is loose — cwe may be a string or an array; advisory_ids may be present or absent. Handle both.vuln-scanner produces the records this skill audits (its write actions — open PR, submit PVR, queue draft — are deliberately NOT here). vuln-scanner's re-submit arm probes repos waiting to open PVR; Arm B picks up once a PVR is submitted. inbox-triage catches inbound maintainer replies via the GitHub notification layer (complementary, not a duplicate of Arm A's branch-name lifecycle audit).memory/pending-disclosures/ from different angles; coordinate via the shared memory/topics/vuln-followup.md dashboard to avoid duplicate escalation.まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Watch Vercel deploys for [REPLACE: VERCEL_PROJECT] — alert on [REPLACE: ALERT_ON] in the last [REPLACE: LOOKBACK_HOURS] hours
日本語の概要は準備中です。原文の説明を表示しています。
Price and volume tracker for [REPLACE: TOKEN_SYMBOL] with anomaly alerts above [REPLACE: ALERT_THRESHOLD_PCT]% movement
日本語の概要は準備中です。原文の説明を表示しています。
Mention/keyword sweep on social platforms for [REPLACE: KEYWORDS] — trends, sentiment, top posts
日本語の概要は準備中です。原文の説明を表示しています。
Summary of the [REPLACE: CHANNEL_PLATFORM] channel [REPLACE: CHANNEL_NAME] — top [REPLACE: TOP_N_THREADS] threads + open questions
日本語の概要は準備中です。原文の説明を表示しています。
First-touch review of newly opened PRs on [REPLACE: WATCHED_REPO] — verdict + welcoming comment + label
日本語の概要は準備中です。原文の説明を表示しています。
Digest of the most interesting new posts on [REPLACE: TOPIC] from RSS feeds and the open web
日本語の概要は準備中です。原文の説明を表示しています。