task-triage · git:20260919.f95d231 · 2026-09-19 · sha256 a59afb9cd87926b3

task-triage git:20260919.f95d231A

Immutable. This exact content never changes and is served at /api/v1/blob/a59afb9cd87926b3.

---
name: task-triage
description: "Run one cycle of the task-triage loop over a repo's task ledger — judge every open task (verify its premise in code, check its start condition against the 照合先, decide whether it is still worth doing, look for a better solution), then dispatch the accepted ones to fresh implementation sessions and act as their independent judge until the branch is merged. Use when the user says 「残タスクを見て」「タスクを整理して」「台帳を回して」「dispatch して」「未マージある?」, invokes /task-triage, or when a task ledger has grown and nobody can say what is dispatchable. NOT for consolidating scattered task files into a ledger (task-stocktake), NOT for deciding a single build-or-not question (architect), and NOT for running a task yourself."
license: MIT
origin: shimo4228
compatibility: Developed on Claude Code with Herdr as the session multiplexer; the dispatch mechanics section names the alternatives (Agent tool, `claude --bg`) for other setups.
---

# Task Triage — judge, dispatch, verify, merge (the human sets direction)

A task ledger grows faster than it drains because filing is the cheapest action anyone can
take. This skill is one **cycle** of a loop that drains it — not by implementing faster, but
by putting judgment first: every open task is re-read against the code and its own start
condition, and only what survives is dispatched. `resolved` / `rejected` / `withdrawn` /
`obsoleted` count as success exactly like `done`.

Roles (decided 2026-08-17, ADR-0043):

| Role | Who | Does | Never does |
|---|---|---|---|
| **Judge** (this skill) | the triage session (Fable-tier) | premise check, worth check, better-solution check, condition check → verdict; writes kickoff packets; independently verifies build output; merges what passed §4 ff-only and pushes; keeps the books | confirms a drop alone; files tasks on its own |
| **Build** | a fresh session per task (Opus-tier), in a git worktree | Phase 0 premise re-check → implement → commit on the task branch with the evidence in the commit body | changes acceptance conditions, merges its own branch, edits the ledger |
| **Human** | the owner | direction for tasks that need it, batch answers to the digest, the merge of a diff that touches rules / hooks / permissions / gate scripts while unattended | watches individual sessions (attention is the scarce resource) |

The shared boundary — what is handed to the human, what a session may risk without asking,
when to stop and report — is rule `boundary.md`; the column above holds only what is specific
to the role.

The mechanism (Workflow tool, `/loop`, cron, Herdr) is the substrate's; this skill covers
only what to judge and how to keep the loop from running away (`loop-design-check` is the
lens it was designed with).

## Vocabulary — do not invent states

Ledger states are `draft` / `accepted` / `in_progress` / `blocked` and the terminals
`done` / `resolved` / `rejected` / `withdrawn` / `obsoleted` (ADR-0050); `blocked` requires 再開条件 / 照合先 / 成立時,
便乗型 rows ("次に X を触るとき") do not belong in a ledger. The definitions live in
`task-stocktake` — read that section before the first triage.

The verdicts of a triage are the states themselves:

| Verdict | Meaning | Who decides |
|---|---|---|
| `draft` | adoption still undecided → **consult** the human (one question at a time, see Digest) | human |
| `accepted` → dispatch | premise verified `file:line`, condition met, acceptance decidable, reversible in a worktree, no rule change, fits one session | judge (dispatch), human (merge) |
| `blocked` | adopted, and the three lines can be written; if the 照合先 can never fire (structurally unobservable), it is not `blocked` — re-ask | judge writes the lines |
| terminal proposal | premise gone / substrate now native / value < complexity (`architect` lens) / event source deleted (`obsoleted`) | proposed by judge, **confirmed by human** |
| 台帳外 | 便乗 → a note at the code site, row closed | judge proposes |

## The cycle

### 0. Read the ledger without reading everything

