Two interacting defects broke the documented PARTIAL repair flow (fix the plan, commit the known-good subset, rm BLOCKED.md, resume) — observed on run wf_81a13333-be3 (aura): the resume replayed all 78 agents from cache in 33 ms and returned the byte-identical stale end-report. 1. Plan content was invisible to the resume cache key. The substrate caches per (prompt, opts) and every plan-reading prompt carried only the plan PATH, so a plan edit changed no prompt and the extraction replayed stale text. New required standard-mode carrier field plan_sha256 (recomputed by the orchestrator at every invocation) is stamped into all three plan-reading prompts (plan-index, plan-extract-all, plan-extract:<id>): a plan edit re-runs the extraction, and a downstream task replays from cache iff its re-extracted text is byte-identical — worst case a redundant live re-run, never stale text. Alternative considered: documenting "plan edits need a fresh run with task_range" — rejected, it keeps the trap armed and forfeits the cache replay a resume exists for. 2. The cross-task discard guard compared boundary path sets across a moved HEAD. Boundary sets are diffs AGAINST HEAD; after the sanctioned interim subset commit, the first live boundary measures against the new HEAD while the replayed boundary carries the old one — the entire committed subset read as "lost paths" and hard-BLOCKed. The snapshot agent now also reports `git rev-parse HEAD`; a boundary pair straddling a HEAD move resets the guard with a concern (which also flags a rogue mid-run commit) instead of comparing. Same-HEAD discards still hard-BLOCK; a blank sha falls through to the comparison so a flaky report cannot disarm the guard. Verified with a zero-token mock harness (AsyncFunction + stubbed agent()): fail-fast on missing plan_sha256, hash present in all three extraction prompts (batched and fan-out paths), same-HEAD discard still BLOCKs, HEAD-move resets with a concern, mini mode unaffected. Adversarially reviewed (4-verifier workflow): SOUND. Known residuals: the script cannot verify the hash matches the plan bytes (operator obligation, documented in the carrier contract and SKILL.md), and a coincident HEAD-move + discard surfaces as a concern rather than a BLOCK. closes #32
28 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).
IN STANDARD MODE THE END-VERIFY RE-RUNS THE FULL SUITE INDEPENDENTLY: SUITE RED IS `BLOCKED` (route back to debug, RED-first) — GREEN IS OBSERVED, NEVER TAKEN FROM PER-TASK SELF-REPORTS.
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 end-
verifyagent runs after the per-task loop but before E2E, with two independently-gating legs:git status --porcelain | wc -l(aDONEoutcome with a zero count is a no-op iteration →BLOCKED) and a full independent suite run (suite red →BLOCKED, route back todebugRED-first — standard mode previously had no end-of-iteration suite gate while mini and compiler-driven both re-run the suite; the asymmetry is closed).git status --porcelain(notgit diff HEAD) is used so a brand-new untracked file — the implementer leaves edits unstaged — still counts; running before E2E keeps fixtures from inflating it; and a non-returning agent is treated asBLOCKED(infra), never waved through. A clean-tree no-op carries noBLOCKED.mdcontent (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.
Quality-tier selection (issue #30). The quality phase runs
opus/xhigh by default, tiered to sonnet/high for a small diff: ≤25
changed lines AND no contract-referenced path, both measured by the
spec-reviewer (which reads git diff HEAD itself — never the
implementer's self-report) via diff_magnitude / touches_contract.
Unmeasured or contract-touching diffs stay opus. The independent
end-verify suite gate is the deterministic backstop; the chosen tier
is logged per task in the end-report (qual_tier) so the split stays
auditable.
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). Boundaries are HEAD-aware (issue #32): each snapshot records
the HEAD its diff ran against, and a boundary pair straddling a HEAD
move — the sanctioned commit-the-subset-then-resume repair — resets
the guard with a concern instead of comparing, so a replayed
pre-commit boundary never counts the committed subset as lost paths;
in a single live run the same concern flags an agent that moved HEAD
mid-run. 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>",
plan_sha256: "<sha256sum of the plan file>", // recompute at EVERY (re-)invocation (issue #32)
task_range: [3, 8], // optional
repo_root: "<abs path of the working tree>" // strongly recommended; effectively required in worktree sessions (issue #25)
}})
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"
repo_root: "<abs path of the working tree>" // strongly recommended; effectively required in worktree sessions (issue #25)
}})
plan_sha256 content-addresses the plan (issue #32): a resume replays
agents cached per (prompt, opts), and with only the plan path in the
extraction prompts an edit-and-resume would replay the stale pre-edit
task text — the repair silently no-ops. Compute the hash fresh
(sha256sum <plan_path>) at every invocation, including a
resumeFromRunId re-invocation after a plan repair; never reuse the
old value.
repo_root is stamped into every stage prompt as a git -C anchor:
in worktree sessions a dispatched agent's cwd can resolve to the
primary checkout and review the wrong tree (false infra_blocked
quality reviews, a false no-op BLOCKED from the end-verify — issue
#25). Without the field the first dispatched stage probes
git rev-parse --show-toplevel as a fallback, which is subject to the
same race — pass the field.
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. The re-run is either a fresh invocation withtask_rangecovering the remaining tasks, or a resume (resumeFromRunIdon the same script) — for a resume, recomputeplan_sha256from the edited plan so the extraction prompts change: the repaired task runs live, and a completed task replays from cache when its re-extracted text is byte-identical (the usual case; worst case it re-runs redundantly, never stale — issue #32). The discard guard tolerates the interim subset commit: boundaries are HEAD-aware, so the moved baseline resets the guard with a concern instead of a false lost-pathsBLOCKED. - 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.