feat(implement-loop): retire the finalize dispatch; the end-report carries the artifacts

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
This commit is contained in:
2026-07-17 15:29:12 +02:00
parent 8272077e9b
commit 615a76eb01
3 changed files with 119 additions and 73 deletions
+5 -3
View File
@@ -186,9 +186,11 @@ orchestrates from the top level, so the per-task phases are real,
independently-invokable agent calls; this retired the former independently-invokable agent calls; this retired the former
`implement-orchestrator` agent's inline-role-switch workaround `implement-orchestrator` agent's inline-role-switch workaround
(the four phase agents survive as the agent-types the script (the four phase agents survive as the agent-types the script
dispatches). Writes code, tests, and stats files directly in the dispatches). Writes code and tests directly in the working tree as
working tree as unstaged changes; never commits. On `PARTIAL` or unstaged changes; never commits. The end-report carries the finished
`BLOCKED`, also writes `BLOCKED.md` at the repo root. stats.json and — on `PARTIAL`/`BLOCKED``BLOCKED.md` contents, which
the orchestrator writes (issue #29 retired the finalize dispatch that
only templated the script's own aggregate).
### compiler-driven ### compiler-driven
+34 -20
View File
@@ -1,6 +1,6 @@
--- ---
name: implement name: implement
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. 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 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 # implement — plan execution on the Workflow substrate
@@ -30,11 +30,17 @@ real build/test outcome). The four phase agents survive unchanged as the
agent-types the script dispatches; only the dispatch/aggregation prose agent-types the script dispatches; only the dispatch/aggregation prose
the orchestrator-agent carried is gone. the orchestrator-agent carried is gone.
All work lives in the working tree: code edits, the stats file, and — All code edits live in the working tree. The stats file and — on
on PARTIAL/BLOCKED — `BLOCKED.md` at the repo root. The script **never 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 commits** and never moves main HEAD. The orchestrator reads one
end-report (the workflow's return value), inspects the unstaged tree, end-report (the workflow's return value), writes the artifacts,
and decides commit shape. inspects the unstaged tree, and decides commit shape.
## When to Use / Skipping ## When to Use / Skipping
@@ -99,13 +105,13 @@ regression around the RED-first gate.
## The Iron Law ## 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. 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.) 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.) 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. 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). TASKS RUN SEQUENTIALLY; A BLOCKED TASK STOPS THE LOOP — NO SKIP-AHEAD (TASK ORDERING DEPENDENCIES ARE UNKNOWN).
NEVER PUSH PAST `BLOCKED` BY HAND. 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.) 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. `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 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). 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).
@@ -227,20 +233,28 @@ worktree branch).
The workflow returns a small structured end-report (its return value): The workflow returns a small structured end-report (its return value):
`status`, `iter_id`, `started_from`, `tasks_total` / `tasks_completed`, `status`, `iter_id`, `started_from`, `tasks_total` / `tasks_completed`,
per-task summaries, concerns, E2E fixtures, the stats path, and — on 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 PARTIAL/BLOCKED — the blocked detail. Per-task chatter stayed inside
the workflow's agent contexts and never reached the orchestrator. Read 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 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 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: preflight, a plan-extract guard) returns a minimal report instead:
`status: BLOCKED` plus an `infra:`-prefixed `reason`, no `blocked_detail` `status: BLOCKED` plus an `infra:`-prefixed `reason`, no `blocked_detail`
and none of the per-task fields — nothing ran. and none of the per-task fields — nothing ran.
### Step 3 — Orchestrator inspect + commit step (on DONE) ### Step 3 — Orchestrator inspect + commit step (on DONE)
The workflow returns with code edits and the stats file sitting in the The workflow returns with code edits sitting in the working tree as
working tree as unstaged changes. Nothing is committed yet, and there unstaged changes; the stats file was just written by you from
is no `BLOCKED.md` (DONE never writes one). `artifacts.stats_json` (Step 2). Nothing is committed yet, and there
is no `BLOCKED.md` (`artifacts.blocked_md` is null on DONE).
1. Inspect: `git status` and `git diff` — confirm the changes match 1. Inspect: `git status` and `git diff` — confirm the changes match
what the end-report claims. The end-report is the per-task summary; what the end-report claims. The end-report is the per-task summary;
@@ -268,16 +282,16 @@ is no `BLOCKED.md` (DONE never writes one).
### Step 4 — Orchestrator handling (on PARTIAL or BLOCKED) ### Step 4 — Orchestrator handling (on PARTIAL or BLOCKED)
The workflow returns with whatever work-in-progress it managed plus The workflow returns with whatever work-in-progress it managed;
`BLOCKED.md` at the repo root carrying the diagnostic. Nothing is `BLOCKED.md` at the repo root carries the diagnostic — written by you
committed. The orchestrator decides what to do with the dirty working in Step 2 from `artifacts.blocked_md`. Nothing is committed. The
tree: orchestrator decides what to do with the dirty working tree:
0. **No-op `BLOCKED` special case** — if the end-report's status is 0. **No-op `BLOCKED` special case** — if the end-report's status is
`BLOCKED` with `blocked_md` null and a no-op `blocked_detail` (the reason `BLOCKED` with `artifacts.blocked_md` null and a no-op `blocked_detail`
names a no-op iteration or a clean-tree no-op fix; `files_touched` is 0 in (the reason names a no-op iteration or a clean-tree no-op fix;
standard mode, null in mini), there is **no `BLOCKED.md`** and the tree is `files_touched` is 0 in standard mode, null in mini), there is
already clean. The diagnostic is in the end-report's `blocked_detail`, not **no `BLOCKED.md`** and the tree is already clean. The diagnostic is in the end-report's `blocked_detail`, not
a file. Nothing to clean up or stash; re-plan or re-run from the same a file. Nothing to clean up or stash; re-plan or re-run from the same
clean tree. clean tree.
The same no-file shape occurs for an **infra early-exit** (`reason` The same no-file shape occurs for an **infra early-exit** (`reason`
+80 -50
View File
@@ -15,10 +15,12 @@
// encodes the project's real build/test outcome), not on a guess. // encodes the project's real build/test outcome), not on a guess.
// //
// Discipline preserved verbatim from the old loop: // Discipline preserved verbatim from the old loop:
// • The script NEVER commits and NEVER moves main HEAD. All code edits, // • The script NEVER commits and NEVER moves main HEAD. All code edits
// the stats file, and (on PARTIAL/BLOCKED) BLOCKED.md live in the // live in the working tree as unstaged changes; the end-report carries
// working tree as unstaged changes; the orchestrator that invoked // the finished stats.json and (on PARTIAL/BLOCKED) BLOCKED.md contents,
// this workflow inspects and commits. // which the orchestrator that invoked this workflow writes, inspects,
// and commits (issue #29 — the former finalize dispatch only templated
// the script's own aggregate).
// • Tasks run SEQUENTIALLY — plan tasks carry implicit ordering // • Tasks run SEQUENTIALLY — plan tasks carry implicit ordering
// dependencies, and the script does not know the graph, so a BLOCKED // dependencies, and the script does not know the graph, so a BLOCKED
// task stops the loop rather than skipping ahead. // task stops the loop rather than skipping ahead.
@@ -70,8 +72,9 @@
// files sit outside the threat model (`git checkout -- <path>` cannot // files sit outside the threat model (`git checkout -- <path>` cannot
// discard them). // discard them).
// • The script has no filesystem or shell access of its own — every // • The script has no filesystem or shell access of its own — every
// working-tree mutation (code, stats.json, BLOCKED.md) happens inside // working-tree mutation (code, tests) happens inside an agent() call;
// an agent() call. // the stats.json / BLOCKED.md contents are built in-script and ride
// the end-report for the orchestrator to write.
// • Model policy: every agent() call pins an explicit model — never the // • Model policy: every agent() call pins an explicit model — never the
// session-model inherit (which could route to an unintended tier, e.g. // session-model inherit (which could route to an unintended tier, e.g.
// fable — banned for all plugin agents and workflows). Mechanical and // fable — banned for all plugin agents and workflows). Mechanical and
@@ -81,7 +84,7 @@
// explicit effort — never the session inherit. Code-writing and review // explicit effort — never the session inherit. Code-writing and review
// steps run high, the opus quality gate runs xhigh, and schema-bound // steps run high, the opus quality gate runs xhigh, and schema-bound
// extraction/check steps (preflight, plan-index, plan-extract, snapshot, // extraction/check steps (preflight, plan-index, plan-extract, snapshot,
// mini-verify, tree-check, finalize) run medium. See // mini-verify, tree-check) run medium. See
// docs/agent-template.md § effort. // docs/agent-template.md § effort.
// //
// Carrier — passed as the Workflow `args` object by the orchestrator: // Carrier — passed as the Workflow `args` object by the orchestrator:
@@ -98,13 +101,12 @@
export const meta = { export const meta = {
name: 'implement-loop', name: 'implement-loop',
description: 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.', '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 and tests to the working tree as unstaged changes; the end-report carries the finished stats.json and (on PARTIAL/BLOCKED) BLOCKED.md contents for the orchestrator to write. Never commits, never moves main HEAD.',
phases: [ phases: [
{ title: 'Preflight' }, { title: 'Preflight' },
{ title: 'Per-task loop' }, { title: 'Per-task loop' },
{ title: 'Verify' }, { title: 'Verify' },
{ title: 'E2E coverage' }, { title: 'E2E coverage' },
{ title: 'Report' },
], ],
} }
@@ -360,8 +362,9 @@ const MINI_VERIFY_SCHEMA = {
} }
// Standard-mode no-op gate (issue #12, facets 1+2): the authoritative ground // Standard-mode no-op gate (issue #12, facets 1+2): the authoritative ground
// truth that the iteration's per-task work actually landed. Runs after the loop // truth that the iteration's per-task work actually landed. Runs after the loop
// but BEFORE E2E/finalize, so neither E2E fixtures nor the stats/BLOCKED.md // but BEFORE E2E, so E2E fixtures cannot inflate the count (stats/BLOCKED.md
// artefacts can inflate the count. Uses `git status --porcelain` (counts brand-new // are no longer written in-loop at all — the orchestrator writes them from the
// end-report, after this gate). Uses `git status --porcelain` (counts brand-new
// UNTRACKED files — the implementer leaves edits unstaged, so a new-file deliverable // UNTRACKED files — the implementer leaves edits unstaged, so a new-file deliverable
// is untracked and `git diff HEAD` would miss it). A self-report can never reach // is untracked and `git diff HEAD` would miss it). A self-report can never reach
// this verdict; it is git ground truth. // this verdict; it is git ground truth.
@@ -403,16 +406,6 @@ const SNAP_SCHEMA = {
}, },
}, },
} }
const FINALIZE_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['stats_path', 'wrote_blocked_md'],
properties: {
stats_path: { type: 'string' },
wrote_blocked_md: { type: 'boolean' },
},
}
// ---- Phase 0 — clean-tree preflight (mini mode only) -------------------- // ---- Phase 0 — clean-tree preflight (mini mode only) --------------------
// Standard mode folds these checks into the plan-index dispatch (issue #29): // Standard mode folds these checks into the plan-index dispatch (issue #29):
// the check itself stays deterministic in the script (schema fields evaluated // the check itself stays deterministic in the script (schema fields evaluated
@@ -840,9 +833,9 @@ else if (completed.length > 0) outcome = 'PARTIAL'
else outcome = 'BLOCKED' else outcome = 'BLOCKED'
// An iteration-level guard (issue #12) may downgrade a DONE outcome to BLOCKED on // An iteration-level guard (issue #12) may downgrade a DONE outcome to BLOCKED on
// positive-evidence grounds. It runs BEFORE finalize so the persisted stats.json // positive-evidence grounds. It runs BEFORE the report assembly so the built
// records the final outcome (not a stale DONE), and so E2E does not run over a // stats.json content records the final outcome (not a stale DONE), and so E2E
// no-op. The verdict is git ground truth, never a per-task self-report — a // does not run over a no-op. The verdict is git ground truth, never a per-task self-report — a
// self-report must never be able to FAIL a run that actually did the work. It // self-report must never be able to FAIL a run that actually did the work. It
// carries its own blocked_detail (no single offending task) and a `noopClean` // carries its own blocked_detail (no single offending task) and a `noopClean`
// flag: a clean-tree no-op needs no BLOCKED.md (nothing to explain). // flag: a clean-tree no-op needs no BLOCKED.md (nothing to explain).
@@ -964,8 +957,14 @@ if (e2e) {
} }
} }
// ---- Phase 4/5 — finalize: stats.json always, BLOCKED.md on non-DONE ---- // ---- Report assembly (issue #29): the script BUILDS the artefact contents —
phase('Report') // no finalize dispatch. The former `finalize` agent only templated the
// already-computed aggregate into two files (~41k real tokens per run); the
// script has no filesystem access, so the templates now live HERE (single
// source) and the END-REPORT carries the finished contents. The orchestrator
// writes them byte-identical, filling exactly the two runtime slots it owns
// anyway: {{DATE}} (`date +%F`) and, in BLOCKED.md, {{FILES_TOUCHED}}
// (`git status --porcelain` — its own Step-3/4 inspection duty).
// A clean-tree no-op (mini or standard) gets NO BLOCKED.md (issue #12 / // A clean-tree no-op (mini or standard) gets NO BLOCKED.md (issue #12 /
// consistency): there is no dirty tree to explain, so the status + reason ride // consistency): there is no dirty tree to explain, so the status + reason ride
// the end-report. `noopClean` was set by whichever no-op gate fired above. // the end-report. `noopClean` was set by whichever no-op gate fired above.
@@ -986,30 +985,58 @@ const aggregate = {
synthBlock || (blocked ? { task: blocked.id, reason: blocked.reason, detail: blocked.detail || null } : null), synthBlock || (blocked ? { task: blocked.id, reason: blocked.reason, detail: blocked.detail || null } : null),
} }
const fin = await agent( const statsJson =
`${STANDING}\n\nFinalize an implement iteration. Using the aggregate below:\n` + JSON.stringify(
'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 ' + iter_id,
'blocked_reason (or null).\n' + date: '{{DATE}}',
(!writeBlockedMd mode,
? '2. Do NOT write BLOCKED.md — ' + outcome,
(outcome === 'DONE' ? 'this run is DONE.\n' : 'this is a clean-tree no-op; there is nothing to explain.\n') tasks_total: tasks.length,
: '2. Write `BLOCKED.md` at the repo ROOT (uncommitted by convention; never staged). Sections: a `# BLOCKED — ' + tasks_completed: completed.length,
'iter ' + iter_id + '` header with Date/Started-from/Status/Tasks-completed, `## What ran` (one line per DONE ' + blocked_reason: aggregate.blocked_detail ? aggregate.blocked_detail.reason : null,
'task), `## What did not` (the blocked task, its reason, and a one-line suggested next step), `## Concerns`, ' + },
'and `## Files touched` (from `git status --porcelain`).\n') + null,
'Report the stats path and whether you wrote BLOCKED.md. Do NOT commit anything.\n\nAGGREGATE:\n' + 2,
JSON.stringify(aggregate, null, 2), ) + '\n'
{ model: 'sonnet', effort: 'medium', label: 'finalize', phase: 'Report', schema: FINALIZE_SCHEMA },
)
// The no-op verdict was already taken before finalize (the tree-check / mini-verify const blockedMd = writeBlockedMd
// gates set outcome + synthBlock + aggregate.blocked_detail), so the end-report ? [
// just relays the final outcome. files_touched comes from the standard-mode `# BLOCKED — iter ${iter_id}`,
// tree-check (git status --porcelain, untracked-aware); it is null for mini / '',
// PARTIAL / BLOCKED, where the orchestrator reads BLOCKED.md and inspects the tree. 'Date: {{DATE}}',
`Started-from: ${startSha}`,
`Status: ${outcome}`,
`Tasks-completed: ${completed.length}/${tasks.length}`,
'',
'## What ran',
...(completed.length ? completed.map((c) => `- #${c.id} ${c.title}`) : ['- (none)']),
'',
'## What did not',
...(aggregate.blocked_detail
? [
`- ${aggregate.blocked_detail.task != null ? `task #${aggregate.blocked_detail.task}: ` : ''}${aggregate.blocked_detail.reason}`,
...(aggregate.blocked_detail.detail ? [`- detail: ${aggregate.blocked_detail.detail}`] : []),
]
: ['- (no blocked detail recorded)']),
'',
'## Concerns',
...(aggregate.concerns.length ? aggregate.concerns.map((c) => `- ${c}`) : ['- (none)']),
'',
'## Files touched',
'{{FILES_TOUCHED}}',
'',
].join('\n')
: null
// ---- End-report — the orchestrator reads this, inspects the tree, commits. // files_touched comes from the standard-mode end-verify (git status
// --porcelain, untracked-aware); it is null for mini / PARTIAL / BLOCKED,
// where the orchestrator inspects the tree itself.
// ---- End-report — the orchestrator reads this, WRITES the artifacts
// (artifacts.stats_json to the stats path; artifacts.blocked_md — when
// non-null — verbatim to BLOCKED.md at the repo root, filling {{DATE}} and
// {{FILES_TOUCHED}}), inspects the tree, and commits.
return { return {
status: outcome, status: outcome,
iter_id, iter_id,
@@ -1020,8 +1047,11 @@ return {
task_summaries: aggregate.task_summaries, task_summaries: aggregate.task_summaries,
concerns: aggregate.concerns, concerns: aggregate.concerns,
e2e_fixtures: e2e ? e2e.fixtures || [] : [], e2e_fixtures: e2e ? e2e.fixtures || [] : [],
stats_path: fin ? fin.stats_path : null, artifacts: {
blocked_md: fin && fin.wrote_blocked_md ? 'BLOCKED.md (uncommitted)' : null, stats_json: statsJson,
stats_path_hint: `/tmp/iter-${iter_id}/stats.json (or the project stats dir if its CLAUDE.md facts declare one)`,
blocked_md: blockedMd,
},
files_touched: treeState ? treeState.files_touched : null, files_touched: treeState ? treeState.files_touched : null,
blocked_detail: aggregate.blocked_detail, blocked_detail: aggregate.blocked_detail,
} }