Triage — Revalidate the Pool Against the Code
An issue goes stale because the code moved, not because time passed. A request filed in March against a surface that shipped in June is done; one filed yesterday against a mutation a sibling renamed this morning is already drifted. This skill reads every issue against the tree as it stands now and disposes of it on that evidence. Never cancel, close, or de-prioritize on age, and never on priority-plus-inactivity: an unprioritized Backlog issue with no comments is the normal state of a valid issue here.
It is the pool-level sibling of two skills and reimplements neither: /spec grooms one issue through a human interview,
and /auto-prep audits the certified pool for fleet safety. Reading the uncertified pool against the code — and
re-reading certified specs for decay — is this skill's whole job.
Two modes, because reading is cheap to parallelize and deciding is not. scan is read-only and runs unattended: it
covers the whole pool through headless agents and writes a proposal per issue. apply is interactive: every write that
changes a state, a label, or a description is a human decision, so it refuses an auto token or an unattended context
outright: ERROR: /triage apply is interactive-only — dispositions are human decisions. Run it directly. A bare
/triage runs apply when proposals exist and otherwise says to scan first.
Read first: skills/linear/SKILL.md (gotchas #6, #9, #13, #15, #20 bite here), standards/issue-spec.md (quality bar, template, collision edges), standards/linear-workflow.md (claims, stage order, filing recipe).
Arguments
scan|apply— the mode.scanmay be launched detached and left running for hours;applyreads what it produced.team:KEY— defaults to$LINEAR_TEAM; refuse to run with neither.stage:planned|backlog|triage— restrict to one stage. Default is all three in the fixed order Planned → Backlog → Triage (standards/linear-workflow.md§ Stage Priorities): the inbox is the least important stage and comes last.lane:uncertified|certified|both— defaultboth; within a stage the certified lane comes first (a fleet picks those next, so their decay costs the most).ISSUE-ID ...— targeted mode for a handful of issues: the deep pass runs in-session on Explore agents (references/scan.md § In-session variant) instead of through the scan, then apply proceeds as usual.
Step 1 and 2: The script pass — pool fetch and per-issue facts
Run the bundled script from the project checkout, in the background (about 1.6 seconds per unmarked issue, near-instant for marked ones; a 600-issue pool is 15 minutes cold):
mkdir -p tmp
zsh ~/.claude/skills/triage/scripts/triage-cheap-pass.sh --team <KEY> >| tmp/triage-run.log 2>&1
It paginates the raw API (linear-cli issues list returns 50 rows by default, silently — linear gotcha #25; --all
exists but the raw hatch is what carries relations and comments — and a first:250 cap truncates a 500-issue Backlog
just as silently), fetching every issue whose state type is triage, backlog, or unstarted, with description,
comments, relations, and creator. For each issue it computes:
- Claim. Assigned to anyone but the viewer →
claimed, dropped from every disposition (assignment is a claim); the count is reported. - Marker and baseline. The newest comment starting
triage-revalidated:(Step 5's format). Its sha is the baseline; without one,createdAtis. - Subjects. With a marker, the marker's own
subjects=list — the previous deep pass curated it. Without one, every slash-bearing token that resolves on disk, plus files that backticked identifiers resolve to after normalizing (line locators stripped; a bareName.extresolved by path;Class#methodandA::B::Csearched by their last segment; a dotted GraphQL path by its head token). An identifier resolving to more than 10 files is dropped as generic; an issue keeps at most 15 subject files, explicit paths first — without those caps a third of a pool carries 40+ subject files and the changed-since test fires on nearly everything. - Changed since baseline —
git log <marker sha>..HEAD -- <subjects>, or--since=<createdAt>without a marker. - Commit-subject test. A commit whose subject line leads with the issue's id is the shipped signal; a body-only mention is almost always the commit that filed it (body mentions outnumber subject hits a hundred to one). A hit already adjudicated by an earlier run (an ancestor of the marker sha) is not re-flagged.
Each issue gets one class: claimed, unchanged (marker present, subjects intact, no commits since — the only
skip), shipped?, drifted (a named subject no longer resolves), touched (commits on the subjects since the
baseline), no-subjects (business-filed, never auto-skipped), or never-revalidated. The script prints the coverage
line — pool N unclaimed · skipped unchanged K · candidates M · never revalidated R — the per-stage counts and each
stage's queue heads, and writes tmp/triage-cheap.ndjson plus tmp/triage-head.sha. Read its output rather than
re-deriving the numbers.
Step 3: Scan — the unattended deep pass
rm -f tmp/triage-scan.done
nohup zsh -c 'zsh ~/.claude/skills/triage/scripts/triage-scan.sh --out tmp --stage all --lane certified --group-size 5 --concurrency 6 > tmp/triage-scan-certified.out 2>&1; echo "EXIT=$?" > tmp/triage-scan.done' >/dev/null 2>&1 &
disown
Read references/scan.md for what a run does, the model tiers, how it resumes (a pending proposal
or a current applied/ one skips the issue; --force re-scans; --max-groups 1 --ids A,B is the trial run), why a
refused call stops it with exit 3, and the in-session variant for targeted mode. triage-scan.sh --summary prints the
counts by disposition and the non-keep lists with evidence; progress and cost go to tmp/triage-scan.log.
Step 4: Apply — dispositions from proposals, batched by verdict
Start from triage-scan.sh --summary. Keeps and hand-to-spec listings need no approval — their only write is the
marker (scripts/triage-apply-markers.sh, which posts from the proposals and files each one under applied/) — so post
those first, and repeat that pass as scan batches land. Then present each remaining verdict group as a numbered list
(ID — title — verdict — evidence line, from the proposal files) and one AskUserQuestion per group, at most four
questions per call, recommended action first. The user corrects a stance; they never adjudicate an unweighted list.
Freeze the id list at presentation time and hand the applier exactly that list —
scripts/triage-apply.sh --disposition <cancel|ship-close|duplicate|narrow|ask-filer> --ids <the approved ids> performs
one batch's writes and skips any id whose proposal is missing or carries another disposition. A selector evaluated at
apply time ("every opus cancel proposal") also picks up proposals that landed after the approval and cancels unapproved
issues (standards/linear-workflow.md § A batch approval covers the presented set). When the filer is the viewer, ask
the filer's questions in the dialog too. Read a proposal's full report only when a line needs it. A proposal whose
report says a command was denied, or that carries no questions for an ask-filer, is a weak read: delete the file and
let the next scan re-read the issue.
- Cancel — the request no longer applies or was satisfied by other work. Post the evidence comment first
(
~/.claude/scripts/linear-post.sh comment <ID> tmp/triage-<id>.md, naming the commit or issue that resolved it, with plain issue URLs for mentions — gotcha #20), then~/.claude/scripts/linear-set-state.sh Canceled <IDs>as one verified batch. Never a cancel without an evidence line. - Ship-close — the issue's own implementation shipped under its id and the state never moved.
git tag --contains <sha>decides: in a release tag →linear-set-state.sh Done; on main but unreleased →Ready for Release. Never Cancel a shipped issue. - Duplicate —
linear-cli relations add <DUP> <CANONICAL> -r duplicate(first argument is the one being absorbed, and Linear moves it to the Duplicate state on its own — gotcha #15; read the state back withissues get <DUP> --no-cacheand set it explicitly only if it did not land). Copy any detail the duplicate carries that the canonical lacks onto the canonical as a comment, and cross-comment both. Before absorbing into a certified canonical, test that the canonical's description reaches the absorbed subject (/auto-prep§ Step 3's scope test); if it does not, wirerelatedinstead and list the pair for/spec. - Narrow — part shipped, a remainder stands. Same problem → rewrite the description in the canonical template — its
Problem a story, issue-spec.md § The Problem is a story — with
the filed text preserved under
## Original request, throughlinear-post.sh descriptionafter a--no-cachere-read. Different problem → file it as a linked child (~/.claude/scripts/linear-create-child.sh <ID> <TEAM> Backlog "<title>" <body-file> <routing label> [priority], never a raw create) and cancel the original with a comment pointing at the child. The label slot carriesspecifiedwhen the child's body clears the bar, orneeds decisionwhen a pick gates it — the helper refuses a slot with no routing label; pass its leading--allow-unroutedonly when this run's Certify pass is what will certify the child. - Certify — its own pass, after the other batches: each candidate needs a drafted spec and per-issue approval of the
delta (what the draft says that the filed text did not), which is the signoff
/specrequires. On approval: description vialinear-post.sh;~/.claude/scripts/linear-add-label.sh <ID> specified(exit 2 is unconfirmed, not failed — gotcha #17);humanwhen the work is console or production operations;simplewhen the risk-low semantics hold and nosecurity/human/epiclabel is present; then Step 5's edges and a certification comment. State is the batch's one question: an inbox issue lands in Backlog by default — the human curates Planned. A long candidate list may instead go to/specin bulk. - Ask the filer — post a comment addressed to the filer by name with the proposal's questions (at most three) and
what each candidate surface does today, then
linear-add-label.sh <ID> 'needs decision'so no fleet ranking reaches it. State unchanged. - Regroom (certified) — subjects renamed with the defect intact → keep
specified; the marker records the new names. A criterion now satisfied, a census that moved, a mechanism-exclusion the code falsified →~/.claude/scripts/linear-remove-label.sh <ID> specifiedwith a comment naming what changed, listed for/spec. A purely factual correction — a pointer to an issue now in the Duplicate state, a checkbox the code proves satisfied, a clause a shipped sibling made false — may be applied here when the user approves the exact delta in the dialog, the same bar as Certify; anything needing judgment stays/spec's. Defect gone entirely → Cancel or Ship-close, whatever the label. - Epic — stays open with open children even when its own brief shipped (the house auto-closes it when every child is terminal); a stale brief is a regroom note.
Never touch In Progress, In Review, or any assignee. After a proposal is applied, move its file to
tmp/triage-proposals/applied/ so a resumed apply session does not re-present it.
Step 5: Collision edges, marker, report
Edges for everything certified, narrowed, or filed — and every collision the scan surfaced on a kept issue — per
issue-spec.md § Certification includes collision edges: blocks only for a
prerequisite, a same-method collision, or a deliberate increment (linear-cli relations add <BLOCKER> <BLOCKED> -r blocks, blocker first); related for a shared file, spec, locale, or mechanism; nothing for no overlap. Never wire
blocks behind an uncertified, needs decision, or non-head solo blocker. When the partner already sits in a chain,
read both .relations[] and .inverseRelations[] (gotcha #22) and attach at the tail. A blocker already in Done,
Canceled, Duplicate, Ready for Release, or In Review is resolved (the ranking's TERMINAL_STATES;
standards/linear-workflow.md § Terminal States).
Marker — one standalone comment on every issue apply considered and left in the pool (keep, narrow, certify,
hand-to-spec, ask-filer, regroom); not on unchanged skips, cancels, ship-closes, or duplicates. It must be its own
comment with this exact first line, because the script pass parses the comment body by prefix — a marker folded into a
certification comment is invisible to it:
triage-revalidated: <the proposal's sha> <YYYY-MM-DD> verdict=<keep|narrowed|certified|spec|filer|regroom> subjects=<the proposal's curated paths, comma-separated, or none>
<one to three lines of what was checked and why the verdict holds>
The sha is the one the agent measured, never HEAD at post time — the branch moves between measurement and posting.
Build the bodies from the proposals in one zsh loop and post each with linear-post.sh comment. Lowercase an id with
${id:l}, never bash's ${id,,} (standards/shell.md), and issues get -o json returns labels as {nodes: […]}
(gotcha #13) — read labels back with [.labels | .. | objects | .name? | select(. != null)]. Do not store the marker
anywhere local — a tmp/ ledger is invisible to the next machine and to the team.
Report, in this order: coverage (the script pass's line, what the scan covered, what remains unproposed);
dispositions applied, grouped (canceled and ship-closed with the resolving commit or release, duplicates, narrowed,
certified with labels and edges, regroom flags, related edges); waiting on people (ask-the-filer issues by filer, the
hand-to-/spec list with each one's open question); found in passing; milestones (Planned: every issue revalidated at <sha>.).
What this skill must NOT do
- No age-based or priority-based closes. Staleness is measured against the code; "no update in 30 days" and "P4 with no activity" are not evidence of anything.
- No
stalledlabel for staleness — it is/auto's pipeline-failure marker. - No implementation planning in a certified draft — WHAT, never HOW (
standards/issue-spec.md). - No persistent claims — no assignee writes, no In Progress, no branches.
- No bare
issues update -l, no rawissues create, noissues listfor the pool — the helpers exist because each of those silently loses data. - No unattended apply. The scan is the only unattended half; it writes nothing to Linear.
Error Handling
linear-cli auth statuslogged out →linear-cli auth oauth, stop.- No team resolvable → stop with the
team:KEYusage line. - The script pass exits 2 (
page without pageInfo) → the query errored; it prints the raw envelope. Fix the cause before classifying an empty pool as drained (gotcha #6:lengthonnullprints0at exit 0). - A scan group logs
FAILED→ readtmp/triage-proposals/raw/group-<n>-<model>.json.err; a rate limit or a permission refusal inside the headless run is the usual cause. Re-running the scan picks the group's issues up again. linear-set-state.shexit non-zero mid-batch → issues already verified stand; report which did not transition and stop the batch.linear-add-label.shexit 2 → the write may have landed; re-read with--no-cachebefore repeating it, and report certification as unconfirmed rather than failed.