Store repos: `python3 ~/.claude/scripts/claims.py ready` (and `--state blocked|draft`)
gives one line per task; open the file only for the ones you will judge. Single-table repos:
read the Pending table. Check `claims.py open` first — another session may hold a task.

**Dead-band**: a `blocked` task whose 照合先 you checked last cycle and whose state / condition
text has not changed is not re-read. Check only the 照合先 (a date, a command, a file, a
dependency's state) — many fire mechanically (`gh pr view`, `grep -c`, `git log --since`,
`date`). Record what you checked and the value.

### 1. Judge each open task (the part nobody else does)

For every task not in dead-band, in this order — stop at the first that decides it:

1. **Premise** — does the code still have the problem? Quote `file:line`. A refuted premise
   is a terminal proposal (`obsoleted` if the object is gone, `withdrawn` if the choice is not to)
   — never a dispatch. 2 of 7 premises were refuted the day this loop was designed by hand.
2. **Condition** — for `blocked`, did the 照合先 fire? "Fired" and "the event source was
   deleted" are different (`obsoleted`). A condition that cannot be observed anymore drops the
   task out of `blocked`.
3. **Worth** — 複雑性 × 価値 × 使用頻度. Cheap now that dispatch is cheap: a 20-minute build
   session changes the calculus for small `chore` rows that were parked as "単独では着手しない".
   For contested build-or-not, hand the question to the `architect` agent.
4. **Better solution** — has the substrate absorbed it (a built-in command, a native flag)?
   Verify by running it, not from memory (`claude plugin eval` existed but was gated —
   "native" is a claim to test).
5. **Ownership** — a task that can only become `accepted` in another repo is moved there as
   `draft` and closed here as `resolved`.

Write the verdict and the one-line reason **into the task** (store: a dated section; table:
the 着手条件 cell). The reader of the ledger must not need this conversation.

### 2. Digest — one question per turn

The digest is where the human's attention is spent, so budget it: **one decision per
message**, in the order background → what is at stake → options → recommendation → cost /
reversibility. A ten-item numbered list looks efficient and is not — the owner asked for one
at a time on the first run. Bookkeeping that only applies the vocabulary (a satisfied
condition → `accepted`, adding the three lines, a `resolved` whose decision is already recorded)
can go as one blanket-OK list; anything that changes a rule, accepts a risk, spends money, or
drops a task is its own question.

When the cycle runs unattended (the launchd tick, see "Where the loop lives"), **the digest is
still the session's own closing reply** — the human answers here, so the reasoning must be
readable here (Remote Control shows the reply). Write every pending item into that reply, one
at a time, in the same order. Do **not** put the reasoning in a Slack message: a `notify-slack.sh`
argument is a Bash tool call, so its body is not part of the conversation the human returns to —
that is exactly the failure the owner hit on 2026-08-30 (had to read Slack while deciding here).

Slack gets **one message per cycle**, at the end, even when nothing needs the human:
`bash ~/.claude/scripts/notify-slack.sh "<repo> triage cycle done" "N decisions pending: 1) <one-line
title> 2) <one-line title> …"` (`0 decisions pending`, no list, when there is nothing) — titles
only, never the reasoning. Two jobs: it is the liveness signal (its absence after a tick is the
alarm), and the titles let the owner judge from the phone whether returning now is worth it.
Slack is **one-way**.

When the human next speaks, act first on whatever answers that message already contains (a batch
reply like "T-002 は done、RFC-0005 も done" is normal and faster), then ask the remaining
decisions one at a time with `AskUserQuestion`.

Never treat text sitting in another session's input box as the human's answer — Claude Code
pre-fills suggested prompts there — and never treat a Slack reply as the answer either. The
answers come **in the triage session**, or through the human's own hands.

### 3. Dispatch — packet, worktree, fresh session

