# Triage

> This skill should be used to move intents and user-story files through the wiki's triage state machine. Intents (`draft` → `active` / archived with `promoted_to`, `dismissed`, or `fixed_by`) are triaged first, ordered by sightings; stories move `needs-triage` → `needs-info` / `ready-for-agent` / `ready-for-human` / `wontfix`. Reads and writes frontmatter, recommends transitions, promotes an intent into a story via the elaboration skill, dismisses with a recorded reason, writes `adr-no-{slug}.md` when rejecting an enhancement with design rationale, and in `github`/`gitlab` mode mirrors story state to remote labels. Use when the user says "$triage", "what needs my attention", "triage the intents", "what's in the intent queue", "promote intent X", "dismiss intent X", "triage the new stories", "move story X to ready-for-agent", or similar.

- Skill: `bartolli/triage-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add bartolli/triage-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bartolli/triage-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: bartolli (https://skillmd.com/u/bartolli)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bartolli/triage-2

---


# 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 under `projects/<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 triage
- `active` — scheduled: accepted, story not yet written
- `archived` with exactly one of `promoted_to` (story slug), `dismissed` (reason), `fixed_by` (regression test path)
- `superseded` with `superseded_by` — merged into another intent, whose `sightings` absorbed the count

Stories carry two label roles.

Two **category** roles (story has exactly one):

- `bug` — something is broken
- `enhancement` — new feature or improvement

Five **state** roles (story has exactly one, lives in `triage_state` frontmatter):

- `needs-triage` — story needs evaluation
- `needs-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`; read `status`,
  `sightings`, `origin`, `updated`.
- Stories: every `projects/<scope>/plan/*/story-*.md`; read `triage_state`,
  `category`, `updated`, and the slice tick counts.
- `search(query, scope, kind="intent")` (MCP) or
  `kmd 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:

1. **Intents, `draft`** — ordered by `sightings` descending, then `updated`
   ascending. Two sightings, or a confirmed falsification, is the promotion
   threshold; flag those first.
2. **Intents, `active`** — scheduled and waiting for a story; oldest first.
3. **Stale AFK stories** — `triage_state: ready-for-agent`, `status: active`,
   zero ticked slices, and `updated` more 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: no `adr-no-*`
     unless the operator wants rationale on record.
   - **Keep** — the operator affirms it; `updated` moves from the clock and
     the story leaves the band for another thirty days.
4. **`needs-triage` stories** — never evaluated, oldest first.
5. **`needs-info` stories** — blocked on user input, oldest first.
6. **`ready-for-agent` stories** — 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}.md` for context.
- Read `glossary.md` for vocabulary.
- Read any wikilinked specs/ADRs the story references.
- Check existing `adr-no-*.md` files 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 `bug` category, 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:
> 1. Which payment gateway? (Stripe, Adyen, …)
> 2. 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-agent` or `ready-for-human`
- **Failed repro** — strong `needs-info` signal
- **Insufficient detail** — strong `needs-info` signal

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-agent` label.

**`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 Notes` section.
- 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>.md` capturing 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: `search` with `kind="intent"` on the intent's distinctive terms, then read the hits. A dismissed twin carries its reason in `dismissed` — 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: active` marks 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>`, `updated` from the clock.
- Parent plan: add the Story Index row. The new story's `triage_state` is whatever the operator chose at promotion — `ready-for-agent` when the brief is complete, `needs-triage` otherwise.

**Hold:**

- Intent frontmatter: `status: active`, `updated` from the clock. Nothing else moves.

**Dismiss:**

- Intent frontmatter: `status: archived`, `dismissed: "<one-line reason>"`, `updated` from the clock.
- No ADR unless the operator wants design rationale on record; then `adr-no-<slug>.md` as for a rejected enhancement, linked from `dismissed`.

**Merge** (two intents for one finding):

- The later one: `status: superseded`, `superseded_by: <survivor slug>`. The survivor's `sightings` absorbs the count and its `updated` moves.

### 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`)

```markdown
## 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`)

```markdown
## 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:

```yaml
---
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:

```markdown
# <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`, or `fixed_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-enhancement` on design grounds, always write `adr-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`'s `synced` line did not advance, run `kmd validate` then `kmd sync`.
- **In GH/GitLab mode, always lead remote comments with the AI disclaimer.**
- **Never grill within `$triage`** — chain to `$intent` if needed.
- **Quote prose-bearing frontmatter scalars** to avoid breaking the sync walker.

