fix(execute): run the plan's Verification on the single-session path (D-03)

A trekplan's `## Verification` section is where the brief's success criteria
land. Phase 7 opened with "**Skip for trekplans.**", and only the multi-session
wave path (Phase 2.6 Step 3) ran master verification. A plan executed in ONE
session therefore reported `completed` without ever running the criteria it was
measured against — the executor's own belief was the only evidence.

The check now exists as code, not as an instruction:

- `lib/verification/criteria-runner.mjs` parses the criteria an artifact
  DECLARES (a plan's `## Verification`, a brief's `## Success Criteria`), runs
  each command, and returns a verdict built from exit codes. Fail-closed
  throughout: a placeholder, a prose-only criterion, or an unavailable screen
  is `unrunnable`/`blocked`, never `passed`. A plan with no `## Verification`
  section exits 1 — a plan that promises no end-to-end check cannot be reported
  as verified.
- Every command is screened through the plugin's own PreToolUse denylist
  (`hooks/scripts/pre-bash-executor.mjs`) before it reaches a shell. Chose
  invoking that hook over its documented stdin protocol rather than copying its
  rules, because a command spawned from node never passes through the Bash tool
  and so the hook cannot fire by itself — this keeps exactly one denylist.
- Phase 7 is now "Exit / verification check": session specs run the exit
  condition, trekplans run the criteria runner. Phase 4's entry-condition skip
  for trekplans stands — a plan carries no entry condition; the exit side is
  not symmetrical.
- A failing criterion FELLS the run: `plan_verification.status != "passed"`
  forbids `result: completed`. That is clause 2 of the stop-signal contract,
  now enforced on the single-session path too.

Red first: `tests/lib/criteria-runner.test.mjs` (26 tests) against two committed
fixture plans, one of which declares a criterion that fails on purpose. The
doc pin in `tests/lib/doc-consistency.test.mjs` guards the wiring — a capability
no phase calls is the same defect wearing a lib/ file; verified red against the
pre-fix Phase 7 (skip present, runner absent, no fell-the-run clause).

Suite: 1108 (1106/0/2), up 27 from 1081.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-18 01:30:57 +02:00
commit e55ca9fc89
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
6 changed files with 756 additions and 6 deletions

View file

@ -1829,3 +1829,26 @@ test('D-07: trekresearch launch rules inject the resolved model instead of a bla
);
assert.match(rules, /phase_signal_result\.model/, 'the launch rules must name the resolved model the spawn sites inject');
});
// End-state defect D-03: a trekplan's `## Verification` is where the brief's success
// criteria land. Phase 7 used to say "Skip for trekplans", so on the single-session path
// the criteria were never run and `completed` meant "the executor believed it". The
// behaviour lives in lib/verification/criteria-runner.mjs; this pin guards the WIRING —
// a capability no phase calls is the same defect wearing a lib/ file. Fix the SOURCE.
test('D-03: trekexecute Phase 7 runs the plan Verification on the single-session path', () => {
const t = read('commands/trekexecute.md');
const phase7 = (t.split('\n## Phase 7 — ')[1] || '').split('\n## ')[0];
assert.ok(phase7.length > 0, 'trekexecute.md must still carry a Phase 7 section');
assert.ok(
!/^\*\*Skip for trekplans\.\*\*/m.test(phase7),
'Phase 7 may no longer skip trekplans — that skip IS defect D-03',
);
assert.match(
phase7, /criteria-runner\.mjs" --plan/,
'Phase 7 must invoke the criteria runner over the plan file, not describe the check in prose',
);
assert.match(
phase7, /MUST NOT emit `result: completed`/,
'a failing criterion must fell the run, not be noted in the final report',
);
});