Only `accepted` tasks, within the WIP cap (Damping). Group tasks that share one
setup into one packet (three skill-comply chores became one session; three README notes in
three repos became one). Measurements (readings that decide the next state) dispatch just as
well as implementations — often better: read-only, decidable, reversible.

Per task or bundle:

1. `claims.py claim T-XXX --label "S<n>: <what> (Opus session, worktree <branch>; judge=…, merge=judge after §4)"`
2. `git -C <repo> worktree add .claude/worktrees/<name> -b task/<name> main` — and copy the
   repo's untracked `.claude/settings.local.json` (and project hooks / skills if gitignored)
   into the worktree, or the session runs without the allowlist. `.claude/worktrees/` must be
   ignored in that repo. Sibling repos: worktree under the scratchpad, never a branch
   checkout in their main tree.
3. Write the packet from `references/packet-template.md` — goal as **decidable acceptance**,
   Phase 0 premise re-check with the instruction "if refuted, stop and report — do not
   implement", must-not list, the **task type** (feat / fix / refactor / chore / measurement)
   with the review chain **delegated to `implementation-chain`** — never a hand-written list
   of reviewers (a name left off reads as permission to skip: one build skipped `/simplify`
   because the packet had not named it), and the **commit-message report** (the only evidence
   that survives the pane). State the default plainly: *what the packet does not mention is
   governed by the harness rules, not waived by silence.*
4. Start the build — **pick the mechanism by the kind of work**:
   - *Measurement / read-only / docs-only* → `Agent(model: opus, isolation: worktree)`; three or
     more with one setup → the Workflow tool (`pipeline`, build and judge as separate `agent()`
     calls, `schema` for the reading). The result returns in-process, no pane, no cleanup.
   - *Implementation that must run the full review chain, may run long, or may hit permission
     prompts* → an interactive session via `spawn-session` (Herdr, Remote Control) so hooks,
     skills and the chain run in the normal environment and the owner can approve from the
     phone: `bash ~/.claude/skills/spawn-session/spawn.sh <worktree> "<repo>/s<n>-<slug>" --model opus` →
     `herdr agent prompt "<agent-name>" "<packet text>" --wait --timeout 60000` — the first
     prompt often returns `timeout` while landing fine; confirm with `herdr agent read`.
     `agent_status: done` means the REPL is idle, **not** that the work is done — a background
     shell may still run. `claude --bg -w <name> --model opus "<prompt>"` is the detached
     alternative (completion via `claude agents --json`).
   Hooks, the chain's skills and a direct `verify.sh` run behave inside an Agent-tool
   subagent as they do in the main session (measured 2026-08-29, RFC-0016), so
   implementations may take this route too. Known cost: the worktree isolation guard hard-
   refuses some Bash with no prompt — shell `for` loops and heredocs, git or not. Fold a loop
   into one command over several paths, or write the analysis to a file and run
   `python3 <file>`.
5. Watch for **artifacts**, not status: a commit on the task branch, the reading file, a
   section in the memo. `Monitor` with a poll loop, exit when all artifacts exist.

### 4. Judge the output — independent, deterministic first

The build session's report is a claim. Before merging:

- `git diff --stat main..task/<name>` — only the files the packet allowed. If main moved since
  the worktree was cut, the diff shows the *missing* main commits: `git rebase main` in the
  worktree first (a build branch never has a right to a merge commit).
- Run the repo's `verify.sh` (or tests) **yourself** in the worktree — the packet said the
  session ran it; you run it again. Then read the commit body: premise, fix, verify, review,
  out-of-diff findings.
