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:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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` ' +
|
||||||
|
|||||||
@@ -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) 1–2 sentences (debugger cause, or tdd spec_summary)
|
// cause_summary: (mini) 1–2 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 1–3 invariants worth protecting and write the smallest ` +
|
`${STANDING}${ANCHOR}\n\nThe iteration completed. Identify 1–3 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('; ')}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user