name: PVR Triage description: Lifecycle check on submitted private vulnerability reports — polls triage state, detects maintainer acceptance or rejection, surfaces action items when PVRs age past 30 days with no response var: "" tags: [security, meta] requires: [GH_GLOBAL?]
${var} — Optional
GHSA-xxxx-xxxx-xxxxto check a single advisory on demand.
Today is ${today}. Read memory/MEMORY.md before starting.
Voice
If soul/SOUL.md and soul/STYLE.md are populated, match the operator's voice in the notification. If empty or absent, use a clear, direct, neutral tone.
Why this skill exists
pvr-watchlist monitors repos waiting to open PVR. This skill 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 file must have ghsa, repo, state, submitted_at fields.
Configuration
The skill 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.ymltop-level keypvr_triage.tracking_issue:(e.g.pvr_triage: { tracking_issue: "owner/repo#123" })- environment variable
AEON_PVR_TRACKING_ISSUE - unset — skip cross-linking entirely
If a tracking issue is configured, mention its URL in notifications and the per-advisory write-up so the operator can navigate to the canonical tracker.
Steps
1. Discover in-flight PVRs
Scan memory/pending-disclosures/ for all .md files. Parse the YAML frontmatter. Keep only those with channel: pvr.
If ${var} is set, filter to just the matching ghsa value (one-off mode).
If no PVR files found:
PVRT_SKIP: no submitted PVRs on disk
Log and stop. No notification.
2. Probe each advisory's triage state
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 |
Sandbox note: gh api uses GH_TOKEN internally (workflow wires GH_GLOBAL). If blocked, fall back to:
curl -s -H "Authorization: Bearer $GH_GLOBAL" \
"https://api.github.com/repos/${REPO}/security-advisories/${GHSA}" \
| grep -o '"state":"[a-z]*"'
3. Detect state changes
Compare the probed state to the state in the frontmatter.
- No change: note it, continue.
- Changed: this is the primary event. Log old → new state.
Also flag:
- Aged triage:
state=triageAND (today−submitted_at) > 30 days → escalate. Most maintainers respond within 30 days; silence past that is actionable. - Accepted (draft): surface the patch branch from
patch_branchfrontmatter field — maintainer may want a PR instead of a private advisory. - Published: advisory is live. The finding is closed. Update state and mark for removal.
- Withdrawn: rejected. Note the reason if visible. Mark for cleanup.
4. Update frontmatter in-place
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
5. Decide whether to notify
- All entries still
triage, no changes, none aged: no notification. Log silently. - Any state change, aged entry, or action item: notify.
6. Format notification
Write to .pending-notify-temp/pvrt-${today}.md, then: ./notify -f .pending-notify-temp/pvrt-${today}.md
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."- aged triage (>30d) → "30d+ no response. consider pinging maintainer or withdrawing."
7. Clean up resolved entries
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).
Do NOT delete — keep as a historical record.
8. Log to memory
Append to memory/logs/${today}.md:
## PVR Triage Monitor
- **Checked:** {total} advisories
- **Changed:** {changed_count} ({list})
- **Aged (>30d):** {aged_count}
- **Still triage:** {waiting_count}
- **Tracking issue:** {url or "none"}
- **Notification:** {sent|skipped}
- PVRT_OK
Required Env Vars
GH_GLOBAL— GitHub PAT withpublic_repo+repository_advisories:writescope. Same token used byvuln-scannerandpvr-watchlist.
Pending Disclosure File Schema
memory/pending-disclosures/*.md files tracked by this skill must include:
---
repo: owner/repo
ghsa: GHSA-xxxx-xxxx-xxxx
ghsa_url: https://github.com/owner/repo/security/advisories/GHSA-xxxx-xxxx-xxxx
channel: pvr
state: triage # triage | draft | published | withdrawn
submitted_at: 2026-05-12T19:54:42Z
last_checked: 2026-05-15 # added/updated by this skill
severity: high
cwe: [CWE-xxx]
patch_branch: https://github.com/<fork-owner>/repo/tree/security/branch-name
patch_commit: abc1234
---
Required fields: repo, ghsa, channel: pvr, state, submitted_at.
Optional: patch_branch, patch_commit, cwe, ghsa_url.