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>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-06 19:58:18 +02:00
commit abc3083c1e
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
28 changed files with 2052 additions and 103 deletions

View file

@ -0,0 +1,88 @@
// tests/lib/done-line.test.mjs
// The done line (P9 (4)): a step's "done" is written by a script, and only when
// the test command exits 0. Exit 0 = written, 1 = the command failed (nothing
// written), 2 = could not run (nothing written, never green).
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const CLI = join(ROOT, 'lib', 'verification', 'done-line.mjs');
function sh(cwd, cmd, args) {
const r = spawnSync(cmd, args, { cwd, encoding: 'utf8' });
assert.equal(r.status, 0, `${cmd} ${args.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
}
function repo() {
const dir = mkdtempSync(join(tmpdir(), 'done-line-'));
sh(dir, 'git', ['init', '-q']);
sh(dir, 'git', ['config', 'user.email', 't@example.invalid']);
sh(dir, 'git', ['config', 'user.name', 'test']);
mkdirSync(join(dir, 'tests'));
writeFileSync(join(dir, 'tests', 'ok.test.mjs'), "import { test } from 'node:test';\ntest('ok', () => {});\n");
writeFileSync(join(dir, 'tests', 'bad.test.mjs'), "import { test } from 'node:test';\ntest('bad', () => { throw new Error('red'); });\n");
sh(dir, 'git', ['add', 'tests']);
sh(dir, 'git', ['commit', '-q', '-m', 'tests']);
return dir;
}
const run = (args) => spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8' });
test('done line: the test command exits 0 -> one DONE line appended, exit 0', () => {
const dir = repo();
const report = join(dir, 'step-3.report.md');
try {
writeFileSync(report, '# Step 3 report\n');
const r = run(['--cwd', dir, '--step', '3', '--cmd', 'node --test tests/ok.test.mjs', '--report', report]);
assert.equal(r.status, 0, r.stdout + r.stderr);
const head = sh(dir, 'git', ['rev-parse', 'HEAD']);
const lines = readFileSync(report, 'utf8').split('\n').filter((l) => l.startsWith('DONE '));
assert.equal(lines.length, 1);
assert.match(lines[0], new RegExp(`^DONE step 3: \`node --test tests/ok\\.test\\.mjs\` exit 0 at ${head} \\d{4}-\\d{2}-\\d{2}T`));
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('done line: the test command fails -> nothing written, exit 1', () => {
const dir = repo();
const report = join(dir, 'step-3.report.md');
try {
writeFileSync(report, '# Step 3 report\n');
const r = run(['--cwd', dir, '--step', '3', '--cmd', 'node --test tests/bad.test.mjs', '--report', report]);
assert.equal(r.status, 1, r.stdout + r.stderr);
assert.equal(readFileSync(report, 'utf8'), '# Step 3 report\n');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('done line: a command outside the test-runner allowlist is not run -> exit 2, nothing written', () => {
const dir = repo();
const report = join(dir, 'step-3.report.md');
try {
for (const cmd of ['echo done', 'node --test tests/bad.test.mjs || true', 'true']) {
const r = run(['--cwd', dir, '--step', '3', '--cmd', cmd, '--report', report]);
assert.equal(r.status, 2, `${cmd}: ${r.stdout}${r.stderr}`);
}
assert.equal(existsSync(report), false);
assert.equal(run(['--cwd', dir, '--cmd', 'node --test tests/ok.test.mjs', '--report', report]).status, 2, 'no --step');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('done line: --check finds the line for that step only', () => {
const dir = repo();
const report = join(dir, 'step-3.report.md');
try {
assert.equal(run(['--check', '--step', '3', '--report', report]).status, 1, 'no report file = not done');
writeFileSync(report, '# Step 3 report\nDONE step 3 (typed by hand)\n');
assert.equal(run(['--check', '--step', '3', '--report', report]).status, 1, 'a hand-typed line is not the script line');
run(['--cwd', dir, '--step', '3', '--cmd', 'node --test tests/ok.test.mjs', '--report', report]);
assert.equal(run(['--check', '--step', '3', '--report', report]).status, 0);
assert.equal(run(['--check', '--step', '4', '--report', report]).status, 1);
} finally { rmSync(dir, { recursive: true, force: true }); }
});