Agent Skills: Triage — Revalidate the Pool Against the Code

Triage and prioritize Linear backlog. Analyzes issues for staleness, blockers, and suggests priorities based on dependencies and capacity.

UncategorizedID: alienfast/claude/triage

Install this agent skill to your local

pnpm dlx add-skill https://github.com/alienfast/claude/tree/HEAD/skills/triage

Skill Files

Browse the full folder contents for triage.

Download Skill

Loading file tree…

skills/triage/SKILL.md

Skill Metadata

Name
triage
Description
Revalidate a team's Linear pool against the code as it stands now, never against age, and dispose of what the check finds — cancel, ship-close, duplicate, narrow, certify, ask the filer, regroom, wire edges. `scan` is the read-only unattended pass; `apply` the interactive write session. Use when the user says 'triage', 'triage scan', 'triage apply', 'revalidate the backlog', 'what's stale', 'is BF-123 still valid', or invokes /triage.

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. scan may be launched detached and left running for hours; apply reads 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 — default both; 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, createdAt is.
  • 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 bare Name.ext resolved by path; Class#method and A::B::C searched 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 with issues get <DUP> --no-cache and 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, wire related instead 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, through linear-post.sh description after a --no-cache re-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 carries specified when the child's body clears the bar, or needs decision when a pick gates it — the helper refuses a slot with no routing label; pass its leading --allow-unrouted only 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 /spec requires. On approval: description via linear-post.sh; ~/.claude/scripts/linear-add-label.sh <ID> specified (exit 2 is unconfirmed, not failed — gotcha #17); human when the work is console or production operations; simple when the risk-low semantics hold and no security/human/epic label 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 /spec in 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> specified with 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 stalled label 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 raw issues create, no issues list for 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 status logged out → linear-cli auth oauth, stop.
  • No team resolvable → stop with the team:KEY usage 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: length on null prints 0 at exit 0).
  • A scan group logs FAILED → read tmp/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.sh exit non-zero mid-batch → issues already verified stand; report which did not transition and stop the batch.
  • linear-add-label.sh exit 2 → the write may have landed; re-read with --no-cache before repeating it, and report certification as unconfirmed rather than failed.