Triage — Move Stories Through the State Machine
A small state machine over wiki story files. Lifts Matt Pocock's triage pedagogy onto the wiki's kind: story artifact.
Prerequisites
WIKI_SCOPE: <scope>declared in the project instructions. If missing, suggest/wiki.- At least one intent under
projects/<scope>/intent/or one story file underprojects/<scope>/plan/<plan-name>/. With neither, suggest/to-stories.
Roles
Intents carry no triage labels; their state is status plus one outcome field ([[adr-intent-kind]]):
draft— filed, unread by triageactive— scheduled: accepted, story not yet writtenarchivedwith exactly one ofpromoted_to(story slug),dismissed(reason),fixed_by(regression test path)supersededwithsuperseded_by— merged into another intent, whosesightingsabsorbed the count
Stories carry two label roles.
Two category roles (story has exactly one):
bug— something is brokenenhancement— new feature or improvement
Five state roles (story has exactly one, lives in triage_state frontmatter):
needs-triage— story needs evaluationneeds-info— agent waits on user clarification (in a solo-operator context: the agent is blocked on a user decision)ready-for-agent— fully specified, AFK-ready (an agent can pick it up with no human context)ready-for-human— needs human implementation (judgment, external access, hardware, design review)wontfix— will not be actioned
State transitions: an unlabeled or needs-triage story moves to needs-info, ready-for-agent, ready-for-human, or wontfix. needs-info returns to needs-triage once the user provides the missing input. The user can override at any time — flag transitions that look unusual and confirm before proceeding.
AI disclaimer (GH/GitLab mode only)
When WIKI_ISSUE_TRACKER is github or gitlab, every comment posted to the remote tracker during triage must start with:
> *This was generated by AI during triage.*
In local mode, no remote comments are posted; the disclaimer doesn't apply.
Invocation patterns
The user invokes /triage and describes what they want in natural language. Examples:
- "Show me anything that needs my attention" → bucket view, intents first
- "Promote intent X" / "Dismiss intent X — it's covered by story Y" → intent outcome
- "Let's look at story 2 of plan-billing-mvp" → triage a specific story
- "Move story 3 to ready-for-agent" → quick state override
- "What's ready for agents to pick up?" → filtered view
Pattern A — Show what needs attention
Read state from the vault filesystem, never from an index query: the index is
disposable and the search tool returns ranked candidates, not frontmatter.
- Intents: every
projects/<scope>/intent/intent-*.md; readstatus,sightings,origin,updated. - Stories: every
projects/<scope>/plan/*/story-*.md; readtriage_state,category,updated, and the slice tick counts. search(query, scope, kind="intent")(MCP) orkmd search "<terms>" --scope <scope> --kind intent(CLI) finds related candidates by content when a finding needs a twin check — the walk above is still the state source.
Present the buckets in this order, each with counts and one line per item:
- Intents,
draft— ordered bysightingsdescending, thenupdatedascending. Two sightings, or a confirmed falsification, is the promotion threshold; flag those first. - Intents,
active— scheduled and waiting for a story; oldest first. - Stale AFK stories —
triage_state: ready-for-agent,status: active, zero ticked slices, andupdatedmore than thirty days before the clock. Accepted work nobody started is the backlog's debt; it is offered three actions, never left in the queue by default:- Demote —
triage_state: needs-triage, a Triage Notes line naming the age; the story re-enters evaluation. - Dismiss —
triage_state: wontfix,status: archived, the reason in Triage Notes. A capacity call, not a design rejection: noadr-no-*unless the operator wants rationale on record. - Keep — the operator affirms it;
updatedmoves from the clock and the story leaves the band for another thirty days.
- Demote —
needs-triagestories — never evaluated, oldest first.needs-infostories — blocked on user input, oldest first.ready-for-agentstories — count plus a one-line summary each, so the AFK queue is visible.
Let the user pick.
Pattern B — Triage a specific story
Step 1 — Gather context
- Read the full story file (frontmatter + body).
- Read the parent
plan/plan-{name}.mdfor context. - Read
glossary.mdfor vocabulary. - Read any wikilinked specs/ADRs the story references.
- Check existing
adr-no-*.mdfiles in the scope — if a similar enhancement was previously rejected, surface it. (This is the wiki-native equivalent of Matt's.out-of-scope/knowledge base.) - If the story is
bugcategory, attempt reproduction: trace the relevant code, run tests if needed.
Step 2 — Recommend
Tell the user your category and state recommendation with reasoning. Wait for direction.
Format:
"Recommendation:
category: enhancement,triage_state: needs-info.Reasoning:
- Acceptance criteria are clear (3 scenarios).
- But Slice 2 references a payment-gateway integration that isn't specified anywhere — needs an ADR or a spec stub before this can be AFK.
Specifically I need from you:
- Which payment gateway? (Stripe, Adyen, …)
- Are we using their hosted form or building our own?
Confirm and I'll update the story frontmatter and post triage notes."
Step 3 — Reproduce (bugs only)
For bug stories, attempt repro before grilling. Read the reporter's steps, trace the relevant code, run tests or commands. Report what happened:
- Successful repro with code path — strong signal toward
ready-for-agentorready-for-human - Failed repro — strong
needs-infosignal - Insufficient detail — strong
needs-infosignal
A confirmed repro makes a much stronger agent brief.
Step 4 — Grill (if needed)
If the story body is too thin for ready-for-agent, chain into /intent to flesh it out. Don't try to grill within /triage — separation of concerns.
Step 5 — Apply the outcome
ready-for-agent:
- Verify scenarios are concrete (Given/When/Then, not free-form).
- Verify slices are vertical (each cuts through every layer).
- Verify
blocked_by:is populated correctly (no missing dependencies). - Update frontmatter:
triage_state: ready-for-agent. - Append an agent brief to the body (see template below) — short, durable, sufficient for an AFK agent to pick up cold.
- In GH/GitLab mode: create a remote issue with the story body inlined and a wikilink back; apply the
ready-for-agentlabel.
ready-for-human:
- Same brief structure as
ready-for-agent. - Add a "Why human" section explaining what makes this non-AFK (judgment, external access, hardware, design decision, manual testing).
- Update frontmatter:
triage_state: ready-for-human.
needs-info:
- Update frontmatter:
triage_state: needs-info. - Post triage notes to the body (see template below).
- In GH/GitLab mode: comment on the remote issue with the same notes (with AI disclaimer).
wontfix-bug:
- Polite explanation in the body's
## Triage Notessection. - Update frontmatter:
triage_state: wontfix,status: archived. - In GH/GitLab mode: close the remote issue with the explanation.
wontfix-enhancement:
- Write
projects/<scope>/adr/adr-no-<slug>.mdcapturing the rejection rationale (see template below). - Update story frontmatter:
triage_state: wontfix,status: archived. - Add wikilink to the new ADR in the story body's
## Triage Notes. - In GH/GitLab mode: close the remote issue with a link to the ADR.
needs-triage:
- Apply the role. Optional comment on partial progress.
Step 6 — Update parent plan
Update the Story Index table in plan/plan-{name}.md to reflect the new state column. Don't drift — the parent plan's table is canonical for the at-a-glance view.
Step 7 — Update updated: field
Set the story's updated: from the clock — date -u +%Y-%m-%dT%H:%M:%SZ, quoted — never composed.
Step 8 — Confirm the resync
Harnesses with the posttool hook validate and sync automatically. Check kmd config: if the synced line did not advance past your edits, the hook is not wired — run kmd validate, fix findings, then kmd sync.
Pattern C — Quick state override
If the user says "move story X to ready-for-agent", trust them. Confirm what you're about to do (frontmatter changes, comment, plan-table update), then act. Skip grilling.
If moving to ready-for-agent without a grilling session, ask whether the body needs an agent brief appended.
Pattern D — Triage an intent
Step 1 — Gather context
- Read the intent: the six sections,
origin,sightings, the Falsification path. - Twin check across every status, archived included:
searchwithkind="intent"on the intent's distinctive terms, then read the hits. A dismissed twin carries its reason indismissed— surface it before recommending anything. - Read any story or spec the intent links; if a story already covers the finding, the outcome is dismiss with that story named.
- If the Falsification path is cheap to run, run it and report the result. A confirmed path meets the promotion threshold on its own.
Step 2 — Recommend
One of three outcomes, with reasoning, then wait for direction:
- Promote —
sightings≥ 2, or the falsification confirmed, or the operator calls it. The story gets written now. - Hold — worth doing, not yet;
status: activemarks it scheduled. - Dismiss — covered elsewhere, not worth the fix, or falsified. The reason is the record.
Step 3 — Apply
Promote:
- Chain to the elaboration skill (
/to-stories) with the intent as input: the story's problem, scenarios, and first slices derive from the intent's sections, under the active plan. - Intent frontmatter:
status: archived,promoted_to: <story slug>,updatedfrom the clock. - Parent plan: add the Story Index row. The new story's
triage_stateis whatever the operator chose at promotion —ready-for-agentwhen the brief is complete,needs-triageotherwise.
Hold:
- Intent frontmatter:
status: active,updatedfrom the clock. Nothing else moves.
Dismiss:
- Intent frontmatter:
status: archived,dismissed: "<one-line reason>",updatedfrom the clock. - No ADR unless the operator wants design rationale on record; then
adr-no-<slug>.mdas for a rejected enhancement, linked fromdismissed.
Merge (two intents for one finding):
- The later one:
status: superseded,superseded_by: <survivor slug>. The survivor'ssightingsabsorbs the count and itsupdatedmoves.
Step 4 — Confirm the resync
As for stories: the posttool hook validates and syncs; if kmd config's synced line did not advance, run kmd validate then kmd sync.
Templates
Agent brief (append to story body when transitioning to ready-for-agent or ready-for-human)
## Agent Brief
**What to build:** <one-sentence description of the slice path>
**Acceptance criteria** (lifted from scenarios above):
- [ ] <criterion 1 — derived from Scenario 1>
- [ ] <criterion 2 — derived from Scenario 2>
**Relevant code paths:**
- `<path/to/file.ts>` — <one line on what it does>
**Out of scope:**
- <bullet>
**Definition of done:**
- All scenarios pass as integration tests
- `<plan/{name}/story-N>` slice checkboxes all ticked
- No unresolved [[spec-X]] divergence
Triage notes (append when transitioning to needs-info)
## Triage Notes
**What we've established so far:**
- <point 1>
- <point 2>
**What we still need from you:**
- <specific actionable question 1>
- <specific actionable question 2>
Capture everything resolved during triage under "established so far" so the work isn't lost. Questions must be specific and actionable, not "please provide more info".
adr-no-{slug}.md (when rejecting an enhancement)
Use wiki://template/project/adr (MCP resource, or kmd resource <uri>) with frontmatter:
---
title: "<Negative title — e.g., 'No multi-currency in v1'>"
kind: adr
scope: <scope>
status: active
summary: "Why <enhancement> was rejected from <plan-name>."
tags: [out-of-scope, ...]
supersedes:
superseded_by:
sources:
- projects/<scope>/plan/<plan-name>/story-N-<slug>.md
created: "<today>"
updated: <today>
---
Body:
# <Title>
## Status
active
## Context
The story `[[story-N-<slug>]]` proposed <enhancement>. Triage rejected this for v1.
## Decision
We will not <enhancement> in <plan-name>.
## Rationale
- <reason 1>
- <reason 2>
## Consequences
What stays simple. What's deferred. Conditions under which we'd revisit.
## Reconsider when
Specific signals that should re-open this decision (e.g., "if >5 customers ask for it", "if a related story emerges").
Resuming a previous session
If prior triage notes exist in a story body, read them, check whether the user has answered any outstanding questions, and present an updated picture before continuing. Don't re-ask resolved questions.
Rules
- Intents before stories. The intent queue is the loop's entry; a session that triages stories while draft intents wait is working the wrong end.
- An archived intent carries exactly one outcome field —
promoted_to,dismissed, orfixed_by. A dismissal without a reason is not a dismissal. - Every triaged story must carry exactly one category and one state. If labels conflict, flag it and ask the user.
- Always show your recommendation with reasoning before transitioning. Don't move state silently.
- For
wontfix-enhancementon design grounds, always writeadr-no-{slug}.md. Rejection without rationale loses institutional memory. A stale-band dismissal is a capacity call and records its reason in Triage Notes instead. - Always update the parent plan's Story Index table when state changes.
- Confirm the resync after frontmatter changes — the posttool hook syncs automatically; if
kmd config'ssyncedline did not advance, runkmd validatethenkmd sync. - In GH/GitLab mode, always lead remote comments with the AI disclaimer.
- Never grill within
/triage— chain to/intentif needed. - Quote prose-bearing frontmatter scalars to avoid breaking the sync walker.