diff --git a/docs/agent-template.md b/docs/agent-template.md index 281dba4..5239ee1 100644 --- a/docs/agent-template.md +++ b/docs/agent-template.md @@ -201,6 +201,12 @@ the per-role standing reading the project lists in its CLAUDE.md project facts. The agent's body says, prosaically: "Read everything in the standing reading list before doing anything else." +The always list binds every *role* (agent file). The anonymous +schema-bound stages inside `implement/workflows/*.js` are tiered +below it (issue #31): verify-type stages read only the project-facts +build/test commands, tree/extraction stages read nothing — see +`conventions.md` § Standing reading. + When the project's CLAUDE.md project facts name a glossary path, that glossary is implicitly part of the always list, so every role reads the project glossary without a per-role entry. diff --git a/docs/conventions.md b/docs/conventions.md index dfae9a0..4232f8d 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -122,6 +122,19 @@ project facts (`standing reading`). If the project declares a glossary, that file is implicitly standing reading for every role too. +**Schema-bound workflow stages are tiered down** (issue #31). The +anonymous script stages in `implement/workflows/*.js` have no agent +file and author no code, and a freshly-primed context costs ~40k+ real +tokens regardless of task size — so they do not take the full read: +verify-type stages (mini-verify, the standard end-verify, the +compiler-driven build/suite verify) read only the project-facts +build/test commands; tree/extraction stages (preflight, plan-index, +plan-extract[-all], snapshot) read nothing beyond their own +instructions. This extends the narrowed per-role standing reading the +named agents already practise (`tester` reads `git log -3`, +`plan-recon`/`grounding-check` `-5`, `fieldtester` `-8`, `docwriter` +`--oneline`); judgment roles keep the full convention. + ## Git discipline - **Only the orchestrator commits.** No skill agent runs `git commit`. diff --git a/implement/workflows/compiler-driven-edit.js b/implement/workflows/compiler-driven-edit.js index 07f6568..ccea9c0 100644 --- a/implement/workflows/compiler-driven-edit.js +++ b/implement/workflows/compiler-driven-edit.js @@ -113,6 +113,15 @@ const STANDING = '(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.' +// Standing-reading tiering (issue #31): the verify stage is a schema-bound +// check that only needs the build/test commands — no git log, no full +// CLAUDE.md read. The edit stage keeps the full standing read (it authors +// code and must match project idiom). +const STANDING_FACTS = + 'Project facts first: read ONLY the "## Skills plugin: project facts" section of the project\'s CLAUDE.md for ' + + 'the build and test commands — no other standing reading (no git log). This is a schema-bound check stage; ' + + 'do not assume a toolchain.' + // 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 @@ -225,7 +234,7 @@ if (!edit.applied_changes) { // ---- Phase 2 — verify (the independently-invokable phase) --------------- phase('Verify') const verify = await agent( - `${STANDING}${ANCHOR}\n\nVerify the edit is behaviour-preserving WITHOUT changing any code or test. Run the project build ` + + `${STANDING_FACTS}${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. ' + '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` ' + diff --git a/implement/workflows/implement-loop.js b/implement/workflows/implement-loop.js index 9465cf8..2dec8cf 100644 --- a/implement/workflows/implement-loop.js +++ b/implement/workflows/implement-loop.js @@ -199,6 +199,24 @@ const STANDING = 'design ledger) and `git log -10 --format=full`. Resolve the build/test commands ' + 'from those facts — do not assume a toolchain.' +// Standing-reading tiering (issue #31): the full standing read primes the +// judgment roles (implementer, reviewers, tester), but a schema-bound check +// stage consumes none of it — at ~40k+ real tokens per freshly-primed context +// the blanket read was pure priming tax on the highest-volume dispatches. +// Two slim variants, extending the narrowed per-role standing reading the +// named agents already practise (conventions.md § Standing reading): +// STANDING_FACTS — verify-type stages that run the suite: only the +// project-facts build/test commands, no git log. +// STANDING_NONE — tree/extraction stages (preflight, plan-index, +// plan-extract[-all], snapshot): nothing beyond their own instructions. +const STANDING_FACTS = + 'Project facts first: read ONLY the "## Skills plugin: project facts" section of the project\'s CLAUDE.md for ' + + 'the build and test commands — no other standing reading (no git log). This is a schema-bound check stage; ' + + 'do not assume a toolchain.' +const STANDING_NONE = + 'Schema-bound mechanical stage: NO standing reading (no CLAUDE.md, no git log) — do exactly what is instructed ' + + 'below, nothing else.' + // 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 @@ -516,7 +534,7 @@ let startSha = null if (mode === 'mini') { phase('Preflight') const pre = await agent( - `${STANDING}${ANCHOR}\n\nYou are the clean-tree preflight for an implement iteration. ` + + `${STANDING_NONE}${ANCHOR}\n\nYou are the clean-tree preflight for an implement iteration. ` + 'Run `git status --porcelain`, `git rev-parse HEAD`, `git rev-parse --abbrev-ref HEAD`, and ' + '`git rev-parse --show-toplevel`. ' + 'Report whether the tree is clean (no porcelain output), the HEAD sha, the branch, the repo_root, and any ' + @@ -584,7 +602,7 @@ if (mode === 'mini') { // 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. const index = await agent( - `${STANDING}${ANCHOR}\n\nFIRST the folded clean-tree preflight: run \`git status --porcelain\`, \`git rev-parse HEAD\`, ` + + `${STANDING_NONE}${ANCHOR}\n\nFIRST the folded clean-tree preflight: run \`git status --porcelain\`, \`git rev-parse HEAD\`, ` + '`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, ' + @@ -707,7 +725,7 @@ if (mode === 'mini') { let extractedTasks = null if (expectedIds.length > 1 && totalApprox !== null && totalApprox <= EXTRACT_ALL_BYTE_CEILING) { const all = await agent( - `${STANDING}${ANCHOR}\n\nRead the plan at ${plan_path}. Extract EXACTLY these tasks (1-based ids): ${expectedIds.join(', ')} — ` + + `${STANDING_NONE}${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, ' + '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 }, @@ -725,7 +743,7 @@ if (mode === 'mini') { extractedTasks = await parallel( expectedIds.map((id) => () => agent( - `${STANDING}${ANCHOR}\n\nRead the plan at ${plan_path}. Extract EXACTLY task ${id} (1-based) as its VERBATIM block — ` + + `${STANDING_NONE}${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 ' + '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 }, @@ -992,7 +1010,7 @@ for (const task of tasks) { results.push(r) if (tasks.length > 1) { const snap = await agent( - `${STANDING}${ANCHOR}\n\nRecord a working-tree snapshot boundary WITHOUT changing anything. Run exactly these two ` + + `${STANDING_NONE}${ANCHOR}\n\nRecord a working-tree snapshot boundary WITHOUT changing anything. Run exactly these two ` + 'commands and report their output:\n' + `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 ' + @@ -1077,7 +1095,7 @@ let treeState = null if (mode === 'mini' && outcome === 'DONE') { phase('Verify') miniVerify = await agent( - `${STANDING}${ANCHOR}\n\nVerify a bug/feature fix WITHOUT changing any code or test. The fix was meant to drive this ` + + `${STANDING_FACTS}${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 ` + '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 ' + @@ -1105,7 +1123,7 @@ if (mode === 'mini' && outcome === 'DONE') { } else if (mode === 'standard' && outcome === 'DONE') { phase('Verify') treeState = await agent( - `${STANDING}${ANCHOR}\n\nEnd-verify an implement iteration WITHOUT changing any code or test. Two independent checks:\n` + + `${STANDING_FACTS}${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 ' + '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 — ' +