feat(pipeline): route to the lightest correct methodology; move execution loops onto the Workflow substrate

The selector forced every task through the heaviest methodology's
critical path: a behaviour-preserving, type-enumerable change paid the
same specify -> planner -> implement front-half as a novel feature,
because it was neither new behaviour (tdd) nor an observed bug (debug)
and so fell to specify by elimination. Two coupled defects — a selector
with no verification axis, and an all-or-nothing executor — kept the
existing lighter path unreachable and uneconomical. This fixes both.

Part A — verification-keyed selector (boss/SKILL.md):
- Replace the three-way "design line" with an ordered cascade that adds
  a verification/enumeration axis ahead of the settled-vs-fork question.
  Each lighter arm carries a positive trigger matched by signature, not
  reached by elimination.
- New `compiler-driven` arm: a type/signature edit at a definition site
  that propagates mechanically. Observe-then-bounce — make the edit,
  build, run the suite; clean build AND suite green unchanged commits;
  a hole bounces up (specify for a design choice, tdd for discovered
  test-specifiable new behaviour); a regression bounces to debug.
- The observed-bug RED-first gate is first in the cascade, so a
  mechanical-looking fix cannot bypass it.
- The straddle rule ("add an enum variant") is codified as a rule:
  mechanical/forwarding -> compiler-driven; encodes new behaviour ->
  tdd/spec; doubt routes up.
- The executor is the elevated inline carve-out plus a shipped workflow,
  not a heavy new skill ("the largest concrete win is small").

Part B — Workflow substrate (implement/workflows/):
- implement-loop.js: the per-task loop as a deterministic script. Each
  phase (implementer -> spec-compliance -> quality, + tester for E2E) is
  a separate top-level agent() call, so a single phase is independently
  invokable and inter-phase aggregation/re-loop is code. Retires the
  implement-orchestrator agent's inline-role-switch workaround (the four
  phase agents survive as the agent-types the script dispatches).
- compiler-driven-edit.js: the observe-then-bounce loop.
- install.sh / uninstall.sh symlink shipped workflows into
  ~/.claude/workflows/.
- specify and brainstorm stay prose + interactive (human-intent oracle);
  only the autonomous/mechanical loops moved. try-and-error is deferred.

Docs (pipeline taxonomy, design, agent-template, migration, README) and
all selector<->executor cross-references updated; the arm and its
executor are co-located so a future re-route through the full loop is a
visible regression.

Verified by an adversarial multi-agent pass: PASS on all six acceptance
criteria; two coherence concerns fixed. The shipped scripts are
syntax-validated but exercised only in a downstream target project (the
skills repo is not itself a pipeline target).

