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

@ -6,6 +6,9 @@
// PROGRESS_REQUIRED_TOP, so legacy progress.json without it still validates. The field's
// liveness (never-decremented) is enforced deterministically by the Phase 7.5 completion-gate
// cross-check in trekexecute, not by this per-validation shape check.
// The same holds for the wave-1 step-loop fields (per step: start_sha, report_path,
// red_proof, test_lock, fix_rounds 0..5; per run: decisions): shape-checked when
// present, never required.
import { readFileSync, existsSync } from 'node:fs';
import { issue, fail } from '../util/result.mjs';
@ -17,6 +20,45 @@ export const PROGRESS_VALID_STATUSES = ['pending', 'in_progress', 'completed', '
// know (lib/util/session-orders.mjs ENDED_BAD).
export const PROGRESS_STEP_STATUSES = ['completed', 'in_progress', 'failed', 'pending', 'deferred', 'skipped', 'blocked'];
// Wave 1 (P8/P9) per-step fields, all optional on read so an older progress.json
// still resumes; `null` means "not set yet". fix_rounds is the step's place on
// the upward ladder: 0 = passed its first control, 1-3 = fix rounds, 4 = sent up
// to the plan, 5 = sent up to the operator. There is no sixth round.
export const PROGRESS_MAX_FIX_ROUNDS = 5;
export const PROGRESS_RED_PROOF_VALUES = ['proven', 'rejected', 'not-run'];
export const PROGRESS_TEST_LOCK_VALUES = ['locked', 'released', 'not-locked'];
function checkStepFields(k, s, errors) {
const set = (v) => v !== undefined && v !== null;
if (s.fix_rounds !== undefined &&
!(Number.isInteger(s.fix_rounds) && s.fix_rounds >= 0 && s.fix_rounds <= PROGRESS_MAX_FIX_ROUNDS)) {
errors.push(issue('PROGRESS_FIX_ROUNDS_INVALID',
`steps["${k}"].fix_rounds=${JSON.stringify(s.fix_rounds)} must be an integer 0..${PROGRESS_MAX_FIX_ROUNDS}`));
}
if (set(s.start_sha) && !(typeof s.start_sha === 'string' && /^[0-9a-f]{7,64}$/.test(s.start_sha))) {
errors.push(issue('PROGRESS_START_SHA_INVALID', `steps["${k}"].start_sha must be a git sha, got ${JSON.stringify(s.start_sha)}`));
}
if (set(s.report_path) && !(typeof s.report_path === 'string' && s.report_path.trim() !== '')) {
errors.push(issue('PROGRESS_REPORT_PATH_INVALID', `steps["${k}"].report_path must be a non-empty path`));
}
if (set(s.red_proof) && !PROGRESS_RED_PROOF_VALUES.includes(s.red_proof)) {
errors.push(issue('PROGRESS_RED_PROOF_INVALID', `steps["${k}"].red_proof "${s.red_proof}" not in [${PROGRESS_RED_PROOF_VALUES.join(', ')}]`));
}
if (set(s.test_lock) && !PROGRESS_TEST_LOCK_VALUES.includes(s.test_lock)) {
errors.push(issue('PROGRESS_TEST_LOCK_INVALID', `steps["${k}"].test_lock "${s.test_lock}" not in [${PROGRESS_TEST_LOCK_VALUES.join(', ')}]`));
}
}
// The run-level decision log (P4): every decision the executor took on its own,
// with what it costs if it is wrong. Optional; when present, every entry carries both.
function checkDecisions(decisions, errors) {
if (decisions === undefined) return;
const ok = Array.isArray(decisions) && decisions.every((d) => d && typeof d === 'object' &&
typeof d.decision === 'string' && d.decision.trim() !== '' &&
typeof d.cost_if_wrong === 'string' && d.cost_if_wrong.trim() !== '');
if (!ok) errors.push(issue('PROGRESS_DECISION_INVALID', 'decisions must be an array of {decision, cost_if_wrong} with both non-empty'));
}
export function validateProgressContent(jsonText, opts = {}) {
let parsed;
try { parsed = JSON.parse(jsonText); }
@ -82,9 +124,12 @@ export function validateProgressObject(parsed, opts = {}) {
if (s.status !== undefined && !PROGRESS_STEP_STATUSES.includes(s.status)) {
warnings.push(issue('PROGRESS_STEP_BAD_STATUS', `steps["${k}"].status "${s.status}" unrecognized`));
}
checkStepFields(k, s, errors);
}
}
checkDecisions(parsed.decisions, errors);
return { valid: errors.length === 0, errors, warnings, parsed };
}