- name
- triage-github
- description
- Triage all open GitHub issues, PRs, and discussions in the current repository by fanning out up to 100 parallel subagents (one per item), then produce a single prioritized report ranking which PRs to review first, which issues to address first, and which discussions need maintainer attention. Use when the user asks to "triage open issues/PRs", "triage discussions", "prioritize the backlog", "what should I review first", "sweep the repo", or any request to bulk-evaluate open GitHub work and recommend an order.
# Triage GitHub Issues, PRs & Discussions in Parallel
## When to use
The user wants a prioritized view of everything open on the current repo's GitHub: which PRs to merge/review first, which issues to fix first, and which discussions need maintainer engagement. Trigger phrases include "triage the backlog", "what should I look at first", "prioritize open PRs and issues", "sweep open work", "triage discussions".
Do **not** invoke for single-item review (just look at it directly) or when the user wants ongoing automation (use `/schedule` instead).
## Prerequisites
- `gh` CLI is authenticated (`gh auth status`). If not, stop and ask the user to authenticate — do not attempt to fix auth automatically.
- Run from inside a git repo whose `origin` points at the GitHub repo to triage. Confirm with `gh repo view --json nameWithOwner,hasDiscussionsEnabled`.
- If `hasDiscussionsEnabled` is `false`, skip the discussions section entirely (don't fetch, don't include in budget, note in report that discussions are disabled).
## Procedure
### 1. Fetch open work
Run these `gh` calls in parallel. Use JSON so the downstream agent prompts are self-contained. The discussions call is a GraphQL query because `gh` has no built-in `discussion list` command.
```bash
gh pr list --state open --limit 200 --json number,title,url,author,assignees,createdAt,updatedAt,isDraft,mergeable,reviewDecision,labels,additions,deletions,changedFiles,statusCheckRollup
gh issue list --state open --limit 200 --json number,title,url,author,createdAt,updatedAt,labels,comments,reactionGroups
gh api graphql -f query='
query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
discussions(first: 100, orderBy: {field: UPDATED_AT, direction: DESC}, states: OPEN) {
totalCount
nodes {
number
title
url
createdAt
updatedAt
upvoteCount
isAnswered
locked
category { name }
author { login }
labels(first: 5) { nodes { name } }
comments(first: 0) { totalCount }
reactions { totalCount }
}
}
}
}' -F owner=<OWNER> -F name=<REPO>
```
Substitute `<OWNER>` and `<REPO>` from `gh repo view --json nameWithOwner`.
If the combined total exceeds 100 items, tell the user the counts (PRs / issues / discussions) and ask whether to cap at 100 most-recently-updated or split into batches. The agent cap is 100 total across all three categories.
### 2. Decide the parallel split
- Count `nPRs`, `nIssues`, `nDiscussions`.
- If `nPRs + nIssues + nDiscussions <= 100`: spawn one agent per item.
- Otherwise: prioritize PRs first (they block contributors), then issues by most-recently-updated, then discussions by most-recently-updated, up to the 100 budget. Note in the final report which items were skipped.
### 3. Fan out subagents
Dispatch **all agents in a single message** using multiple `Agent` tool calls (Claude Code parallelizes when they're in one block). Use `subagent_type: "general-purpose"` and `run_in_background: false` — you need the results synchronously to write the report.
Per-PR prompt template (substitute the bracketed values):
```
Triage GitHub PR [URL]. You have read-only access via `gh` and web tools.
Gather:
- `gh pr view [NUMBER] --json title,body,author,assignees,createdAt,updatedAt,isDraft,mergeable,mergeStateStatus,reviewDecision,labels,additions,deletions,changedFiles,statusCheckRollup,comments,closingIssuesReferences`
- `gh pr diff [NUMBER]` (skim — don't dump it)
- Recent review comments if any
`closingIssuesReferences` is GitHub's authoritative "this PR closes #X" linkage (populated by closing keywords like `Closes #123`). Put those issue numbers in `closesIssues`. Also scan the PR body/title for issue references that use closing keywords but weren't auto-linked (cross-repo, typos) and include those too. `assignees` is the array of assigned logins (may be empty).
Return ONLY a JSON object on a single line (no prose, no fences), matching:
{"kind":"pr","number":N,"title":"...","url":"...","author":"...","assignees":["login",...],"closesIssues":[N,...],"ageDays":N,"sizeLOC":N,"ciStatus":"passing|failing|pending|none","mergeable":true|false,"reviewState":"approved|changes_requested|review_required|none","draft":true|false,"priority":"P0|P1|P2|P3","reason":"<=140 chars","blockedBy":"<=80 chars or empty","recommendedAction":"merge|review|request-changes|close|wait"}
Priority rubric:
- P0: ready-to-merge (approved + green CI + mergeable + non-draft), or fixes broken main
- P1: small/focused, passing CI, needs review; or bug fix with clear reproduction
- P2: feature work, larger diff, no blockers
- P3: draft, stale (>30 days no activity), or has unresolved conflicts/failures
Be terse. One JSON object. No commentary.
```
Per-issue prompt template:
```
Triage GitHub issue [URL]. Read-only access via `gh`.
Gather:
- `gh issue view [NUMBER] --json title,body,author,createdAt,updatedAt,labels,comments,reactionGroups,assignees`
- Cross-referenced/linked PRs from the timeline: `gh api "repos/<OWNER>/<REPO>/issues/[NUMBER]/timeline" --jq '[.[] | select(.event=="cross-referenced" and .source.issue.pull_request != null) | .source.issue.html_url] | unique'` (substitute `<OWNER>`/`<REPO>`). Any URL returned is a PR that references this issue — capture it in `linkedPR`.
- Skim comments for repro steps, workarounds, and any "fixed by #NNN" / PR links
Set `linkedPR` to the URL of an open PR that addresses this issue if one exists (from the timeline query or comments), else empty. This is used to dedup: issues with a linked PR are folded under that PR in the report. `assignees` is the array of assigned logins (may be empty).
Return ONLY a JSON object on one line:
{"kind":"issue","number":N,"title":"...","url":"...","author":"...","assignees":["login",...],"ageDays":N,"reactions":N,"comments":N,"hasRepro":true|false,"linkedPR":"<url or empty>","category":"bug|feature|docs|question|chore","priority":"P0|P1|P2|P3","reason":"<=140 chars","recommendedAction":"fix|investigate|answer|close|wait-for-info"}
Priority rubric:
- P0: regression / data loss / security / blocks many users (high reactions + recent activity)
- P1: confirmed bug with reproduction, or high-engagement feature request
- P2: feature requests, minor bugs, docs gaps
- P3: questions, unreproducible, no activity in 60+ days
One JSON object. No commentary.
```
Per-discussion prompt template:
```
Triage GitHub discussion [URL]. Read-only access via `gh api graphql`.
Gather:
- `gh api graphql -f query='{ repository(owner:"<OWNER>", name:"<REPO>") { discussion(number: [NUMBER]) { title body url createdAt updatedAt upvoteCount isAnswered locked category { name } author { login } labels(first: 10) { nodes { name } } comments(first: 30) { totalCount nodes { author { login } body createdAt isAnswer upvoteCount } } reactions { totalCount } } } }'`
- Skim body + comments for: maintainer engagement, repro steps that suggest a real bug, and any links to issues/PRs (`#NNN` or full github.com/.../issues|pull/NNN URLs)
If the discussion body or comments reference an existing issue or PR that tracks it (or it was converted to an issue), capture that URL in `linkedIssueOrPR`, else empty. This is used to dedup: discussions already tracked by an issue or PR are folded away and only surface if unlinked.
Return ONLY a JSON object on one line:
{"kind":"discussion","number":N,"title":"...","url":"...","author":"...","linkedIssueOrPR":"<url or empty>","category":"Q&A|Ideas|General|Show and tell|Announcements|Polls|other","ageDays":N,"updatedDaysAgo":N,"upvotes":N,"comments":N,"reactions":N,"isAnswered":true|false|null,"maintainerEngaged":true|false,"looksLikeBug":true|false,"priority":"P0|P1|P2|P3","reason":"<=140 chars","recommendedAction":"answer|convert-to-issue|engage|mark-answered|close|wait"}
Priority rubric (category-aware):
- Q&A:
- P0: unanswered AND (looksLikeBug OR upvotes>=5 OR ageDays>=7 with no maintainer reply)
- P1: unanswered with clear question, some engagement, recent
- P2: recently asked, no engagement yet, awaiting community signal
- P3: effectively answered but not marked, stale (60+ days), or low-effort
- Ideas:
- P0: upvotes>=10 AND updatedDaysAgo<=14 — strong roadmap demand
- P1: upvotes>=3 with clear scope, or moderate engagement
- P2: legitimate idea, low engagement so far
- P3: stale, duplicate, off-roadmap, or off-topic
- General / Show and tell / Announcements / Polls / other:
- P1: high engagement (upvotes+comments>=10) and recent — surfaces trends
- P2: normal engagement
- P3: low engagement, off-topic, or stale (60+ days)
Notes for recommendedAction:
- "convert-to-issue" if looksLikeBug is true and no linked issue exists
- "answer" for unanswered Q&A
- "mark-answered" for Q&A where a comment clearly answers but isAnswered is false
- "engage" for high-signal Ideas needing maintainer feedback
- "close" for off-topic, duplicate, or out-of-scope
- "wait" if community signal is still forming
One JSON object. No commentary.
```
### 4. Aggregate
Collect every agent's JSON line. If an agent returned prose instead of JSON (rare), extract what you can or mark `priority: "P3", reason: "agent parse failed"`.
#### 4a. Build the linkage/dedup map (do this BEFORE sorting)
The report is a strict hierarchy — **PRs > issues > discussions**. Each unit of work appears **once**, in the highest tier that covers it. A PR outranks the issue it closes; an issue outranks the discussion that spawned it.
1. **`coveredIssues`** = the union of every PR's `closesIssues`, PLUS every issue whose own `linkedPR` is non-empty. These issues are represented by a PR, so they are **removed from the Issues section**.
2. For each PR, attach the human-readable `closes #X[, #Y]` list from its `closesIssues`. Pull each closed issue's title from the issue results (if that issue was triaged) so the PR line can name what it closes.
3. **`coveredDiscussions`** = every discussion whose `linkedIssueOrPR` is non-empty. These are tracked elsewhere, so they are **removed from the Discussions section**.
4. Keep a short "Folded away" tally (counts only) so the report can note how many issues/discussions were hidden because a PR/issue already covers them.
Edge cases:
- A PR closing an issue that is itself **closed/not in the open set** — still list `closes #X` on the PR (it's informative), just don't try to fetch a title.
- An issue with a `linkedPR` pointing at a PR that is **not open** (already merged/closed) — treat the issue as still open work: keep it in the Issues section, but note the merged PR in its reason. Only fold an issue away when the linked PR is **open**.
- Two PRs closing the same issue (competing fixes) — list the issue under both PRs; fold the issue once.
#### 4b. Sort
1. PRs by priority (P0→P3), then by `ageDays` ascending within each tier (newer first for P0/P1 to capture momentum; for P3 by oldest first — those are stalest).
2. Issues (after removing `coveredIssues`) by priority, then by `reactions + comments` desc within each tier.
3. Discussions (after removing `coveredDiscussions`) by priority, then by `upvotes * 2 + comments + reactions` desc within each tier. Inside the same tier, surface Q&A above Ideas above other categories (response latency matters most for Q&A).
#### 4c. Compute deltas vs the previous run
**Triage data dir.** Machine-readable snapshots live in a stable directory so runs can be compared: use `.agent/triage/` if the repo has a `.agent/` directory, else `.triage/` at the repo root (create it, and add `/.triage/` to `.gitignore` if not already ignored). These snapshots are local working data — never commit them.
Find the most recent snapshot `triage-*.json` in that dir whose date is **before today** (ignore any from today so a same-day re-run doesn't diff against itself). If none exists, skip deltas and note "first tracked run — no prior snapshot to compare" in the report.
Otherwise load it. Each snapshot is `{ "date", "repo", "items": [ <agent JSON> + "highStreak": N ] }`. Match items across runs by `kind` + `number` and compute:
- **New** — in current, not in prior.
- **Resolved** — in prior, not in current (merged / closed / answered since last run).
- **Escalated** — priority increased (e.g. `P2 → P0`). Record `old → new`.
- **De-escalated** — priority decreased.
- **Recurring high priority** — `highStreak >= 2` (see below): P0/P1 now and also high-priority last run. A P0 that recurs across runs is being ignored despite "act today" — the single most actionable delta signal.
**`highStreak`** propagates the streak so you don't need full history: when building the current snapshot, for each item look up its prior entry. If the item is P0/P1 **and** its prior entry was P0/P1, set `highStreak = priorHighStreak + 1`; otherwise `highStreak = 1`. Store it on each current item.
Deltas are additive — they never change an item's score or its placement in the sections below. If the prior snapshot is missing or unparseable, skip the delta section entirely rather than guessing; do **not** fabricate a comparison.
### 5. Write the snapshot, then the report
First write the machine-readable snapshot to `<triage data dir>/triage-YYYY-MM-DD.json` (the dir from 4c) — `{ "date", "repo", "items": [...] }`, one entry per triaged item (the agent's JSON verbatim plus the computed `highStreak`). This is the source of truth the next run diffs against; the markdown report is human-facing only. Overwrite today's snapshot if re-running.
Then save the human report to `TRIAGE_REPORT.md` at the repo root (or `.agent/triage/TRIAGE_REPORT-YYYY-MM-DD.md` if the repo has a `.agent/` directory). Ask before overwriting an existing report from today.
Report skeleton:
```markdown
# Triage Report — <repo nameWithOwner> — <YYYY-MM-DD>
Scanned **N PRs**, **M issues**, and **D discussions**. Folded away **X issues** covered by an open PR and **Y discussions** already tracked by an issue/PR (they appear under the covering item, not in their own section). Skipped K items over the 100-agent budget (listed at bottom).
PR lines carry the metadata format: **by @author** · **assigned @assignee1, @assignee2** (or _unassigned_) · **closes #X** (when the PR closes an issue).
## What changed since <prior date>
_Diffed against the previous snapshot (<prior date>). Omit this whole section on the first tracked run._
- **New (<count>):** [#NUM Title](url) _(pr/issue/discussion, P0)_, …
- **Resolved (<count>):** [#NUM Title](url), … — open last run, now merged/closed/answered.
- **Escalated (<count>):** [#NUM Title](url) **P2 → P0**, … — priority rose; look here first.
- **De-escalated (<count>):** [#NUM Title](url) **P0 → P2**, …
- **Recurring high priority (<count>):** [#NUM Title](url) — **P0 for 3 consecutive runs**, still _<action>_, … — keeps surfacing without resolution.
## PRs to review first
### P0 — merge/fix today
View on GitHub