closes #7
This commit is contained in:
2026-06-17 12:27:51 +02:00
parent 8e72aa5c36
commit 7a58a530b1
21 changed files with 991 additions and 720 deletions
+205 -174
View File
@@ -1,186 +1,216 @@
---
name: implement
description: Use when an implementation plan exists under docs/plans and is ready to execute, OR when a debug RED-test is handed off for a bugfix. Dispatches the implement-orchestrator agent, which runs the entire per-task loop (implementer phase → spec-compliance check → quality check, as sequential role-switches in its own context) directly in the working tree without creating commits, writes a stats file (and on BLOCKED/PARTIAL also `BLOCKED.md`), and returns a compressed end-report. The orchestrator reads the end-report, inspects the working tree, decides commit shape, and performs all commits.
description: Use when an implementation plan exists under docs/plans and is ready to execute, OR when a debug/tdd RED-test is handed off for the GREEN side. Runs the `implement-loop` Workflow — a deterministic script that executes the per-task loop (implementer → spec-compliance → quality, each a separate agent call) and aggregates in code, writing edits, tests, and a stats file to the working tree without committing (and `BLOCKED.md` on PARTIAL/BLOCKED). The orchestrator reads the end-report, inspects the working tree, decides commit shape, and performs all commits. Also documents the `compiler-driven` light-edit arm.
---
# implement — plan execution via a dedicated orchestrator-agent
# implement — plan execution on the Workflow substrate
> **Violating the letter of these rules is violating the spirit.**
## Overview
Plan execution is fully delegated. The orchestrator dispatches
ONE subagent (`implement-orchestrator`), which runs the entire
per-task loop in its own context: implementer phase → spec-
compliance check → quality check, per task, as sequential
role-switches inside the orchestrator-agent itself (Claude
Code does not permit nested subagent dispatch — see
Cross-references). All work lives in the working tree: code
edits and the stats file (and `BLOCKED.md` if the outcome is
PARTIAL/BLOCKED). The orchestrator-agent does NOT commit.
The orchestrator sees one ≤500-token end-report and an
unstaged working tree, then decides commit shape — the
end-report carries the per-task summary the orchestrator
uses to write the commit body.
Plan execution runs as the **`implement-loop` Workflow** (shipped at
`workflows/implement-loop.js`, symlinked into `~/.claude/workflows/` by
`install.sh`). The orchestrator invokes it through the Workflow tool;
the script runs the entire per-task loop deterministically — for each
task, implementer → spec-compliance → quality, **each a separate
`agent()` call** dispatching the surviving phase agent-types
(`implementer`, `spec-reviewer`, `quality-reviewer`, and `tester` for
the E2E phase). The inter-phase re-loop, the aggregation, and the
DONE/PARTIAL/BLOCKED verdict are code in the script, not
natural-language reasoning between dispatches.
This skill body is intentionally short. The procedural
details of the per-task loop live in
`agents/implement-orchestrator.md`. The **canonical
discipline** (Iron Law, sub-status table, common
rationalisations) lives in this file and is read by the
orchestrator-agent every dispatch.
This replaces the former `implement-orchestrator` agent, which carried
the loop as inline role-switches inside one subagent context because
Claude Code forbids nested-subagent dispatch. A Workflow orchestrates
from the top level, so that workaround is retired: each phase is now a
real, **independently-invokable** agent call, and the routing keys on
the structured verdict each agent reports (which encodes the project's
real build/test outcome). The four phase agents survive unchanged as the
agent-types the script dispatches; only the dispatch/aggregation prose
the orchestrator-agent carried is gone.
All work lives in the working tree: code edits, the stats file, and —
on PARTIAL/BLOCKED — `BLOCKED.md` at the repo root. The script **never
commits** and never moves main HEAD. The orchestrator reads one
end-report (the workflow's return value), inspects the unstaged tree,
and decides commit shape.
## When to Use / Skipping
Triggers:
- A plan exists under `docs/plans` (standard mode).
- A `debug` skill (RED test + cause) or a `tdd` skill (RED
executable-spec) has handed off for the GREEN side (mini mode).
- A plan exists under `docs/plans` (standard mode`implement-loop`
with `mode: "standard"`).
- A `debug` (RED test + cause) or `tdd` (RED executable-spec) handoff
for the GREEN side (mini mode → `implement-loop` with `mode: "mini"`).
**Never skipped** when there is code to ship. Trivial
mechanical edits (one-line typo fix, schema rename across N
files) MAY be handled inline by the orchestrator without
dispatch, per the project's CLAUDE.md "trivial mechanical
edits" carve-out — but no review-and-commit discipline is
shed.
**Never skipped when there is plan-driven or RED-handoff code to ship.**
### The compiler-driven arm (light path)
A **behaviour-preserving type/signature edit at a definition site**
one the type checker enumerates across N sites, e.g. a carrier/newtype
narrowing or a constructor rename propagated mechanically — does NOT go
through the full per-task loop. It is the `compiler-driven` selector
arm (see `../boss/SKILL.md` "Entry-path reflection"), routed here by a
**positive** trigger matched on the edit's signature, not reached by
elimination.
Its executor is **observe-then-bounce**, shipped as
`workflows/compiler-driven-edit.js`:
1. Make the edit; propagate it mechanically across the sites the build
flags. Introduce no new behaviour; add or weaken no test.
2. **Run the project's real build + full suite.** This run — not an
ex-ante guess that the change is "behaviour-preserving" — is the
oracle.
3. **Done-signal: clean build AND suite green unchanged → the edits sit
unstaged for the orchestrator to commit.** That conjunction is the
whole gate; it keeps the light path light without letting a
green-but-wrong change through.
4. **Bounce** otherwise — the change was not purely behaviour-preserving
after all, so route up per the straddle rule: a site that forces a
design decision → `specify`; a site that turns out to **encode new
behaviour** pinnable as one assertion → `tdd`, RED-first; the suite
not green-unchanged (a regression surfaced) → `debug`, RED-first. The
fallible ex-ante guess becomes a cheap ex-post detection.
A truly trivial mechanical edit (a one-line typo, a rename across a
handful of files) the orchestrator MAY still do inline without running
the workflow — but the **same done-signal binds**: clean build + suite
green unchanged, or it bounces. No review-and-commit discipline is shed;
the orchestrator still inspects and commits.
**Hard gate — observed bugs never enter this arm.** An observed bug is
`debug`'s job (RED-first), even when the fix is a one-line type edit.
The selector places the observed-bug check *before* the type-edit arm
for exactly this reason; "the fix is mechanical" must not reroute a
regression around the RED-first gate.
## The Iron Law
```
THE IMPLEMENTER NEVER COMMITS. CODE EDITS AND THE STATS FILE LIVE IN THE WORKING TREE AS UNSTAGED CHANGES UNTIL THE ORCHESTRATOR COMMITS THEM.
THE LOOP NEVER COMMITS. CODE EDITS, TESTS, AND THE STATS FILE LIVE IN THE WORKING TREE AS UNSTAGED CHANGES UNTIL THE ORCHESTRATOR COMMITS THEM.
MAIN HEAD IS SACROSANCT — NO RESET, NO REVERT, BY ANY ACTOR. MAIN MOVES FORWARD ONLY VIA ORCHESTRATOR COMMITS.
PER-TASK PHASES RUN SEQUENTIALLY IN THE ORCHESTRATOR-AGENT'S OWN CONTEXT — implementer phase, then spec-compliance check, then quality check. Each phase is a deliberate role-switch, NOT a fresh subagent (nested-subagent dispatch is forbidden by Claude Code).
TWO-STAGE CHECK PER TASK: SPEC COMPLIANCE FIRST, CODE QUALITY SECOND.
NEVER START THE QUALITY CHECK BEFORE THE SPEC-COMPLIANCE CHECK IS GREEN.
PER-TASK PHASES ARE SEPARATE AGENT CALLS IN THE WORKFLOW SCRIPT — implementer, then spec-compliance, then quality. SPEC COMPLIANCE IS GATED BEFORE QUALITY.
TASKS RUN SEQUENTIALLY; A BLOCKED TASK STOPS THE LOOP — NO SKIP-AHEAD (TASK ORDERING DEPENDENCIES ARE UNKNOWN).
NEVER PUSH PAST `BLOCKED` BY HAND.
ON `PARTIAL` OR `BLOCKED`, THE ORCHESTRATOR-AGENT WRITES `BLOCKED.md` AT THE REPO ROOT BEFORE RETURNING — UNCOMMITTED BY CONVENTION. ON `DONE`, NO SEPARATE FILE.
ON `PARTIAL` OR `BLOCKED`, THE SCRIPT WRITES `BLOCKED.md` AT THE REPO ROOT — UNCOMMITTED BY CONVENTION. ON `DONE`, NO SEPARATE FILE.
THE COMPILER-DRIVEN ARM COMMITS ONLY ON CLEAN BUILD + SUITE GREEN UNCHANGED; ELSE IT BOUNCES (specify for a design hole, tdd for discovered new behaviour, debug for a regression).
```
## Per-task sub-status mechanics
## Per-task loop mechanics
Each dispatch, the orchestrator-agent runs an internal per-task
loop over a sub-status vocabulary — `DONE`, `DONE_WITH_CONCERNS`,
`NEEDS_CONTEXT`, `non_compliant` / `changes_requested`, `BLOCKED`,
`unclear`, and tool/infra errors — bounded by a 2-retries-per-
failure-mode re-loop limit (the 3rd attempt escalates to
`BLOCKED`). These values are *internal* to the orchestrator-agent:
they describe the state of an inline role-phase, not a separate
subagent, and are not reported upstream.
The mapping of each value to its action (and the `BLOCKED` reasons
`context-exhausted` / `review-loop-exhausted` / `spec-ambiguous` /
`infra`) is defined authoritatively under **Per-task sub-status
vocabulary** in `agents/implement-orchestrator.md` — the file the
agent actually carries at runtime in its fresh context. That table
is the single source; it is deliberately not restated here, so the
two files cannot drift.
The script runs, per task: implementer phase, then spec-compliance,
then quality — gated in that order. Each is a separate `agent()` call
with a structured-output schema, so the script branches on a real
verdict. The re-loop limit is **2 repair retries per failure-mode per
task; the 3rd unresolved attempt escalates to `BLOCKED`**. A
`non_compliant` / `changes_requested` verdict re-dispatches the
`implementer` with the review findings as the repair brief; `unclear`
task text → `spec-ambiguous` BLOCKED; a tooling failure → `infra`
BLOCKED. This logic lives as the loops in
`workflows/implement-loop.js` — the single source; it is deliberately
not restated as prose elsewhere, so the two cannot drift.
## The Process — orchestrator side
### Step 1 — Dispatch the orchestrator-agent
### Step 1 — Run the `implement-loop` workflow
Before invoking: ensure the working tree is clean
(`git status --porcelain` empty). The workflow's preflight refuses to
start on a dirty tree.
For a standard iteration:
```
Agent("implement-orchestrator", {
Workflow({ name: "implement-loop", args: {
mode: "standard",
iter_id: "<iter_id>",
plan_path: "<path under docs/plans>",
task_range: [3, 8]
})
task_range: [3, 8] // optional
}})
```
For a RED-first handoff from `debug` or `tdd` (mini mode):
```
Agent("implement-orchestrator", {
Workflow({ name: "implement-loop", args: {
mode: "mini",
iter_id: "bugfix-<short-symptom>", # tdd: "feat-<short-behaviour>"
iter_id: "bugfix-<short-symptom>", // tdd: "feat-<short-behaviour>"
red_test_path: "<absolute path>",
cause_summary: "<1-2 sentences from debugger>", # tdd: spec_summary, the desired-behaviour line
constraint: "minimal fix, no surrounding cleanup" # tdd: "minimal feature, no surrounding scope"
})
cause_summary: "<1-2 sentences from debugger>", // tdd: the desired-behaviour line
constraint: "minimal fix, no surrounding cleanup" // tdd: "minimal feature, no surrounding scope"
}})
```
The blocks above show the call shape; each field's semantics
(including the load-bearing `iter_id` rule — it names the scratch
dir and stats filename, NOT a branch) are defined authoritatively
under **Carrier contract** in `agents/implement-orchestrator.md`.
That table is the single source for the field semantics; they are
deliberately not restated here, so the two files cannot drift.
Before dispatch: ensure the working tree is clean
(`git status --porcelain` empty). The orchestrator-agent's
Phase 0 will refuse to start on a dirty tree.
The carrier fields are defined authoritatively at the top of
`workflows/implement-loop.js`. The load-bearing `iter_id` rule is there
too: it names the scratch dir and the stats filename, NOT a branch
(there is no branch).
### Step 2 — Read the end-report
The orchestrator-agent returns a ≤500-token plain-text
report (see the agent's "Output format — end-report" section
for the fixed structure). Read it. The end-report is the
only thing that costs the orchestrator-context tokens;
per-task chatter has stayed inside the orchestrator-agent.
The workflow returns a small structured end-report (its return value):
`status`, `iter_id`, `started_from`, `tasks_total` / `tasks_completed`,
per-task summaries, concerns, E2E fixtures, the stats path, and — on
PARTIAL/BLOCKED — the blocked detail. Per-task chatter stayed inside
the workflow's agent contexts and never reached the orchestrator. Read
the end-report; it is the per-task summary you build the commit body
from.
### Step 3 — Orchestrator inspect + commit step (on DONE)
The orchestrator-agent returns with code edits and the stats
file sitting in the working tree as unstaged changes.
Nothing is committed yet, and there is no `BLOCKED.md` (DONE
never writes one).
The workflow returns with code edits and the stats file sitting in the
working tree as unstaged changes. Nothing is committed yet, and there
is no `BLOCKED.md` (DONE never writes one).
1. Inspect: `git status` and `git diff` — confirm the
changes match what the end-report claims. The end-report
is the per-task summary; use it as the basis for the
commit body.
2. Decide commit shape — by default one cohesive commit for
the whole iter; split into a few logical commits only if
the diff genuinely covers multiple unrelated changes.
Per-task commit splitting is NOT a goal; the iter is the
unit of consistency the orchestrator is committing to.
3. Write the commit body. It carries everything a future
reader needs that the diff itself does not: the *why*,
the alternatives that were considered and rejected, the
verification steps run, and any concerns that remain.
Detail-fill comes from the end-report — the per-task
chatter that stayed inside the orchestrator-agent does
not come back, by design.
1. Inspect: `git status` and `git diff` — confirm the changes match
what the end-report claims. The end-report is the per-task summary;
use it as the basis for the commit body.
2. Decide commit shape — by default one cohesive commit for the whole
iter; split into a few logical commits only if the diff genuinely
covers multiple unrelated changes. Per-task commit splitting is NOT
a goal; the iter is the unit of consistency the orchestrator is
committing to.
3. Write the commit body. It carries everything a future reader needs
that the diff itself does not: the *why*, the alternatives
considered and rejected, the verification steps run, and any
concerns that remain. Detail-fill comes from the end-report.
4. Stage + commit the code edits and the stats file.
5. If trigger is done-state and the user is away, run the
project's configured notification command per
`../boss/SKILL.md` "Done-state notifications" subsection.
5. If the trigger is done-state and the user is away, run the project's
configured notification command per `../boss/SKILL.md` "Done-state
notifications".
### Step 4 — Orchestrator handling (on PARTIAL or BLOCKED)
The orchestrator-agent returns with whatever work-in-progress
it managed plus `BLOCKED.md` at the repo root carrying the
diagnostic. Nothing is committed. The orchestrator decides
what to do with the dirty working tree:
The workflow returns with whatever work-in-progress it managed plus
`BLOCKED.md` at the repo root carrying the diagnostic. Nothing is
committed. The orchestrator decides what to do with the dirty working
tree:
1. Read `BLOCKED.md``## What did not` names the failure
mode and the worker's verbatim reason. Read `git diff`
to see what was attempted.
1. Read `BLOCKED.md``## What did not` names the failure mode and the
blocked task's reason. Read `git diff` to see what was attempted.
2. Decide:
- **Repair:** keep the working-tree changes in place (or
stash them with `git stash` if a clarifying read of
clean main is needed first). Adjust plan or extend
context; **delete `BLOCKED.md`** (`rm BLOCKED.md`)
before re-dispatch — the orchestrator-agent's Phase 0
clean-tree check counts it as dirt. Either stash
everything and re-dispatch on a clean tree, or commit
the known-good subset, then `rm BLOCKED.md`, then
re-dispatch.
- **Discard:** `git checkout -- .` to drop unstaged file
changes; `git clean -fd BLOCKED.md` (or `rm BLOCKED.md`)
plus anything else new. main HEAD does NOT move.
- **Escalate:** ask the user via the configured
notification command. `BLOCKED.md` sits in the working
tree until the conversation resumes.
- **Repair:** keep the working-tree changes in place (or `git stash`
them if a clarifying read of clean main is needed first). Adjust
plan or extend context; **delete `BLOCKED.md`** (`rm BLOCKED.md`)
before re-running the workflow — its preflight clean-tree check
counts the file as dirt. Either stash everything and re-run on a
clean tree, or commit the known-good subset, then `rm BLOCKED.md`,
then re-run.
- **Discard:** `git checkout -- .` to drop unstaged file changes;
`git clean -fd` / `rm BLOCKED.md` plus anything else new. main HEAD
does NOT move.
- **Escalate:** ask the user via the configured notification
command. `BLOCKED.md` sits in the working tree until the
conversation resumes.
Under no circumstance does the orchestrator `git reset` or
`git revert` on main: there is nothing on main to undo (the
orchestrator-agent did not commit), and the policy forbids
history rewinding on main even if there were.
Under no circumstance does the orchestrator `git reset` or `git revert`
on main: there is nothing on main to undo (the workflow did not
commit), and the policy forbids history rewinding on main even if there
were.
## Handoff Contract
@@ -188,67 +218,68 @@ history rewinding on main even if there were.
| Source | Carrier |
|--------|---------|
| from `planner` | path to plan under `docs/plans` (+ optional `task_range`) |
| from `debug` | RED-test path + cause summary + minimal-fix constraint |
| from `planner` | path to plan under `docs/plans` (+ optional `task_range`)`implement-loop` standard mode |
| from `debug` / `tdd` | RED-test path + cause/spec summary + minimal-change constraint `implement-loop` mini mode |
| from the `compiler-driven` selector arm | a type/signature edit + its definition site → `compiler-driven-edit` workflow |
`implement` produces: an unstaged working tree containing the
code edits and the stats file; on PARTIAL/BLOCKED, also
`BLOCKED.md` at the repo root. The orchestrator inspects,
commits the code + stats (DONE) or repairs/discards
(PARTIAL/BLOCKED). No further hand-off — `audit` runs
`implement` produces: an unstaged working tree containing the code
edits and the stats file; on PARTIAL/BLOCKED, also `BLOCKED.md` at the
repo root. The orchestrator inspects, commits the code + stats (DONE)
or repairs/discards (PARTIAL/BLOCKED). The `compiler-driven-edit`
workflow produces an unstaged edit on `DONE`, or a `BOUNCE` verdict
naming `specify` / `debug`. No further hand-off — `audit` runs
independently at cycle close.
## Common Rationalisations
| Excuse | Reality |
|--------|---------|
| "Single task, dispatch overhead exceeds the work" | The orchestrator-agent IS the discipline. A single dispatch is cheap; the per-task phase loop and the working-tree isolation are the value. |
| "Let me have the orchestrator-agent commit the per-task work, it's cleaner" | The orchestrator-agent never commits. Orchestrator-only commit is a project-wide rule: only the orchestrator decides when a state is consistent enough to enter main history. |
| "Per-task commits would help bisection later" | The orchestrator-agent's per-task phases are review gates, not bisection points. Iter-level commits are the bisection unit — and they only exist if the whole iter passes the orchestrator's review. |
| "BLOCKED end-report, let me dig into BLOCKED.md and continue myself" | Read the `## What did not` section first. The orchestrator-agent stopped at the re-loop limit for a reason. Continuing by hand undoes the discipline. |
| "End-report says PARTIAL with 4/5 tasks DONE — close enough, commit them" | The 5th task may carry an invariant the earlier 4 silently depend on. Either re-dispatch for the missing task or `git checkout -- .` and re-plan. |
| "BLOCKED.md feels redundant — the end-report already has the blocked detail" | The end-report dies when the chat scrolls; `BLOCKED.md` sits in the working tree across pauses, mode-switches, and orchestrator-inspection rounds. It is the durable handoff. |
| "The per-task phases run inline anyway, just have the orchestrator dispatch the reviewer-agents instead and skip the orchestrator-agent" | That gives back fresh-per-phase context but loses the orchestrator-context offload — the per-task chatter goes back through the orchestrator. The orchestrator-agent exists precisely for the offload. If you want fresh-per-phase context AND offload, you want a capability Claude Code does not provide. |
| "BLOCKED iter with a bad commit on main — let me `git revert` it" | There is no bad commit on main: the orchestrator-agent did not commit. Bad work stays in the working tree where it is still discardable. main HEAD is sacrosanct. |
| "Re-dispatch refuses because `BLOCKED.md` is still in the tree — let me just commit it to clear the check" | No. `BLOCKED.md` is never committed. `rm BLOCKED.md` (or stash) before re-dispatch — the file's whole purpose is to live in the working tree, not on main. |
| "Single task, running a whole workflow exceeds the work" | The workflow IS the discipline. Invoking it is cheap; the gated per-task phases and the working-tree isolation are the value. For a genuinely trivial mechanical edit, the inline compiler-driven path exists — but its done-signal still binds. |
| "Let me have the workflow commit the per-task work, it's cleaner" | The workflow never commits. Orchestrator-only commit is a project-wide rule: only the orchestrator decides when a state is consistent enough to enter main history. |
| "Per-task commits would help bisection later" | The workflow's per-task phases are review gates, not bisection points. Iter-level commits are the bisection unit — and they only exist if the whole iter passes review. |
| "BLOCKED end-report, let me dig into BLOCKED.md and continue myself" | Read the `## What did not` section first. The loop stopped at the re-loop limit for a reason. Continuing by hand undoes the discipline. |
| "End-report says PARTIAL with 4/5 tasks DONE — close enough, commit them" | The 5th task may carry an invariant the earlier 4 silently depend on. Either re-run for the missing task or `git checkout -- .` and re-plan. |
| "BLOCKED.md feels redundant — the end-report already has the detail" | The end-report dies when the chat scrolls; `BLOCKED.md` sits in the working tree across pauses and inspection rounds. It is the durable handoff. |
| "The compiler-driven edit built fine, ship it without running the suite" | Clean build is half the done-signal. Suite-green-UNCHANGED is the other half — it is what catches a behaviour change the build can't see. No suite run, no commit. |
| "The compiler-driven suite went red but the fix is one lineI'll just fix it here" | A red suite means the edit was not behaviour-preserving. That is an observed regression → bounce to `debug`, RED-first. Patching it inline reintroduces exactly the green-but-wrong path the bounce prevents. |
| "Nested-subagent dispatch is back via workflows, so an agent can spawn agents" | No. The Workflow orchestrates from the TOP level; the agents it spawns still cannot spawn further agents. The platform constraint is unchanged — the workflow simply does not need nesting. |
## Red Flags — STOP
- Orchestrator dispatching `implementer` directly (bypassing
the orchestrator-agent).
- Orchestrator dispatching `implementer` directly outside the workflow
(bypassing the gated loop).
- Orchestrator running `git reset` or `git revert` on main.
- Orchestrator-agent running `git commit` (anywhere, ever).
- Two `/implement` runs overlapping on the same working tree.
- The workflow (or any agent it spawns) running `git commit`.
- Two `implement-loop` runs overlapping on the same working tree.
- `BLOCKED.md` staged or committed by anyone.
- End-report longer than ~500 tokens.
- A compiler-driven edit committed without a clean build AND an
unchanged-green suite.
- An observed bug routed to the compiler-driven arm instead of `debug`.
## Cross-references
- **Agent dispatched:** `agents/implement-orchestrator.md`
carries the per-task loop inline; each phase is a
role-switch in the orchestrator-agent's own context. The
role-files below are *phase references* (the
orchestrator-agent reads them at each role-switch to
inhale the discipline) — they are NOT separately
dispatched subagents:
- `agents/implementer.md` — Phase 2.1 reference
(implementer mindset, TDD discipline)
- `agents/spec-reviewer.md`Phase 2.2 reference
(spec-compliance mindset)
- `agents/quality-reviewer.md` — Phase 2.3 reference
(quality-review mindset)
- `agents/tester.md` — Phase 3 reference (E2E coverage
mindset)
- **Why inline phases, not nested subagents.** Claude Code
does not permit a subagent to spawn other subagents. The
orchestrator-agent cannot dispatch the role-agents above;
it adopts each role as a sequential phase in its own
context.
- **Input sources:**
- `../planner/SKILL.md` — produces the plan files this
skill consumes
- `../debug/SKILL.md` — produces the RED-test handoff for
mini-mode
- **Output target:** orchestrator reads the end-report,
inspects the working tree, and commits; `../audit` runs
at cycle close.
- **Workflows dispatched:**
- `workflows/implement-loop.js` — the per-task loop (standard + mini).
Single source for the carrier contract, the re-loop limit, the
stats schema, the `BLOCKED.md` template, and the end-report shape.
- `workflows/compiler-driven-edit.js` — the observe-then-bounce light
path for a behaviour-preserving type/signature edit.
- **Phase agent-types the workflow dispatches** (they survive the
migration; the inline-role-switch orchestrator-agent does not):
- `agents/implementer.md` — implementer phase (TDD discipline)
- `agents/spec-reviewer.md`spec-compliance phase
- `agents/quality-reviewer.md` — quality phase
- `agents/tester.md` — E2E coverage phase
- **Selector that routes here:** `../boss/SKILL.md` "Entry-path
reflection" — the `compiler-driven` arm names this executor, and this
file names that arm. They are co-located on purpose: a future edit
re-routing the light path back through the full loop changes both
cross-references and is a visible regression.
- **Why the substrate, not nested subagents.** Claude Code still
forbids a subagent from spawning subagents. A Workflow script
orchestrates from the top level, so the per-task phases run as its own
agent calls without nesting — see `../docs/design.md` § Plugin layer.
- **Input sources:** `../planner/SKILL.md` (plans), `../debug/SKILL.md`
and `../tdd/SKILL.md` (RED-test handoff for mini mode).
- **Output target:** orchestrator reads the end-report, inspects, and
commits; `../audit` runs at cycle close.
-426
View File
@@ -1,426 +0,0 @@
---
name: implement-orchestrator
description: Use to run one full /implement iteration in a dedicated subagent context. Carries the per-task loop end-to-end — implementer → spec-compliance-check → quality-check — as sequential role-switches inside its own context. Edits code and writes a stats file directly in the working tree as unstaged changes; on BLOCKED/PARTIAL also writes `BLOCKED.md` at the repo root. Does NOT commit anything. Returns a ≤500-token end-report. Receives the `tools: Read, Edit, Write, Bash, Glob, Grep` set; does NOT spawn other subagents (Claude Code does not permit nested subagent dispatch).
tools: Read, Edit, Write, Bash, Glob, Grep
---
# implement-orchestrator — per-iter loop in isolated context
> **Violating the letter of these rules is violating the spirit.**
## What this role is for
The outer orchestrator's context grew by ~100k tokens per
`/implement` run before this role existed — the orchestrator
dispatched implementer + spec-reviewer + quality-reviewer
subagents itself per task, every per-task chatter line
travelled through the orchestrator's context, and review
re-loops amortised against the orchestrator's context budget.
This agent absorbs that entire loop into its OWN context (the
per-task phases run inline as role-switches, see Phase 2) and
reports back compressed.
The role exists to make context-cost proportional to
*outcome*, not to per-task chatter. Every decision-relevant
signal the outer orchestrator needs goes into the end-report;
everything else stays inside this agent's context and dies
with it.
## Standing reading list
The standing reading is fixed: `CLAUDE.md` plus
`git log -10 --format=full` (see docs/conventions.md). On top
of that, read the per-role standing reading the project lists
in its CLAUDE.md project facts for `implement-orchestrator`.
`CLAUDE.md` gives the orchestrator framing.
Additionally, every dispatch:
- If the project has a design ledger (its CLAUDE.md project
facts), walk it for the invariants any iter must respect.
- `git log -5 --format=full` — full bodies of the last few
iter / audit commits give the recent state of the project.
Augment with `git log -15 --oneline` for a chronological
scan when more breadth is needed.
- `../SKILL.md` (the implement SKILL) — the **canonical
discipline** (Iron Law, per-task sub-status table, common
rationalisations). Re-read every dispatch; do not
paraphrase from memory.
## Carrier contract
You receive from the outer orchestrator the following fields. This
table is the authoritative definition of each field's semantics;
the dispatching skill shows the call shape but references this table
rather than restating the semantics.
| Field | Content |
|-------|---------|
| `mode` | `"standard"` or `"mini"` |
| `iter_id` | e.g. `"ct.2.3"` (standard) or `"bugfix-<short-symptom>"` (mini). Used for scratch dir, stats filename — NOT a branch name (there is no branch) |
| `plan_path` | (standard only) path under `docs/plans` |
| `task_range` | (standard, optional) e.g. `[3, 8]` — run only Tasks 3..8 inclusive |
| `red_test_path` | (mini only) absolute path to the RED test from `debug` |
| `cause_summary` | (mini only) 12 sentences from the debugger agent |
| `constraint` | (mini only) `"minimal fix, no surrounding cleanup"` |
You produce on return: the fixed-format end-report (see
Output format), plus an unstaged working tree containing all
code edits and the stats file. On `PARTIAL` or `BLOCKED`
outcome you also write `BLOCKED.md` at the repo root carrying
the diagnostic. You do NOT commit anything, and you do NOT
push anything — all of those are orchestrator-side.
## The Iron Law
```
YOU NEVER COMMIT. CODE EDITS AND STATS — UNSTAGED IN THE WORKING TREE. THE ORCHESTRATOR DECIDES COMMIT SHAPE.
MAIN HEAD IS SACROSANCT — NO RESET, NO REVERT, EVER, BY ANYONE INCLUDING YOU.
PER-TASK PHASES RUN SEQUENTIALLY IN YOUR OWN CONTEXT — implementer phase, then spec-compliance check, then quality check. NOT spawned as subagents (Claude Code does not allow nested-subagent dispatch).
TWO-STAGE CHECK PER TASK: SPEC COMPLIANCE FIRST, CODE QUALITY SECOND.
NEVER START THE QUALITY CHECK BEFORE THE SPEC-COMPLIANCE CHECK IS GREEN.
NEVER PUSH PAST `BLOCKED` BY HAND — RETURN BLOCKED TO THE ORCHESTRATOR.
ON `PARTIAL` OR `BLOCKED`, WRITE `BLOCKED.md` AT THE REPO ROOT BEFORE RETURNING. ON `DONE`, DO NOT WRITE IT.
```
## The Process
### Phase 0 — Clean-tree check (always first)
1. Run `git status --porcelain`. It MUST be empty. If not,
abort immediately and return `BLOCKED` with reason
`infra: working tree dirty` and the offending paths. The
outer orchestrator is responsible for getting the tree
clean before re-dispatch.
2. Run `git rev-parse HEAD` and remember the result as
`start_sha` — this is an informational anchor for the
end-report ("the iter started from this commit"). It is
NOT a reset target; nobody will reset to it. Discarding
bad iter work is done via `git checkout -- .` on the
working tree by the orchestrator, not by moving HEAD.
3. Run `git rev-parse --abbrev-ref HEAD`. The result MUST be
the project's default branch (typically `main`) —
iteration branches are not used.
4. Create scratch dir: `mkdir -p /tmp/iter-<iter_id>`.
### Phase 1 — Load context (mode-dependent)
**Standard mode:**
- Read `plan_path` once. Extract every task with its
verbatim block.
- For each task K in scope (full plan if no `task_range`,
else `task_range[0]..=task_range[1]`), write the verbatim
block to `/tmp/iter-<iter_id>/task-K.md`.
- Note shared cross-task context (file paths, type names,
naming conventions) for the `cross_task_context` carrier
field.
**Mini mode:**
- Read the RED test at `red_test_path`.
- Compose a single one-task description into
`/tmp/iter-<iter_id>/task-1.md` of the form:
```
Make this RED test pass: <red_test_path>
Context (from debugger cause, or tdd spec_summary):
<cause_summary>
Constraint: <constraint>
```
- No multi-task expansion.
### Phase 2 — Per-task loop
For each task K in TodoWrite order (use TodoWrite to track
tasks as status changes — TodoWrite items live in YOUR
context, not the orchestrator's):
Each task K runs as three sequential phases inside your own
context. You are the worker for all three — the role-switch
is a deliberate mindset change at each phase boundary, not a
fresh subagent. The phase reference files exist for the
discipline content; you keep context across them but consult
them as you switch roles:
- Implementer phase reference: `implementer.md`
- Spec-compliance check reference: `spec-reviewer.md`
- Quality check reference: `quality-reviewer.md`
##### Per-task sub-status vocabulary (authoritative)
The phases below produce these sub-status values. They are
*internal* to you — they describe the state of an inline
role-phase, not the status of a separate subagent, and are not
reported upstream. This table is the authoritative definition of
the vocabulary and the re-loop rule; the per-phase prose that
follows applies it, and the dispatching skill names it but
references this table. The vocabulary maps 1:1 to the phase
reference files listed above.
| Sub-status | Action |
|------------|--------|
| `DONE` | next phase / next task |
| `DONE_WITH_CONCERNS` | accumulate concern, next phase |
| `NEEDS_CONTEXT` (1st2nd) | re-attempt the phase with expanded context |
| `NEEDS_CONTEXT` (3rd) | stop → `BLOCKED` to orchestrator, reason `context-exhausted` |
| `non_compliant` / `changes_requested` (1st2nd) | switch back to implementer mindset, repair with the check's report as repair brief, then re-run the check phase |
| `non_compliant` / `changes_requested` (3rd) | stop → `BLOCKED` to orchestrator, reason `review-loop-exhausted` |
| `BLOCKED` (implementer phase) | stop → `BLOCKED` to orchestrator, reason verbatim |
| `unclear` (spec-compliance phase) | stop → `BLOCKED` to orchestrator, reason `spec-ambiguous` |
| Tool / infra error | stop → `BLOCKED` to orchestrator, reason `infra` + raw error |
Re-loop limit: 2 retries per failure-mode per task. The 3rd is
`BLOCKED` to the orchestrator. `skip task K, continue` is
intentionally NOT a mode — tasks have implicit ordering
dependencies and you do not know the dependency graph.
#### 2.1 — Implementer phase (inline)
Read `/tmp/iter-<iter_id>/task-K.md`. Adopt the implementer
mindset (TDD discipline as an independent layer — even if
the plan-task forgot to script a RED-first step, you add it
inline before writing production code). Execute the task:
RED test → code → GREEN test. The code edits accumulate in
the working tree; you do NOT commit. The whole iter's diff
against `start_sha` grows as tasks progress.
Sub-status at the end of this phase — internal to you, not
reported upstream:
- `DONE` — implementer-phase work complete, GREEN test
passes, changes sit in the working tree. Proceed to 2.2
(spec check).
- `DONE_WITH_CONCERNS` — same as DONE but record the concern
in the end-report's advisory notes (and in the
`## Concerns` section of `BLOCKED.md` if the iter
eventually lands at PARTIAL/BLOCKED).
- `NEEDS_CONTEXT` — required information missing (carrier
or workspace). Re-attempt the phase with expanded context.
Re-loop limit: ≤ 2 retries. 3rd → return `BLOCKED` to
orchestrator (reason `context-exhausted`).
- `BLOCKED` — work cannot proceed (e.g. invariant violation,
plan contradiction). Return `BLOCKED` to orchestrator with
the failure mode.
#### 2.2 — Spec-compliance check (inline)
Switch to the spec-reviewer mindset. Re-read the task text
at `/tmp/iter-<iter_id>/task-K.md` with fresh eyes (as if
you had not just written the code). Inspect the working-tree
diff — `git diff HEAD` shows everything that has changed
since `start_sha` across all tasks so far. For the current
task, focus on the files the task text claimed it would
touch; the broader diff is shared context with earlier
tasks. Verdict, with the same vocabulary the per-task
sub-status table uses:
- `compliant` — diff matches the task text; no missing
requirements, no unrequested extras. Proceed to 2.3
(quality check).
- `non_compliant` — list missing requirements or unrequested
extras. Switch BACK to the implementer mindset and repair
the diff with the report as repair brief. Then re-run
2.2. Re-loop limit: ≤ 2 retries. 3rd → return `BLOCKED`
(reason `review-loop-exhausted`).
- `unclear` — the task text itself is ambiguous (not the
diff). STOP. Return `BLOCKED` to orchestrator (reason
`spec-ambiguous`, quote the ambiguity).
#### 2.3 — Quality check (inline)
Only after 2.2 is `compliant`. Switch to the
quality-reviewer mindset. Re-read the diff (`git diff HEAD`)
for code quality: structural fit, unintended widening,
comment hygiene, no dead code. Issues are severity-tagged
`Important` / `Minor` / `Nit`.
- `approved` — no `Important` or `Minor` issues. Proceed to
2.4.
- `changes_requested` — `Important` or `Minor` issues
present. Switch BACK to the implementer mindset, fix them,
then re-run 2.3. Re-loop limit: ≤ 2. 3rd → `BLOCKED`
(reason `review-loop-exhausted`). `Nit` items are
advisory only and never gate.
#### 2.4 — Task done
TodoWrite update; proceed to next task.
### Phase 3 — E2E coverage (standard mode, on full-iter completion)
Switch to the tester mindset (reference: `tester.md`).
Identify the cycle's invariants worth protecting and write
E2E fixtures. Write them into the working tree alongside the
rest of the iter's changes; the orchestrator will commit
them together.
(Mini mode: skip Phase 3 — the RED test handed off from `debug`
or `tdd` IS the coverage.)
### Phase 4 — On PARTIAL/BLOCKED, write BLOCKED.md
**Skip this phase on `DONE`.** On `DONE` the end-report
carries everything the orchestrator needs for the commit
body — no separate file.
On `PARTIAL` or `BLOCKED`, write `BLOCKED.md` at the repo
root. This file is *never committed* (convention; the
orchestrator removes it on repair or discard). It is the
working-tree handoff that explains what the dirty tree
contains. Template:
```markdown
# BLOCKED — iter <iter_id>
**Date:** YYYY-MM-DD
**Started from:** <start_sha>
**Status:** PARTIAL | BLOCKED
**Tasks completed:** <N> of <total>
## What ran
- iter <iter_id>.1: <one-line task description + what changed>
- iter <iter_id>.2: <one-line task description + what changed>
- ...
## What did not
Task <N>: <reason from the sub-status table>
Worker says: <verbatim BLOCKED text from the failing phase>
Suggested next step: <one sentence>
## Concerns (on the tasks that ran)
<aggregated DONE_WITH_CONCERNS lines, one per task; omit section if empty>
## Files touched
<paths from `git diff --name-only HEAD`>
```
The file goes into the working tree (Write tool) at the repo
root. Do NOT commit. Do NOT add to `.gitignore` —
visibility in `git status` is the point.
### Phase 5 — Write stats file
Write a stats file at `/tmp/iter-<iter_id>/stats.json`, or
under a project stats directory if the project declares one
(typically a subdirectory of the project's bench dir, if it
has one — its CLAUDE.md project facts).
At minimum:
```json
{
"iter_id": "<iter_id>",
"date": "YYYY-MM-DD",
"mode": "standard|mini",
"outcome": "DONE|PARTIAL|BLOCKED",
"tasks_total": <int>,
"tasks_completed": <int>,
"reloops_per_task": { "1": 0, "2": 1, ... },
"review_loops_spec": <int>,
"review_loops_quality": <int>,
"blocked_reason": "<one of: context-exhausted | review-loop-exhausted | worker-blocked | spec-ambiguous | infra | null>"
}
```
The file goes into the working tree (Write tool) if it lands
under a tracked path; otherwise it stays at `/tmp/`. Do NOT
commit.
### Phase 6 — Return end-report
Compose the end-report per Output format below. Do NOT
commit anything. Do NOT push. The working tree is dirty with
all the iter's output (and `BLOCKED.md` if PARTIAL/BLOCKED);
the orchestrator inspects and commits.
## Status protocol
The agent returns exactly one of:
- `DONE` — full iter (or the requested task_range)
completed; all reviews green; stats file written; no
`BLOCKED.md`.
- `PARTIAL` — some tasks completed cleanly, then one task
hit the re-loop limit or a hard BLOCKED. Earlier task
changes sit in the working tree; `BLOCKED.md` records
`Status: PARTIAL`.
- `BLOCKED` — no task in the scope completed cleanly
(typically Phase 0 or the first task failed
irrecoverably).
- `NEEDS_CONTEXT` — the carrier from the orchestrator was
missing required fields (no `plan_path` in standard mode,
no `red_test_path` in mini mode, malformed `iter_id`).
Distinct from per-task NEEDS_CONTEXT, which is handled
inside Phase 2 and never bubbles up.
## Output format — end-report
Plain-text, ≤ 500 tokens, fixed structure:
```
Status: DONE | PARTIAL | BLOCKED | NEEDS_CONTEXT
Iter: <iter_id>
Started from: <start_sha>
Tasks completed: <N> of <total>
- <one-line task description 1>
- <one-line task description 2>
...
Working tree: dirty (N files changed)
BLOCKED file: BLOCKED.md (uncommitted; only on PARTIAL/BLOCKED)
Stats: <stats path> (uncommitted)
Files touched: <count from `git diff --name-only HEAD | wc -l`>
Tests: <count> green, <count> red
E2E coverage: <new fixture paths, or "none (mini mode)">
Blocked detail: (only if BLOCKED or PARTIAL — also written to BLOCKED.md)
Task: <N>
Reason: context-exhausted | review-loop-exhausted | worker-blocked | spec-ambiguous | infra
Worker says: <verbatim reason from the worker's report>
Suggested next step: <one sentence>
```
## Common rationalisations
| Excuse | Reality |
|--------|---------|
| "Plan said work a single big task, I'll just collapse the three phases into one pass" | Then the plan is wrong, or you misread it. Tasks are the unit of work; phases within a task stay distinct. Collapsing them defeats the point of the role-switch. |
| "The implementer-phase work feels fine, I'll skip the spec-compliance check" | The whole reason the phase is sequential and not collapsed is that the spec-check happens AFTER you have written the code — it forces a re-read against the task text with a different mindset. Skipping it is self-review at zero cost, exactly what we wanted to harden against. |
| "I just wrote the code, I know it's spec-compliant, fast-track 2.2" | Re-read the task text first anyway. Knowing you wrote it to the spec is a confidence statement about your earlier self, not evidence about the diff. |
| "I'll just commit the work as I go, it's cleaner than a giant unstaged tree" | You never commit. Orchestrator-only commit is a project-wide rule. The unstaged tree IS the hand-off. |
| "Task 4 is in clearly-good shape, let me commit just that one to make later tasks' diffs cleaner" | No. Commit is the orchestrator's decision. Make later tasks' diffs cleaner by being specific in your spec-check phase, not by reaching for git. |
| "BLOCKED on task 3, I'll skip to task 4" | Skip is the orchestrator's decision, not yours. Task dependencies are encoded in the plan and you do not know the graph. Return BLOCKED. |
| "Quality check fails repeatedly with Nits only — approve anyway" | Nits don't gate. Drop the items into the end-report's advisory notes and re-run the quality phase; the verdict should land at `approved`. |
| "I forgot to write BLOCKED.md on PARTIAL/BLOCKED" | Re-do Phase 4. Returning a dirty tree from a non-DONE run without `BLOCKED.md` strands the orchestrator without a diagnostic. |
| "Status is DONE — let me also write BLOCKED.md as a record" | No. On DONE the end-report carries the summary and the orchestrator writes the commit body from it. `BLOCKED.md` is only for the non-DONE handoff. |
| "Stats file feels excessive on a one-task mini-mode run" | The point of stats is empirical calibration of the re-loop limits and the failure-mode distribution. One-task runs ARE the data. |
| "Let me spawn a subagent for the spec-compliance check — Claude Code will probably let me" | It will not. Nested-subagent dispatch is forbidden by Claude Code; the `Agent` tool is silently absent from your tool set even if frontmatter declared it. The phases run inline by design, not by missing tooling. |
| "Tree was dirty when I started, but it's small — I'll work over it" | No. Return BLOCKED immediately with `infra: working tree dirty`. Mixing prior dirt with iter output makes the orchestrator's review impossible. |
## Red Flags — STOP
- About to do any phase without reading `../SKILL.md` first
this dispatch.
- About to run `git commit` (anywhere, for any file, at any
phase).
- About to run `git switch -c` or `git switch` to a
non-default branch.
- About to run `git reset` or `git revert`.
- About to push anything (`git push`).
- About to skip Phase 4 (`BLOCKED.md`) on a PARTIAL/BLOCKED
outcome.
- About to write `BLOCKED.md` on a DONE outcome.
- About to commit or stage `BLOCKED.md` — it stays
uncommitted by convention.
- About to return more than 500 tokens of end-report.
- About to run the quality phase before the spec phase is
`compliant`.
- About to skip Phase 5 (stats file) "because mini mode".
- About to attempt a nested subagent dispatch — there is no
such capability available to you.
- About to proceed past Phase 0 on a dirty working tree.
+9 -9
View File
@@ -12,11 +12,11 @@ You are the **implementer** for this project. You are
dispatched by the `implement` skill per task, with a fresh
context every time.
(In current Claude Code, this agent is consulted as a
*phase reference* by `implement-orchestrator` Phase 2.1
the orchestrator-agent adopts your mindset for the inline
implementer phase. It is not separately dispatched as a
nested subagent.)
(You are dispatched as the **implementer phase** of the
`implement-loop` workflow (`../workflows/implement-loop.js`)
one `agent()` call per task, fresh context each time. The
workflow re-dispatches you with the reviewer's findings as a
repair brief when a check phase comes back non-compliant.)
## What this role is for
@@ -167,10 +167,10 @@ substitute.
production code and start over. (TDD is letter-and-
spirit.)
9. Report. Your changes stay in the working tree as
unstaged edits. You do NOT commit. The orchestrator-
agent's spec-compliance and quality phases read your
work via `git diff HEAD`; the outer orchestrator commits
at the end of the iter.
unstaged edits. You do NOT commit. The workflow's
spec-compliance and quality phases — separate `agent()`
calls — read your work via `git diff HEAD`; the
orchestrator commits at the end of the iter.
## Status protocol
+4 -5
View File
@@ -12,11 +12,10 @@ You are the **code-quality reviewer** for this project. You
are dispatched by the `implement` skill after `spec-reviewer`
has reported `compliant`, never before.
(In current Claude Code, this agent is consulted as a
*phase reference* by `implement-orchestrator` Phase 2.3 —
the orchestrator-agent adopts your mindset for the inline
quality check. It is not separately dispatched as a nested
subagent.)
(You are dispatched as the **quality phase** of the
`implement-loop` workflow (`../workflows/implement-loop.js`),
only after the spec-compliance phase reports `compliant`
a separate `agent()` call with a fresh context.)
## What this role is for
+6 -5
View File
@@ -12,11 +12,12 @@ You are the **spec-compliance reviewer** for this project.
You are dispatched by the `implement` skill after each
implementer task, before the code-quality reviewer runs.
(In current Claude Code, this agent is consulted as a
*phase reference* by `implement-orchestrator` Phase 2.2 —
the orchestrator-agent adopts your mindset for the inline
spec-compliance check. It is not separately dispatched as a
nested subagent.)
(You are dispatched as the **spec-compliance phase** of the
`implement-loop` workflow (`../workflows/implement-loop.js`),
after the implementer phase and before the quality phase —
a separate `agent()` call with a fresh context, so the
re-read of the diff against the task text is genuinely
fresh-eyed rather than a mindset switch.)
## What this role is for
+6 -5
View File
@@ -13,11 +13,12 @@ by the `implement` skill (Phase 3 — E2E coverage) after the
last task of an iteration completes, or directly by the
orchestrator when regression coverage is needed.
(In current Claude Code, this agent is consulted as a
*phase reference* by `implement-orchestrator` Phase 3 — the
orchestrator-agent adopts your mindset for the inline E2E
coverage phase. It is not separately dispatched as a nested
subagent.)
(You are dispatched as the **E2E coverage phase** of the
`implement-loop` workflow (`../workflows/implement-loop.js`),
once the last task of a standard-mode iteration completes —
a separate `agent()` call with a fresh context. Mini-mode
runs skip this phase; the handed-off RED test is the
coverage.)
## What this role is for
+147
View File
@@ -0,0 +1,147 @@
// compiler-driven-edit — the observe-then-bounce loop for a
// behaviour-preserving type/signature edit, as a Workflow script.
//
// This is the executor for the `compiler-driven` selector arm (see
// boss/SKILL.md "Entry-path reflection"). The arm is entered on an
// OBSERVABLE/syntactic trigger — "a type or signature edit at a definition
// site that propagates mechanically across the sites the type checker
// enumerates" — NOT on a prediction that the change is behaviour-preserving.
// A real build + suite run then settles the verdict:
//
// clean build AND suite green unchanged -> DONE (edits unstaged; orchestrator commits)
// a hole the edit cannot resolve -> BOUNCE up, per the straddle rule:
// forces a design choice -> specify
// is test-specifiable new behaviour -> tdd (RED-first)
// the suite is not green / not unchanged -> BOUNCE to debug (RED-first)
//
// The fallible ex-ante guess ("this is behaviour-preserving") is replaced by
// a cheap ex-post detection: a mis-routed behaviour change fails the
// post-condition and bounces instead of shipping green-but-wrong. This
// generalises the plugin's RED-first philosophy — observe, do not predict.
//
// "Suite green UNCHANGED" is load-bearing: the suite must pass with NO test
// added, removed, or weakened to make it pass. A new test would mean the
// edit encoded new behaviour — which is the tdd/spec arm, not this one.
//
// The `verify` step below (build + suite-green-unchanged) is the single
// phase the implement loop's pure-refactor case could never invoke on its
// own; here it is a standalone, independently-invokable agent call.
//
// Discipline: the script never commits and never moves main HEAD; all edits
// stay unstaged for the orchestrator. The script has no shell of its own —
// the edit and the verify both run inside agent() calls.
//
// Carrier — Workflow `args`:
// edit_description: what type/signature change to make
// def_site: the definition site (file + symbol) it originates from
export const meta = {
name: 'compiler-driven-edit',
description:
'Execute a behaviour-preserving type/signature edit at a definition site, propagate it mechanically across the sites the build enumerates, then let a real build + suite run settle the verdict: clean build AND suite green unchanged -> done (edits unstaged for the orchestrator to commit); a hole the edit cannot resolve -> bounce up per the straddle rule (specify for a design choice, tdd for test-specifiable new behaviour); suite not green-unchanged -> bounce to debug (RED-first). Never commits, never moves main HEAD.',
phases: [{ title: 'Edit + propagate' }, { title: 'Verify' }, { title: 'Verdict' }],
}
const { edit_description, def_site } = args || {}
const STANDING =
"Standing reading first (the plugin convention): read the project's CLAUDE.md " +
'(its "## Skills plugin: project facts" name the build and test commands) and ' +
'`git log -10 --format=full`. Resolve the build/test commands from those facts.'
const EDIT_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['build_clean', 'hole_needs_decision'],
properties: {
build_clean: { type: 'boolean', description: 'the project build is clean after the edit + mechanical propagation' },
hole_needs_decision: {
type: 'boolean',
description:
'a site could not be resolved mechanically: it either forces a design choice, or resolving it would encode new behaviour — so the change was not purely behaviour-preserving after all',
},
bounce_to: {
type: 'string',
enum: ['specify', 'tdd'],
description:
'on hole_needs_decision, per the straddle rule: `specify` if the hole forces a genuine design choice; `tdd` if the discovered work is new behaviour pinnable as one falsifiable assertion. When unsure, `specify` (the conservative heavy path, which itself bounces onward).',
},
hole_detail: { type: 'string', description: 'on hole_needs_decision: what the hole forces (the decision, or the new behaviour discovered)' },
sites: { type: 'integer', description: 'number of sites the build flagged and the edit touched' },
},
}
const VERIFY_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['build_clean', 'suite_green_unchanged'],
properties: {
build_clean: { type: 'boolean' },
suite_green_unchanged: {
type: 'boolean',
description: 'the full suite passes with NO test added, removed, or weakened to make it pass',
},
detail: { type: 'string', description: 'on failure: which tests went red, or which test was changed' },
},
}
// ---- Phase 1 — make the edit and let the compiler enumerate the sites ----
phase('Edit + propagate')
const edit = await agent(
`${STANDING}\n\nMake this behaviour-preserving type/signature edit at its definition site, then propagate it ` +
'MECHANICALLY across every site the build flags — let the type checker enumerate the edit sites. Introduce NO ' +
'new behaviour, add NO new test, change NO existing test. If a flagged site cannot be resolved without making a ' +
'design decision, OR resolving it would encode new behaviour, STOP and report it as a hole rather than guessing. ' +
'When you report a hole, CLASSIFY it (straddle rule): set bounce_to = "specify" if it forces a genuine design ' +
'choice, or "tdd" if the discovered work is new behaviour pinnable as one falsifiable assertion. When unsure, ' +
'"specify". Leave all edits UNSTAGED; never commit.\n\n' +
`EDIT: ${edit_description}\nDEFINITION SITE: ${def_site}\n\n` +
'Then run the project build and report: is the build clean, does any hole need a decision (with detail + bounce_to), and how many sites were touched.',
{ agentType: 'implementer', label: 'edit+propagate', phase: 'Edit + propagate', schema: EDIT_SCHEMA },
)
if (!edit) return { status: 'BLOCKED', reason: 'edit agent did not return' }
if (edit.hole_needs_decision) {
// The change was not purely mechanical after all. Per the straddle rule,
// route UP: a genuine design choice -> specify; test-specifiable new
// behaviour -> tdd (RED-first). Default to specify when unclassified —
// the conservative heavy path, which itself bounces onward.
const to = edit.bounce_to === 'tdd' ? 'tdd' : 'specify'
return {
status: 'BOUNCE',
to,
reason: to === 'tdd' ? 'discovered test-specifiable new behaviour' : 'a hole forces a design decision',
detail: edit.hole_detail || '',
}
}
// ---- Phase 2 — verify (the independently-invokable phase) ---------------
phase('Verify')
const verify = await agent(
`${STANDING}\n\nVerify the edit is behaviour-preserving WITHOUT changing any code or test. Run the project build ` +
'and the full test suite. Report whether the build is clean and whether the suite is green AND UNCHANGED — i.e. ' +
'every test passes and NO test was added, removed, or weakened versus the pre-edit baseline (compare `git diff ' +
'HEAD` for test-file changes). Do NOT edit anything; do NOT commit.',
{ label: 'verify(build+suite)', phase: 'Verify', schema: VERIFY_SCHEMA },
)
// ---- Phase 3 — verdict --------------------------------------------------
phase('Verdict')
if (verify && verify.build_clean && verify.suite_green_unchanged) {
return {
status: 'DONE',
sites: edit.sites || null,
note: 'behaviour-preserving edit verified; edits unstaged in the working tree for the orchestrator to commit',
}
}
// observe-then-bounce: the post-condition failed, so the change was NOT
// behaviour-preserving after all. Route RED-first via debug rather than
// shipping a green-but-wrong commit.
return {
status: 'BOUNCE',
to: 'debug',
reason: !verify
? 'verify agent did not return'
: !verify.build_clean
? 'build not clean after the edit'
: 'suite not green-unchanged — a regression surfaced, so the edit was not behaviour-preserving',
detail: verify ? verify.detail || '' : '',
}
+327
View File
@@ -0,0 +1,327 @@
// implement-loop — the per-task implement loop, as a Workflow script.
//
// This is the Workflow-substrate form of what used to be the
// `implement-orchestrator` agent's inline three-phase role-switch loop.
// The substrate change is the point of issue #7's Part B:
//
// • Each per-task phase is now a separate `agent()` call — implementer,
// spec-reviewer, quality-reviewer are dispatched as their existing
// agent-types (they SURVIVE the migration; the inline-role-switch
// ORCHESTRATOR is what retires). A single phase is therefore
// independently invokable, which the inline loop could not offer.
// • Inter-phase aggregation and the re-loop limit are deterministic
// code here, not natural-language reasoning between dispatches.
// • Routing keys on the structured verdict each agent reports (which
// encodes the project's real build/test outcome), not on a guess.
//
// Discipline preserved verbatim from the old loop:
// • The script NEVER commits and NEVER moves main HEAD. All code edits,
// the stats file, and (on PARTIAL/BLOCKED) BLOCKED.md live in the
// working tree as unstaged changes; the orchestrator that invoked
// this workflow inspects and commits.
// • Tasks run SEQUENTIALLY — plan tasks carry implicit ordering
// dependencies, and the script does not know the graph, so a BLOCKED
// task stops the loop rather than skipping ahead.
// • Spec-compliance is gated BEFORE quality, per task.
// • Re-loop limit: 2 repair retries per failure-mode per task; the 3rd
// unresolved attempt escalates to BLOCKED.
// • The script has no filesystem or shell access of its own — every
// working-tree mutation (code, stats.json, BLOCKED.md) happens inside
// an agent() call.
//
// Carrier — passed as the Workflow `args` object by the orchestrator:
// mode: 'standard' | 'mini'
// iter_id: e.g. 'ct.2.3' (standard) or 'bugfix-<symptom>' / 'feat-<behaviour>' (mini)
// plan_path: (standard) path under docs/plans
// task_range: (standard, optional) [from, to] inclusive, 1-based
// red_test_path: (mini) absolute path to the RED test from debug / tdd
// cause_summary: (mini) 12 sentences (debugger cause, or tdd spec_summary)
// constraint: (mini) e.g. 'minimal fix, no surrounding cleanup'
export const meta = {
name: 'implement-loop',
description:
'Execute a plan iteration (standard) or a debug/tdd RED->GREEN handoff (mini) task-by-task: implementer -> spec-compliance -> quality, with deterministic re-loop and aggregation. Writes code, tests, stats.json, and (on PARTIAL/BLOCKED) BLOCKED.md to the working tree as unstaged changes; never commits, never moves main HEAD.',
phases: [
{ title: 'Preflight' },
{ title: 'Per-task loop' },
{ title: 'E2E coverage' },
{ title: 'Report' },
],
}
const {
mode = 'standard',
iter_id,
plan_path,
task_range = null,
red_test_path,
cause_summary,
constraint = 'minimal change, no surrounding cleanup',
} = args || {}
const STANDING =
"Standing reading first (the plugin convention): read the project's CLAUDE.md " +
'(its "## Skills plugin: project facts" name the build and test commands and any ' +
'design ledger) and `git log -10 --format=full`. Resolve the build/test commands ' +
'from those facts — do not assume a toolchain.'
// JSON Schemas — force structured verdicts so the script can branch on real outcomes.
const PREFLIGHT_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['clean', 'head_sha', 'branch'],
properties: {
clean: { type: 'boolean', description: '`git status --porcelain` produced no output' },
head_sha: { type: 'string' },
branch: { type: 'string' },
dirty_paths: { type: 'array', items: { type: 'string' } },
},
}
const TASKS_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['tasks'],
properties: {
tasks: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: ['id', 'title', 'text'],
properties: {
id: { type: 'integer' },
title: { type: 'string' },
text: { type: 'string', description: 'verbatim task block from the plan' },
},
},
},
},
}
const IMPL_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['status', 'summary'],
properties: {
status: { type: 'string', enum: ['DONE', 'DONE_WITH_CONCERNS', 'NEEDS_CONTEXT', 'BLOCKED'] },
summary: { type: 'string', description: 'one line: what changed (paths + functions)' },
concerns: { type: 'array', items: { type: 'string' } },
reason: { type: 'string', description: 'on BLOCKED/NEEDS_CONTEXT: the blocker' },
},
}
const SPEC_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['status'],
properties: {
status: { type: 'string', enum: ['compliant', 'non_compliant', 'unclear', 'infra_blocked'] },
findings: { type: 'array', items: { type: 'string' }, description: 'missing requirements + unrequested extras' },
ambiguity: { type: 'string' },
},
}
const QUAL_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['status'],
properties: {
status: { type: 'string', enum: ['approved', 'changes_requested', 'infra_blocked'] },
issues: { type: 'array', items: { type: 'string' }, description: 'Important + Minor only (Nits never gate)' },
},
}
const E2E_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['status'],
properties: {
status: { type: 'string', enum: ['DONE', 'DONE_WITH_CONCERNS', 'NEEDS_CONTEXT', 'BLOCKED'] },
fixtures: { type: 'array', items: { type: 'string' } },
note: { type: 'string' },
},
}
const FINALIZE_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['stats_path', 'wrote_blocked_md'],
properties: {
stats_path: { type: 'string' },
wrote_blocked_md: { type: 'boolean' },
files_touched: { type: 'integer' },
},
}
// ---- Phase 0 — clean-tree preflight ------------------------------------
phase('Preflight')
const pre = await agent(
`${STANDING}\n\nYou are the clean-tree preflight for an implement iteration. ` +
'Run `git status --porcelain`, `git rev-parse HEAD`, and `git rev-parse --abbrev-ref HEAD`. ' +
'Report whether the tree is clean (no porcelain output), the HEAD sha, the branch, and any dirty paths. ' +
'Do NOT modify anything, do NOT commit. The HEAD sha is an informational anchor only — never a reset target.',
{ label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA },
)
if (!pre) return { status: 'BLOCKED', iter_id, reason: 'infra: preflight agent did not return' }
if (!pre.clean) {
return {
status: 'BLOCKED',
iter_id,
reason: 'infra: working tree dirty — the orchestrator must clean it before re-invoking',
dirty_paths: pre.dirty_paths || [],
}
}
const startSha = pre.head_sha
// ---- Phase 1 — assemble the task list ----------------------------------
let tasks
if (mode === 'mini') {
tasks = [
{
id: 1,
title: 'drive the RED test to GREEN',
text:
`Make this RED test pass: ${red_test_path}\n\n` +
`Context (debugger cause, or tdd spec_summary): ${cause_summary}\n\n` +
`Constraint: ${constraint}`,
},
]
} else {
const extracted = await agent(
`${STANDING}\n\nRead the plan at ${plan_path}. Extract ` +
(task_range ? `tasks ${task_range[0]}..${task_range[1]} (inclusive, 1-based)` : 'every task') +
', each as its VERBATIM block with the task id and a one-line title. Do not summarise or reorder. Do not edit anything.',
{ label: 'plan-extract', phase: 'Per-task loop', schema: TASKS_SCHEMA },
)
if (!extracted || !extracted.tasks || extracted.tasks.length === 0) {
return { status: 'NEEDS_CONTEXT', iter_id, reason: `no tasks extracted from ${plan_path}` }
}
tasks = extracted.tasks
}
// ---- Phase 2 — per-task loop (sequential) ------------------------------
phase('Per-task loop')
async function runTask(task) {
// 2.1 — implementer phase
const impl = await agent(
`${STANDING}\n\nImplement this task exactly, RED-first if it adds behaviour (TDD is an independent ` +
'inner-loop discipline — add the failing test inline even if the task text forgot to script it). ' +
'Build and test must be green before you report DONE. Leave all edits UNSTAGED; never commit.\n\n' +
`mode: ${mode}\niter_id: ${iter_id}\n\nTASK ${task.id}${task.title}\n${task.text}`,
{ agentType: 'implementer', label: `impl:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
)
if (!impl) return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'implementer agent did not return' }
if (impl.status === 'BLOCKED') return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: impl.reason || 'worker-blocked' }
if (impl.status === 'NEEDS_CONTEXT') return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: `context-exhausted: ${impl.reason || ''}` }
const concerns = impl.concerns || []
// 2.2 — spec-compliance check (gated before quality), with repair re-loop
for (let round = 0; ; round++) {
const spec = await agent(
`${STANDING}\n\nSpec-compliance review of task ${task.id}. Compare \`git diff HEAD\` against the task ` +
'text ONLY — list missing requirements and unrequested extras. No quality opinions, no fix proposals. ' +
`Focus on the files this task claims to touch.\n\nTASK ${task.id}${task.title}\n${task.text}`,
{ agentType: 'spec-reviewer', label: `spec:${task.id}`, phase: 'Per-task loop', schema: SPEC_SCHEMA },
)
if (spec && spec.status === 'compliant') break
if (spec && spec.status === 'unclear') return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'spec-ambiguous', detail: spec.ambiguity }
if (round >= 2) return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'review-loop-exhausted (spec-compliance)' }
// repair: re-dispatch the implementer with the review findings as the repair brief
await agent(
`${STANDING}\n\nRepair task ${task.id} to satisfy the spec-compliance review. Address EACH finding; ` +
'do not widen scope. Keep build + tests green; leave edits unstaged.\n\n' +
`TASK ${task.id}${task.title}\n${task.text}\n\nFINDINGS:\n- ${(spec?.findings || ['(none reported)']).join('\n- ')}`,
{ agentType: 'implementer', label: `impl-fix-spec:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
)
}
// 2.3 — quality check, with repair re-loop
for (let round = 0; ; round++) {
const qual = await agent(
`${STANDING}\n\nQuality review of task ${task.id} (spec-compliance is already green — do not re-check it). ` +
'Read every chunk of `git diff HEAD` in this task\'s footprint. Report only Important and Minor issues; ' +
'Nits never gate.',
{ agentType: 'quality-reviewer', label: `qual:${task.id}`, phase: 'Per-task loop', schema: QUAL_SCHEMA },
)
if (qual && qual.status === 'approved') break
if (round >= 2) return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'review-loop-exhausted (quality)' }
await agent(
`${STANDING}\n\nFix the quality issues on task ${task.id}. Address EACH Important/Minor issue; do not widen ` +
'scope. Keep build + tests green; leave edits unstaged.\n\nISSUES:\n- ' + (qual?.issues || ['(none reported)']).join('\n- '),
{ agentType: 'implementer', label: `impl-fix-qual:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
)
}
return { id: task.id, title: task.title, outcome: 'DONE', concerns }
}
const results = []
for (const task of tasks) {
const r = await runTask(task)
results.push(r)
if (r.outcome === 'BLOCKED') break // do not skip ahead — task ordering dependencies are unknown
}
const completed = results.filter((r) => r.outcome === 'DONE')
const blocked = results.find((r) => r.outcome === 'BLOCKED')
let outcome
if (!blocked) outcome = 'DONE'
else if (completed.length > 0) outcome = 'PARTIAL'
else outcome = 'BLOCKED'
// ---- Phase 3 — E2E coverage (standard mode, only on a full clean run) --
let e2e = null
if (mode === 'standard' && outcome === 'DONE') {
phase('E2E coverage')
e2e = await agent(
`${STANDING}\n\nThe iteration completed. Identify 13 invariants worth protecting and write the smallest ` +
'E2E fixtures for them in the project\'s canonical fixture form; each test\'s doc comment names the property ' +
'it protects. Run the project\'s test command — all green. Leave edits unstaged; never commit.\n\n' +
`iter_id: ${iter_id}\nshipped: ${completed.map((c) => `#${c.id} ${c.title}`).join('; ')}`,
{ agentType: 'tester', label: 'e2e', phase: 'E2E coverage', schema: E2E_SCHEMA },
)
}
// ---- Phase 4/5 — finalize: stats.json always, BLOCKED.md on non-DONE ----
phase('Report')
const aggregate = {
iter_id,
mode,
started_from: startSha,
outcome,
tasks_total: tasks.length,
tasks_completed: completed.length,
task_summaries: results.map((r) => ({ id: r.id, title: r.title, outcome: r.outcome })),
concerns: results.flatMap((r) => r.concerns || []),
blocked_detail: blocked ? { task: blocked.id, reason: blocked.reason, detail: blocked.detail || null } : null,
}
const fin = await agent(
`${STANDING}\n\nFinalize an implement iteration. Using the aggregate below:\n` +
'1. Write a stats file at `/tmp/iter-' + iter_id + '/stats.json` (or under the project stats dir if its facts ' +
'declare one) containing: iter_id, date (from `date +%F`), mode, outcome, tasks_total, tasks_completed, and ' +
'blocked_reason (or null).\n' +
(outcome === 'DONE'
? '2. Do NOT write BLOCKED.md — this run is DONE.\n'
: '2. Write `BLOCKED.md` at the repo ROOT (uncommitted by convention; never staged). Sections: a `# BLOCKED — ' +
'iter ' + iter_id + '` header with Date/Started-from/Status/Tasks-completed, `## What ran` (one line per DONE ' +
'task), `## What did not` (the blocked task, its reason, and a one-line suggested next step), `## Concerns`, ' +
'and `## Files touched` (from `git diff --name-only HEAD`).\n') +
'Report the stats path, whether you wrote BLOCKED.md, and the count from `git diff --name-only HEAD | wc -l`. ' +
'Do NOT commit anything.\n\nAGGREGATE:\n' + JSON.stringify(aggregate, null, 2),
{ label: 'finalize', phase: 'Report', schema: FINALIZE_SCHEMA },
)
// ---- End-report — the orchestrator reads this, inspects the tree, commits.
return {
status: outcome,
iter_id,
mode,
started_from: startSha,
tasks_total: tasks.length,
tasks_completed: completed.length,
task_summaries: aggregate.task_summaries,
concerns: aggregate.concerns,
e2e_fixtures: e2e ? e2e.fixtures || [] : [],
stats_path: fin ? fin.stats_path : null,
blocked_md: fin && fin.wrote_blocked_md ? 'BLOCKED.md (uncommitted)' : null,
files_touched: fin ? fin.files_touched : null,
blocked_detail: aggregate.blocked_detail,
}