Files
msd-core/tests/tracer-bullet.test.cjs
Tom Boucher 315d94f6d4 feat(#1945): tracer-first planning default + executor feedback gate (#2294)
* feat(#1945): tracer-first planning default + executor feedback gate

Make "thin end-to-end slice first, verify, then expand" the default planning + execution discipline instead of the opt-in --mvp mode.

- gsd-planner: first-class `type="tracer"` task; every plan LEADS with one production-quality end-to-end tracer slice by default; --no-tracer restores horizontal layers; --mvp/--tdd compose on top.
- gsd-executor + execute-plan: post-tracer feedback gate — autonomous runs halt-on-fail before expansion, interactive runs emit checkpoint:human-verify after the tracer.
- --no-tracer flag wired through plan-phase workflow/command/help/skill.
- CONTEXT.md glossary defines tracer bullet vs prototype; docs + references reconciled.
- tests/tracer-bullet.test.cjs: prose-contract + behavioral (verify plan-structure accepts tracer) coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1945): backfill changeset PR number to 2294

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 09:41:36 -04:00

371 lines
18 KiB
JavaScript

// allow-test-rule: source-text-is-the-product [#1945]
// Agent .md / workflow .md / command .md / reference .md / docs .md files —
// their text IS the deployed contract the runtime (and the changelog/docs
// surface) loads. The planner/executor "task type" enum and the tracer-first
// decomposition discipline are prose contracts, not compiled code, so the
// contract test asserts on the shipped text. The behavioral suite at the bottom
// exercises the ONE code seam (verify plan-structure) through the CLI.
/**
* Tracer-bullet vertical slices (#1945).
*
* Feature: make "thin end-to-end slice first, verify, then expand" a first-class,
* default planning + execution discipline (not an opt-in `--mvp` mode).
*
* 1. Planner — a first-class `tracer` task type + a tracer-first default.
* 2. Executor — a feedback gate after the tracer slice.
* 3. Terminology — `tracer bullet` promoted to the CONTEXT.md glossary.
*
* Acceptance criteria (verbatim from the issue) mapped to tests below.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const ROOT = path.join(__dirname, '..');
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf-8');
const PLANNER = read('agents/gsd-planner.md');
const EXECUTOR = read('agents/gsd-executor.md');
const EXECUTE_PLAN = read('gsd-core/workflows/execute-plan.md');
const WORKFLOW = read('gsd-core/workflows/plan-phase.md');
const COMMAND = read('commands/gsd/plan-phase.md');
const HELP_FULL = read('gsd-core/workflows/help/modes/full.md');
const MVP_REF = read('gsd-core/references/planner-mvp-mode.md');
const CONTEXT = read('CONTEXT.md');
const COMMANDS_DOC = read('docs/COMMANDS.md');
const PLAN_MD_REF = read('docs/reference/plan-md.md');
const HOWTO = read('docs/how-to/plan-a-phase.md');
const AGENTS_DOC = read('docs/AGENTS.md');
// ─── contract parsers (typed views over the deployed prose) ──────────────────
// Isolate the planner's default-decomposition section so we can prove tracer-first
// is NOT gated behind a flag/mode conditional.
function plannerTracerSection(md) {
const start = md.indexOf('## Tracer-First Decomposition');
if (start === -1) return '';
const rest = md.slice(start + 3);
const nextHeading = rest.search(/\n## /);
return nextHeading === -1 ? md.slice(start) : md.slice(start, start + 3 + nextHeading);
}
function parsePlannerContract(md) {
const section = plannerTracerSection(md);
return {
hasTracerFirstSection: section.length > 0,
// "default" and "not gated behind a flag" — the whole point of #1945.
declaresDefault: /\bdefault\b/i.test(section) && /not gated behind a flag/i.test(section),
leadsWithTracer: /LEADS with one `type="tracer"`/.test(section),
documentsTracerTaskType: /<task type="tracer">/.test(section),
// Production-quality, not a prototype (the book's core distinction).
productionQualityNotPrototype:
/production-quality, not a prototype/i.test(section) &&
/architectural gaps are not/i.test(section),
// A real, runnable END-TO-END verify (not a per-layer unit check).
endToEndVerify: /END-TO-END/i.test(section) && /not a per-layer unit test/i.test(section),
// --no-tracer / TRACER_MODE=false restores horizontal layers.
documentsNoTracerOptOut:
/--no-tracer/.test(section) && /TRACER_MODE=false/.test(section) && /horizontal layers/i.test(section),
// The break_into_tasks step itself leads with the tracer by default.
breakStepLeadsWithTracer:
/\*\*Lead with the tracer\.\*\*/.test(md) &&
/Unless `TRACER_MODE=false`/.test(md),
// Composition with --tdd (tracer starts red).
composesWithTdd: /TDD composition/i.test(section) && /starts red/i.test(section),
// MVP is now enrichment on top, not the toggle for vertical slices.
mvpIsEnrichment: /MVP enrichment/i.test(section) && /no longer \*turns on\* vertical slices/i.test(section),
};
}
function parseExecutorContract(md) {
return {
recognizesTracerType: /\*\*If `type="tracer"`:\*\*/.test(md),
// The gate runs BEFORE expansion tasks — an early integration checkpoint.
earlyIntegrationGate:
/tracer feedback gate BEFORE any expansion task/i.test(md) &&
/early integration checkpoint/i.test(md),
// Autonomous: halt-on-fail before any expansion task.
// Keyed on the file's own auto-mode definition (AUTO_CHAIN or AUTO_CFG),
// not AUTO_CFG alone — see <auto_mode_detection>.
autoHaltsOnFailure:
/Autonomous run \(auto mode active/i.test(md) &&
/`AUTO_CHAIN` or `AUTO_CFG`/.test(md) &&
/HALT and surface it/i.test(md) &&
/do NOT proceed to expansion tasks/i.test(md),
// Interactive: emit checkpoint:human-verify immediately after the tracer.
interactiveHumanVerify:
/Interactive run \(auto mode not active\)/i.test(md) &&
/checkpoint:human-verify/.test(md),
// Cross-referenced in the checkpoint protocol section too.
documentedInCheckpointProtocol: /\*\*Tracer feedback gate:\*\*/.test(md),
};
}
function parseWorkflowContract(md) {
const lines = md.split(/\r?\n/);
const argLine = lines.find((l) => l.includes('Extract from $ARGUMENTS:')) || '';
return {
argListDocumentsNoTracer: argLine.includes('--no-tracer'),
resolvesTracerMode:
md.includes('TRACER_MODE=true') &&
md.includes('--no-tracer') &&
md.includes('TRACER_MODE=false'),
injectsTracerModeToPlanner: /\*\*TRACER_MODE:\*\* \$\{TRACER_MODE\}/.test(md),
// Guard: must not eagerly @-import the reference (size-budget rule, mirrors
// tests/workflow-size-budget.test.cjs). An eager import is an @-path at line start.
noEagerImportOfMvpRef: !/^\s*@[^\n]*planner-mvp-mode\.md/m.test(md),
};
}
function parseCommandContract(md) {
const argHint = (md.split(/\r?\n/).find((l) => l.startsWith('argument-hint:')) || '');
return {
argHintHasNoTracer: argHint.includes('--no-tracer'),
flagsDocumentNoTracer: /- `--no-tracer` —/.test(md),
};
}
// ─── Suite 1: Planner — first-class tracer task + tracer-first default ────────
describe('#1945 planner: first-class tracer task + tracer-first default', () => {
const c = parsePlannerContract(PLANNER);
test('planner has a Tracer-First Decomposition section that is the DEFAULT (not flag-gated)', () => {
assert.ok(c.hasTracerFirstSection, 'planner must document a "Tracer-First Decomposition" section');
assert.ok(c.declaresDefault, 'the section must declare tracer-first the default, not gated behind a flag');
});
// Acceptance: with no flags, PLAN.md leads with exactly one tracer task touching every layer.
test('every plan LEADS with one type="tracer" task (acceptance #1)', () => {
assert.ok(c.leadsWithTracer, 'planner must instruct leading every plan with one type="tracer" task');
assert.ok(c.documentsTracerTaskType, 'planner must document the <task type="tracer"> shape');
assert.ok(c.breakStepLeadsWithTracer, 'the break_into_tasks step must lead with the tracer by default');
});
// Acceptance: the tracer includes a real end-to-end <verify>, not a per-layer unit check.
test('tracer task carries a real end-to-end <verify> (acceptance #2)', () => {
assert.ok(c.endToEndVerify, 'planner must require a real END-TO-END verify, not a per-layer unit test');
});
// Acceptance: --no-tracer reproduces today's horizontal-layer default.
test('--no-tracer / TRACER_MODE=false restores horizontal layers (acceptance #5)', () => {
assert.ok(c.documentsNoTracerOptOut, 'planner must document the --no-tracer horizontal-layer opt-out');
});
test('tracer is production-quality, not a prototype', () => {
assert.ok(c.productionQualityNotPrototype, 'planner must state a tracer is production-quality, not a prototype');
});
test('composes with --tdd (tracer starts red) and --mvp is enrichment on top', () => {
assert.ok(c.composesWithTdd, 'planner must document tracer + --tdd composition');
assert.ok(c.mvpIsEnrichment, 'planner must reframe MVP as enrichment, no longer the toggle for vertical slices');
});
test('vertical-slice reference is reconciled to tracer-first-by-default', () => {
assert.match(MVP_REF, /Tracer-First Decomposition/, 'reference title must reflect tracer-first');
assert.match(MVP_REF, /the \*\*default\*\* tracer-first decomposition/, 'reference must state tracer-first is the default');
assert.doesNotMatch(
MVP_REF,
/only when `MVP_MODE=true`/,
'reference must no longer gate vertical slices behind MVP_MODE only',
);
});
});
// ─── Suite 2: Executor — post-tracer feedback gate ───────────────────────────
describe('#1945 executor: post-tracer feedback gate', () => {
const c = parseExecutorContract(EXECUTOR);
test('executor recognizes type="tracer"', () => {
assert.ok(c.recognizesTracerType, 'executor must handle type="tracer"');
});
test('runs an early integration gate BEFORE expansion tasks', () => {
assert.ok(c.earlyIntegrationGate, 'executor must run the tracer verify as an early integration checkpoint before expansion');
});
// Acceptance: autonomous run halts before any expansion task on a failing tracer.
test('autonomous run HALTS before expansion on a failing tracer (acceptance #3)', () => {
assert.ok(c.autoHaltsOnFailure, 'autonomous run must halt (surfaced) before expansion when the tracer verify fails');
});
// Acceptance: interactive run presents a human-verify checkpoint after the tracer.
test('interactive run emits checkpoint:human-verify after the tracer (acceptance #4)', () => {
assert.ok(c.interactiveHumanVerify, 'interactive run must emit checkpoint:human-verify immediately after the tracer');
});
test('gate is cross-referenced in the checkpoint protocol', () => {
assert.ok(c.documentedInCheckpointProtocol, 'checkpoint protocol must cross-reference the tracer feedback gate');
});
// The execute-plan orchestrator has its OWN inline per-task dispatch (used for
// step-by-step / non-Claude-Code / inline execution) — it must know tracer too,
// else the gate silently no-ops on those paths.
test('execute-plan.md inline dispatch also handles type="tracer" with the gate', () => {
assert.match(EXECUTE_PLAN, /`type="tracer"`/, 'execute-plan.md inline dispatch must handle type="tracer"');
assert.match(EXECUTE_PLAN, /tracer feedback gate BEFORE any expansion task/i, 'execute-plan.md must run the tracer gate before expansion');
assert.match(EXECUTE_PLAN, /Auto mode active \(`AUTO_CHAIN` or `AUTO_CFG`\)/, 'execute-plan.md tracer gate must key on auto mode (AUTO_CHAIN or AUTO_CFG)');
});
});
// ─── Suite 3: Orchestrator + command wire --no-tracer ────────────────────────
describe('#1945 plan-phase orchestrator + command: --no-tracer wiring', () => {
const w = parseWorkflowContract(WORKFLOW);
const cmd = parseCommandContract(COMMAND);
test('workflow argument list documents --no-tracer', () => {
assert.ok(w.argListDocumentsNoTracer, 'plan-phase workflow must extract --no-tracer from $ARGUMENTS');
});
test('workflow resolves TRACER_MODE (default true, --no-tracer -> false)', () => {
assert.ok(w.resolvesTracerMode, 'workflow must resolve TRACER_MODE with a --no-tracer -> false path');
});
test('workflow injects TRACER_MODE into the planner subagent prompt', () => {
assert.ok(w.injectsTracerModeToPlanner, 'workflow must wire **TRACER_MODE:** ${TRACER_MODE} into the planner prompt');
});
test('workflow does not eagerly @-import planner-mvp-mode.md (size-budget guard)', () => {
assert.ok(w.noEagerImportOfMvpRef, 'planner-mvp-mode.md must stay lazily loaded by the planner, not eagerly imported');
});
test('command argument-hint and flags document --no-tracer', () => {
assert.ok(cmd.argHintHasNoTracer, 'command argument-hint must advertise --no-tracer');
assert.ok(cmd.flagsDocumentNoTracer, 'command flags list must document --no-tracer');
});
test('/gsd:help full listing documents --no-tracer', () => {
assert.match(HELP_FULL, /\[--no-tracer\]/, 'help/modes/full.md plan-phase usage line must list --no-tracer');
assert.match(HELP_FULL, /- `--no-tracer` —/, 'help/modes/full.md must describe the --no-tracer flag');
});
});
// ─── Suite 4: Terminology — CONTEXT glossary + docs ──────────────────────────
describe('#1945 glossary + docs', () => {
// Acceptance: CONTEXT.md glossary defines tracer bullet vs prototype.
test('CONTEXT.md glossary defines "Tracer Bullet" against "prototype" (acceptance #7)', () => {
assert.match(CONTEXT, /^### Tracer Bullet$/m, 'CONTEXT.md must have a ### Tracer Bullet glossary entry');
const start = CONTEXT.indexOf('### Tracer Bullet');
const entry = CONTEXT.slice(start, start + 1400);
assert.match(entry, /production-quality/i, 'entry must call a tracer production-quality');
assert.match(entry, /\bprototype\b/i, 'entry must contrast tracer with a prototype');
assert.match(entry, /throwaway/i, 'entry must describe a prototype as throwaway');
});
test('docs/COMMANDS.md documents the --no-tracer flag', () => {
assert.match(COMMANDS_DOC, /\| `--no-tracer` \|/, 'COMMANDS.md flag table must include --no-tracer');
});
test('docs/reference/plan-md.md task-types table includes tracer', () => {
assert.match(PLAN_MD_REF, /\| `tracer` \|/, 'plan-md.md Task types table must include a tracer row');
});
test('docs/how-to and docs/AGENTS reflect tracer-first + the executor gate', () => {
assert.match(HOWTO, /tracer/i, 'how-to must mention tracer-first');
assert.match(HOWTO, /--no-tracer/, 'how-to must mention the --no-tracer opt-out');
assert.match(AGENTS_DOC, /task types: auto, tracer/i, 'AGENTS.md must list tracer among task types');
assert.match(AGENTS_DOC, /Tracer feedback gate/i, 'AGENTS.md must describe the executor tracer gate');
});
});
// ─── Suite 5: Behavioral — the one code seam accepts tracer ──────────────────
// Acceptance #6: `tracer` is accepted everywhere the task-type enum is validated;
// no schema/validation path rejects it. `verify plan-structure` is the only code
// path that inspects <task type=...>. Prove it accepts tracer and never confuses
// a tracer for a checkpoint.
// Minimal valid PLAN.md; `taskType` and `n` let us sweep the tracer-count boundary.
function planWith({ taskType = 'auto', n = 1, autonomous = 'true' } = {}) {
const tasks = [];
for (let i = 0; i < n; i++) {
tasks.push(
`<task type="${taskType}">`,
` <name>Task ${i + 1}: End-to-end slice</name>`,
' <files>some/file.ts</files>',
' <action>Wire one path through every layer</action>',
' <verify><automated>echo ok</automated></verify>',
' <done>Happy path works end-to-end</done>',
'</task>',
'',
);
}
return [
'---',
'phase: 01-test',
'plan: 01',
'type: execute',
'wave: 1',
'depends_on: []',
'files_modified: [some/file.ts]',
`autonomous: ${autonomous}`,
'must_haves:',
' truths:',
' - "something is true"',
'---',
'',
'<tasks>',
'',
...tasks,
'</tasks>',
].join('\n');
}
function verifyPlan(tmpDir, content) {
const rel = path.join('.planning', 'phases', '01-test', '01-01-PLAN.md');
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-test'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, rel), content);
const result = runGsdTools(`verify plan-structure ${rel}`, tmpDir);
assert.ok(result.success, `verify plan-structure failed to run: ${result.error}`);
return JSON.parse(result.output);
}
describe('#1945 behavioral: verify plan-structure accepts type="tracer" (acceptance #6)', () => {
test('a type="tracer" plan validates with no errors', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const out = verifyPlan(tmpDir, planWith({ taskType: 'tracer', n: 1 }));
assert.strictEqual(out.valid, true, `tracer plan must be valid, errors: ${JSON.stringify(out.errors)}`);
assert.deepStrictEqual(out.errors, [], 'no validation path may reject a tracer task');
assert.ok(
!out.errors.some((e) => /tracer/i.test(e)) && !(out.warnings || []).some((w) => /tracer/i.test(w)),
'nothing may flag the tracer task type specifically',
);
});
// verify plan-structure is task-type-agnostic: it accepts any count of tracer
// tasks (0/1/2) with no type-based rejection. This supports acceptance #6; it is
// NOT a claim about the planner's "exactly one leading tracer" contract, which is
// planner prose (asserted in Suite 1), not something plan-structure validates.
test('verify plan-structure accepts 0 / 1 / 2 tracer tasks (type-agnostic, #6)', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
for (const n of [0, 1, 2]) {
const content = n === 0 ? planWith({ taskType: 'auto', n: 1 }) : planWith({ taskType: 'tracer', n });
const out = verifyPlan(tmpDir, content);
assert.strictEqual(out.valid, true, `${n}-tracer plan must be valid, errors: ${JSON.stringify(out.errors)}`);
}
});
// A tracer task is NOT a checkpoint: an autonomous:true tracer plan must not trip
// the "Has checkpoint tasks but autonomous is not false" rule.
test('a tracer task is not misclassified as a checkpoint', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const out = verifyPlan(tmpDir, planWith({ taskType: 'tracer', n: 1, autonomous: 'true' }));
assert.ok(
!out.errors.some((e) => /checkpoint/i.test(e)),
`tracer must not be treated as a checkpoint, errors: ${JSON.stringify(out.errors)}`,
);
});
});