The finalize agent only templated the script's already-computed
aggregate into stats.json / BLOCKED.md — ~41k real tokens per run
(197 dispatches / 8.0M in the 206-run corpus) for pure formatting.
The templates now live in the script (single source): the end-report
carries artifacts.stats_json and artifacts.blocked_md (null on DONE
and clean no-ops), and the orchestrator writes them byte-identical,
filling exactly two runtime slots it owns anyway — {{DATE}} (date +%F)
and {{FILES_TOUCHED}} (git status --porcelain, its Step-3/4 inspection
duty). File paths, consumers (tdd decompose reads BLOCKED.md,
postmortem parses it), and content shape are unchanged; the
agent-authored 'suggested next step' line is dropped — blocked_detail
carries the diagnosis.
refs #29
24 KiB
name, description
| name | description |
|---|---|
| implement | 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 and tests to the working tree without committing. The orchestrator reads the end-report, writes the stats file (and `BLOCKED.md` on PARTIAL/BLOCKED) from the contents the end-report carries, inspects the working tree, decides commit shape, and performs all commits. Also documents the `compiler-driven` light-edit arm. |
implement — plan execution on the Workflow substrate
Violating the letter of these rules is violating the spirit.
Overview
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 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 code edits live in the working tree. The stats file and — on
PARTIAL/BLOCKED — BLOCKED.md are built by the script and carried in
the end-report (artifacts.stats_json / artifacts.blocked_md): the
script has no filesystem access of its own, and dispatching an agent
just to template the script's own aggregate cost ~41k real tokens per
run (issue #29). The orchestrator writes them byte-identical, filling
the two runtime slots it owns anyway — {{DATE}} (date +%F) and
{{FILES_TOUCHED}} (git status --porcelain). The script never
commits and never moves main HEAD. The orchestrator reads one
end-report (the workflow's return value), writes the artifacts,
inspects the unstaged tree, and decides commit shape.
When to Use / Skipping
Triggers:
- A plan exists under
docs/plans(standard mode →implement-loopwithmode: "standard"). - A
debug(RED test + cause) ortdd(RED executable-spec) handoff for the GREEN side (mini mode →implement-loopwithmode: "mini").
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:
- Make the edit; propagate it mechanically across the sites the build flags. Introduce no new behaviour; add or weaken no test.
- Run the project's real build + full suite. This run — not an ex-ante guess that the change is "behaviour-preserving" — is the oracle.
- Done-signal: an edit actually landed AND 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. The
"edit actually landed" conjunct is load-bearing:
clean build AND suite greenis vacuously true on an empty tree, so a no-op edit would otherwise read as DONE. A clean working tree after the edit phase is a no-op, not a success → distinctBLOCKED(no-op-edit), never DONE. Positive evidence (tree differs from HEAD) is required, checked both at the edit phase and independently at verify. (A malformed or missing carrier is likewise a distinctBLOCKED(bad-carrier) before any agent runs — never a bounce.) - 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 LOOP NEVER COMMITS. CODE EDITS AND TESTS LIVE IN THE WORKING TREE AS UNSTAGED CHANGES UNTIL THE ORCHESTRATOR COMMITS THEM; THE STATS FILE AND BLOCKED.md ARE WRITTEN BY THE ORCHESTRATOR FROM THE CONTENTS THE END-REPORT CARRIES.
WITHIN THIS LOOP, MAIN HEAD IS SACROSANCT — NO RESET, NO REVERT; THE LOOP ONLY EVER WRITES UNSTAGED CHANGES. MAIN MOVES FORWARD ONLY VIA ORCHESTRATOR COMMITS. (THE `/boss` ROLLBACK SANDBOX WINDS BACK ALREADY-COMMITTED AUTONOMOUS WORK — A BOSS-LEVEL RECOVERY, NOT A STEP IN THIS LOOP.)
NO IN-LOOP AGENT EVER DISCARDS WORKING-TREE STATE VIA `git checkout` / `git restore` — WHOLE-FILE OR `--patch`: NOTHING COMMITS BETWEEN TASKS, SO A SHARED FILE CARRIES EARLIER TASKS' UNCOMMITTED DONE WORK, AND A CHECKOUT SILENTLY DESTROYS IT (issue #23). RECOVERY IS FORWARD — EDIT BACK, OR REPORT `BLOCKED`. (THE ORCHESTRATOR'S OWN END-OF-ITERATION `git checkout -- .` DISPOSITION HAPPENS OUTSIDE THE LOOP.)
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 END-REPORT CARRIES THE `BLOCKED.md` CONTENT (`artifacts.blocked_md`); THE ORCHESTRATOR'S FIRST ACTION ON READING SUCH A REPORT IS WRITING IT VERBATIM TO `BLOCKED.md` AT THE REPO ROOT — UNCOMMITTED BY CONVENTION. ON `DONE`, `artifacts.blocked_md` IS NULL. (EXCEPTIONS WHERE IT IS ALSO NULL: A NO-OP ITERATION — ITS TREE IS ALREADY CLEAN — AND ANY PRE-LOOP EARLY EXIT (MALFORMED CARRIER, DIRTY-TREE PREFLIGHT, PLAN-EXTRACT GUARD): NOTHING RAN AND NOTHING WAS WRITTEN, SO THE STATUS + `infra:`-PREFIXED REASON RIDE THE END-REPORT AND THERE IS NOTHING TO CLEAN UP OR EXPLAIN.)
`DONE` REQUIRES POSITIVE EVIDENCE THAT AN EDIT LANDED — A NO-OP ITERATION (`git status --porcelain` EMPTY, untracked files INCLUDED) IS NEVER `DONE`, IT IS `BLOCKED` (the issue #11 vacuous-green shape, guarded on the main path). THE VERDICT IS THE GIT TREE COUNT, NEVER A SELF-REPORT.
IN MINI MODE THE RED->GREEN IS OBSERVED, NOT ASSERTED: AN INDEPENDENT VERIFY RE-RUNS THE HANDED-OFF RED TEST AND THE SUITE; A STILL-RED TEST OR A REGRESSION IS `BLOCKED` (route back to debug, RED-first).
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 loop mechanics
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 — except that
a quality round whose findings are all Minor, arriving after at least
one repair dispatch, surfaces them as concerns and closes the task
instead of gating (cosmetic residue must not discard a green task). 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.
Positive-evidence precondition on DONE (issue #12 — the issue #11
vacuous-green shape on the main path). build green AND suite green is
not enough: it is satisfiable by doing nothing. So DONE also requires
positive evidence that an edit landed. The verdict is taken from git
ground truth, never a self-report — a self-report must never be able to
fail a run that actually did the work:
- Iteration gate (standard mode) — a dedicated
tree-checkagent runsgit status --porcelain | wc -lafter the per-task loop but before E2E and finalize. ADONEoutcome with a zero count is a no-op iteration →BLOCKED.git status --porcelain(notgit diff HEAD) is used so a brand-new untracked file — the implementer leaves edits unstaged — still counts; running before E2E/finalize keeps fixtures and the stats/BLOCKED.mdartefacts from inflating it; and a non-returning agent is treated asBLOCKED(infra), never waved through. A clean-tree no-op writes noBLOCKED.md(nothing to clean up — see Step 4). - Mini mode gate — an independent
mini-verifyagent re-runs the handed-off RED test and the suite and checks the tree is dirty (git status --porcelain); a still-red test, a regression, or a clean tree isBLOCKED. The GREEN is observed, not taken from the implementer's self-report. - Per-task
applied_changes(self-report, OR-ed across the initial dispatch and every repair) does NOT gate the outcome — a self-report must never be able to fail a run that actually did the work (e.g. the initial implementer no-ops but a repair writes the files). It feeds only a neutral concern when a task wrote nothing (it may be genuinely already-satisfied — the orchestrator weighs it).
The E2E phase's status is also read now (not just its fixtures): a
non-DONE E2E status or a zero-fixture run surfaces as a concern.
Cross-task discard guard (issue #23). In a multi-task run nothing
commits between tasks, so one task's file-level git checkout/git restore can silently destroy an earlier task's DONE work in a shared
file — invisible to the reviewers (scoped to the current task's
footprint) and to the tree-count gate (the surviving changes keep the
count nonzero). The loop therefore records a snapshot boundary after
every task (git stash create — a dangling commit; HEAD, index, and
working tree untouched) and hard-BLOCKs when a path that was
HEAD-modified at one boundary is HEAD-identical at the next: the
end-report names the lost paths, the boundary, and the recovery
snapshot sha the discarded content can be read from
(git show <sha>:<path>). Coarse by design, in both directions: a
checkout followed by re-edits of the same file, or a --patch-level
restore of a single hunk, escapes the comparison (the path never
leaves the diff) — the snapshot still keeps any loss diagnosable and
recoverable; conversely a task that legitimately edits a shared file
back to byte-identical HEAD content trips the guard, so a tripped
verdict means adjudicate against the snapshot, not proven malice.
Renames do not trip it (the boundary diff runs --no-renames, so a
renamed file's old path stays listed as a deletion). Untracked files
sit outside the threat model (git checkout -- <path> cannot discard
them). Cost: one extra sonnet/medium snapshot call per task of a
multi-task run; single-task and mini runs skip the guard. The
detection details live in workflows/implement-loop.js, the single
source.
The Process — orchestrator side
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:
Workflow({ name: "implement-loop", args: {
mode: "standard",
iter_id: "<iter_id>",
plan_path: "<path under docs/plans>",
task_range: [3, 8] // optional
}})
For a RED-first handoff from debug or tdd (mini mode):
Workflow({ name: "implement-loop", args: {
mode: "mini",
iter_id: "bugfix-<short-symptom>", // tdd: "feat-<short-behaviour>"
red_test_path: "<absolute path>",
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 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
(the loop creates no branch of its own; it runs in whatever checkout
the session sits in — under the worktree convention, the run's
worktree branch).
Step 2 — Read the end-report
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 artifacts block
(stats_json, stats_path_hint, blocked_md), 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. First action, before anything else: materialise the
artifacts — write artifacts.stats_json to the stats path (fill
{{DATE}} with date +%F), and when artifacts.blocked_md is
non-null write it verbatim to BLOCKED.md at the repo root (fill
{{DATE}} and {{FILES_TOUCHED}} from git status --porcelain). The
contents are the script's single-sourced templates — do not rephrase,
restructure, or embellish them. An infra early-exit (malformed or missing carrier, dirty-tree
preflight, a plan-extract guard) returns a minimal report instead:
status: BLOCKED plus an infra:-prefixed reason, no blocked_detail
and none of the per-task fields — nothing ran.
Step 3 — Orchestrator inspect + commit step (on DONE)
The workflow returns with code edits sitting in the working tree as
unstaged changes; the stats file was just written by you from
artifacts.stats_json (Step 2). Nothing is committed yet, and there
is no BLOCKED.md (artifacts.blocked_md is null on DONE).
- Inspect:
git statusandgit 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. - 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.
- 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.
A concern phrased as a held or unresolved quality finding (the
per-task quality gate kept a plan-prescribed element against a
reviewer finding, or a repair was a no-op) is the gate handing you
the judgement it could not make in code: verify that finding by
hand against the diff before committing — do not auto-sign past it,
even under
/boss. Keep it if it is a principled plan-hold; if it is an unaddressed defect, send the iter back rather than commit. - Stage + commit the code edits and the stats file.
- 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 workflow returns with whatever work-in-progress it managed;
BLOCKED.md at the repo root carries the diagnostic — written by you
in Step 2 from artifacts.blocked_md. Nothing is committed. The
orchestrator decides what to do with the dirty working tree:
- No-op
BLOCKEDspecial case — if the end-report's status isBLOCKEDwithartifacts.blocked_mdnull and a no-opblocked_detail(the reason names a no-op iteration or a clean-tree no-op fix;files_touchedis 0 in standard mode, null in mini), there is noBLOCKED.mdand the tree is already clean. The diagnostic is in the end-report'sblocked_detail, not a file. Nothing to clean up or stash; re-plan or re-run from the same clean tree. The same no-file shape occurs for an infra early-exit (reasonprefixedinfra:— malformed/missing carrier, dirty preflight tree, a plan-extract guard): noBLOCKED.md, noblocked_detail, and the tree exactly as the loop found it. Fix the invocation (or clean the tree) and re-invoke. - Read
BLOCKED.md—## What did notnames the failure mode and the blocked task's reason. Readgit diffto see what was attempted. - Decide:
- Repair: keep the working-tree changes in place (or
git stashthem if a clarifying read of clean main is needed first). Adjust plan or extend context; deleteBLOCKED.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, thenrm BLOCKED.md, then re-run. - Discard:
git checkout -- .to drop unstaged file changes;git clean -fd/rm BLOCKED.mdplus anything else new. main HEAD does NOT move. - Escalate: ask the user via the configured notification
command.
BLOCKED.mdsits in the working tree until the conversation resumes.
- Repair: keep the working-tree changes in place (or
Within the implement loop the orchestrator does not git reset or
git revert on main: there is nothing on main to undo — the workflow
never commits, it only writes unstaged working-tree changes. (The one
place autonomous work is ever wound back is the /boss rollback sandbox,
which hard-resets already-committed, unpushed autonomous commits above
the session anchor on a dead-ended run — a boss-level recovery, not a
step in this loop. See ../boss/SKILL.md § Direction freedom.)
Handoff Contract
implement consumes:
| Source | Carrier |
|---|---|
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). 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, 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 line — I'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
implementerdirectly outside the workflow (bypassing the gated loop). - Orchestrator running
git resetorgit reverton main. - The workflow (or any agent it spawns) running
git commit. - Two
implement-loopruns overlapping on the same working tree. BLOCKED.mdstaged or committed by anyone.- 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
- 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, theBLOCKED.mdtemplate, 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 phaseagents/quality-reviewer.md— quality phaseagents/tester.md— E2E coverage phase
- Selector that routes here:
../boss/SKILL.md"Entry-path reflection" — thecompiler-drivenarm 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.mdand../tdd/SKILL.md(RED-test handoff for mini mode). - Output target: orchestrator reads the end-report, inspects, and
commits;
../auditruns at cycle close.