voyage/tests/lib/executor-prose-pins.test.mjs
Kjell Tore Guttormsen abc3083c1e
feat(execute): one fresh helper per step, a controller on the diff, proof by command
/trekexecute no longer builds steps itself. Each plan step is built by one
fresh helper (new agent step-implementer: opus, effort high, no Agent tool)
and judged by one controller (a general helper on opus) that reads the
diff from a recorded start_sha and gives two verdicts, spec and quality.
Fix rounds go up, maximum 5: three fixes by a fresh helper, then back to
the plan, then to the operator. No cheaper-model ladder.

Proof is a command, never the model's word:
- lib/verification/red-proof.mjs: the step's test must fail at the start
  point and pass at the result (exit 0/1/2; 2 is never green). Reproduces
  the 02243c6 red-first measurement (6 of 55) through the shim fixture.
- lib/verification/test-integrity.mjs: the floor guard on the diff
  (removed asserts, skipped/deleted tests, muted checks, stubs, loosened
  thresholds). Over the last 150 commits: 149 clean, 1 real finding.
- lib/verification/done-line.mjs: DONE step N only when the test command
  exits 0.

Test first is required from plan_version 1.8. The test lock (K8) is used
through its CLI and reported as not-locked while it is missing. A run
stops only on a closed list of four classes; every other call goes into a
decision log with its cost if wrong. Without a passing run-the-thing
criterion the result is not verified end to end, never green.

Handover 5 gains optional per-step start_sha, report_path, red_proof,
test_lock, fix_rounds (0-5) and run-level decisions and end_to_end; old
progress files still validate and resume.

No isolation on the helper: measured, a worktree-isolated subagent
branches from the default branch (02f320f), not the executor's HEAD
(2c89a6b), so it would miss the earlier steps.

Borrowings credited in commands/trekexecute.md ## Credits (Superpowers,
ai-native-sdlc, Kiro, GitHub Spec Kit, agent-skills, Cherny).

