test(execute): pin the main-merge gate and the wave's branch on the wave path (red)

Round 3 of the review of the session-order flow:

- the orders dir is cleared only under the main-merge gate's confirm
  (wave path, next-wave none, Step 4 passed); Step 6 keeps it, and a
  declined gate keeps the merged-waves record
- the gate text is true on the wave path: the waves are already merged
  on the current branch, nothing is finalized, and re-running the same
  command (not --resume) returns to the gate; the single-session
  --resume statement stays
- the report footer names the multi-session way to continue
- wave.json stores the branch the wave was prepared on, and Step 3's
  status call (executed from the doc in a temp repo) refuses another
  branch, keeping wave.json

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-02 16:22:08 +02:00
commit d9e091c143
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q

View file

@ -392,12 +392,8 @@ test('trekexecute.md Step 3 archives before any worktree goes, records the merge
}
const step6 = section('### Step 6 — Aggregate results', '### --session N mode');
assert.ok(step6.includes('$ORDERS_DIR/wave-*-session-*.progress.json'), 'Step 6 reads the archived copies');
assert.ok(step6.includes('lib/util/session-orders.mjs" clear'), 'Step 6 clears the orders dir');
assert.match(step6, /`none`[\s\S]*Step 4 passed|Step 4 passed[\s\S]*`none`/,
'the clear is keyed on every wave merged AND Step 4 passed, not on the Phase 8 label');
const p8 = section('## Phase 8 — Final report', '## Phase 9 —');
assert.ok(!p8.includes('lib/util/session-orders.mjs" clear'), 'Phase 8 no longer decides the clear');
assert.ok(!step6.includes('lib/util/session-orders.mjs" clear'),
'Step 6 keeps the orders dir: a declined main-merge gate must still find the merged-waves record');
});
test('trekexecute.md: Phase 7.6 promises no unwired re-run and names a path that still exists; Hard rule 12 carries the awaiting-sessions exception', () => {
@ -680,6 +676,88 @@ test('trekexecute.md: the stop paths name their way out', () => {
assert.match(step3, /MERGE_REFUSED[\s\S]*keep `wave\.json`/i, 'a refused merge stops and keeps the wave in flight');
});
test('the orders dir is cleared only under the main-merge gate\'s confirm; decline keeps the merged-waves record', () => {
const p26 = section('## Phase 2.6 —', '## Phase 3 ');
const step0 = section('### Step 0 — Resolve paths', '### Step 1 —');
const clears = p26.split('lib/util/session-orders.mjs" clear').length - 1;
const step0Clears = step0.split('lib/util/session-orders.mjs" clear').length - 1;
assert.equal(clears, step0Clears, 'Phase 2.6 clears nothing itself; Step 0 only names the command for a record of another plan');
const gate = section('### Main-merge gate', '### Progress + state-file writes');
const iConfirm = gate.indexOf('On confirm:');
const iDecline = gate.indexOf('On decline:');
const iClear = gate.indexOf('lib/util/session-orders.mjs" clear');
assert.ok(iConfirm >= 0 && iClear > iConfirm && iDecline > iClear, 'the clear sits under On confirm, before On decline');
assert.equal(gate.split('lib/util/session-orders.mjs" clear').length - 1, 1, 'exactly one clear, under confirm');
const confirm = gate.slice(iConfirm, iDecline);
assert.match(confirm, /`none`/, 'only when next-wave said none');
assert.match(confirm, /Step 4 passed/, 'only when master verification passed');
const decline = gate.slice(iDecline);
assert.match(decline, /merged-waves record is kept/, 'decline keeps the record');
});
test('the main-merge gate tells the truth on the wave path: merged on the current branch, nothing finalized, re-run returns to the gate', () => {
const gate = section('### Main-merge gate', '### Progress + state-file writes');
assert.doesNotMatch(gate, /Wave commits remain on their session branches/, 'by the gate the waves are merged');
assert.doesNotMatch(gate, /trekplan\/<slug>\/session-N/, 'the branches are trek/{slug}/session-N');
assert.match(gate, /already merged on the current branch/);
assert.match(gate, /Nothing is finalized/);
assert.match(gate, /re-run the same `\/trekexecute` command[\s\S]*not `--resume`/,
'the wave path continues by re-running the command (Phase 2.5 runs --resume as a single session)');
assert.match(gate, /returns to this gate/);
assert.ok(gate.includes('--resume re-enters'), 'the single-session --resume statement stays');
const footer = section('### What Remains', '**Result vocabulary');
assert.ok(footer.includes('To resume (single session): /trekexecute --resume {path}'));
assert.match(footer, /multi-session[^\n]*re-run the same \/trekexecute command/);
});
test('wave.json remembers the branch the wave was prepared on; status refuses another branch', () => {
withRepo((root) => {
const sessionDir = join(root, '.claude', 'trekplan-sessions', 'demo');
const run = (...a) => cli(root, sessionDir, ...a, '--plan', PLAN_REL);
assert.equal(run('write', '--wave', '1', '--branch', 'main').status, 0);
assert.equal(JSON.parse(readFileSync(join(sessionDir, 'orders', 'wave.json'), 'utf-8')).original_branch, 'main');
assert.equal(run('status', '--wave', '1', '--branch', 'main').status, 0);
const r = run('status', '--wave', '1', '--branch', 'feature-x');
assert.equal(r.status, 1);
assert.match(r.stderr, /switch back to main/i);
assert.match(r.stderr, /feature-x/);
});
});
test('Step 3 stops, touching nothing, when the main checkout is on another branch than the wave was prepared on', () => {
withGitRepo((root) => {
const env = { CLAUDE_PLUGIN_ROOT: ROOT };
const step0 = fill(bashBlock('### Step 0 — Resolve paths'), { slug: 'demo', 'plan-path': PLAN_REL })
+ '\nprintf "%s\\n" "$ORIGINAL_BRANCH"\n';
const vars = () => {
const r = runBash(step0, root, env);
assert.equal(r.status, 0, r.stderr);
const sd = join(root, '.claude', 'trekplan-sessions', 'demo');
return { REPO_ROOT: root, VOYAGE_ROOT: ROOT, PLAN_REL, SESSION_DIR: sd, ORIGINAL_BRANCH: r.stdout.trim().split('\n').pop() };
};
const prepared = vars();
const write = runBash(fill(bashBlock('### Step 2 — Write the session orders'), { W: '1' }), root, prepared);
assert.equal(write.status, 0, write.stderr);
const waveFile = join(prepared.SESSION_DIR, 'orders', 'wave.json');
assert.equal(JSON.parse(readFileSync(waveFile, 'utf-8')).original_branch, prepared.ORIGINAL_BRANCH);
const status = fill(bashBlock('### Step 3 — Collect wave W'), { W: '1' });
gitc(root, 'checkout', '-q', '-b', 'elsewhere');
let r = runBash(status, root, vars());
assert.equal(r.status, 1, 'collect on another branch stops');
assert.ok(r.stderr.includes(prepared.ORIGINAL_BRANCH), 'the message names the branch to switch back to');
assert.ok(existsSync(waveFile), 'wave.json is kept');
gitc(root, 'checkout', '-q', prepared.ORIGINAL_BRANCH);
r = runBash(status, root, vars());
assert.equal(r.status, 0, r.stderr);
});
const step3 = section('### Step 3 — Collect wave W', '### Step 4 —');
assert.match(step3, /another branch/, 'Step 3 says what the exit-1 stop covers');
});
test('CLI refuses a plan without an Execution Strategy, loudly', () => {
withRepo((root) => {
writeFileSync(join(root, PLAN_REL), '# Plan\n\n## Verification\n');