Agent Issue Tracker
ruvnet/ruflo
Agent skill for issue-tracker - invoke with $agent-issue-tracker
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.
$ npx skills add aeonfun/aeon --skill vuln-tracker -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install aeonfun/aeon vuln-tracker --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/aeonfun/aeon.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/vuln-tracker .claude/skills/vuln-tracker && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "vuln-tracker" agent skill from https://github.com/aeonfun/aeon/tree/main/skills/vuln-tracker into .claude/skills/vuln-tracker/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vuln-tracker", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/aeonfun/aeon/tree/main/skills/vuln-trackerType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add aeonfun/aeon --skill vuln-tracker -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install aeonfun/aeon vuln-tracker --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/aeonfun/aeon.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/vuln-tracker .agents/skills/vuln-tracker && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "vuln-tracker" agent skill from https://github.com/aeonfun/aeon/tree/main/skills/vuln-tracker into .agents/skills/vuln-tracker/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vuln-tracker", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add aeonfun/aeon --skill vuln-tracker -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install aeonfun/aeon vuln-tracker --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/aeonfun/aeon.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/vuln-tracker .cursor/skills/vuln-tracker && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "vuln-tracker" agent skill from https://github.com/aeonfun/aeon/tree/main/skills/vuln-tracker into .cursor/skills/vuln-tracker/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vuln-tracker", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/aeonfun/aeon.git --path skills/vuln-tracker--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add aeonfun/aeon --skill vuln-tracker -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install aeonfun/aeon vuln-tracker --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/aeonfun/aeon.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/vuln-tracker .gemini/skills/vuln-tracker && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "vuln-tracker" agent skill from https://github.com/aeonfun/aeon/tree/main/skills/vuln-tracker into .gemini/skills/vuln-tracker/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vuln-tracker", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install aeonfun/aeon vuln-trackerInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add aeonfun/aeon --skill vuln-tracker -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/aeonfun/aeon.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/vuln-tracker .github/skills/vuln-tracker && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "vuln-tracker" agent skill from https://github.com/aeonfun/aeon/tree/main/skills/vuln-tracker into .github/skills/vuln-tracker/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vuln-tracker", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add aeonfun/aeon --skill vuln-tracker -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install aeonfun/aeon vuln-tracker --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/aeonfun/aeon.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/vuln-tracker .opencode/skills/vuln-tracker && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "vuln-tracker" agent skill from https://github.com/aeonfun/aeon/tree/main/skills/vuln-tracker into .opencode/skills/vuln-tracker/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vuln-tracker", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
vuln-trackerOne 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.
Vuln Tracker is an agent skill from aeonfun/aeon. 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.
Its SKILL.md is about 9.8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
The repository describes itself as: The most autonomous AI agent framework: runs unattended on GitHub Actions, self-healing skills, drives Claude Code, Grok, Codex & more. No approval loops. Configure once, forget… The licence is MIT.
5 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit f252074. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
ghjqgitFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
github.comapi.github.comFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
GH_TOKENGITHUB_TOKENRESEND_API_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Vuln Tracker loads about 9.8k tokens when it runs. Until then it costs about 52 tokens; SKILL.md has 4,016 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from aeonfun/aeon at commit f252074, republished under its MIT licence (© aeonfun). 4,016 words, ~9,805 tokens.
.claude/skills/vuln-tracker/SKILL.md (or your agent's skills folder).${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/nullA 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,authorPer-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/nullRefetch 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")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 diskRecord 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>&1Expected 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-21For published or withdrawn entries, add:
resolved_at: 2026-05-21triage, 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/nullIf 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}.mdIf 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_OKOnly 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.© aeonfun, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/vuln-tracker of aeonfun/aeon.
Open the folder on GitHubat commit f252074
Vuln Tracker next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Vuln Tracker this skillaeonfun/aeon | 767 | — | ~9.8k | Automated safety check: Pass | MIT | |
| Agent Issue Trackerruvnet/ruflo | 74k | 3 repos | ~2.4k | Automated safety check: Pass | MIT | |
| Aeon Vuln ScannerBankrBot/skills | 1.2k | — | ~858 | Automated safety check: Pass | None | |
| Vulndavepoon/buildwithclaude | 3.6k | — | ~3.6k | Automated safety check: Notes | MIT | |
| Kpi Trackersickn33/agentic-awesome-skills | 47k | 1 repos | ~3.6k | Automated safety check: Pass | MIT | |
| Probation Trackersickn33/agentic-awesome-skills | 47k | 1 repos | ~3.8k | Automated safety check: Pass | MIT |
ruvnet/ruflo
Agent skill for issue-tracker - invoke with $agent-issue-tracker
BankrBot/skills
Audit trending repos for real exploitable vulnerabilities and disclose responsibly — Private Vulnerability Reporting for code flaws and verified secrets, public PRs only for already-disclosed…
davepoon/buildwithclaude
Look up a vulnerability by ID or list all vulnerabilities for a package
sickn33/agentic-awesome-skills
KPI register: metric by level, department, job title and employee, linked OKR, unit and direction, target against actual, achievement and weight percentages, period and owner.
sickn33/agentic-awesome-skills
Probation register: employee, start and end dates, 30, 60 and 90-day review scores, overall score and confirmation letter.
0xShe/PHP-Code-Audit-Skill
PHP 组件版本漏洞检测工具。扫描 composer.json / composer.lock 或 jar/包元信息,匹配已知漏洞规则并输出报告(包含可能的触发点分析框架)。
aeonfun/aeon
Browses open tasks on the TaskMarket agent-worker market and, with explicit operator approval, creates tasks, tracks submissions and submits finished work.
aeonfun/aeon
Sets up and manages an Aeon agent instance that runs skills on a schedule through GitHub Actions: starting, rescheduling, debugging, editing skills and mining chat history.
aeonfun/aeon
Reads a Base Account's address, portfolio and transaction history through the Base MCP server, and stays strictly read-only in unattended Aeon runs, reporting only changes.
aeonfun/aeon
Audits every page of a site each day from its sitemap, scores on-page and technical SEO, checks duplicates across pages and reports what changed since the last run.
aeonfun/aeon
5 concrete real-life actions, leverage-scored against open loops with specificity and anti-fluff gates
aeonfun/aeon
Static linter for an Aeon instance's configuration that catches silent failures such as unquoted schedules, duplicate keys, unconfigured skills and broken MCP references.
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. Vuln Tracker is an agent skill from aeonfun/aeon. 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.
Run `npx skills add aeonfun/aeon --skill vuln-tracker -a claude-code`. Or copy the skill folder (skills/vuln-tracker in aeonfun/aeon) into .claude/skills/vuln-tracker in your project. Claude Code loads it when a task matches its description.
Run `npx skills add aeonfun/aeon --skill vuln-tracker -a codex`. Or copy the skill folder (skills/vuln-tracker in aeonfun/aeon) into .agents/skills/vuln-tracker in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add aeonfun/aeon --skill vuln-tracker -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/vuln-tracker, .gemini/skills/vuln-tracker, .github/skills/vuln-tracker and .opencode/skills/vuln-tracker in your project.
Going by SKILL.md and its folder, Vuln Tracker needs the command-line tools its instructions call (gh, jq and git) and credentials named GH_TOKEN, GITHUB_TOKEN and RESEND_API_KEY.
SKILL.md names 2 domains. In commands or code: github.com and api.github.com; the agent is likely to contact these when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Vuln Tracker is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 9.8k tokens (SKILL.md is roughly 39k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Vuln Tracker: Agent Issue Tracker (ruvnet/ruflo, 74k stars), Aeon Vuln Scanner (BankrBot/skills, 1.2k stars), Vuln (davepoon/buildwithclaude, 3.6k stars) and Kpi Tracker (sickn33/agentic-awesome-skills, 47k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
aeonfun (a GitHub organization) maintains it in aeonfun/aeon, which has 767 GitHub stars. The repository holds 82 skills in this directory. The repository was last updated on October 6, 2026.
Source: aeonfun/aeon on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.