Both workflow scripts destructured object-form `args` behind `args || {}`
only. A named-workflow invocation can deliver `args` as a string; a
non-empty string is truthy, so every carrier field silently read
`undefined` and the failure surfaced far from the cause: implement-loop
dispatched its plan-index agent against "Read the plan at undefined" and
stopped only via a mislabelled NEEDS_CONTEXT after burning agent calls;
compiler-driven-edit briefed its edit agent with "EDIT: undefined" and
mapped the agent's refusal onto the straddle-rule BOUNCE to specify —
a mechanics failure dressed as a design finding.
Guards now run before any agent dispatch, mirroring the existing
malformed-input pattern (the task_range guard):
- A string carrier is first JSON-parsed: a JSON-serialized object
carrier (a stringifying caller or substrate layer that still authored
the documented object form) is accepted; free text does not parse to
an object and is rejected.
- A carrier that is not an object (string, number, boolean, array) is a
distinct infra BLOCKED naming the received type (compiler-driven-edit:
kind bad-carrier) — never a bounce, never NEEDS_CONTEXT.
- Missing or blank required fields fail fast by name, per mode:
standard iter_id+plan_path, mini iter_id+red_test_path+cause_summary,
compiler-driven-edit edit_description+def_site. implement-loop also
rejects an unknown mode, since the required-field set keys off it.
String-form args are NOT a documented interface — the only invocation
examples in the plugin are object-form (implement/SKILL.md); the issue's
free-text-passthrough fix part was dropped on that ground (triage
verification on the issue). implement/SKILL.md now also documents the
infra early-exit end-report shape (minimal {status, iter_id, reason},
no BLOCKED.md, no blocked_detail) in the Iron Law exceptions, Step 2,
and Step 4's no-file special case.
Verified: both scripts pass node --check (async-wrapped, as the Workflow
substrate runs them) and a 17-case stub-agent harness — malformed
carriers block with zero agent dispatches; valid and JSON-serialized
object carriers reach the first agent unchanged.
closes #24
21 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, 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 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 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 →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, TESTS, AND THE STATS FILE LIVE IN THE WORKING TREE AS UNSTAGED CHANGES UNTIL THE ORCHESTRATOR COMMITS THEM.
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.)
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 SCRIPT WRITES `BLOCKED.md` AT THE REPO ROOT — UNCOMMITTED BY CONVENTION. ON `DONE`, NO SEPARATE FILE. (EXCEPTIONS THAT WRITE NO `BLOCKED.md`: 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. 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.
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
(there is no 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 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. 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 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).
- 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 plus
BLOCKED.md at the repo root carrying the diagnostic. Nothing is
committed. The orchestrator decides what to do with the dirty working
tree:
- No-op
BLOCKEDspecial case — if the end-report's status isBLOCKEDwithblocked_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.