fix(implement-loop): stamp a repo-root anchor into every stage prompt

In worktree sessions a dispatched agent's cwd can resolve to the
primary checkout instead of the loop's working tree — observed as a
per-dispatch race: 6/9 quality dispatches returning infra_blocked with
the sha256-of-empty fingerprint over a real diff, and a tree-check
counting the primary checkout's clean tree into a false no-op BLOCKED
on a genuinely-DONE iteration.

Remedy (validated in the field on the reporting run): the carrier
gains repo_root, stamped into every stage prompt as an explicit
'git -C <root>' anchor with a wrong-directory tripwire (an empty diff
or unexpected HEAD sha = wrong cwd, re-run against the anchor). When
the carrier omits it, the first dispatched stage (mini preflight /
standard plan-index) probes 'git rev-parse --show-toplevel' as a
fallback — itself subject to the same race, so SKILL.md documents the
field as effectively required in worktree sessions.
compiler-driven-edit gets the same carrier field (same defect class,
carrier-only). agent-template stage list updated in passing
(plan-extract[-all], end-verify).

closes #25
This commit is contained in:
2026-07-17 15:35:11 +02:00
parent f5ba8c3747
commit 1cb52fb821
4 changed files with 92 additions and 26 deletions
+2 -2
View File
@@ -159,8 +159,8 @@ pin exists to remove. Effort follows the model split:
Workflow scripts pin a third tier inline: schema-bound Workflow scripts pin a third tier inline: schema-bound
extraction/verification stages that author no code (preflight, extraction/verification stages that author no code (preflight,
plan-index, plan-extract, snapshot, mini-verify, tree-check, finalize, plan-index, plan-extract[-all], snapshot, mini-verify, the standard
build/suite verify) run `medium` via the `agent()` call's `effort:` option. As with end-verify, build/suite verify) run `medium` via the `agent()` call's `effort:` option. As with
`model:`, every `agent()` call passes `effort:` explicitly — whether `model:`, every `agent()` call passes `effort:` explicitly — whether
frontmatter effort propagates through an `agentType` dispatch is frontmatter effort propagates through an `agentType` dispatch is
undocumented, so the scripts do not rely on it (see the policy header undocumented, so the scripts do not rely on it (see the policy header
+12 -2
View File
@@ -210,7 +210,8 @@ Workflow({ name: "implement-loop", args: {
mode: "standard", mode: "standard",
iter_id: "<iter_id>", iter_id: "<iter_id>",
plan_path: "<path under docs/plans>", plan_path: "<path under docs/plans>",
task_range: [3, 8] // optional task_range: [3, 8], // optional
repo_root: "<abs path of the working tree>" // strongly recommended; effectively required in worktree sessions (issue #25)
}}) }})
``` ```
@@ -222,10 +223,19 @@ Workflow({ name: "implement-loop", args: {
iter_id: "bugfix-<short-symptom>", // tdd: "feat-<short-behaviour>" iter_id: "bugfix-<short-symptom>", // tdd: "feat-<short-behaviour>"
red_test_path: "<absolute path>", red_test_path: "<absolute path>",
cause_summary: "<1-2 sentences from debugger>", // tdd: the desired-behaviour line cause_summary: "<1-2 sentences from debugger>", // tdd: the desired-behaviour line
constraint: "minimal fix, no surrounding cleanup" // tdd: "minimal feature, no surrounding scope" 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)
}}) }})
``` ```
`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 The carrier fields are defined authoritatively at the top of
`workflows/implement-loop.js`. The load-bearing `iter_id` rule is there `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 too: it names the scratch dir and the stats filename, NOT a branch
+21 -3
View File
@@ -50,6 +50,12 @@
// Carrier — Workflow `args`: // Carrier — Workflow `args`:
// edit_description: what type/signature change to make // edit_description: what type/signature change to make
// def_site: the definition site (file + symbol) it originates from // def_site: the definition site (file + symbol) it originates from
// repo_root: optional but strongly recommended (required in worktree
// sessions — issue #25): absolute path of the working tree
// this edit operates on, stamped into both stage prompts
// as a `git -C` anchor so a dispatched agent whose cwd
// resolves to the primary checkout cannot verify the
// wrong tree.
// A non-object carrier or a missing required field fails fast as an infra // A non-object carrier or a missing required field fails fast as an infra
// BLOCKED before any agent is dispatched (issue #24). // BLOCKED before any agent is dispatched (issue #24).
@@ -87,7 +93,7 @@ if ((carrier !== undefined && carrier !== null && typeof carrier !== 'object') |
} }
} }
const { edit_description, def_site } = carrier || {} const { edit_description, def_site, repo_root } = carrier || {}
const missingFields = Object.entries({ edit_description, def_site }) const missingFields = Object.entries({ edit_description, def_site })
.filter(([, v]) => typeof v !== 'string' || !v.trim()) .filter(([, v]) => typeof v !== 'string' || !v.trim())
@@ -107,6 +113,18 @@ const STANDING =
'(its "## Skills plugin: project facts" name the build and test commands) and ' + '(its "## Skills plugin: project facts" name the build and test commands) and ' +
'`git log -10 --format=full`. Resolve the build/test commands from those facts.' '`git log -10 --format=full`. Resolve the build/test commands from those facts.'
// Working-directory anchor (issue #25): same defect class as implement-loop —
// in a worktree session a dispatched agent's cwd can resolve to the primary
// checkout and verify the wrong (clean) tree. Carrier-supplied only here (two
// stages, no cheap probe slot); '' when absent.
const ANCHOR =
typeof repo_root === 'string' && repo_root.trim()
? `\n\nWORKING-DIRECTORY ANCHOR (do not skip): the working tree lives at ${repo_root.trim()}. Run EVERY git ` +
`command as \`git -C ${repo_root.trim()} ...\` and run the build/test commands from that directory. An empty ` +
'`git diff HEAD` or an unexpected HEAD sha means you are in the WRONG directory — re-run against the anchor; ' +
'never report a clean tree from an unanchored cwd.'
: ''
const EDIT_SCHEMA = { const EDIT_SCHEMA = {
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
@@ -159,7 +177,7 @@ const VERIFY_SCHEMA = {
// ---- Phase 1 — make the edit and let the compiler enumerate the sites ---- // ---- Phase 1 — make the edit and let the compiler enumerate the sites ----
phase('Edit + propagate') phase('Edit + propagate')
const edit = await agent( const edit = await agent(
`${STANDING}\n\nMake this behaviour-preserving type/signature edit at its definition site, then propagate it ` + `${STANDING}${ANCHOR}\n\nMake this behaviour-preserving type/signature edit at its definition site, then propagate it ` +
'MECHANICALLY across every site the build flags — let the type checker enumerate the edit sites. Introduce NO ' + 'MECHANICALLY across every site the build flags — let the type checker enumerate the edit sites. Introduce NO ' +
'new behaviour, add NO new test, change NO existing test. If a flagged site cannot be resolved without making a ' + 'new behaviour, add NO new test, change NO existing test. If a flagged site cannot be resolved without making a ' +
'design decision, OR resolving it would encode new behaviour, STOP and report it as a hole rather than guessing. ' + 'design decision, OR resolving it would encode new behaviour, STOP and report it as a hole rather than guessing. ' +
@@ -207,7 +225,7 @@ if (!edit.applied_changes) {
// ---- Phase 2 — verify (the independently-invokable phase) --------------- // ---- Phase 2 — verify (the independently-invokable phase) ---------------
phase('Verify') phase('Verify')
const verify = await agent( const verify = await agent(
`${STANDING}\n\nVerify the edit is behaviour-preserving WITHOUT changing any code or test. Run the project build ` + `${STANDING}${ANCHOR}\n\nVerify the edit is behaviour-preserving WITHOUT changing any code or test. Run the project build ` +
'and the full test suite. Report whether the build is clean and whether the suite is green AND UNCHANGED — i.e. ' + 'and the full test suite. Report whether the build is clean and whether the suite is green AND UNCHANGED — i.e. ' +
'every test passes and NO test was added, removed, or weakened versus the pre-edit baseline (compare `git diff ' + 'every test passes and NO test was added, removed, or weakened versus the pre-edit baseline (compare `git diff ' +
'HEAD` for test-file changes). Also report working_tree_dirty — whether `git status --porcelain` / `git diff HEAD` ' + 'HEAD` for test-file changes). Also report working_tree_dirty — whether `git status --porcelain` / `git diff HEAD` ' +
+57 -19
View File
@@ -86,8 +86,8 @@
// • Effort policy mirrors the model policy: every agent() call pins an // • Effort policy mirrors the model policy: every agent() call pins an
// 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[-all],
// mini-verify, tree-check) run medium. See // snapshot, mini-verify, verify) 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,6 +98,17 @@
// red_test_path: (mini) absolute path to the RED test from debug / tdd // red_test_path: (mini) absolute path to the RED test from debug / tdd
// cause_summary: (mini) 12 sentences (debugger cause, or tdd spec_summary) // cause_summary: (mini) 12 sentences (debugger cause, or tdd spec_summary)
// constraint: (mini) e.g. 'minimal fix, no surrounding cleanup' // constraint: (mini) e.g. 'minimal fix, no surrounding cleanup'
// repo_root: (both modes; optional but STRONGLY recommended, and
// effectively required in worktree sessions — issue #25)
// absolute path of the working tree this loop operates on.
// Stamped into every stage prompt as a `git -C` anchor so a
// dispatched agent whose cwd resolves to the PRIMARY checkout
// (the observed per-dispatch race) cannot review the wrong
// tree — false infra_blocked quality reviews, a false no-op
// BLOCKED from the end-verify. When absent, the first
// dispatched stage probes `git rev-parse --show-toplevel` as
// a fallback (itself subject to the same race the carrier
// field exists to remove — pass the field).
// A non-object carrier, an unknown mode, or a missing required field fails // A non-object carrier, an unknown mode, or a missing required field fails
// fast as an infra BLOCKED before any agent is dispatched (issue #24). // fast as an infra BLOCKED before any agent is dispatched (issue #24).
@@ -148,6 +159,7 @@ const {
red_test_path, red_test_path,
cause_summary, cause_summary,
constraint = 'minimal change, no surrounding cleanup', constraint = 'minimal change, no surrounding cleanup',
repo_root,
} = carrier || {} } = carrier || {}
// Blank-safe echo for the guard verdicts below: never mirror a blank/non-string // Blank-safe echo for the guard verdicts below: never mirror a blank/non-string
// iter_id into the end-report while also naming it missing. // iter_id into the end-report while also naming it missing.
@@ -183,15 +195,35 @@ const STANDING =
'design ledger) and `git log -10 --format=full`. Resolve the build/test commands ' + 'design ledger) and `git log -10 --format=full`. Resolve the build/test commands ' +
'from those facts — do not assume a toolchain.' 'from those facts — do not assume a toolchain.'
// Working-directory anchor (issue #25): in worktree sessions a dispatched
// agent's cwd can resolve to the PRIMARY checkout instead of the worktree the
// loop operates on — observed as a per-dispatch race producing false
// infra_blocked quality reviews (empty `git diff HEAD`, the sha256-of-empty
// fingerprint) and a false no-op BLOCKED from the end-verify counting the
// wrong (clean) tree. The remedy validated on issue #25: stamp the loop's
// repo root into every stage prompt as a `git -C` instruction. The root comes
// from the carrier (deterministic — preferred) or, as a fallback, from the
// first dispatched stage's `git rev-parse --show-toplevel` probe.
const anchorText = (root) =>
root
? `\n\nWORKING-DIRECTORY ANCHOR (do not skip): the working tree under review lives at ${root}. Run EVERY git ` +
`command as \`git -C ${root} ...\` and run the build/test commands from that directory. An empty ` +
'`git diff HEAD`, an unexpected HEAD sha, or a clean tree right after edits were reported means you are in ' +
'the WRONG directory — re-run against the anchor; never report a clean tree from an unanchored cwd.'
: ''
const carrierRoot = typeof repo_root === 'string' && repo_root.trim() ? repo_root.trim() : ''
let ANCHOR = anchorText(carrierRoot)
// JSON Schemas — force structured verdicts so the script can branch on real outcomes. // JSON Schemas — force structured verdicts so the script can branch on real outcomes.
const PREFLIGHT_SCHEMA = { const PREFLIGHT_SCHEMA = {
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
required: ['clean', 'head_sha', 'branch'], required: ['clean', 'head_sha', 'branch', 'repo_root'],
properties: { properties: {
clean: { type: 'boolean', description: '`git status --porcelain` produced no output' }, clean: { type: 'boolean', description: '`git status --porcelain` produced no output' },
head_sha: { type: 'string' }, head_sha: { type: 'string' },
branch: { type: 'string' }, branch: { type: 'string' },
repo_root: { type: 'string', description: 'output of `git rev-parse --show-toplevel` (the anchor fallback, issue #25)' },
dirty_paths: { type: 'array', items: { type: 'string' } }, dirty_paths: { type: 'array', items: { type: 'string' } },
}, },
} }
@@ -217,6 +249,7 @@ const TASK_INDEX_SCHEMA = {
clean: { type: 'boolean', description: '`git status --porcelain` produced no output' }, clean: { type: 'boolean', description: '`git status --porcelain` produced no output' },
head_sha: { type: 'string' }, head_sha: { type: 'string' },
branch: { type: 'string' }, branch: { type: 'string' },
repo_root: { type: 'string', description: 'output of `git rev-parse --show-toplevel` (the anchor fallback, issue #25)' },
dirty_paths: { type: 'array', items: { type: 'string' } }, dirty_paths: { type: 'array', items: { type: 'string' } },
total_tasks: { total_tasks: {
type: 'integer', type: 'integer',
@@ -464,9 +497,11 @@ let startSha = null
if (mode === 'mini') { if (mode === 'mini') {
phase('Preflight') phase('Preflight')
const pre = await agent( const pre = await agent(
`${STANDING}\n\nYou are the clean-tree preflight for an implement iteration. ` + `${STANDING}${ANCHOR}\n\nYou are the clean-tree preflight for an implement iteration. ` +
'Run `git status --porcelain`, `git rev-parse HEAD`, and `git rev-parse --abbrev-ref HEAD`. ' + 'Run `git status --porcelain`, `git rev-parse HEAD`, `git rev-parse --abbrev-ref HEAD`, and ' +
'Report whether the tree is clean (no porcelain output), the HEAD sha, the branch, and any dirty paths. ' + '`git rev-parse --show-toplevel`. ' +
'Report whether the tree is clean (no porcelain output), the HEAD sha, the branch, the repo_root, and any ' +
'dirty paths. ' +
'Do NOT modify anything, do NOT commit. The HEAD sha is an informational anchor only — never a reset target.', 'Do NOT modify anything, do NOT commit. The HEAD sha is an informational anchor only — never a reset target.',
{ model: 'sonnet', effort: 'medium', label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA }, { model: 'sonnet', effort: 'medium', label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA },
) )
@@ -480,6 +515,7 @@ if (mode === 'mini') {
} }
} }
startSha = pre.head_sha startSha = pre.head_sha
if (!carrierRoot && typeof pre.repo_root === 'string' && pre.repo_root.trim()) ANCHOR = anchorText(pre.repo_root.trim())
} }
// ---- Phase 1 — assemble the task list ---------------------------------- // ---- Phase 1 — assemble the task list ----------------------------------
@@ -529,8 +565,9 @@ if (mode === 'mini') {
// narrower than the closed across-tasks vector and rests on the planner's // narrower than the closed across-tasks vector and rests on the planner's
// bite-sized-task invariant; it is the one truncation slice left open here. // bite-sized-task invariant; it is the one truncation slice left open here.
const index = await agent( const index = await agent(
`${STANDING}\n\nFIRST the folded clean-tree preflight: run \`git status --porcelain\`, \`git rev-parse HEAD\`, ` + `${STANDING}${ANCHOR}\n\nFIRST the folded clean-tree preflight: run \`git status --porcelain\`, \`git rev-parse HEAD\`, ` +
'and `git rev-parse --abbrev-ref HEAD`; report `clean` (no porcelain output), `head_sha`, `branch`, and any ' + '`git rev-parse --abbrev-ref HEAD`, and `git rev-parse --show-toplevel`; report `clean` (no porcelain output), ' +
'`head_sha`, `branch`, `repo_root`, and any ' +
'`dirty_paths`. The HEAD sha is an informational anchor only — never a reset target. If the tree is DIRTY, ' + '`dirty_paths`. The HEAD sha is an informational anchor only — never a reset target. If the tree is DIRTY, ' +
'stop there: return task_ids: [] and total_tasks: 0 without reading the plan.\n\n' + 'stop there: return task_ids: [] and total_tasks: 0 without reading the plan.\n\n' +
`Otherwise read the plan at ${plan_path}. List the task ` + `Otherwise read the plan at ${plan_path}. List the task ` +
@@ -555,6 +592,7 @@ if (mode === 'mini') {
} }
} }
startSha = index.head_sha startSha = index.head_sha
if (!carrierRoot && typeof index.repo_root === 'string' && index.repo_root.trim()) ANCHOR = anchorText(index.repo_root.trim())
if (!index.task_ids || index.task_ids.length === 0) { if (!index.task_ids || index.task_ids.length === 0) {
return { status: 'NEEDS_CONTEXT', iter_id, reason: `no tasks found in ${plan_path}` } return { status: 'NEEDS_CONTEXT', iter_id, reason: `no tasks found in ${plan_path}` }
} }
@@ -650,7 +688,7 @@ if (mode === 'mini') {
let extractedTasks = null let extractedTasks = null
if (expectedIds.length > 1 && totalApprox !== null && totalApprox <= EXTRACT_ALL_BYTE_CEILING) { if (expectedIds.length > 1 && totalApprox !== null && totalApprox <= EXTRACT_ALL_BYTE_CEILING) {
const all = await agent( const all = await agent(
`${STANDING}\n\nRead the plan at ${plan_path}. Extract EXACTLY these tasks (1-based ids): ${expectedIds.join(', ')}` + `${STANDING}${ANCHOR}\n\nRead the plan at ${plan_path}. Extract EXACTLY these tasks (1-based ids): ${expectedIds.join(', ')}` +
'EACH as its VERBATIM block: the full task text as written, including any code blocks. Do not summarise, ' + 'EACH as its VERBATIM block: the full task text as written, including any code blocks. Do not summarise, ' +
'truncate, reorder, or merge tasks into each other. Return exactly those tasks, nothing else. Do not edit anything.', 'truncate, reorder, or merge tasks into each other. Return exactly those tasks, nothing else. Do not edit anything.',
{ model: 'sonnet', effort: 'medium', label: 'plan-extract-all', phase: 'Per-task loop', schema: TASK_TEXTS_SCHEMA }, { model: 'sonnet', effort: 'medium', label: 'plan-extract-all', phase: 'Per-task loop', schema: TASK_TEXTS_SCHEMA },
@@ -668,7 +706,7 @@ if (mode === 'mini') {
extractedTasks = await parallel( extractedTasks = await parallel(
expectedIds.map((id) => () => expectedIds.map((id) => () =>
agent( agent(
`${STANDING}\n\nRead the plan at ${plan_path}. Extract EXACTLY task ${id} (1-based) as its VERBATIM block — ` + `${STANDING}${ANCHOR}\n\nRead the plan at ${plan_path}. Extract EXACTLY task ${id} (1-based) as its VERBATIM block — ` +
'the full task text as written, including any code blocks. Do not summarise, truncate, reorder, or merge in ' + 'the full task text as written, including any code blocks. Do not summarise, truncate, reorder, or merge in ' +
'any other task. Return that one task only. Do not edit anything.', 'any other task. Return that one task only. Do not edit anything.',
{ model: 'sonnet', effort: 'medium', label: `plan-extract:${id}`, phase: 'Per-task loop', schema: TASK_TEXT_SCHEMA }, { model: 'sonnet', effort: 'medium', label: `plan-extract:${id}`, phase: 'Per-task loop', schema: TASK_TEXT_SCHEMA },
@@ -712,7 +750,7 @@ phase('Per-task loop')
async function runTask(task) { async function runTask(task) {
// 2.1 — implementer phase // 2.1 — implementer phase
const impl = await agent( const impl = await agent(
`${STANDING}\n\nImplement this task exactly, RED-first if it adds behaviour (TDD is an independent ` + `${STANDING}${ANCHOR}\n\nImplement this task exactly, RED-first if it adds behaviour (TDD is an independent ` +
'inner-loop discipline — add the failing test inline even if the task text forgot to script it). ' + 'inner-loop discipline — add the failing test inline even if the task text forgot to script it). ' +
'Build and test must be green before you report DONE. Leave all edits UNSTAGED; never commit.\n\n' + 'Build and test must be green before you report DONE. Leave all edits UNSTAGED; never commit.\n\n' +
`mode: ${mode}\niter_id: ${iter_id}\n\nTASK ${task.id}${task.title}\n${task.text}`, `mode: ${mode}\niter_id: ${iter_id}\n\nTASK ${task.id}${task.title}\n${task.text}`,
@@ -732,7 +770,7 @@ async function runTask(task) {
// 2.2 — spec-compliance check (gated before quality), with repair re-loop // 2.2 — spec-compliance check (gated before quality), with repair re-loop
for (let round = 0; ; round++) { for (let round = 0; ; round++) {
const spec = await agent( const spec = await agent(
`${STANDING}\n\nSpec-compliance review of task ${task.id}. Compare \`git diff HEAD\` against the task ` + `${STANDING}${ANCHOR}\n\nSpec-compliance review of task ${task.id}. Compare \`git diff HEAD\` against the task ` +
'text ONLY — list missing requirements and unrequested extras. No quality opinions, no fix proposals. ' + 'text ONLY — list missing requirements and unrequested extras. No quality opinions, no fix proposals. ' +
'If a requirement can only be judged missing because its scripted literal is contradicted by reality ' + 'If a requirement can only be judged missing because its scripted literal is contradicted by reality ' +
'you have verified yourself (the actual behaviour provably differs from what the task text asserts), or ' + 'you have verified yourself (the actual behaviour provably differs from what the task text asserts), or ' +
@@ -746,7 +784,7 @@ async function runTask(task) {
if (round >= 2) return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'review-loop-exhausted (spec-compliance)' } if (round >= 2) return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'review-loop-exhausted (spec-compliance)' }
// repair: re-dispatch the implementer with the review findings as the repair brief // repair: re-dispatch the implementer with the review findings as the repair brief
const specFix = await agent( const specFix = await agent(
`${STANDING}\n\nRepair task ${task.id} to satisfy the spec-compliance review. Address EACH finding; ` + `${STANDING}${ANCHOR}\n\nRepair task ${task.id} to satisfy the spec-compliance review. Address EACH finding; ` +
'do not widen scope. Keep build + tests green; leave edits unstaged.\n\n' + 'do not widen scope. Keep build + tests green; leave edits unstaged.\n\n' +
`TASK ${task.id}${task.title}\n${task.text}\n\nFINDINGS:\n- ${(spec?.findings || ['(none reported)']).join('\n- ')}`, `TASK ${task.id}${task.title}\n${task.text}\n\nFINDINGS:\n- ${(spec?.findings || ['(none reported)']).join('\n- ')}`,
{ agentType: 'implementer', model: 'sonnet', effort: 'high', label: `impl-fix-spec:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA }, { agentType: 'implementer', model: 'sonnet', effort: 'high', label: `impl-fix-spec:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
@@ -767,7 +805,7 @@ async function runTask(task) {
const seenFingerprints = new Set() const seenFingerprints = new Set()
for (let round = 0; ; round++) { for (let round = 0; ; round++) {
const qual = await agent( const qual = await agent(
`${STANDING}\n\nQuality review of task ${task.id} (spec-compliance is already green — do not re-check it). ` + `${STANDING}${ANCHOR}\n\nQuality review of task ${task.id} (spec-compliance is already green — do not re-check it). ` +
'Read every chunk of `git diff HEAD` in this task\'s footprint. Report only Important and Minor issues, ' + 'Read every chunk of `git diff HEAD` in this task\'s footprint. Report only Important and Minor issues, ' +
'each with its `severity` classified honestly per your severity definitions; Nits never gate. ' + 'each with its `severity` classified honestly per your severity definitions; Nits never gate. ' +
'Also report `diff_fingerprint` (REQUIRED): the sha256 hex of the FULL `git diff HEAD` ' + 'Also report `diff_fingerprint` (REQUIRED): the sha256 hex of the FULL `git diff HEAD` ' +
@@ -828,7 +866,7 @@ async function runTask(task) {
if (round >= 2) return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'review-loop-exhausted (quality)' } if (round >= 2) return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'review-loop-exhausted (quality)' }
if (qual && qual.diff_fingerprint) seenFingerprints.add(qual.diff_fingerprint) if (qual && qual.diff_fingerprint) seenFingerprints.add(qual.diff_fingerprint)
const fix = await agent( const fix = await agent(
`${STANDING}\n\nThe quality review of task ${task.id} requested changes. Address EACH Important/Minor issue; ` + `${STANDING}${ANCHOR}\n\nThe quality review of task ${task.id} requested changes. Address EACH Important/Minor issue; ` +
'do not widen scope; keep build + tests green; leave edits unstaged. The task text below is the ratified plan.\n\n' + 'do not widen scope; keep build + tests green; leave edits unstaged. The task text below is the ratified plan.\n\n' +
'HOLD CLAUSE — most issues you simply fix. But if honouring an issue would force you to deviate from a ' + 'HOLD CLAUSE — most issues you simply fix. But if honouring an issue would force you to deviate from a ' +
'name, signature, or structure THIS TASK prescribes (or to break the downstream callers the plan ' + 'name, signature, or structure THIS TASK prescribes (or to break the downstream callers the plan ' +
@@ -901,7 +939,7 @@ for (const task of tasks) {
results.push(r) results.push(r)
if (tasks.length > 1) { if (tasks.length > 1) {
const snap = await agent( const snap = await agent(
`${STANDING}\n\nRecord a working-tree snapshot boundary WITHOUT changing anything. Run exactly these two ` + `${STANDING}${ANCHOR}\n\nRecord a working-tree snapshot boundary WITHOUT changing anything. Run exactly these two ` +
'commands and report their output:\n' + 'commands and report their output:\n' +
`1. \`git stash create "iter ${iter_id} post-task ${task.id}"\` — prints a sha (a dangling commit; it ` + `1. \`git stash create "iter ${iter_id} post-task ${task.id}"\` — prints a sha (a dangling commit; it ` +
'touches neither HEAD, nor the index, nor the working tree). Report it as snapshot_sha; empty string if ' + 'touches neither HEAD, nor the index, nor the working tree). Report it as snapshot_sha; empty string if ' +
@@ -986,7 +1024,7 @@ let treeState = null
if (mode === 'mini' && outcome === 'DONE') { if (mode === 'mini' && outcome === 'DONE') {
phase('Verify') phase('Verify')
miniVerify = await agent( miniVerify = await agent(
`${STANDING}\n\nVerify a bug/feature fix WITHOUT changing any code or test. The fix was meant to drive this ` + `${STANDING}${ANCHOR}\n\nVerify a bug/feature fix WITHOUT changing any code or test. The fix was meant to drive this ` +
`RED test to GREEN: ${red_test_path}\n\nRun that test by name and confirm it now PASSES (it was RED before the ` + `RED test to GREEN: ${red_test_path}\n\nRun that test by name and confirm it now PASSES (it was RED before the ` +
'fix). Then run the full suite and confirm it is green (no regression). Also report working_tree_dirty — ' + 'fix). Then run the full suite and confirm it is green (no regression). Also report working_tree_dirty — ' +
'whether `git status --porcelain` (NOT `git diff HEAD`, which misses new untracked files) shows any change at ' + 'whether `git status --porcelain` (NOT `git diff HEAD`, which misses new untracked files) shows any change at ' +
@@ -1014,7 +1052,7 @@ if (mode === 'mini' && outcome === 'DONE') {
} else if (mode === 'standard' && outcome === 'DONE') { } else if (mode === 'standard' && outcome === 'DONE') {
phase('Verify') phase('Verify')
treeState = await agent( treeState = await agent(
`${STANDING}\n\nEnd-verify an implement iteration WITHOUT changing any code or test. Two independent checks:\n` + `${STANDING}${ANCHOR}\n\nEnd-verify an implement iteration WITHOUT changing any code or test. Two independent checks:\n` +
'1. Run `git status --porcelain` and report files_touched = its line count (every changed, added, or ' + '1. Run `git status --porcelain` and report files_touched = its line count (every changed, added, or ' +
'untracked path). Use `git status --porcelain`, NOT `git diff`, so brand-new untracked files are counted.\n' + 'untracked path). Use `git status --porcelain`, NOT `git diff`, so brand-new untracked files are counted.\n' +
'2. Run the FULL project suite (the test command from the CLAUDE.md project facts) and report suite_green — ' + '2. Run the FULL project suite (the test command from the CLAUDE.md project facts) and report suite_green — ' +
@@ -1048,7 +1086,7 @@ let e2e = null
if (mode === 'standard' && outcome === 'DONE') { if (mode === 'standard' && outcome === 'DONE') {
phase('E2E coverage') phase('E2E coverage')
e2e = await agent( e2e = await agent(
`${STANDING}\n\nThe iteration completed. Identify 13 invariants worth protecting and write the smallest ` + `${STANDING}${ANCHOR}\n\nThe iteration completed. Identify 13 invariants worth protecting and write the smallest ` +
'E2E fixtures for them in the project\'s canonical fixture form; each test\'s doc comment names the property ' + 'E2E fixtures for them in the project\'s canonical fixture form; each test\'s doc comment names the property ' +
'it protects. Run the project\'s test command — all green. Leave edits unstaged; never commit.\n\n' + 'it protects. Run the project\'s test command — all green. Leave edits unstaged; never commit.\n\n' +
`iter_id: ${iter_id}\nshipped: ${completed.map((c) => `#${c.id} ${c.title}`).join('; ')}`, `iter_id: ${iter_id}\nshipped: ${completed.map((c) => `#${c.id} ${c.title}`).join('; ')}`,