Tests: 1411 -> 1457 (1455 pass, 0 fail, 2 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:58:18 +02:00

199 lines
9.4 KiB
JavaScript

// tests/lib/executor-prose-pins.test.mjs
// Wave 1, process 1 (the executor): prose pins for P8 (fresh helper per step,
// one controller with two verdicts, fix rounds that go up), P9 on the executor
// side (proof of red, the floor guard, the done line, end to end, back to the
// architecture after three failed fixes) and P4 (the closed stop list and the
// decision log). The mechanisms themselves are tested as code in
// tests/lib/{red-proof,test-integrity,done-line}.test.mjs; these pins hold the
// command prose to them. Fix the SOURCE a pin guards, not the pin.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
const CMD = 'commands/trekexecute.md';
const AGENT = 'agents/step-implementer.md';
function frontmatter(text) {
const m = text.match(/^---\n([\s\S]*?)\n---/);
assert.ok(m, 'frontmatter missing');
return m[1];
}
const fmValue = (fm, key) => (fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm')) || [])[1];
/** The text between `## {start}` (or `#### {start}`) and the next heading of the same or higher level. */
function section(text, heading) {
const at = text.indexOf(heading);
assert.ok(at >= 0, `${CMD} must have "${heading}"`);
const level = heading.match(/^#+/)[0].length;
const rest = text.slice(at + heading.length);
const next = rest.search(new RegExp(`\\n#{1,${level}} `));
return heading + (next >= 0 ? rest.slice(0, next) : rest);
}
// --- P8: the Agent tool is back, for one purpose -------------------------------
test('P8: /trekexecute may spawn - Agent is allowed and no longer disallowed', () => {
const fm = frontmatter(read(CMD));
assert.match(fmValue(fm, 'allowed-tools'), /\bAgent\b/, 'allowed-tools must list Agent');
const disallowed = fmValue(fm, 'disallowed-tools') || '';
assert.doesNotMatch(disallowed, /\bAgent\b/, 'disallowed-tools must no longer remove Agent');
assert.match(disallowed, /\bTeamCreate\b/, 'TeamCreate stays removed - helpers, never a team');
});
test('P8: Hard rule 10 is rewritten - one fresh helper per plan step, and helpers never start helpers', () => {
const text = read(CMD);
assert.doesNotMatch(text, /\*\*No sub-agents\.\*\*/, 'the old "No sub-agents" rule must be gone');
const rule = text.slice(text.indexOf('10. **'), text.indexOf('11. **'));
assert.match(rule, /fresh helper per plan step/i);
assert.match(rule, /voyage:step-implementer/);
assert.match(rule, /helpers never start helpers/i);
assert.match(rule, /controller/i);
});
test('P8: the step agent exists, is opus at effort >= high, and cannot spawn', () => {
assert.ok(existsSync(join(ROOT, AGENT)), `${AGENT} must exist`);
const fm = frontmatter(read(AGENT));
assert.equal(fmValue(fm, 'name'), 'step-implementer');
assert.equal(fmValue(fm, 'model'), 'opus');
assert.ok(['high', 'xhigh', 'max'].includes(fmValue(fm, 'effort')), 'effort must be >= high');
assert.doesNotMatch(fmValue(fm, 'tools') || '', /\bAgent\b/, 'a helper must not hold the Agent tool');
// Measured 2026-10-06 (CC 2.1.291): a worktree-isolated helper branched from
// the default branch (02f320f), not from the executor's HEAD (2c89a6b), and so
// did not see the earlier steps. A plugin cannot set worktree.baseRef. So no
// isolation: the helper works in the executor's tree, from a recorded start_sha.
assert.doesNotMatch(fm, /^isolation:/m, 'no isolation: worktree on the step agent (measured: wrong base)');
});
test('P8: the helper report contract is ran / saw / did NOT check', () => {
const body = read(AGENT);
for (const h of ['### Ran', '### Saw', '### Did NOT check']) assert.ok(body.includes(h), `${AGENT} report must have "${h}"`);
assert.ok(read(CMD).includes('Did NOT check'), 'the command must name the report contract too');
});
test('P8: hand-over is in files, from a recorded start point', () => {
const text = read(CMD);
assert.match(text, /start_sha/);
assert.match(text, /report_path/);
assert.match(text, /git diff \{start_sha\}/, 'the review package is the diff from the recorded start point');
});
test('P8: one controller, two verdicts, reads the diff not the summary, re-checks only what was fixed', () => {
const ctl = section(read(CMD), '#### Sub-step E — Control');
assert.match(ctl, /one controller/i);
assert.match(ctl, /two verdicts/i);
assert.match(ctl, /"spec"/);
assert.match(ctl, /"quality"/);
assert.match(ctl, /reads the diff, not the helper's (summary|report)/i);
assert.match(ctl, /only what (was|the round) fixed/i);
assert.match(ctl, /general-purpose/, 'the controller is a general helper with a prompt from the command, not agent no. 2');
});
test('P8: fix rounds go up - maximum 5, plan then operator, never a cheaper-model ladder', () => {
const fix = section(read(CMD), '#### Sub-step F — Fix rounds');
assert.match(fix, /maximum 5 fix rounds[^\n]*upward/i);
assert.match(fix, /back to (the )?planning/i);
assert.match(fix, /operator/i);
assert.match(fix, /does not improvise/i);
assert.match(fix, /no ladder of cheaper models/i);
// P9 (7): after three failed fixes the question is the architecture, not a fourth fix.
assert.match(fix, /three failed fix(es| rounds)[^\n]*architecture/i);
});
test('P8: the no-name rule covers the step helper and the controller', () => {
const text = read(CMD);
assert.ok(text.includes('in_process_teammate'));
assert.match(text, /never pass .*`name`/i);
assert.match(text, /^Launch\b/m, 'the spawn site starts with "Launch" (the S85 detector reads it)');
});
// --- P9 (executor side) ---------------------------------------------------------
test('P9 (1): proof of red is a command and it blocks - the old "do not block" sentence is gone', () => {
const text = read(CMD);
assert.doesNotMatch(text, /Do not block on test-first failures/);
assert.match(text, /lib\/verification\/red-proof\.mjs/);
const red = section(text, '#### Sub-step B — Test first and proof of red');
assert.match(red, /must FAIL/);
assert.match(red, /must PASS/);
assert.match(red, /not-run/);
assert.match(red, /never (a pass|green|proven)/i);
});
test('K4: Test first is required from plan_version 1.8; Interface in/out and Review focus are read, not required', () => {
const text = read(CMD);
assert.match(text, /plan_version ≥ 1\.8[^\n]*`Test first:`|`Test first:`[^\n]*plan_version ≥ 1\.8/);
for (const f of ['Interface in:', 'Interface out:', 'Review focus:']) assert.ok(text.includes(f), `${CMD} must read ${f}`);
assert.match(text, /tolerat(e|es) (their|its) absence|never required/i);
});
test('K8: the test lock is used through its CLI, and a missing CLI is said out loud', () => {
const text = read(CMD);
assert.match(text, /lib\/util\/test-lock\.mjs lock --files/);
assert.match(text, /test-lock\.mjs unlock/);
assert.match(text, /"not-locked"/);
assert.match(text, /never silent/i);
});
test('P9 (2): the floor guard runs on the step diff and exit 2 is never green', () => {
const text = read(CMD);
assert.match(text, /lib\/verification\/test-integrity\.mjs --base \{start_sha\}/);
assert.match(text, /exit 2[^\n]*never (green|a pass)/i);
});
test('P9 (4): the done line is written by a script, only when the test command exits 0', () => {
const text = read(CMD);
assert.match(text, /lib\/verification\/done-line\.mjs/);
assert.match(text, /done-line\.mjs --check/);
assert.match(text, /only (when|if) the test command exits 0/i);
});
test('P9 (6): without a run-the-thing check the result is "not verified end to end", never green', () => {
const text = read(CMD);
assert.match(text, /not verified end to end/);
assert.match(text, /end_to_end/);
});
test('P9: the three new CLIs the prose names exist', () => {
for (const f of ['red-proof.mjs', 'test-integrity.mjs', 'done-line.mjs']) {
assert.ok(existsSync(join(ROOT, 'lib', 'verification', f)), `lib/verification/${f}`);
}
});
// --- P4: the closed stop list and the decision log ------------------------------
test('P4: the run stops only on the closed list of four, and the final report shows it', () => {
const text = read(CMD);
const list = section(text, '### Closed stop list');
for (const cls of [/cannot be undone/i, /security-sensitive/i, /outside the working copy/i, /so broken that (every|any)thing (after it )?is guesswork/i]) {
assert.match(list, cls);
}
assert.match(list, /closed/i);
// Phase 8's report template is a fence that holds its own "## " heading, so
// the phase is read up to Phase 9, not up to the next heading.
const report = text.slice(text.indexOf('## Phase 8 — Final report'), text.indexOf('## Phase 9'));
assert.match(report, /### Stops/);
assert.match(report, /### Decisions taken/);
});
test('P4: every decision the executor took is logged with what it costs if it is wrong', () => {
const text = read(CMD);
assert.match(text, /cost_if_wrong/);
assert.match(text, /"decisions"/);
});
// --- Credits: every borrowing is named, never silent ----------------------------
test('every borrowing in the executor is credited', () => {
const text = read(CMD);
const credits = section(text, '## Credits');
for (const src of [/Superpowers/, /subagent-driven-development/, /ai-native-sdlc/, /Kiro/, /Spec Kit/, /agent-skills/, /Cherny/]) {
assert.match(credits, src);
}
assert.match(credits, /MIT/);
});