Wave 2, order 6. /trekexecute now: - refuses a plan whose logged approval does not cover its current sha256 (K2, approval-log verify --kind plan), before step 1; - runs the step check (K9) and the step's optional Regression: (K10, new lib/verification/regression-check.mjs through the criteria runner) at step end, before the checkpoint; exit 2 is "not measured", never green; - runs the architecture rules (K5) and the plan-file sweep (K6) after the last step; a violation or a file outside the plan turns completed into partial; - reviews by itself in the closing run (Phase 7.2): run the thing, /simplify, /trekreview, verdict-gate; a BLOCK adds the review's remediation steps after the plan validator accepts them and the operator re-approves the plan, capped at TREKEXECUTE_MAX_REVIEW_ROUNDS (3); - makes the main-merge gate a lock on the verdict: BLOCK or undecided keeps the merge closed; when open, the executor merges and the guard's question is the one question (operator decision 2026-10-08). The --gates pause before the wave merge-back is gone for the same reason. The guard refuses a merge into main and a push in the permission mode that shows no prompts, since no one can be asked (operator decision 2026-10-09; two mode-read exemptions approved by the operator). progress.json gains optional coupling fields (Handover 5); the validator rejects the runner's exitCode in plan_verification.failed_criteria (C15). Pointer lines name the skills test-first and debugging (S2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
5.8 KiB
JavaScript
104 lines
5.8 KiB
JavaScript
// tests/lib/coupling-prose-pins.test.mjs
|
|
// Wave 2, order 6 — prose pins for the coupling in commands/trekexecute.md:
|
|
// the review at the end of a run (P11), the one question before a merge
|
|
// (operator decision 2026-10-08: the guard stays, the executor's own question
|
|
// goes where it doubled), the checks wired in at step end and in Phase 7
|
|
// (K5, K6, K9, K10), and the pointer lines to the skills `test-first` and
|
|
// `debugging` (S2). The behaviour behind each pin runs in
|
|
// tests/commands/trekexecute-coupling.test.mjs.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const CMD = readFileSync(join(ROOT, 'commands', 'trekexecute.md'), 'utf8');
|
|
|
|
function section(text, heading) {
|
|
const at = text.indexOf(`\n${heading}\n`);
|
|
assert.ok(at >= 0, `trekexecute.md must have the heading "${heading}"`);
|
|
const level = heading.match(/^#+/)[0].length;
|
|
const lines = text.slice(at + heading.length + 2).split('\n');
|
|
const stop = new RegExp(`^#{1,${level}} `);
|
|
let fenced = false;
|
|
const out = [];
|
|
for (const l of lines) {
|
|
if (/^\s*(```|~~~)/.test(l)) fenced = !fenced;
|
|
else if (!fenced && stop.test(l)) break;
|
|
out.push(l);
|
|
}
|
|
return out.join('\n');
|
|
}
|
|
|
|
test('S2: Sub-step B asks for test-first by name, Sub-step F for debugging', () => {
|
|
assert.match(section(CMD, '#### Sub-step B — Test first and proof of red'), /the `test-first` skill/);
|
|
assert.match(section(CMD, '#### Sub-step F — Fix rounds'), /the `debugging` skill/);
|
|
const c = section(CMD, '#### Sub-step C — Implement through one fresh helper');
|
|
assert.match(c, /`skill: test-first`/, 'the build helper is told the skill by name');
|
|
assert.match(c, /`skill: debugging`/, 'the fix helper is told the skill by name');
|
|
// The skill is the advice, the gate is the guarantee: proof of red stays a command.
|
|
assert.match(section(CMD, '#### Sub-step B — Test first and proof of red'), /red-proof\.mjs --base \{start_sha\}/);
|
|
});
|
|
|
|
test('P11: the review runs by itself after Phase 7, in Cherny\'s order, before Phase 8', () => {
|
|
const p = section(CMD, '## Phase 7.2 — Simplify, review, verdict (P11)');
|
|
const order = ['run the thing', 'Skill("simplify"', 'Skill("voyage:trekreview"', 'verdict-gate.mjs'];
|
|
const at = order.map((s) => p.indexOf(s));
|
|
at.forEach((i, k) => assert.ok(i >= 0, `Phase 7.2 must name ${order[k]}`));
|
|
assert.deepEqual([...at].sort((a, b) => a - b), at, `order must be ${order.join(' -> ')}`);
|
|
assert.ok(CMD.indexOf('## Phase 7.2 —') > CMD.indexOf('## Phase 7 —'), 'after Phase 7');
|
|
assert.ok(CMD.indexOf('## Phase 7.2 —') < CMD.indexOf('## Phase 8 —'), 'before Phase 8');
|
|
assert.match(CMD.split('\n---\n')[0], /\nallowed-tools:[^\n]*\bSkill\b/, 'the command may use the Skill tool');
|
|
assert.match(p, /never typed by the operator/i);
|
|
});
|
|
|
|
test('P11: a BLOCKER becomes new numbered plan steps, in a bounded loop that ends with the operator', () => {
|
|
const p = section(CMD, '## Phase 7.2 — Simplify, review, verdict (P11)');
|
|
assert.match(p, /TREKEXECUTE_MAX_REVIEW_ROUNDS/, 'the cap is named');
|
|
assert.match(p, /default \*\*3\*\*/);
|
|
assert.match(p, /### Step R<n>/, 'the review\'s remediation steps are the source');
|
|
assert.match(p, /never reworded/);
|
|
assert.match(p, /at the cap[^\n]*operator/i);
|
|
assert.match(p, /not improvised/);
|
|
});
|
|
|
|
test('P11: nothing is dropped silently - simplify edits outside the run are reverted and listed', () => {
|
|
const p = section(CMD, '## Phase 7.2 — Simplify, review, verdict (P11)');
|
|
assert.match(p, /outside the run's changed files/i);
|
|
assert.match(p, /"reverted"/);
|
|
assert.match(p, /"not-run"/, 'a simplify that could not start is said, not hidden');
|
|
});
|
|
|
|
test('P12: one question before a merge - the guard asks, the executor does not ask again', () => {
|
|
const gate = section(CMD, '### Main-merge gate (MAIN_MERGE_GATE — always pauses)');
|
|
assert.doesNotMatch(gate, /AskUserQuestion/, 'the gate is a lock, not a question');
|
|
assert.match(gate, /operator decision 2026-10-08/);
|
|
assert.match(gate, /the guard's question is the one question/i);
|
|
const step3 = section(CMD, '### Step 3 — Collect wave W (a later run)');
|
|
assert.doesNotMatch(step3, /pause for operator\s+confirmation before merge-back/, 'no executor pause before the wave merge');
|
|
const allDone = /- \*\*`all_completed: true`:\*\*([\s\S]*?)\n\n/.exec(step3);
|
|
assert.ok(allDone, 'Step 3 has its all_completed rule');
|
|
assert.match(allDone[1], /The executor asks\s+nothing here, `gates_mode` or not/);
|
|
assert.doesNotMatch(allDone[1], /AskUserQuestion|pause for operator/i);
|
|
assert.match(step3, /one Bash call/, 'push and merge go in one call, so the guard asks once');
|
|
assert.match(section(CMD, '## Hard rules'), /never asks again/);
|
|
});
|
|
|
|
test('P7/P3/P9: K9 at step end, K5 and K6 in Phase 7', () => {
|
|
const d4 = section(CMD, '#### Sub-step D4 — Step check and regression');
|
|
assert.match(d4, /lib\/verification\/step-check\.mjs"? --cwd "\$CHECK_CWD"/);
|
|
const p7 = section(CMD, '### Architecture rules (K5) and the plan-file sweep (K6)');
|
|
assert.match(p7, /lib\/verification\/arch-rules\.mjs"? --repo "\$CHECK_CWD"/);
|
|
assert.match(p7, /lib\/verification\/plan-scope-sweep\.mjs"? --plan "\{plan_path\}" --base "\{session_start_sha\}"/);
|
|
assert.match(p7, /a finding in the final report with the file list/, 'a K6 deviation is a finding, not a silent warning');
|
|
assert.match(p7, /not measured/);
|
|
});
|
|
|
|
test('Credits: every borrowing of the coupling is named', () => {
|
|
const credits = section(CMD, '## Credits');
|
|
for (const s of [/Cherny[^\n]*order/i, /Kiro[^\n]*gate/i, /Spec Kit[^\n]*loop/i, /Superpowers[^\n]*silent/i]) {
|
|
assert.match(credits, s);
|
|
}
|
|
});
|