Feedback Triage
The downstream half of the tool-feedback loop. tool-feedback captures one report
per session; this pass reads the accumulated corpus and turns it into the tool's
improvement backlog. Capture is tuned for recall; triage is tuned for precision —
the bar is a maintainer can pick the top item and build it without re-reading the
reports. It ends at the backlog document: building promotions, version bumps,
and CHANGELOGs belong to the tool's release process.
The pipeline — run in order
- Scope. Resolve the tool from the
feedback-targetstable in loaded context (ask once if absent; never hunt). Rebuild the dir'sINDEX.mdfirst (runuv run --no-project python "${CLAUDE_PLUGIN_ROOT}/skills/feedback-triage/scripts/build_feedback_index.py" <dir>): its### Untriagedsection is the input list — reports in no triage doc's Inputs, detection by input lists, not dates — and theextends-lookup in steps 2–3 is one Read. A triage doc is detected by its# TriageH1, never its filename (references/mechanics.md). State the count:N un-triaged reports. N of 1 with no prior triage doc is too thin — stop with a note (nothing to cluster); 1 new report over an existing baseline is a valid delta pass (step 7). When the invocation names a different count or set, the directory is authoritative — note the discrepancy under Inputs. Note any triage doc already dated today and re-check at emit (step 7). - Reconcile shipped first — against a checkout you have confirmed is
current. The reports come from the installed artifact and the grounding
comes from the checkout; nothing makes those meet on its own.
git fetch, then read the currency linetool-feedback/scripts/plugin_version.py <plugin> --tree <repo root>prints beneath the version: it names how far behind the checkout is and any cache-versus-tree skew. Then read the tool's CHANGELOG since the last triage — on a first run, the whole CHANGELOG to date. For a component without its own CHANGELOG (a harness, a scripts dir, a doc set), also readgit logover the window — increments land as commits, invisible to CHANGELOG-only reconciliation. Map each finding to the version or commit that resolved it. Open the doc with "Already shipped — NOT re-proposed"; a cluster that goes further than a shipped change is marked as extending it. Reconcile OPEN rows too, and read them rather than recall them:feedback-triage/scripts/triage_audit.py open-rows <dir>lists every row a prior doc leftproposed/watch, with the doc that set it. Carry or re-disposition each — a row is not closed until a later doc lists it, and a row recorded only in a prior doc's prose is the kind that orphans. An off-main-chain cycle-scoped triage has the same failure (keel'sreflection-triageP3a is the twin; co-land). - Cluster by underlying cause, not symptom. Three reports saying "the cited
file didn't exist", "the helper didn't handle our shape", and "the precedent
was counterfactual" are one cluster: ungrounded referents. Collapsing has a
dual — split one super-cause into separate clusters when its corollaries
have distinct homes and distinct concrete fixes; each piece must be
promotable on its own. Follow
extendschains while clustering: a finding belongs with its ancestors, and chain length is recurrence evidence. Cite each cluster's evidence as finding IDs (or stem + section for narrative findings), with counts. For a same-wave-execution/-authoringpair, read-executionfirst — it holds the evidence. - Assign a disposition per cluster:
- ATTACK — a real increment to this tool; name the home (template / gate / skill / doc / ADR) and the fix shape, derived from the cause, not the symptom. Prefer shapes in this order: remove/simplify what produces the failure; restructure the section or mechanism so the class can't recur; mechanize (test / script / gate / hook); append prose — last, and only naming what it displaces (a clause folded, tightened, or retired) — loop bodies measurably grow one clause per promoted finding until a cold reader drops load-bearing ones. Escalate the layer on a recurrence: when a finding recurred after a fix already shipped at the same enforcement layer (≥2 post-fix reports) and its cause is mechanically reachable at the next layer, attack one rung down — advisory prose → required structure → script/gate → hook → linter/CI — instead of re-prosing the same advice. A judgment-bound recurrence no mechanism can reach (a dispatch-timing nudge, a naming call) takes sharper prose or DECLINE, not a forced rung.
- ROUTE OUT — it belongs to another registered tool; record the target.
- DECLINE — project-specific or out of charter; record why.
FORWARD — a sibling pass over this same tool owns it (a corpus split by scope across several passes). Route-out crosses tools, forward crosses passes, and a forwarded finding is not closed: name it outside
## Inputsso it stays un-triaged for the pass that clusters it.Tie-breaker when this tool's artifact participates in behavior another tool owns: route by where the fix lands, not where the artifact lives. Fan-out digest briefs must enumerate each tool's own components in the owner taxonomy — misrouting case in
references/mechanics.md.
Ground, then apply the promotion gate. Before writing a row, ground it against the tool's current source: verify the mechanism it names is actually absent (or present, for an extension), implementable as stated (the API allows it), and truthfully named for the shape it will carry — cite the check in the ledger. A CHANGELOG window cannot see work shipped releases ago; only the source can — ungrounded rows have re-proposed the shipped and proposed the impossible. Then promote only clusters that are reinforced (≥2 reports, ideally across arcs — a single-report BLOCKER is exempt), specific (a concrete change with a home), and actionable. The exemption's scope is the BLOCKER's own row — siblings from the same report justify themselves in the ledger or take
watch. Under-promote rather than pollute; unpromoted clusters stay listed as raw, and the Promotion-gate ledger shows the gate's work either way.Two limits on what grounding answers. The reinforcement bar governs proposed changes; a statement in the tool's own shipped artefacts that grounding proves false is corrected on sight and says so in the ledger. And source answers "is the mechanism there?", never "was this already decided?" — before declining anything for want of a home, check step 2's open-row set: an unbuilt promoted row is a pending home, not a missing one.
Consolidate before you grow. A standing debt check on every pass: a home that takes an appending promotion this round, carries clauses no report has exercised across recent rounds, or nears the validator's size cap gets a consolidation row of its own — fold accumulated sub-cases into a reference file, merge overlapping clauses, retire dead ones. Shrink rows ride the same table and statuses as any other promotion; a loop that can only add converges on bodies too dense to execute.
Emit the triage doc (template below) into the tool's feedback dir as
<YYYY-MM-DD>-triage-<scope>.md, clusters leverage-ordered. A later pass over a corpus with a baseline emits a NEW doc in the delta form — Inputs list only the new reports, the new table supersedes the baseline as status of record, a consolidated backlog table carries every open row, cluster IDs continue the baseline's namespace (references/mechanics.md). Before emitting, assert input coverage with `triage_audit.py coverageDefer to a tool-owned template. If the binding's
extrasregisters a triage template (keel'sreflection-triage), follow its structure and homes; otherwise the template below is authoritative — don't hunt for one. Triaging "everything" when one tool owns its own flow? That tool's slice is a digest-for-handoff: extracted, clustered, owner-tagged (per step 4) findings written as INPUT to its flow (a<date>-new-findings-digest.md) — not a competing triage, not skipped.
Triage doc template
# Triage — <tool> feedback backlog (<N> reports, <date-range>)
## Already shipped — NOT re-proposed
<changelog reconciliation; clusters below that extend shipped work say so>
## Inputs
<stems this pass CLOSES, one per line — the parser credits every stem named
anywhere in this section, prose included, so a report with any un-dispositioned
or forwarded finding is named elsewhere and stays un-triaged; a factored-out
date prefix reads as zero coverage>
## Headline
<2–4 sentences: what this round establishes about the tool>
## Clusters
### T1 — <underlying cause> (<disposition>; <recurrence count>)
<evidence: cited finding IDs / report stems>
| # | proposed promotion | fix shape | home | status |
|---|--------------------|-----------|------|--------|
| T1a | <the concrete change> | <remove / restructure / mechanize / prose (displaces: …) / new artifact> | <template/gate/skill/doc/ADR> | proposed |
### T2 — …
## Routed out
<cluster → target tool, what was routed>
## Declined
<cluster → reason>
## Promotion-gate ledger
<the gate's work, auditable per cluster: which cleared on reinforcement, which
promoted via the BLOCKER exemption (name the exempting finding), which sit at
`watch`, which stayed raw — and why. Close with three assertions: no singleton
non-BLOCKER was promoted, no prose append shipped without a named displacement,
and every Inputs finding is dispositioned (input coverage).>
Status vocabulary: proposed / watch / accepted / shipped(<version>) /
declined — watch parks an anchored-but-singleton row until a second report
corroborates it, and a row whose sole evidence is one measured wave names the
replication it is pending and waits there too. Later passes update statuses. (Report finding IDs <stem>#<n>
are minted by tool-feedback; promotion IDs T1a are minted here — two
namespaces, don't conflate them.)
Anti-patterns — hunt these
- Re-proposing shipped work.
- Symptom clusters — ten shallow ones where two deep ones exist.
- Over-promotion — a singleton promoted as if reinforced.
- Absorbing what should be routed out.
- Re-prosing a recurrence — a fourth sentence where a rung is needed.
- A decline for want of a home that is already an open row.
What this skill does NOT do
- Build promotions, edit the tool, bump versions, or write CHANGELOG entries.
- Run proactively.
- Triage GitHub issues, PR queues, or task backlogs.