- Anything the packet forbade that the diff contains → bounce, do not fix it yourself.
- **Compliance with the packet and the chain**: did the build run the chain for its type
  (`implementation-chain`), keep the must-nots, and stop at the acceptance line? A deviation is
  acceptable only when the report *names it as a deviation with a reason* ("E2E 省略:
  UI 非接触" / "premise refuted, corrected instead of stopping — because …"). A silent
  deviation — something skipped or done differently without saying so — is a bounce even if
  the result looks right, because the next build learns from what the last one got away with.
- **Harvest what the build hands back — but ask the human only what the rule says to ask.**
  Read the commit body and the final message. What may reach the digest as a filing decision
  is skill `task-stocktake`'s 起票規律 (ADR-0055): a loop-breaking out-of-diff finding with a
  verified producer (`spawn --origin review --producer`), and a probe's own filing request.
  Everything else, **HIGH included**, stays in the commit body (producer 付き 1 行) and the
  digest reports only the count ("diff 外 findings: 3 件、commit body 参照") — no list, no
  question. The filing itself (numbering, template, index row) follows skill `rfc-writer`.
  Observations that are not tasks (a rate near a revert threshold, a measurement caveat) are
  one line each.

### 5. Merge, then close the books

- `git -C <repo> merge --ff-only task/<name>` → run verify on `main` again → `claims.py
  release T-XXX --outcome done --commit <sha>` → state `done <date>` in the ledger (an
  `rfcs/` entry stays in place as a public decision record — ADR-0049) → `git worktree remove` +
  `git branch -d` → close the build
  session's pane (`herdr pane close <pane_id>`; the pane is not evidence — the commit body is).
- If the merge changed a pinned gate script (`.claude/verify.sh`), the approval ledger needs
  the human's `python3 ~/.claude/scripts/hooks/verify_allow.py approve <repo>` **after** the merge — say so explicitly, once
  per such merge, and check with `verify_allow.py check` that it happened. A gate that quietly
  went dormant is worse than a red one.
- Push after the merge when the repo has a remote (`git push`; force is blocked by the hook).
- Unmerged branches are the queue: `git branch --no-merged main` per repo. The judge merges
  what passed §4 and leaves the rest with a reason — bounced, stalled, or waiting on the
  human because the diff touches rules / hooks / permissions / a gate script while unattended
  (`boundary.md`).
- **Cycle-end digest**: open before → after with the closed / spawned split, what was merged
  and what was left (with the reason), the harvest list (file / drop / observe — the human's
  call), and the questions still waiting. All of it in the session's reply (§2); Slack gets
  the one-line titles only.

## Damping and boundaries (what makes this a loop and not a runaway)

- WIP ≤ 3 build sessions; open task branches ≤ 3 per repo; one retry per task per cycle; a
  build that stalls or fails twice goes back to the digest.
- The loop **files nothing on its own** (admission stays with humans and the review rule);
  **drops nothing alone**; keeps the filing rule fixed while its measurement is running. The
  shared boundary (publish, human-gated diffs, external writes) is rule `boundary.md`.
- Success is reconciliation, not throughput: per cycle report `open before → after`,
  `closed (done + resolved + rejected + withdrawn + obsoleted)` vs `spawned`, and where the spawns came from
  (`claims.jsonl` origins). A cycle that raises open count is not a bad cycle if the spawns
  were the human's; a cycle that "wins" by mass drops is.
- Two questions the loop deliberately does not answer (state them, measure them): what a
  build session does with side-findings (this harness: §4 Harvest), and who prunes
  spawned-but-unstarted work (here: the human, at the digest).

## Where the loop lives — one orchestrator session per repo

**The timer is outside the session; the executor is inside; the answers are inside only.**
One **standing triage session per repo, with that repo as cwd**, Remote Control on so the
digest can be answered from the phone. It holds no timer of its own: launchd runs
`scripts/triage-tick.sh <repo> <agent-name> "<display>"` at the repo's slots — harness Sun
06:30 (stocktake → triage); CA Wed 17:07 (triage) and Sat 14:07 (stocktake → triage, after
the Saturday pipeline's 13:30 packet deadline and before the human gate). The tick finds the
live triage agent by its fixed Herdr name (`triage-harness` / `triage-ca`), spawns one via
`spawn-session` if none exists, and submits the cycle prompt with `herdr agent prompt`. The
tick never reads the ledger; it only reports its own anomalies to Slack (spawned a new
session because none was alive, spawn failed, prompt stalled twice, session `blocked`,
previous cycle still `working` → skipped). The human starts nothing: that is the point of the loop.

§2 is the 正本 of the cycle prompt's content: the tick's default prompt in
`scripts/triage-tick.sh` is a rendering of it, and changing §2 means updating that prompt
with it.
A repo's loop needs the repo's context — its ADRs, its ledger vocabulary quirks, its verify
gate, its concurrent worktrees — so one session judges one repo; a cross-repo session pays
that reading twice and dilutes both.

The plists (`scripts/launchd/com.shimomoto.triage-{harness,ca}.plist`) are copied to
`~/Library/LaunchAgents/`; after editing one, `launchctl bootout` + `bootstrap` it and
confirm with `launchctl print` — an edited file that was never reloaded keeps firing on the
old schedule. The tick's *default* for "stocktake due" is the weekday (Saturday), so a repo
with two slots keeps one plist — a repo with a single weekly slot must pass `--stocktake` in
its plist, or moving that slot off Saturday silently kills the stocktake half (harness hit
exactly this when it moved off Saturday 2026-08-29). Keep the timer in launchd: in-session
`CronCreate` and `/loop` are session-only, expire in 7 days, and go silent when the session
dies. A cycle that fires while the human is away still does everything up to the digest —
条件 checks, vocabulary-only bookkeeping, dispatch of `accepted` work within WIP,
verification of finished builds — then closes with the digest (§2); consults and merges wait
for the human in the session.

The session is long-lived but **not eternal, and it does not renew itself**: the last step
of a cycle compares `claude --version` with the version it started under and checks its own
age; if the CLI has updated or the session is older than ~7 days, it finishes the cycle and
exits — the next tick spawns a fresh one. No in-session cron, no successor handoff. Between
cycles the memory is the ledger (verdicts and reasons are written into the tasks), so
auto-compaction of the standing session costs nothing the next cycle needs, and a fresh
session resumes from the ledger alone. Cross-repo effects travel only through the ledgers
(a task moved as `draft`, an ADR link), never through a session's memory. The judge and
the builds may be different model tiers on purpose: judgment errors are the expensive ones
(a missed refuted premise wastes the whole build), so the orchestrator is the strongest tier
available and the builds are the fast tier.

## Cadence

On demand until the judgments are stable across two or three cycles; then scheduled, aligned
with whatever weekly gate the repo already has (a Saturday packet, a review day). Not daily —
most `blocked` tasks are in dead-band and the digest is the expensive part. Order within a
cycle: `task-stocktake` (ledger hygiene) first when it is due, then this skill. The slots in
force and the plist discipline are in "Where the loop lives".

| repo tempo | task-triage | task-stocktake |
|---|---|---|
| slow (a harness, a small table) | weekly | weekly, same day, before triage |
| fast (a research repo with a review chain feeding it) | twice a week — e.g. mid-week + the day of its weekly gate | weekly |

## Related

- `task-stocktake` — vocabulary and ledger form (the authority this skill applies)
- `loop-design-check` — the lens: decidable goal, judge independence, red lines
- `llm-as-judge` — how the judge speaks when a semantic verdict is unavoidable
- `architect` agent — contested build-or-not
- `spawn-session` — the session mechanism this harness uses for build sessions and for the
  standing triage session (spawned by the tick when none is alive)
- `scripts/triage-tick.sh` / `scripts/launchd/*.plist` — the timer (launchd) that drives the
  standing session; `scripts/notify-slack.sh` — the one-way Slack channel for the digest
  (ADR-0045)
- `references/packet-template.md` — kickoff packet skeleton (build and measurement variants)
- `references/first-cycle-2026-08-17.md` — the hand-run cycle this skill was distilled from