feat(workflows): tier the standing reading for schema-bound stages

The anonymous script stages received the full standing-reading
instruction (project CLAUDE.md + git log -10 --format=full) although
a tree-footprint or text-extraction stage consumes none of it and a
verify stage needs only the build/test commands — re-executed on
every dispatch including tight repair rounds, at ~40k+ real tokens
per freshly-primed context.

Two slim variants in both workflow scripts: STANDING_FACTS
(verify-type stages — mini-verify, standard end-verify,
compiler-driven build/suite verify: project-facts build/test commands
only) and STANDING_NONE (tree/extraction stages — preflight,
plan-index, plan-extract[-all], snapshot: nothing beyond their own
instructions). Judgment roles (implementer, spec/quality reviewers,
tester/e2e, compiler-driven edit stage) keep the full read.
conventions.md § Standing reading and agent-template.md § Standing
reading list carry the tiering sentence; the precedent is the
narrowed per-role reading the named agents already practise
(tester -3, plan-recon/grounding-check -5, fieldtester -8,
docwriter --oneline).

closes #31
This commit is contained in:
2026-07-17 15:39:06 +02:00
parent 44f45d16a9
commit fe39658d67
4 changed files with 54 additions and 8 deletions
+6
View File
@@ -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 facts. The agent's body says, prosaically: "Read everything in the
standing reading list before doing anything else." 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 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 glossary is implicitly part of the always list, so every role reads the
project glossary without a per-role entry. project glossary without a per-role entry.
+13
View File
@@ -122,6 +122,19 @@ project facts (`standing reading`). If the project declares a
glossary, that file is implicitly standing reading for every role glossary, that file is implicitly standing reading for every role
too. 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 ## Git discipline
- **Only the orchestrator commits.** No skill agent runs `git commit`. - **Only the orchestrator commits.** No skill agent runs `git commit`.
+10 -1
View File
@@ -113,6 +113,15 @@ 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.'
// 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 — // 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 // 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 // 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 2 — verify (the independently-invokable phase) ---------------
phase('Verify') phase('Verify')
const verify = await agent( 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. ' + '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` ' +
+25 -7
View File
@@ -199,6 +199,24 @@ 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.'
// 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 // Working-directory anchor (issue #25): in worktree sessions a dispatched
// agent's cwd can resolve to the PRIMARY checkout instead of the worktree the // 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 // loop operates on — observed as a per-dispatch race producing false
@@ -516,7 +534,7 @@ let startSha = null
if (mode === 'mini') { if (mode === 'mini') {
phase('Preflight') phase('Preflight')
const pre = await agent( 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 ' + 'Run `git status --porcelain`, `git rev-parse HEAD`, `git rev-parse --abbrev-ref HEAD`, and ' +
'`git rev-parse --show-toplevel`. ' + '`git rev-parse --show-toplevel`. ' +
'Report whether the tree is clean (no porcelain output), the HEAD sha, the branch, the repo_root, and any ' + '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 // 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}${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), ' + '`git rev-parse --abbrev-ref HEAD`, and `git rev-parse --show-toplevel`; report `clean` (no porcelain output), ' +
'`head_sha`, `branch`, `repo_root`, and any ' + '`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, ' +
@@ -707,7 +725,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}${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, ' + '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 },
@@ -725,7 +743,7 @@ if (mode === 'mini') {
extractedTasks = await parallel( extractedTasks = await parallel(
expectedIds.map((id) => () => expectedIds.map((id) => () =>
agent( 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 ' + '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 },
@@ -992,7 +1010,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}${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' + '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 ' +
@@ -1077,7 +1095,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}${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 ` + `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 ' +
@@ -1105,7 +1123,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}${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 ' + '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 — ' +