issue-grooming · git:20261019.0b11bce · 2026-10-19 · sha256 bd0202e318b1b985
issue-grooming git:20261019.0b11bceA
Immutable. This exact content never changes and is served at /api/v1/blob/bd0202e318b1b985.
--- name: issue-grooming description: >- Use when the user wants to groom, triage, prioritize, or clean up the GitHub issue backlog of a repo: "groom the issues", "triage the backlog", "which issues are already done", "prioritize the open issues", "close stale issues", "label issues 1-5", "refactor old issues to match the repo", or a periodic backlog review — even when they do not say "grooming". Also use when a single old issue looks stale and the user asks whether it is still valid. Not for reviewing or merging PRs (pr-list / gss) and not for planning one objective (mbo-plan). --- # issue-grooming — groom the backlog against what actually shipped Grooming answers three questions for every open issue, with evidence from the repo rather than from the issue's own text: **is it already addressed**, **how important is it now**, and **does its body still describe work that fits the repo**. The output is a labelled, trimmed backlog and a report the owner can scan in a minute. Why the rigor: issues are written at a moment in time. In this repo code moves (`src/` → `sdk/`), leaves of a plan merge under PR titles that never mention the issue, follow-ups get absorbed by later objectives, and a bot keeps filing reports. An issue's text is the *claim*; `main`, merged PRs, and `docs/mbo/index.md` are the *evidence*. Only the evidence decides. ## 0 — Setup (one command) ```bash bash ai/skills/issue-grooming/scripts/groom-snapshot.sh -o <scratch>/groom bash ai/skills/issue-grooming/scripts/groom-labels.sh # ensures priority:1..5 exist (idempotent) ``` The snapshot writes `issues.json`/`issues.txt` (bodies + comments), `labels.json`, `merged-prs.json` (with the issues each PR closed), and `clusters.txt` (a first-cut grouping). Read `clusters.txt` and regroup by hand: a cluster is a set of issues that share a program, tool, or plan (e.g. all `fleet-connect` sub-issues, all `gss` tech-debt, one bot's reports). Clusters are the unit of triage because verifying them needs the same files. Hooks note (this repo): the privacy hook blocks command text containing the login name or an absolute home path, so refer to scratch dirs by a glob or `~`. The Bash tool runs zsh; build flag lists as arrays. ## 1 — Verify, one cluster at a time For each cluster, establish what shipped **before** judging any issue: - `docs/mbo/index.md` row(s) and the plan's `TRACKING.md` ledger — the state column and the "pending" lists are the fastest map of what landed and what is still owed. - `merged-prs.json` — grep titles for the slug / leaf names and the issue number; many fixes cite the issue only in the PR title (`… (#331)`), not in a closing reference. - The code or docs the issue names — open the path on `main` (`git show main:<path>` when the working tree is on a branch) and confirm the verb/flag/test/doc exists. An issue with no index row or ledger (a follow-up filed after its parent merged) is verified from the code on `main` plus `merged-prs.json` alone; say so in its evidence. Clusters are independent, so fan them out: one subagent per cluster, each given the issue numbers, the verification pointers above, the rubric below, and the exact JSON return shape in `references/verdict-schema.md`. Insist on *evidence you actually checked* (paths, PR numbers, SHAs) — a verdict without a citation is a guess. Have each subagent write its array into the scratch dir (`verdicts-<cluster>.json`) rather than returning it in prose, then merge: `jq -s 'add | sort_by(.number)' verdicts-*.json > verdicts.json`. A pass of a handful of issues needs no fan-out — triage it inline with the same rubric and shape. ## 2 — Verdicts | Verdict | Means | Action | | :-- | :-- | :-- | | `addressed` | the whole ask is on `main` (code, docs, or a merged PR) | close as **completed** with an evidence comment | | `obsolete` | superseded by a later objective, or the repo no longer has the thing it describes | close as **not planned** with a comment naming the successor | | `partially` | some of it shipped; the rest is still wanted | keep open; refresh the body so it lists only what remains | | `open` | nothing shipped; still valid | keep open; relabel | A tracker whose remaining work is *only* a human/live gate (run it on a real host, sign off) is `partially`, not `addressed` — closing it would lose the gate. ## 3 — Priority 1–5 (one label per issue, `priority:N`) | P | Rule | | :-: | :-- | | 1 | Blocks daily work, or a correctness/safety bug in a shipped daily-use tool (install, git flow, CI gate, fleet). | | 2 | In-flight objective with design/spec on `main` and a clear next build step. | | 3 | Valuable follow-up to a shipped feature; nothing is blocked without it. | | 4 | Nice-to-have, speculative, or a large program that has not started. | | 5 | Parked, likely obsolete or superseded; kept open only as a reference. | Re-evaluate from scratch; ignore any priority label the issue already carries — it reflects the moment it was filed. Agreeing with a label set by a recent pass is the expected outcome, not a sign of anchoring. Within a program, sub-issues inherit the parent's priority unless they are the current blocking leaf (then one step higher). ## 4 — Refactor bodies that no longer fit Refresh a body when it carries stale paths (`src/<tool>` for Go that now lives in `sdk/`), leaves or PRs that already merged, sequencing that changed, or a checklist that is mostly done. Write the **remaining** work as the new body — a short "Groomed <date>" header, what shipped (with PR links), what remains, and the next step. The apply script folds the original body under a `<details>` block automatically, so never paste the old text back in; rewrite. Do not refactor a bot-generated report (drift reports, eval batches): close it if a newer run exists, otherwise leave it and lower its priority. ## 5 — Apply, then report ```bash bash ai/skills/issue-grooming/scripts/groom-apply.sh <scratch>/groom/verdicts.json --dry-run bash ai/skills/issue-grooming/scripts/groom-apply.sh <scratch>/groom/verdicts.json bash ai/skills/issue-grooming/scripts/groom-report.sh <scratch>/groom/verdicts.json ``` Read the dry-run before sending: it is the only review step. A dry run still *reads* GitHub (current labels and bodies) but sends nothing; an issue whose label is already right prints `already priority:N`. `groom-apply.sh` sets exactly one `priority:N` label, refuses to close anything that is not `addressed`/`obsolete`, requires a `close_comment` for every close, retries transient `gh` failures three times, and logs each action to `applied.log` — compare its row count with the verdicts count before calling the pass done. Closing is reversible (`gh issue reopen`), so a grooming pass may close without asking; it must never delete content. **Targeted re-check** (a few issues, or "is #N still valid?"): snapshot as usual, filter with `jq '[.[] | select(.number == 359 or .number == 360)]'`, verify inline, and apply with `--only 359,360`. A `suggested_action` of `none` records "verified, nothing to change". Finish with the report from `groom-report.sh` (counts, then one table per verdict) in the final message, and — in this repo, when anything changed — one new row in the `docs/mbo/index.md` audit log naming the issues closed and any objective rows whose state changed. Commit that row through the normal gss flow. A dry-run or no-change pass adds no row. ## Common mistakes - **Trusting the title.** A sub-issue titled "PR 3 · leaf C" may have merged under a PR named after the branch. Grep `merged-prs.json` for the leaf name and the slug, not the issue number. - **Closing a human gate.** TRACKING rows that read "pending on the operator" keep the issue open as `partially`. - **Counting absence as proof.** "grep found nothing" only proves something when the grep demonstrably ran against `main` and the term is the one the code would use. - **Re-labelling without the rubric.** Two graders with different rubrics produce a backlog nobody trusts; every subagent gets the same table from §3, verbatim.