voyage/tests/lib/fix-round-4-5a-prose.test.mjs
2026-10-24 20:41:53 +02:00

1729 lines
108 KiB
JavaScript

// tests/lib/fix-round-4-5a-prose.test.mjs
// Prose pins for fix round 4-5a (the serious findings, plan
// .claude/projects/2026-10-20-fix-round-serious-findings): the fixes that are
// text a session or a helper reads. The name is the brief's;
// lib/util/test-census.mjs counts it as a behaviour test (it does not match
// PIN_FILE_RE). Every test carries its id tag in brackets, and every tagged
// test fails on an assertion at ab6b32c (the round's start point).
// #23 code-correctness-reviewer cites a "MANDATORY OVERRIDE rule above"
// that is no longer there; the sentence says the rule itself.
// #24 research-scout says "Try Tavily first" but its tools held no Tavily
// tool; its tools line now holds both, in docs-researcher's order.
// #111 A reviewer drops a finding that feels off-tier only when it can say
// why it is wrong, and then as one Declined to Judge line with that
// reason; the skill's line agrees; an unknown rule_key is routed to
// `unverified`, not dropped (lib/review/coordinator-contract.mjs).
// Fix the SOURCE a pin guards, not the pin.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { findSection } from '../../lib/parsers/plan-schema.mjs';
import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs';
import { childEnv } from '../helpers/child-env.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
/** A repo file's text; the file must exist. */
function read(rel) {
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
return readFileSync(join(ROOT, rel), 'utf8');
}
/** Whitespace (line breaks too) collapsed to one space. */
const flat = (s) => s.replace(/\s+/g, ' ').trim();
/**
* The text of a heading's section, up to the next heading of the same or a
* higher level outside a fence. The heading must exist.
*/
function section(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}
/** From the line holding `marker` to the first line where `stop` is true, flattened. */
function partAt(rel, marker, stop) {
const lines = read(rel).split('\n');
const start = lines.findIndex((l) => l.includes(marker));
assert.ok(start !== -1, `${rel} must hold "${marker}"`);
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) if (stop(lines[i])) { end = i; break; }
return flat(lines.slice(start, end).join(' '));
}
/** The single line in `text` that runs `lib/<mod>` (and holds `verb`, when given). */
function line(text, mod, verb = '') {
const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
const re = new RegExp(`^\\s*node \\$\\{CLAUDE_PLUGIN_ROOT\\}/lib/${esc}\\b`);
const hits = text.split('\n').filter((l) => re.test(l) && l.includes(verb));
assert.equal(hits.length, 1, `expected exactly one \`${mod}${verb ? ` ${verb}` : ''}\` line; found ${hits.length}`);
return hits[0].trim();
}
/** Run a command line from the prose with the plugin root filled in. */
function bash(cmd, cwd) {
return spawnSync('bash', ['-c', cmd.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT)], { cwd, encoding: 'utf8' });
}
// --- #23 ---------------------------------------------------------------------------
test('[#23] no agent file refers to a MANDATORY OVERRIDE rule', () => {
const hits = readdirSync(join(ROOT, 'agents'))
.filter((f) => f.endsWith('.md'))
.filter((f) => read(`agents/${f}`).includes('MANDATORY OVERRIDE'));
assert.deepEqual(hits, [], 'an agent still points at a MANDATORY OVERRIDE rule that is not there');
const d5 = flat(section(read('agents/code-correctness-reviewer.md'), '### 5. Placeholder code — `PLACEHOLDER_IN_CODE` (MAJOR)'));
assert.match(d5, /Never accept "not implemented" placeholders as fine "for now"/);
});
// --- #24 ---------------------------------------------------------------------------
test('[#24] research-scout holds both Tavily tools, in docs-researcher\'s order, and still tries Tavily first', () => {
const text = read('agents/research-scout.md');
const fm = readFrontmatterFields(text);
assert.ok(fm.ok, fm.error);
assert.deepEqual(fm.fields.tools.value,
['WebSearch', 'WebFetch', 'Read', 'mcp__tavily__tavily_search', 'mcp__tavily__tavily_research']);
assert.match(text, /\*\*Try Tavily first\*\*/);
});
// --- #111 --------------------------------------------------------------------------
/** No bare "drop it" choice; a drop needs a reason, written as one Declined to Judge line. */
function dropsOnlyWithTrace(part, where) {
assert.doesNotMatch(part, /either drop (it|the finding)|or drop (it|the finding)\s*\.|drop (it|the finding) \(it was wrong\)/i,
`${where}: an off-tier finding may still be dropped with no trace`);
assert.match(part, /drop[^.]{0,60}only when you can say why it is wrong[^.]{0,120}Declined to Judge[^.]{0,60}with that reason/i,
`${where}: a drop must carry its reason on a Declined to Judge line`);
assert.match(part, /catalogue('s)? (tier|severity)/i, `${where}: the catalogue tier stays the default`);
// Every sentence that drops (other than "not / never dropped") carries the reason and the trace.
for (const s of part.split(/(?<=\.)\s+/)) {
if (!/\bdrop/i.test(s) || /\b(not|never) dropped\b/i.test(s)) continue;
assert.match(s, /only when you can say why it is wrong/i, `${where}: a drop sentence names no reason: ${s}`);
assert.match(s, /Declined to Judge/i, `${where}: a drop sentence leaves no Declined to Judge line: ${s}`);
}
}
const toBlank = (l) => l.trim() === '';
// Untagged: these test the helper itself, so they pass at ab6b32c by construction.
const COMPLIANT_DROP = 'If a finding feels less severe than its catalogue tier, keep the catalogue tier; drop it only '
+ 'when you can say why it is wrong, and then write one Declined to Judge line with that reason.';
test('step 25: dropsOnlyWithTrace throws on a compliant off-tier sentence followed by " Drop it."', () => {
assert.throws(() => dropsOnlyWithTrace(`${COMPLIANT_DROP} Drop it.`, 'planted'), /a drop sentence names no reason: Drop it\./);
});
test('step 25: dropsOnlyWithTrace does not throw on the compliant sentence alone (control)', () => {
assert.doesNotThrow(() => dropsOnlyWithTrace(COMPLIANT_DROP, 'control'));
});
test('[#111] code-correctness-reviewer dimension 4: a nice-to-have test gap is dropped only with a Declined to Judge line', () => {
dropsOnlyWithTrace(partAt('agents/code-correctness-reviewer.md', 'When the brief explicitly asks for tests', toBlank),
'code-correctness-reviewer dimension 4');
});
test('[#111] code-correctness-reviewer severity rules: an off-tier finding is dropped only with a Declined to Judge line', () => {
dropsOnlyWithTrace(partAt('agents/code-correctness-reviewer.md', 'If a finding feels off-tier', toBlank),
'code-correctness-reviewer ## Severity rules');
});
test('[#111] code-correctness-reviewer rules: "No silent severity downgrades" drops only with a Declined to Judge line', () => {
dropsOnlyWithTrace(partAt('agents/code-correctness-reviewer.md', '**No silent severity downgrades.**', (l) => /^- /.test(l) || toBlank(l)),
'code-correctness-reviewer ## Rules');
});
test('[#111] brief-conformance-reviewer severity rules: a finding is dropped only with a Declined to Judge line', () => {
dropsOnlyWithTrace(partAt('agents/brief-conformance-reviewer.md', 'If a finding feels less severe than its catalogue tier', toBlank),
'brief-conformance-reviewer ## Severity rules');
});
test('[#111] reviewing-adversarially: keep the tier, or drop as wrong with its reason on a Declined to Judge line', () => {
const s5 = flat(section(read('skills/reviewing-adversarially/SKILL.md'), '### 5. No downgrade without a reason'));
assert.match(s5, /keep the tier, or drop the finding as wrong with its reason on a Declined to Judge line; never soften it in between/);
assert.doesNotMatch(s5, /keep the tier or drop the finding as wrong;/);
});
/** The stale "unknown rule keys are dropped" line now says they are routed to `unverified`. */
function routedNotDropped(part, where) {
assert.doesNotMatch(part, /dropped by (the coordinator's reasonableness filter|Pass 3)/i, `${where}: still says unknown rule keys are dropped`);
assert.match(part, /routed to `unverified`/, `${where}: must say an unknown rule key is routed to \`unverified\``);
}
const toBullet = (l) => /^- /.test(l) || toBlank(l) || /^#/.test(l);
test('[#111] code-correctness-reviewer: an unknown rule_key is routed to `unverified`, not dropped', () => {
routedNotDropped(partAt('agents/code-correctness-reviewer.md', '`rule_key` MUST be in the catalogue', toBullet),
'code-correctness-reviewer ## JSON output rules');
});
test('[#111] brief-conformance-reviewer: an unknown rule_key is routed to `unverified`, not dropped', () => {
routedNotDropped(partAt('agents/brief-conformance-reviewer.md', '`rule_key` MUST be in the catalogue', toBullet),
'brief-conformance-reviewer ## JSON output rules');
});
test('[#111] review-coordinator input: a rule_key outside the catalogue is routed to `unverified` by Pass 3', () => {
routedNotDropped(partAt('agents/review-coordinator.md', '**Rule catalogue** — `lib/review/rule-catalogue.mjs`', toBullet),
'review-coordinator ## Input');
});
// The coordinator's Pass 3 as its 4-pass lists say it (agents/review-coordinator.md ### Pass 3).
const PASS3_ITEM = '**Cloudflare reasonableness** — drop a finding with no `file:line` citation (`no-citation`) or whose '
+ 'file is in neither the working tree nor the diff (`file-existence:refuted`); route one whose `rule_key` is not in '
+ '`RULE_CATALOGUE` (`unknown-rule_key`) or whose file cannot be checked (`file-existence:indeterminate`) to '
+ '`unverified`, which forbids `ALLOW`; reset a severity that does not match its catalogue tier.';
/** Item 3 of a 4-pass list: from "3. **Cloudflare reasonableness**" to item 4. */
const pass3Item = (rel) => partAt(rel, '3. **Cloudflare reasonableness**', (l) => /^4\. /.test(l));
/** No item 3 says an unknown rule_key or a "catalogue-violating" finding is dropped. */
function pass3DropsNoUnknownKey(item, where) {
assert.doesNotMatch(item, /catalogue-violating/, `${where}: item 3 still drops catalogue-violating findings`);
assert.doesNotMatch(item, /drop findings whose `rule_key` is not in/, `${where}: item 3 still drops an unknown rule_key`);
}
test('[#111] sibling: agents/review-orchestrator.md item 3 says what Pass 3 does', () => {
const item = pass3Item('agents/review-orchestrator.md');
assert.ok(item.includes(`3. ${PASS3_ITEM}`), `item 3 is not the Pass 3 sentence: ${item}`);
pass3DropsNoUnknownKey(item, 'agents/review-orchestrator.md');
});
test('[#111] sibling: commands/trekreview.md item 3 says what Pass 3 does, skipped in quick mode', () => {
const item = pass3Item('commands/trekreview.md');
const want = `3. ${PASS3_ITEM.replace('**Cloudflare reasonableness** —', '**Cloudflare reasonableness** (skipped in `quick` mode) —')}`;
assert.ok(item.includes(want), `item 3 is not the Pass 3 sentence: ${item}`);
pass3DropsNoUnknownKey(item, 'commands/trekreview.md');
});
test('[#111] sibling: the reasonablenessFilter docblock routes an unknown rule_key to `unverified`', () => {
const doc = partAt('lib/review/coordinator-contract.mjs', 'Pass 3 — Cloudflare reasonableness (deterministic subset)',
(l) => /^\s*\*\/\s*$/.test(l));
assert.ok(doc.includes('route an unknown rule_key to `unverified`'), `the docblock does not route an unknown rule_key: ${doc}`);
assert.ok(!doc.includes('or an unknown rule_key;'), 'the docblock still drops an unknown rule_key');
});
// Steps 2-24 use these helpers; referenced here so the file states its interface.
test('helpers: line and bash are the shared line-runner pair', () => {
assert.equal(typeof line, 'function');
assert.equal(typeof bash, 'function');
assert.equal(line('x\n node ${CLAUDE_PLUGIN_ROOT}/lib/util/a.mjs go\n', 'util/a.mjs', 'go'), 'node ${CLAUDE_PLUGIN_ROOT}/lib/util/a.mjs go');
const r = bash('printf %s "${CLAUDE_PLUGIN_ROOT}"', ROOT);
assert.equal(r.status, 0);
assert.equal(r.stdout, ROOT);
});
// --- #2 ----------------------------------------------------------------------------
// planning-orchestrator.md is a reference document that nothing runs; the text
// that sent a reader to it ("re-run", "must emit", "should have prevented",
// Consumer/Producer) now names /trekplan.
test('[#2] /trekexecute never sends anyone to re-run the planning reference document', () => {
const text = flat(read('commands/trekexecute.md'));
assert.doesNotMatch(text, /re-run planning-orchestrator/i, 'still says to re-run planning-orchestrator');
assert.doesNotMatch(text, /planning-orchestrator must/i, 'still says planning-orchestrator must emit or include');
assert.doesNotMatch(text, /planning-orchestrator should have prevented/i, 'still says planning-orchestrator should have prevented this');
assert.match(text, /Re-run \/trekplan — the plan is not executable\./);
assert.match(text, /Fix: re-run \/trekplan — the plan must use v1\.7 format/);
assert.match(text, /Fix: re-run \/trekplan — every step needs a Manifest YAML block/);
assert.doesNotMatch(text, /validator should have prevented this/, 'Check 3 still names a validator check that does not exist');
assert.match(text, /Stop execution\. This check runs at execution time, so it also catches a plan that was edited by hand after planning\./);
});
test('[#2] sibling: commands/trekbrief.md names /trekplan as what finishes', () => {
const text = flat(read('commands/trekbrief.md'));
assert.doesNotMatch(text, /When the planning-orchestrator finishes/);
assert.match(text, /When \/trekplan finishes/);
});
test('[#2] sibling: agents/brief-reviewer.md names /trekplan Phase 4b as its reader', () => {
const text = flat(read('agents/brief-reviewer.md'));
assert.doesNotMatch(text, /`planning-orchestrator` Phase 1b/);
assert.match(text, /\/trekplan Phase 4b/);
});
test('[#2] sibling: docs/HANDOVER-CONTRACTS.md has /trekplan, not planning-orchestrator, as Consumer, Producer and the one that must run', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
assert.doesNotMatch(text, /^\*\*(Consumer|Producer):\*\*[^\n]*planning-orchestrator/m, 'planning-orchestrator is still a Consumer or Producer');
assert.doesNotMatch(text, /planning-orchestrator\)? \*\*must\*\* run/, 'planning-orchestrator is still the one that must run the validator');
const lines = text.split('\n');
for (const want of [
'**Consumer:** `/trekplan` Phase 1 (project-dir auto-discovery) + `/trekplan` Phase 3 and Phase 7 (consumes findings as context).',
'**Consumer:** `/trekplan` Phase 1 (architecture-discovery) + `/trekplan` Phase 7 (cross-reference architecture-note as priors during synthesis).',
'**Producer:** `/trekplan` Phase 8 (plan synthesis, then the schema self-check via `plan-validator --strict`).',
]) assert.ok(lines.includes(want), `missing line: ${want}`);
assert.match(text, /`\/trekplan` Phase 8 \*\*must\*\* run `plan-validator --strict` before handing the plan to plan-critic\./);
});
test('[#2] sibling: docs/architecture.md names /trekplan Phase 8 as the plan-validator caller', () => {
const text = read('docs/architecture.md');
assert.doesNotMatch(text, /`planning-orchestrator` Phase 5\.5/);
assert.match(text, /^- `\/trekplan` Phase 8 → `plan-validator --strict` \(replaces 3 `grep -cE` calls\)$/m);
});
// --- #26 ---------------------------------------------------------------------------
test('[#26] session-decomposer says to re-run /trekplan, not the planning reference document', () => {
const text = flat(read('agents/session-decomposer.md'));
assert.doesNotMatch(text, /Re-run planning-orchestrator/i);
assert.match(text, /Re-run \/trekplan\./);
});
test('[#26] the three reference documents hold read-only tools', () => {
for (const name of ['planning-orchestrator', 'research-orchestrator', 'review-orchestrator']) {
const fm = readFrontmatterFields(read(`agents/${name}.md`));
assert.ok(fm.ok, fm.error);
assert.deepEqual(fm.fields.tools.value, ['Read', 'Glob', 'Grep'], `${name}: tools must be Read, Glob, Grep`);
assert.equal(fm.fields.model.value, 'opus', `${name}: model stays opus`);
}
});
// --- #52 ---------------------------------------------------------------------------
// /trekplan's "execute" answer began implementing the plan in the planning
// session, past /trekexecute's approval check (K2), test lock and step helpers.
// It now prints the /trekexecute line and stops.
/** The `/trekexecute` line /trekplan prints, in both forms. */
function printsTrekexecuteLine(part, where) {
assert.match(part, /`\/trekexecute --project \{project_dir\}`/, `${where}: must print the /trekexecute --project line`);
assert.match(part, /`\/trekexecute \{plan_path\}`/, `${where}: must print the /trekexecute {plan_path} line`);
}
/**
* No text that has /trekplan build the plan itself: no "Begin implementing",
* and no "in this session" other than the one prohibition sentence.
*/
function neverImplementsHere(part, where) {
const rest = part.split('Do not implement in this session.').join('');
assert.doesNotMatch(rest, /Begin implementing/i, `${where}: still begins implementing`);
assert.doesNotMatch(rest, /in this session/i, `${where}: still implements in this session`);
}
test('[#52] Phase 11 "execute": prints the /trekexecute line and stops, never implements in this session', () => {
const block = flat(section(read('commands/trekplan.md'), '### "execute" / "go" / "start"'));
neverImplementsHere(block, 'Phase 11 "execute"');
assert.match(block, /^### "execute" \/ "go" \/ "start" Do not implement in this session\./);
printsTrekexecuteLine(block, 'Phase 11 "execute"');
assert.match(block, /without a project directory — and stop\./);
assert.match(block, /`\/trekexecute` is what checks the plan's approval \(K2\), locks the tests, runs one step helper per step and the review\./);
});
test('[#52] Phase 11 "team": no team path, the same /trekexecute line, then stop', () => {
const block = flat(section(read('commands/trekplan.md'), '### "execute with team" / "team"'));
neverImplementsHere(block, 'Phase 11 "team"');
assert.doesNotMatch(block, /proceed exactly as for "execute"/, 'Phase 11 "team": still proceeds as "execute" did');
assert.match(block, /There is no team path\. Print the same `\/trekexecute` line and stop; parallel work is `--decompose` followed by `\/trekexecute`'s session orders\./);
});
test('[#52] Phase 10 menu: "execute" is for the /trekexecute line, and a stopped gate withholds it', () => {
const p10 = flat(section(read('commands/trekplan.md'), '## Phase 10 — Present and refine'));
assert.doesNotMatch(p10, /start implementing/i, 'Phase 10 still offers or withholds "start implementing"');
assert.match(p10, /- Say \*\*"execute"\*\* for the `\/trekexecute` line to run ← only when the gate exited 0/);
assert.match(p10, /`Execution is blocked: \{the gate's reasons\}` and do not print the `\/trekexecute` line, whatever is said next, until the gate exits 0\./);
});
test('[#52] sibling: plan mode step 3 goes through Phase 11 (its plan approval, then the /trekexecute line) and stops', () => {
const pm = flat(section(read('commands/trekplan.md'), '### In plan mode'));
assert.ok(pm.includes('Then go through Phase 11 (its plan approval, then the `/trekexecute` line) and stop — leaving plan mode is not an order to implement. '
+ 'Phase 11 asks again because the plan-mode approval is logged under Claude Code\'s plan file, and `/trekexecute` checks the copy at the plan destination.'),
'plan mode step 3 does not go through Phase 11\'s plan approval');
assert.doesNotMatch(pm, /Then continue with the operator's choice \(Phase 11\)\./, 'plan mode still continues with the operator\'s choice');
assert.ok(!pm.includes('Then print the `/trekexecute` line (Phase 11) and stop'), 'plan mode still skips Phase 11\'s plan approval');
});
test('[#52] hard rule: "No premature execution" holds, and nothing in Phases 10-11 implements the plan', () => {
const text = read('commands/trekplan.md');
const rules = flat(section(text, '## Hard rules'));
assert.match(rules, /\*\*No premature execution\*\*: Do not modify any project files until the user explicitly approves the plan, and never while `plan-gate\.mjs` exits non-zero on it\./);
for (const h of ['## Phase 10 — Present and refine', '## Phase 11 — Handoff']) {
const part = flat(section(text, h));
neverImplementsHere(part, h);
assert.doesNotMatch(part, /start implementing|Follow the plan exactly|Mark each step complete/i, `${h}: still has /trekplan build the plan`);
}
});
test('[#52] sibling: agents/planning-orchestrator.md says "execute" gives the /trekexecute line', () => {
const text = read('agents/planning-orchestrator.md');
assert.doesNotMatch(text, /Say "execute" to implement/, 'planning-orchestrator still says "execute" implements');
assert.ok(text.split('\n').includes('- Say "execute" for the /trekexecute line'), 'missing line: - Say "execute" for the /trekexecute line');
});
// --- #48 / #12 / #57 ---------------------------------------------------------------
// The planning and research spawn sites pinned `model: "opus"`, past the
// resolved model (brief signal > profile > agent frontmatter); every spawn now
// takes `phase_signal_result.model`. No `maxTurns` budget is promised: the
// Agent tool takes none, so "scale turns by size" was a promise nothing kept.
/**
* From the line holding `marker` (it must be the only one) to the end of the
* blank-line-separated paragraph after the marker's own paragraph, flattened.
*/
function paraAfter(rel, marker) {
const lines = read(rel).split('\n');
const hits = lines.filter((l) => l.includes(marker)).length;
assert.equal(hits, 1, `${rel}: expected exactly one line holding "${marker}"; found ${hits}`);
const start = lines.findIndex((l) => l.includes(marker));
let i = start;
while (i < lines.length && lines[i].trim() !== '') i++;
while (i < lines.length && lines[i].trim() === '') i++;
while (i < lines.length && lines[i].trim() !== '') i++;
return flat(lines.slice(start, i).join('\n'));
}
const PASS_MODEL = /Pass `model: \{phase_signal_result\.model\}` at this spawn; without it \(the `--brief` branch, where the resolver does not run, or a resolver that printed nothing\) the agent's own `model:` applies\./;
test('[#48] commands/trekplan.md pins no spawn to `model: "opus"`', () => {
assert.doesNotMatch(read('commands/trekplan.md'), /model: "opus"/);
});
for (const [where, marker] of [
['brief-reviewer (Phase 4b)', 'Launch the **brief-reviewer** agent.'],
['### Always launch (Phase 5)', '### Always launch (all codebase sizes):'],
['research-scout (Phase 5)', 'launch **research-scout** in'],
['plan-critic and scope-guardian (Phase 9)', 'it traces into `## Verification`.'],
]) {
test(`[#48] ${where}: the spawn takes phase_signal_result.model, the agent's own model: as the fallback`, () => {
const part = paraAfter('commands/trekplan.md', marker);
assert.match(part, PASS_MODEL, `${where}: missing the resolved-model paragraph after "${marker}"`);
assert.doesNotMatch(part, /model: "opus"/);
});
}
test('[#48] sibling: session-decomposer (Phase 1.5): --decompose has no resolved model, so the spawn passes no model:', () => {
const part = paraAfter('commands/trekplan.md', 'Launch the **session-decomposer** agent:');
assert.ok(part.includes('Phase 1 runs the model resolver only for `--project`, so `--decompose` has no resolved model: this spawn '
+ 'passes no `model:`, and the agent\'s own `model:` applies.'), `the session-decomposer paragraph does not say it passes no model: ${part}`);
assert.ok(!part.includes('phase_signal_result'), 'the session-decomposer spawn still names phase_signal_result');
assert.doesNotMatch(part, /model: "opus"/);
});
test('[#48] convention-scanner (Phase 5): the spawn takes phase_signal_result.model, the frontmatter as the fallback', () => {
const part = paraAfter('commands/trekplan.md', 'use the `convention-scanner` plugin agent');
assert.match(part, /\(model: phase_signal_result\.model, the agent's frontmatter as the fallback\)/);
assert.doesNotMatch(part, /model: "opus"/);
});
test('[#48] deep-dives (Phase 6): the spawn takes phase_signal_result.model, else opus', () => {
const part = paraAfter('commands/trekplan.md', 'spawn a targeted deep-dive agent');
assert.match(part, /\(model: phase_signal_result\.model, else opus — Explore is Claude Code's built-in agent and has no Voyage frontmatter to fall back to, subagent_type: "Explore"\)/);
assert.doesNotMatch(part, /model: "opus"/);
});
test('[#48] sibling: agents/planning-orchestrator.md:137 takes phase_signal_result.model', () => {
const text = read('agents/planning-orchestrator.md');
assert.doesNotMatch(text, /model: "opus"/);
const part = partAt('agents/planning-orchestrator.md', '**Convention Scanner** — use the `convention-scanner` plugin agent', toBlank);
assert.match(part, /\(model: phase_signal_result\.model, the agent's frontmatter as the fallback\)/);
});
test('[#12] trekresearch Phase 5 follow-ups take phase_signal_result.model, else opus', () => {
const text = read('commands/trekresearch.md');
const p5 = flat(section(text, '## Phase 5 — Targeted follow-ups'));
assert.doesNotMatch(p5, /model: "opus"/);
const part = partAt('commands/trekresearch.md', 'launch a targeted follow-up agent', toBlank);
assert.match(part, /launch a targeted follow-up agent \(model: phase_signal_result\.model, else opus — a follow-up may be one of Claude Code's built-in agents, which have no Voyage frontmatter to fall back to; see Launch rules\)/);
assert.match(flat(text), /captured as `phase_signal_result` and used at Agent-spawn sites in Phase 4 and Phase 5 to inject the resolved model/);
});
test('[#57] commands/trekresearch.md promises no maxTurns budget', () => {
const text = read('commands/trekresearch.md');
assert.doesNotMatch(text, /maxTurns/);
assert.match(text, /- convention-scanner: medium\+ codebases only \(50\+ files\)/);
});
test('[#57] sibling: commands/trekplan.md promises no maxTurns budget, and Phase 4 calibrates no turns', () => {
const text = read('commands/trekplan.md');
assert.doesNotMatch(text, /maxTurns/);
const p4 = flat(section(text, '## Phase 4 — Codebase sizing'));
assert.doesNotMatch(p4, /calibrate agent turns/);
assert.match(p4, /Determine codebase scale to decide whether convention-scanner runs\./);
assert.match(text, /^\*\*All agents run for all codebase sizes\.\*\*$/m);
});
test('[#57] sibling: agents/planning-orchestrator.md promises no maxTurns budget and scales no turns', () => {
const text = read('agents/planning-orchestrator.md');
assert.doesNotMatch(text, /maxTurns/);
assert.doesNotMatch(text, /Scale turns/i);
assert.match(text, /^\*\*All agents run for all codebase sizes\.\*\*$/m);
assert.match(text, /^- \*\*Adaptive:\*\* All agents run for all sizes\.$/m);
});
test('keep: /trekexecute\'s controller and its model note stay pinned to opus', () => {
const text = read('commands/trekexecute.md');
assert.match(text, /`subagent_type: "general-purpose"`, `model: "opus"`, no `name`/);
assert.match(flat(text), /controller's `model: "opus"` — so `phase_signal_result\.model` still has no spawn site here/);
});
// --- #46 ---------------------------------------------------------------------------
// Six commands said "Stats records emit `profile`, `profile_source` ..." under
// a stats heredoc that writes none of those keys. A paragraph outside a code
// fence that mentions stats may name a profile key only when the command's own
// `stats-append.mjs <kind> <<'JSON'` record writes it.
const STATS_PROFILE_KEYS = ['profile', 'phase_models', 'parallel_agents', 'external_research_enabled',
'profile_source', 'model_used', 'phase_models_resolved'];
/** The keys of every `stats-append.mjs <kind> <<'JSON'` … `JSON` heredoc in `text`. */
function statsHeredocs(text) {
const out = [];
for (const m of text.matchAll(/stats-append\.mjs (\S+) <<'JSON'\n([\s\S]*?)\nJSON\s*$/gm)) {
out.push({ kind: m[1], keys: new Set([...m[2].matchAll(/"([A-Za-z_][A-Za-z0-9_]*)"\s*:/g)].map((k) => k[1])) });
}
return out;
}
/**
* Every backticked profile key named in a paragraph (outside code fences,
* split on blank lines) that mentions stats, and not in `written`; sorted.
*/
function unwrittenStatsKeys(text, written) {
const paras = [];
let cur = [];
let fenced = false;
for (const l of text.split('\n')) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(l)) { fenced = !fenced; paras.push(cur); cur = []; continue; }
if (fenced) continue;
if (l.trim() === '') { paras.push(cur); cur = []; } else cur.push(l);
}
paras.push(cur);
const bad = new Set();
for (const p of paras.map((x) => x.join('\n')).filter((p) => /stats/i.test(p))) {
for (const m of p.matchAll(/`([^`\n]+)`/g)) {
if (STATS_PROFILE_KEYS.includes(m[1]) && !written.has(m[1])) bad.add(m[1]);
}
}
return [...bad].sort();
}
for (const cmd of ['trekbrief', 'trekresearch', 'trekplan', 'trekexecute', 'trekreview', 'trekcontinue']) {
test(`[#46] commands/${cmd}.md: every profile key its stats prose names is one its stats record writes`, () => {
const text = read(`commands/${cmd}.md`);
const docs = statsHeredocs(text);
assert.equal(docs.length, 1, `${cmd}: expected exactly one stats-append.mjs heredoc; found ${docs.length}`);
assert.equal(docs[0].kind, cmd, `${cmd}: the stats heredoc writes kind ${docs[0].kind}`);
assert.ok(docs[0].keys.has('ts'), `${cmd}: the stats heredoc's keys were not read`);
assert.deepEqual(unwrittenStatsKeys(text, docs[0].keys), [],
`${cmd}: stats prose promises keys its stats record does not write`);
});
}
test('control: the stats-key checker reports a planted paragraph that promises `profile`', () => {
const written = statsHeredocs(read('commands/trekplan.md'))[0].keys;
assert.deepEqual(unwrittenStatsKeys('Intro.\n\nStats rows carry `profile`.\n', written), ['profile']);
assert.deepEqual(unwrittenStatsKeys('Intro.\n\n```\nStats rows carry `profile`.\n```\n', written), []);
assert.deepEqual(unwrittenStatsKeys('Rows carry `profile`.\n', written), []);
});
test('[#46] profile_source appears in no file under commands/', () => {
const hits = readdirSync(join(ROOT, 'commands'))
.filter((f) => read(`commands/${f}`).includes('profile_source'));
assert.deepEqual(hits, [], 'a command still names profile_source, a key no stats record writes');
});
test('[#46] sibling: docs/profiles.md claims no profile field in the stats records', () => {
const text = read('docs/profiles.md');
const f = flat(text);
assert.doesNotMatch(f, /recorded in two places/);
assert.doesNotMatch(f, /recorded in one place/);
assert.ok(text.split('\n').includes('The plan-file fields for the resolved value:'),
'missing line: The plan-file fields for the resolved value:');
assert.doesNotMatch(f, /Stats stream/);
assert.doesNotMatch(f, /`profile_source` distinguishes/);
assert.doesNotMatch(f, /Per-profile actuals are emitted/);
assert.match(f, /The stats records carry no profile field, so they cannot split cost by profile\./);
});
/** The one blank-line paragraph of `rel` that opens with `opening`, flattened. */
function paraOpening(rel, opening) {
const hits = read(rel).split(/\n[ \t]*\n/).filter((p) => p.trimStart().startsWith(opening));
assert.equal(hits.length, 1, `${rel}: expected exactly one paragraph opening "${opening}"; found ${hits.length}`);
return flat(hits[0]);
}
for (const rel of ['README.md', 'docs/operations.md']) {
test(`[#46] sibling: ${rel} profile paragraph says nothing is emitted to stats`, () => {
const p = paraOpening(rel, 'Four built-in model profiles');
assert.doesNotMatch(p, /emitted to/, `${rel}: the profile paragraph still says the profile is emitted`);
assert.doesNotMatch(p, /stats/i, `${rel}: the profile paragraph still names stats`);
});
}
// --- #47 ---------------------------------------------------------------------------
// /trekbrief Phase 5 said "Skip this phase if research_topics = 0. Proceed
// directly to Phase 6.", so a brief with no research topics walked into the
// auto-only research dispatch nobody opted into. It now prints the manual
// next steps without the /trekresearch lines, runs Phase 7 and stops.
test('[#47] Phase 5: a brief with no research topics prints /trekplan --project and stops, never proceeds to Phase 6', () => {
const text = read('commands/trekbrief.md');
const p5 = flat(section(text, '## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)'));
assert.doesNotMatch(p5, /Skip this phase if research_topics = 0/, 'Phase 5 still skips straight on with zero topics');
const opening = partAt('commands/trekbrief.md', '## Phase 5 — Auto-orchestration opt-in', (l) => l.startsWith('Ask the user'));
assert.doesNotMatch(opening, /proceed directly to Phase 6/i, 'a zero-topic brief still proceeds directly to Phase 6');
assert.match(opening, /\*\*With research_topics = 0\*\*, print the manual-path output below without the `\/trekresearch` lines \(Brief, then `\/trekplan --project \{PROJECT_DIR\}`, then `\/trekexecute --project \{PROJECT_DIR\}`\), then run Phase 7 and stop\./);
assert.match(opening, /Phase 6 is never entered without the operator's opt-in\./);
const manual = flat(section(text, '### Manual path (default)'));
assert.match(manual, /Then: \/trekplan --project \{PROJECT_DIR\} Then: \/trekexecute --project \{PROJECT_DIR\}/);
assert.match(manual, /Run Phase 7 \(`auto_result: manual`\), then stop\. Do not continue to Phase 6\./);
});
test('[#47] Phase 6 runs only after the operator opted into auto mode and Phase 4h approved the intent', () => {
const p6 = flat(section(read('commands/trekbrief.md'), '## Phase 6 — Auto research dispatch (auto path only)'));
assert.match(p6, /^## Phase 6 — Auto research dispatch \(auto path only\) \*\*Runs only when user opted into auto mode,? and only after Phase 4h approved the intent\.\*\*/);
});
test('keep: /trekbrief\'s auto path still requires an approved intent', () => {
assert.equal(partAt('commands/trekbrief.md', '**Auto requires an approved intent.**', toBlank),
'**Auto requires an approved intent.** If Phase 4h ended "Leave unapproved", the auto chain would halt at `/trekplan` anyway — print the manual-path output instead, run Phase 7 (`auto_result: manual`) and stop.');
});
// Every exit of /trekbrief's manual and auto paths before the plan runs Phase 7
// first, with the auto_result Phase 7 lists: manual, cancelled or failed.
const BRIEF_OLD_EXITS = [
'Stop. Do not continue to Phase 6.',
'print the manual-path output instead and stop.',
'fall back to the manual path output and stop.',
'If cancelled → fall back to manual path output and stop.',
];
/** No old exit sentence stands in commands/trekbrief.md (case-sensitive). */
function noOldBriefExit() {
const f = flat(read('commands/trekbrief.md'));
for (const old of BRIEF_OLD_EXITS) assert.ok(!f.includes(old), `an exit still stops without Phase 7: ${old}`);
}
test('[#47] sibling: Phase 5 runs Phase 7 before each of its three exits', () => {
const p5 = flat(section(read('commands/trekbrief.md'), '## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)'));
for (const s of [
'Run Phase 7 (`auto_result: manual`), then stop. Do not continue to Phase 6.',
'print the manual-path output instead, run Phase 7 (`auto_result: manual`) and stop.',
'fall back to the manual path output, run Phase 7 (`auto_result: cancelled`) and stop.',
]) assert.ok(p5.includes(s), `Phase 5 is missing the exit: ${s}`);
noOldBriefExit();
});
test('[#47] sibling: Phase 6 runs Phase 7 before each of its three exits', () => {
const p6 = flat(section(read('commands/trekbrief.md'), '## Phase 6 — Auto research dispatch (auto path only)'));
for (const s of [
'If cancelled → fall back to manual path output, run Phase 7 (`auto_result: cancelled`) and stop.',
'ask the user how to proceed (retry, skip missing topic, cancel). On cancel, run Phase 7 (`auto_result: cancelled`) and stop.',
'Wait for the plan to be written to `{PROJECT_DIR}/plan.md` before continuing. If `/trekplan` stops before it writes the plan, '
+ 'report where it stopped, run Phase 7 (`auto_result: failed`) and stop.',
]) assert.ok(p6.includes(s), `Phase 6 is missing the exit: ${s}`);
noOldBriefExit();
});
// --- #1 ----------------------------------------------------------------------------
// `--gates` meant one thing in the shim (a boolean), another in the docs (an
// open/closed/adaptive strictness policy), and /trekexecute promised pauses it
// never makes. Each of the four commands now holds one rule: the next token is
// the flag's value only when it is true, false, closed or open; the shim maps
// it, run once; a broken shim falls back to the same mapping in prose and the
// run goes on. `gates_mode` changes nothing in `/trekexecute`: its one stop is
// the merge into main.
const GATES_COMMANDS = ['trekbrief', 'trekresearch', 'trekplan', 'trekexecute'];
const GATES_RULE = [
'`--gates` takes the next token as its value only when it is `true`, `false`, `closed` or `open` (strip both); any other next token is not its value (strip only the flag).',
'Run the shim once to map the value, bare when there is no value:',
'`next_state: gates_on` sets `gates_mode = true`, `auto_running` sets it false, and nothing else reads the state.',
'On exit 1 or no JSON (a broken shim; the four values cannot cause it), print the error in one line and map in prose instead: bare, `true`, `closed` → true; `false`, `open` → false. The run goes on either way.',
];
/** The command's one shim line, `[value]` filled in with `value` (left out when ''). */
function gatesShim(cmd, value) {
const l = line(read(`commands/${cmd}.md`), 'util/autonomy-gate.mjs', '--event start');
assert.match(l, / --state idle --event start --gates \[value\]$/, `${cmd}: the shim line must end "--gates [value]"`);
return l.replace(' [value]', value ? ` ${value}` : '');
}
/** Run a shim line from the prose; its `next_state`. */
function shimNext(cmdLine) {
const r = bash(cmdLine, ROOT);
assert.equal(r.status, 0, `shim exited ${r.status}: ${r.stdout}${r.stderr}`);
return JSON.parse(r.stdout).next_state;
}
for (const cmd of GATES_COMMANDS) {
test(`[#1] commands/${cmd}.md holds the shared --gates rule and a shim line that maps closed to gates_on`, () => {
const text = read(`commands/${cmd}.md`);
const f = flat(text);
for (const s of GATES_RULE) assert.ok(f.includes(s), `${cmd}: missing the shared --gates sentence: ${s}`);
assert.doesNotMatch(text, /--state X --event Y/, `${cmd}: the placeholder shim line is still there`);
assert.doesNotMatch(f, /consumed by the autonomy-gate/, `${cmd}: still says the flag is consumed by the state machine`);
assert.equal(shimNext(gatesShim(cmd, 'closed')), 'gates_on');
});
}
// Review focus: `/trekplan --gates --project d` and `/trekexecute --gates plan.md`
// are a bare --gates (the next token is not a value), so the shim runs bare.
test('keep: every command\'s shim line maps a bare --gates to gates_on and open to auto_running', () => {
for (const cmd of GATES_COMMANDS) {
assert.equal(shimNext(gatesShim(cmd, '')), 'gates_on', `${cmd}: bare --gates`);
assert.equal(shimNext(gatesShim(cmd, 'open')), 'auto_running', `${cmd}: --gates open`);
}
});
const GATES_ONE_PAUSE = '`gates_mode = true` adds one pause, before the auto chain starts (Phase 5); the chain does not pass '
+ '`--gates` on to `/trekresearch` or `/trekplan`. Default `gates_mode = false` skips that pause.';
test('[#1] sibling: commands/trekbrief.md promises no execute pause and no pause at every wave in the executor', () => {
const f = flat(read('commands/trekbrief.md'));
assert.doesNotMatch(f, /every wave in the executor/, 'trekbrief still promises a pause at every wave in the executor');
assert.doesNotMatch(f, /\(research, plan, execute\)/, 'trekbrief still promises an execute pause');
assert.ok(f.includes(GATES_ONE_PAUSE), 'trekbrief does not say --gates adds one pause before its auto chain');
});
test('[#1] sibling: /trekbrief Phase 1 says --gates adds one pause, before the auto chain', () => {
const p1 = flat(section(read('commands/trekbrief.md'), '## Phase 1 — Parse mode and validate input'));
assert.ok(p1.includes(GATES_ONE_PAUSE), 'Phase 1 does not say --gates adds one pause before the auto chain');
assert.ok(!p1.includes('every phase boundary in the downstream pipeline'), 'Phase 1 still promises a pause at every downstream phase boundary');
assert.ok(!p1.includes('runs continuously until the main-merge gate'), 'Phase 1 still says the auto chain runs until the main-merge gate');
});
test('[#1] sibling: /trekbrief Phase 5 says the auto chain ends at the plan, never at the main-merge gate', () => {
const p5 = flat(section(read('commands/trekbrief.md'), '## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)'));
assert.ok(p5.includes('The chain ends when `/trekplan` has written the plan (Step 6e); it never starts `/trekexecute`.'),
'Phase 5 does not say the chain ends when /trekplan has written the plan');
assert.ok(!p5.includes('The chain stops only at the main-merge gate'), 'Phase 5 still says the chain stops only at the main-merge gate');
});
test('[#1] sibling: docs/operations.md, docs/command-modes.md and README.md say /trekexecute accepts the flag and pauses on nothing', () => {
for (const rel of ['docs/operations.md', 'docs/command-modes.md', 'README.md']) {
assert.ok(flat(read(rel)).includes('`/trekexecute` accepts the flag and pauses on nothing'), `${rel}: missing "accepts the flag and pauses on nothing"`);
}
for (const rel of ['docs/operations.md', 'docs/command-modes.md', 'README.md', EXEC]) {
const f = flat(read(rel));
for (const old of ['records `gates_mode`', 'Record gates_mode', 'records the setting']) {
assert.ok(!f.includes(old), `${rel}: still says "${old}"`);
}
}
});
test('[#1] sibling: commands/trekexecute.md: `gates_mode` changes nothing in `/trekexecute`, and its one stop is the merge into main', () => {
const f = flat(read('commands/trekexecute.md'));
assert.doesNotMatch(f, /consumed by the autonomy-gate state machine/);
assert.ok(f.includes('`gates_mode` changes nothing in `/trekexecute`: no phase of it pauses on it. The one stop is the merge into main, which always asks (Phase 8).'),
'trekexecute does not say gates_mode changes nothing there and the one stop is the merge into main');
});
test('[#1] sibling: docs/operations.md gives the four values and the bare form, and no executor strictness table', () => {
const text = read('docs/operations.md');
const sec = flat(section(text, '## Autonomy mode (`--gates`, v3.4.0)'));
assert.doesNotMatch(text, /\{open\\?\|closed\\?\|adaptive\}/);
assert.doesNotMatch(sec, /\| `gates_mode` \| Derived from/, 'the executor strictness table is still there');
assert.doesNotMatch(sec, /\| `adaptive` \|/, 'the executor strictness table is still there');
assert.doesNotMatch(sec, /how strict/);
assert.doesNotMatch(sec, /gates_mode_flag_override/, 'still names an override record the executor does not write');
assert.match(sec, /accept `--gates \[true\|false\|closed\|open\]`/);
assert.match(sec, /A bare `--gates`, `--gates true` or `--gates closed` turns gating \*\*on\*\*/);
for (const s of ['gates_on', 'auto_running', 'paused_for_gate']) assert.ok(sec.includes(s), `the state ${s} stays named`);
});
test('[#1] sibling: docs/architecture.md does not call --gates open the executor\'s low effort', () => {
const text = read('docs/architecture.md');
assert.doesNotMatch(text, /--gates open/);
assert.match(text, /\(`\/trekexecute` low effort = one session at a time\)/);
});
test('[#1] sibling: docs/command-modes.md gives every --gates row the four values and no strictness policy', () => {
const text = read('docs/command-modes.md');
assert.doesNotMatch(text, /--gates \{true\\\|false\}/);
assert.doesNotMatch(text, /\{open\\?\|closed\\?\|adaptive\}/);
assert.doesNotMatch(text, /Policy \(`gates_mode`\)/, 'a row still points at a gates_mode strictness policy');
const rows = text.split('\n').filter((l) => l.startsWith('| `--gates'));
assert.equal(rows.length, 4, 'one --gates row per command');
for (const r of rows) {
assert.ok(r.startsWith('| `--gates [true\\|false\\|closed\\|open]` |'), `row does not give the four values: ${r}`);
assert.match(r, /`## Autonomy mode` in `docs\/operations\.md`/);
}
});
test('[#1] sibling: README.md gives the four values, and no strictness table', () => {
const text = read('README.md');
const sec = flat(section(text, '### Autonomy mode (`--gates`, v3.4.0)'));
assert.doesNotMatch(text, /\{open\|closed\|adaptive\}/);
assert.doesNotMatch(sec, /\| Value \| Behavior \|/, 'the strictness table is still there');
assert.doesNotMatch(sec, /`adaptive` \(default\)/);
assert.match(sec, /accept `--gates \[true\|false\|closed\|open\]`/);
});
// --- #30 ---------------------------------------------------------------------------
const EXEC = 'commands/trekexecute.md';
const SAME_ON_OPUS = 'the step helper and the controller stay on opus';
/** Phase 1's top-level numbered rules, each as [number, text of its first line]. */
function phase1Rules() {
const sec = section(read(EXEC), '## Phase 1 — Parse mode and validate input').split('\n');
const stop = sec.findIndex((l) => l.startsWith('If no path is provided'));
assert.ok(stop !== -1, 'Phase 1 must still say what happens when no path is provided');
return sec.slice(0, stop)
.map((l) => l.match(/^(\d+)\. (.*)$/))
.filter(Boolean)
.map((m) => [Number(m[1]), m[2]]);
}
test('[#30] the frontmatter hint names --profile <name> and --gates', () => {
const m = read(EXEC).match(/^argument-hint: (.*)$/m);
assert.ok(m, 'trekexecute has no argument-hint');
assert.ok(m[1].includes('[--profile <name>]'), `the hint does not name --profile <name>: ${m[1]}`);
assert.ok(m[1].includes('[--gates [true|false|closed|open]]'), `the hint does not name --gates: ${m[1]}`);
assert.equal(m[1],
'"[--project <dir>] [--profile <name>] [--gates [true|false|closed|open]] [--fg | --resume | --dry-run | --validate | --step N | --session N] [plan.md]"');
});
test('[#30] Phase 1 strips --profile <name>, and its last rule reads what is left after the flags are stripped', () => {
const f = flat(section(read(EXEC), '## Phase 1 — Parse mode and validate input'));
assert.doesNotMatch(f, /the entire argument string is the file path/i,
'Phase 1 still reads the whole argument string as the file path');
const rules = phase1Rules();
assert.deepEqual(rules.map(([n]) => n), rules.map((_, i) => i + 1), 'Phase 1 rules are not numbered 1..n');
const [last, profile] = [rules[rules.length - 1][1], rules[rules.length - 2][1]];
assert.ok(last.startsWith('Otherwise: the arguments left after every flag above is stripped are the file path.'),
`the last rule does not read what is left after the flags: ${last}`);
assert.ok(profile.includes('`--profile <name>`: set `profile = {name}`; strip both tokens'),
`the rule before the last one is not the --profile strip rule: ${profile}`);
});
test('[#30] the usage block names --profile and --gates, and its --profile line keeps the helper and the controller on opus', () => {
const usage = partAt(EXEC, 'Usage: /trekexecute <plan.md>', (l) => l.startsWith('```'));
assert.ok(usage.includes('/trekexecute --profile <name> <plan.md>'), 'the usage block has no --profile line');
assert.ok(usage.includes(`--profile Profile for this run's resolver; ${SAME_ON_OPUS}`),
'the Modes list has no --profile line, or it does not keep the helper and the controller on opus');
assert.ok(usage.includes('--gates Accepted, pauses on nothing: bare, true or closed = on; false or open = off'),
'the Modes list has no --gates line');
});
test('[#30] sibling: Profile section says the profile changes no model here', () => {
const f = flat(section(read(EXEC), '## Profile (v4.1)'));
assert.doesNotMatch(f, /drives `phase_models\.execute`/, 'the Profile section still says the profile drives the execute model');
assert.ok(f.includes('The selected profile goes to the resolver. It changes no model here: the step helper and the '
+ 'controller run opus (Composition rule below), and nothing in this command reads `parallel_agents`.'),
'the Profile section does not say the profile changes no model here');
});
test('[#30] sibling: docs/command-modes.md /trekexecute --profile row keeps the helper and the controller on opus', () => {
const sec = section(read('docs/command-modes.md'), '## /trekexecute modes');
const rows = sec.split('\n').filter((l) => l.startsWith('| `--profile <name>` |'));
assert.equal(rows.length, 1, 'expected one --profile row under /trekexecute modes');
assert.doesNotMatch(rows[0], /Model profile for the execute phase/);
assert.ok(rows[0].includes(`| (v4.1.0) Profile for this run's resolver; ${SAME_ON_OPUS}.`), rows[0]);
});
test('[#30] sibling: README.md /trekexecute Profile row keeps the helper and the controller on opus', () => {
const rows = read('README.md').split('\n').filter((l) => l.startsWith('| **Profile** | `/trekexecute '));
assert.equal(rows.length, 1, 'expected one /trekexecute Profile row in README.md');
assert.doesNotMatch(rows[0], /Pin model profile for execution/);
assert.ok(rows[0].includes(`| (v4.1.0) Profile for this run's resolver; ${SAME_ON_OPUS}.`), rows[0]);
});
// --- #33 ---------------------------------------------------------------------------
// /trekexecute promised a brief check and an effort-driven gates mode that no
// line ran. Phase 1's --project branch now runs the two lines /trekplan runs,
// for the execute phase, and halts only on BRIEF_V51_MISSING_SIGNALS (a brief
// edited since planning does not stop a --resume on any other error). Low
// effort means one session, step after step; high effort changes nothing.
/** Phase 1's rule 1 (the --project branch), as lines. */
function projectRule() {
const sec = section(read(EXEC), '## Phase 1 — Parse mode and validate input').split('\n');
const start = sec.findIndex((l) => l.startsWith('1. If arguments contain `--project <dir>`'));
assert.ok(start !== -1, 'Phase 1 must still open with the --project rule');
const end = sec.findIndex((l, i) => i > start && /^2\. /.test(l));
assert.ok(end !== -1, 'Phase 1 must still have a rule 2');
return sec.slice(start, end).join('\n');
}
test('[#33] Phase 1 --project runs brief-validator --soft --json and the execute resolver, and halts only on BRIEF_V51_MISSING_SIGNALS', () => {
const rule = projectRule();
const validator = line(rule, 'validators/brief-validator.mjs', '--soft --json');
const resolver = line(rule, 'profiles/resolver.mjs', '--resolve-phase-model --phase execute');
assert.equal(validator, 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"');
assert.equal(resolver,
'node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase execute --brief-path "{dir}/brief.md" [--profile {profile}] --json');
const f = flat(rule);
for (const s of [
'The validator exits 1 when its `errors` are not empty; read its JSON either way.',
'Halt only when the validator\'s `errors` hold `BRIEF_V51_MISSING_SIGNALS`, with the message from "Sequencing gate surface" below.',
'Any other error is printed in one line and the run continues.',
'A validator or resolver that cannot run (no JSON) is "not measured" in one line, and the run continues.',
'Capture the resolver\'s JSON as `phase_signal_result` (Composition rule below).',
'No `{dir}/brief.md`: skip both lines.',
]) assert.ok(f.includes(s), `the --project rule is missing: ${s}`);
// The two lines run as printed: the validator names the one halting code for a
// 2.1 brief without signals, and the resolver hands Phase 2.5 its low effort.
const dir = mkdtempSync(join(tmpdir(), 'fix-33-'));
try {
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-v21-no-signals.md'), join(dir, 'brief.md'));
const v = bash(validator.split('{dir}').join(dir), ROOT);
assert.equal(v.status, 1, `validator: ${v.stdout}${v.stderr}`);
assert.ok(JSON.parse(v.stdout).errors.some((e) => e.code === 'BRIEF_V51_MISSING_SIGNALS'), v.stdout);
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-effort-low.md'), join(dir, 'brief.md'));
const r = bash(resolver.split('{dir}').join(dir).replace(' [--profile {profile}]', ''), ROOT);
assert.equal(r.status, 0, `resolver: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).effort, 'low');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('[#33] the Composition rule says the resolver runs in Phase 1, and no line says Phase 2.4 runs it', () => {
assert.doesNotMatch(flat(read(EXEC)), /invoked in Phase 2\.4/);
assert.ok(flat(section(read(EXEC), '## Composition rule (v5.1)')).includes('invoked in Phase 1 alongside the sequencing-gate brief-validator call'),
'the Composition rule does not say the resolver is invoked in Phase 1');
});
test('[#33] sibling: Composition rule has no "uniformity and stats" and no gates auto-escalation', () => {
const f = flat(section(read(EXEC), '## Composition rule (v5.1)'));
assert.doesNotMatch(f, /uniformity and stats/, 'the Composition rule still says the model is returned for stats');
assert.ok(f.includes('it is returned for cross-command uniformity; no stats record carries it.'),
'the Composition rule does not say no stats record carries the model');
assert.doesNotMatch(f, /gates auto-escalation/);
});
test('[#33] Phase 2.5 names low effort as one session, steps in order, never while a wave is in flight', () => {
const sec = section(read(EXEC), '## Phase 2.5 — Execution strategy decision').split('\n');
const start = sec.findIndex((l) => l.startsWith('**Run as single session (foreground)** when ANY of these are true:'));
assert.ok(start !== -1, 'Phase 2.5 must still list when it runs as a single session');
const end = sec.findIndex((l, i) => i > start && l.trim() === '');
const bullets = sec.slice(start + 1, end);
assert.ok(bullets.includes("- `phase_signal_result.effort == 'low'` → one session, steps in order, unless a wave is in flight: when "
+ '`.claude/trekplan-sessions/{slug}/orders/wave.json` exists in the main checkout (the first entry of `git worktree list`), '
+ 'this bullet does not apply and the multi-session path below collects the wave. The check only reads the file and never stops the run.'),
`the single-session list does not name low effort: ${bullets.join(' | ')}`);
});
test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_override is left, and high effort says it changes nothing', () => {
const text = read(EXEC);
const f = flat(text);
assert.doesNotMatch(f, /gates_mode = 'closed'/);
assert.doesNotMatch(f, /strictest/);
assert.doesNotMatch(f, /halts immediately/);
assert.doesNotMatch(text, /--event gates_mode_flag_override/);
assert.doesNotMatch(f, /gates_mode = '(adaptive|open)'/);
assert.equal(flat(section(text, '### High-effort behavior (v5.1.1)')),
'### High-effort behavior (v5.1.1) High effort changes nothing in `/trekexecute`: the step helper and the controller '
+ 'already run opus, and every gate already runs. No `gates_mode` is set from the effort.');
});
// --- #28 ---------------------------------------------------------------------------
/** The completion gate's text with its blockquote markers removed, flattened. */
function completionGate() {
return flat(section(read(EXEC), '### Machine-verifiable completion gate (stop-signal contract)').replace(/^> ?/gm, ''));
}
test('[#28] the completion gate calls nothing deterministic that is executor judgement, and cites no line number', () => {
const gate = completionGate();
assert.ok(!gate.includes('Counter liveness (deterministic'), 'clause 3 still calls the counter check deterministic');
assert.ok(!gate.includes('deterministic audit'), 'the gate still says termination rests on a deterministic audit');
assert.ok(!gate.includes('`:1221`'), 'the gate still cites `:1221`');
assert.doesNotMatch(gate, /`:\d+`/, 'the gate cites a line number; line numbers rot');
assert.ok(gate.includes('Counter liveness (checked by the executor against the audit\'s counts, defeats "never decremented")'),
'clause 3 does not say the executor checks the counter against the audit\'s counts');
assert.ok(gate.includes('rests on the audit and an objective stop-signal, never on the model\'s narrative'),
'the termination sentence does not rest on the audit and an objective stop-signal');
});
test('[#28] the completion gate names its commands and its executor judgement, and each command is in Phase 7.5', () => {
const gate = completionGate();
const c = gate.indexOf('Commands:');
const j = gate.indexOf('Executor judgement:');
assert.ok(c !== -1, 'the gate does not say which checks are commands');
assert.ok(j > c, 'the gate does not say which checks are executor judgement, after its commands');
const commands = gate.slice(c, j);
const judgement = gate.slice(j);
// Phase 7.5's own steps only: the gate subsection after them holds the Commands part under test.
const phase75 = section(read(EXEC), '## Phase 7.5 — Manifest audit (independent)');
const gateAt = phase75.indexOf('\n### Machine-verifiable completion gate');
assert.ok(gateAt !== -1, 'Phase 7.5 no longer holds the completion gate subsection');
const audit = flat(phase75.slice(0, gateAt));
assert.ok(!audit.includes('Commands:'), 'the audit slice still holds the gate\'s Commands part');
for (const cmd of ['git rev-list --count', 'bash -n', 'git diff --name-only', 'done-line.mjs --check']) {
assert.ok(commands.includes(cmd), `the Commands part does not name ${cmd}`);
assert.ok(audit.includes(cmd), `Phase 7.5 does not run ${cmd}, so the gate cannot call it a command`);
}
// `git diff --name-only` in its step-6 form (step 5's line has no `-- {path}`).
assert.ok(commands.includes("step 6's `git diff --name-only`"), 'the Commands part does not name step 6\'s git diff --name-only');
assert.ok(audit.includes('`git diff --name-only {session_start_sha} HEAD -- {path}`'),
'Phase 7.5 step 6 does not run git diff --name-only {session_start_sha} HEAD -- {path}');
for (const s of ['criteria runner', 'arch-rules.mjs', 'plan-scope-sweep.mjs']) {
assert.ok(commands.includes(s), `the Commands part does not name ${s}`);
}
for (const s of ['steps 1, 2 and 4', 'the expected count in step 3', 'clause 3\'s formula']) {
assert.ok(judgement.includes(s), `the Executor judgement part does not name ${s}`);
}
});
test('[#28] sibling: the completion gate says the judgement parts are not yet a script, and qualifies clauses 2 and 4', () => {
const gate = completionGate();
assert.ok(gate.includes('The judgement parts are not yet a script.'), 'the gate does not say the judgement parts are not yet a script');
assert.ok(gate.includes('the criteria runner behind clauses 2 and 4 for a trekplan (a session spec\'s clause 2 is Phase 7\'s '
+ '`## Exit Condition` checklist, and clause 4 is a trekplan\'s only);'), 'the gate does not qualify the criteria runner behind clauses 2 and 4');
assert.ok(!gate.includes('part c of the hardening'), 'the gate still points at part c of the hardening');
});
// ---- Architecture rule (plan, Step 12): lib/ never imports from scripts/ ----
/**
* The import specifiers in `text` (a file at `file`) that resolve into the
* repo's scripts/ directory: static and side-effect imports, re-exports and
* dynamic imports with a string literal.
*/
function importsIntoScripts(file, text) {
const re = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)|\b(?:import|export)\s+(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g;
const scripts = join(ROOT, 'scripts') + sep;
const hits = [];
for (const m of text.matchAll(re)) {
const spec = m[1] ?? m[2];
if (!spec.startsWith('.') && !spec.startsWith('/')) continue;
if (resolve(dirname(file), spec).startsWith(scripts)) hits.push(spec);
}
return hits;
}
/** Every .mjs file under `dir`, recursively. */
function mjsFiles(dir) {
const out = [];
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) out.push(...mjsFiles(p));
else if (e.name.endsWith('.mjs')) out.push(p);
}
return out;
}
test('arch: lib/ never imports from scripts/, and the scan reports a planted import (control)', () => {
const planted = importsIntoScripts(join(ROOT, 'lib', 'review', 'x.mjs'), "import x from '../../scripts/release-check.mjs';\n");
assert.deepEqual(planted, ['../../scripts/release-check.mjs'], 'control: the scan does not report a planted import from scripts/');
const files = mjsFiles(join(ROOT, 'lib'));
assert.ok(files.length > 10, `the scan found only ${files.length} files under lib/`);
const bad = files.flatMap((f) => importsIntoScripts(f, readFileSync(f, 'utf8')).map((s) => `${f.slice(ROOT.length + 1)}: ${s}`));
assert.deepEqual(bad, [], 'lib/ imports from scripts/');
});
// --- R6 ----------------------------------------------------------------------------
// The merge lock (Phase 7.2 step 4 and the Phase 8 Main-merge gate) runs the
// verdict gate with `--repo .`, so a review of an older commit than the one
// being merged is refused (`review-stale`), and only the closing run's own
// review opens the merge.
const MERGE_GATE = '### Main-merge gate (MAIN_MERGE_GATE — always pauses)';
const PHASE_72 = '## Phase 7.2 — Simplify, review, verdict (P11)';
const LOCK_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/review/verdict-gate.mjs --json "{review_path}" --repo .';
const STEP4_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/review/verdict-gate.mjs --json "{project_dir}/review.md" --repo .';
/** A review_version 1.1 review.md of `sha`: ALLOW with no findings, or BLOCK over one BLOCKER. */
function r6Review(verdict, sha) {
const id = 'd'.repeat(40);
const findings = verdict === 'BLOCK'
? [{ id, severity: 'BLOCKER', rule_key: 'MISSING_TEST', file: 'a.mjs', line: 1, title: 't', detail: 'd', recommended_action: 'Add a test.' }]
: [];
const counts = { BLOCKER: findings.length, MAJOR: 0, MINOR: 0, SUGGESTION: 0 };
const block = JSON.stringify({ verdict, counts, allow_blocked_by: [], findings }, null, 2);
return [
'---', 'type: trekreview', 'review_version: "1.1"', 'task: "t"', 'slug: s',
'project_dir: .claude/projects/2026-10-23-s/', 'brief_path: .claude/projects/2026-10-23-s/brief.md',
`scope_sha_end: ${sha}`, 'reviewed_files_count: 1', `verdict: ${verdict}`,
findings.length ? `findings:\n - ${id}` : 'findings: []', '---', '', '# Review: t', '',
'## Executive Summary', '', 'x', '', '## Coverage', '', '| File | Treatment | Reason |', '|---|---|---|',
'| a.mjs | summary-only | default |', '', '## Remediation Summary', '', 'x', '', '```json', block, '```', '',
].join('\n');
}
/**
* A throwaway repo (no git hooks): the reviewed commit, then a later commit
* that changes source. `review(verdict, sha)` writes review.md outside the
* repo and returns its folder.
*/
function staleRepo() {
const base = mkdtempSync(join(tmpdir(), 'fix-round-r6-'));
const repo = join(base, 'repo');
const out = join(base, 'project');
mkdirSync(repo);
mkdirSync(out);
mkdirSync(join(base, 'hooks-none'));
const g = (...args) => {
const r = spawnSync('git', args, { cwd: repo, encoding: 'utf8', env: childEnv() });
assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
};
g('init', '-q', '-b', 'main');
g('config', 'user.name', 'Test');
g('config', 'user.email', 'test@example.invalid');
g('config', 'commit.gpgsign', 'false');
g('config', 'core.hooksPath', join(base, 'hooks-none'));
writeFileSync(join(repo, 'a.mjs'), 'export const a = 1;\n');
g('add', 'a.mjs');
g('commit', '-q', '-m', 'feat: the reviewed work');
const reviewed = g('rev-parse', 'HEAD');
writeFileSync(join(repo, 'a.mjs'), 'export const a = 2;\n');
g('add', 'a.mjs');
g('commit', '-q', '-m', 'feat: a later source change');
const head = g('rev-parse', 'HEAD');
const review = (verdict, sha) => {
writeFileSync(join(out, 'review.md'), r6Review(verdict, sha));
return out;
};
return { repo, reviewed, head, review, cleanup: () => rmSync(base, { recursive: true, force: true }) };
}
/** The lock line with its review path filled in, run in `repo` through bash(). */
function runLock(cmd, dir, repo) {
const filled = cmd.split('{review_path}').join(join(dir, 'review.md')).split('{project_dir}').join(dir);
return bash(filled, repo);
}
test('[R6] the Main-merge gate and Phase 7.2 step 4 each hold exactly one verdict-gate.mjs line, with --repo .', () => {
const text = read(EXEC);
assert.equal(line(section(text, MERGE_GATE), 'review/verdict-gate.mjs'), LOCK_LINE,
'the Phase 8 lock does not run the verdict gate against the repo being merged');
assert.equal(line(section(text, PHASE_72), 'review/verdict-gate.mjs'), STEP4_LINE,
'Phase 7.2 step 4 does not run the verdict gate against the repo being merged');
});
test('[R6] the extracted lock lines, run on an ALLOW review of an older commit with a later source commit, exit 2 with review-stale', () => {
const text = read(EXEC);
const lines = [line(section(text, MERGE_GATE), 'review/verdict-gate.mjs'), line(section(text, PHASE_72), 'review/verdict-gate.mjs')];
const fx = staleRepo();
try {
for (const cmd of lines) {
const ok = runLock(cmd, fx.review('ALLOW', fx.head), fx.repo);
assert.equal(ok.status, 0, `control: an ALLOW review of HEAD opens the lock: ${ok.stdout}${ok.stderr}`);
const r = runLock(cmd, fx.review('ALLOW', fx.reviewed), fx.repo);
assert.equal(r.status, 2, `${cmd}: an ALLOW review of an older commit must exit 2: ${r.stdout}${r.stderr}`);
const reason = JSON.parse(r.stdout).reason;
assert.ok(reason.startsWith('review-stale'), `the reason must start review-stale: ${reason}`);
}
} finally { fx.cleanup(); }
});
test('[R6] both decline lists name review-stale, the exit table says to re-run /trekreview on this commit, and --resume runs the same gate', () => {
const gate = flat(section(read(EXEC), MERGE_GATE));
const closed = gate.match(/\*\*Closed:\*\* emit `main-merge-declined` with the reason ([^.]*)\./);
assert.ok(closed, 'the gate no longer says which reasons a closed lock emits');
assert.ok(closed[1].includes('`review-stale`'), `the closed-lock reasons do not name review-stale: ${closed[1]}`);
const literal = gate.match(/The reason is one of the literal words above \(([^)]*)\)/);
assert.ok(literal, 'the gate no longer lists the literal decline reasons');
assert.ok(literal[1].includes('`review-stale`'), `the literal decline reasons do not name review-stale: ${literal[1]}`);
assert.match(gate, /a reason that starts `review-stale`[^.]*decline reason `review-stale`[^.]*re-run \/trekreview on this commit/,
'the exit table does not map a review-stale reason to the decline reason and its "re-run /trekreview on this commit" line');
assert.match(gate, /`--resume re-enters at the gate`[^.]*runs the same gate[^.]*review of an older commit stays refused/,
'the --resume paragraph does not say the re-read runs the same gate');
});
test('keep: Phase 7.2 step 4 run on a BLOCK review of an older commit still exits 1, so the fix loop opens', () => {
const step4 = line(section(read(EXEC), PHASE_72), 'review/verdict-gate.mjs');
const fx = staleRepo();
try {
const r = runLock(step4, fx.review('BLOCK', fx.reviewed), fx.repo);
assert.equal(r.status, 1, `a BLOCK review must exit 1 before any staleness check: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).verdict, 'BLOCK');
} finally { fx.cleanup(); }
});
test('keep: the other verdict-gate callers carry no --repo', () => {
for (const rel of ['commands/trekreview.md', 'agents/review-orchestrator.md', 'README.md', 'docs/release-runbook.md', 'tests/e2e-files-trial4.sh']) {
const hits = read(rel).split('\n').filter((l) => /^\s*(?:out="\$\()?node \S*lib\/review\/verdict-gate\.mjs\b/.test(l));
assert.ok(hits.length > 0, `${rel} no longer runs verdict-gate.mjs; the check would be empty`);
for (const h of hits) assert.ok(!h.includes('--repo'), `${rel} gained --repo: ${h.trim()}`);
}
});
// --- D67: the final report says whether the app was seen with `run` ---------
// Phase 7's "run the thing" says to use Claude Code's built-in `run`, but the
// final report never said whether that happened. The line is said for every
// run, also one with no app to launch (the "no — {reason}" form).
const E2E = '### End to end — "run the thing" (P9 (6))';
/** Phase 8 of /trekexecute, read up to Phase 9 (its report fence holds its own `## ` heading). */
function phase8(text) {
const start = text.indexOf('## Phase 8 — Final report');
const end = text.indexOf('## Phase 9');
assert.ok(start !== -1 && end > start, 'Phase 8 — Final report and Phase 9 must both exist, in that order');
return text.slice(start, end);
}
test('[D67] the Phase 8 report template says, right after End to end, whether the app was seen with `run`', () => {
const lines = phase8(read(EXEC)).split('\n');
const e2e = lines.findIndex((l) => l.startsWith('**End to end:**'));
assert.ok(e2e !== -1, 'the report template has no **End to end:** line');
assert.equal(lines[e2e + 1], '**Seen with `run`:** {yes | no — {reason}}',
'the line after **End to end:** is not the "Seen with `run`" line in its yes / no — {reason} form');
});
test('[D67] the summary JSON carries "run_seen" right after "end_to_end"', () => {
const text = read(EXEC);
const sumIdx = text.indexOf('"trekexecute_summary": {');
assert.ok(sumIdx !== -1, 'summary JSON block missing');
const lines = text.slice(sumIdx, text.indexOf('\n```', sumIdx)).split('\n').map((l) => l.trim());
const e2e = lines.findIndex((l) => l.startsWith('"end_to_end":'));
assert.ok(e2e !== -1, 'the summary JSON has no "end_to_end" key');
assert.equal(lines[e2e + 1], '"run_seen": "{yes | no — reason}",',
'the key after "end_to_end" is not "run_seen" with its yes / no — reason value');
});
test('[D67] the End to end section says to record run_seen, also when no app was seen, and keeps the End-to-end: command as the verdict', () => {
const s = flat(section(read(EXEC), E2E));
assert.match(s, /Record `run_seen`: `yes` when the app was seen and driven with Claude Code's built-in `run` in this run, else `no — \{reason\}`/,
'the End to end section does not say to record run_seen as yes, or no with a reason');
assert.match(s, /no app to launch/, 'the no-form does not name the run with no app to launch');
assert.match(s, /The verdict is still the `End-to-end:` command\./,
'the End to end section no longer says the End-to-end: command is the verdict');
});
// --- #15 / #67 -----------------------------------------------------------------------
// #15: /trekendsession's Phase 0 printed its usage when $ARGUMENTS merely
// contained "--help" or "-h", so a slug such as "fix-header" asked for help.
// It now runs the arg parser and dispatches only on its exact result.
// #67: "NEVER auto-invoke this command" was a sentence only; the frontmatter
// now carries disable-model-invocation, so the model cannot start it.
const ENDSESSION = 'commands/trekendsession.md';
const ENDSESSION_PARSER_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekendsession -- $ARGUMENTS';
/** The Phase 0 parser line run through bash() with `args` in place of $ARGUMENTS. */
function runEndsessionParser(cmd, args) {
return bash(cmd.split('$ARGUMENTS').join(args), ROOT);
}
test('[#15] /trekendsession Phase 0 has no "contains --help or -h" check, and its parser line run on fix-header gives no help', () => {
const phase0 = section(read(ENDSESSION), '## Phase 0 — `--help` handling');
assert.doesNotMatch(flat(phase0), /contains\s+`?--help`?\s+or\s+`?-h`?/i,
'Phase 0 still prints help when $ARGUMENTS merely contains --help or -h');
assert.match(flat(phase0), /`flags\['--help'\] === true`/, 'Phase 0 does not dispatch on the parsed --help flag');
assert.match(flat(phase0), /`positional\[0\] === '-h'`/, 'Phase 0 does not dispatch on a lone -h as the first positional');
const cmd = line(phase0, 'parsers/arg-parser.mjs');
assert.equal(cmd, ENDSESSION_PARSER_LINE, 'the Phase 0 parser line is not the trekendsession parser call on $ARGUMENTS');
const block = phase0.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0]).find((b) => b.includes(cmd));
assert.ok(block, 'the parser line is not in a runtime ```bash block');
assert.equal(block.split('\n')[0], 'setopt nonomatch 2>/dev/null || true', 'the block does not open with setopt nonomatch');
const plain = runEndsessionParser(cmd, 'fix-header');
assert.equal(plain.status, 0, `the parser line refused fix-header: ${plain.stdout}${plain.stderr}`);
const r = JSON.parse(plain.stdout);
assert.equal(r.flags['--help'], undefined, 'fix-header set the --help flag');
assert.deepEqual(r.positional, ['fix-header']);
assert.notEqual(r.positional[0], '-h');
const help = runEndsessionParser(cmd, '--help');
assert.equal(help.status, 0, help.stdout + help.stderr);
assert.equal(JSON.parse(help.stdout).flags['--help'], true, 'control: --help must set the help flag');
const two = runEndsessionParser(cmd, 'fix-header "Session 2"');
assert.equal(two.status, 0, two.stdout + two.stderr);
assert.deepEqual(JSON.parse(two.stdout).positional, ['fix-header', 'Session 2']);
});
test('[#15] /trekendsession Phase 2 reads the brief path and the label from the parser\'s positional', () => {
const phase2 = flat(section(read(ENDSESSION), '## Phase 2 — Required args check (headless-safe)'));
assert.match(phase2, /`positional\[0\]`/, 'Phase 2 does not read <next-brief-path> from positional[0]');
assert.match(phase2, /`positional\[1\]`/, 'Phase 2 does not read <next-label> from positional[1]');
});
test('[#67] /trekendsession sets disable-model-invocation, and CLAUDE.md names it as the one command that does', () => {
const text = read(ENDSESSION);
const fm = readFrontmatterFields(text);
assert.ok(fm.ok, fm.error);
assert.equal(fm.fields['disable-model-invocation']?.value, 'true',
'commands/trekendsession.md does not set disable-model-invocation: true');
assert.match(flat(text),
/\*\*NEVER auto-invoke this command\.\*\* It is operator-typed only at session-end \(the frontmatter field keeps the model from starting it; the operator types it\)/,
'the hard rule does not say the frontmatter field keeps the model from starting it');
const withField = readdirSync(join(ROOT, 'commands')).filter((f) => f.endsWith('.md'))
.filter((f) => readFrontmatterFields(read(`commands/${f}`)).fields['disable-model-invocation'] !== undefined);
assert.deepEqual(withField, ['trekendsession.md'], 'trekendsession must be the one command with the field');
const trunk = read('CLAUDE.md').split('\n').find((l) => l.startsWith('> **Commands are the trunk**'));
assert.ok(trunk, 'CLAUDE.md has no "Commands are the trunk" note');
assert.doesNotMatch(trunk, /none sets `disable-model-invocation`/, 'CLAUDE.md still says no command or skill sets the field');
assert.ok(trunk.includes('no skill sets `disable-model-invocation`; the one command that does is `/trekendsession`, operator-typed only (Claude Code still lets the operator type it; the model cannot start it)'),
'CLAUDE.md does not name /trekendsession as the one command that sets the field');
});
test('keep: commands/trekreview.md carries no disable-model-invocation field (/trekexecute starts it through the Skill tool)', () => {
const fm = readFrontmatterFields(read('commands/trekreview.md'));
assert.ok(fm.ok, fm.error);
assert.equal(fm.fields['disable-model-invocation'], undefined);
});
// --- #66 / #102 ----------------------------------------------------------------------
// #66: /trekcontinue's Phase 0 told the session to call parseArgs($ARGUMENTS, ...)
// as a function, which no Bash line runs; it now runs the arg-parser CLI and
// reads its JSON. agents/review-orchestrator.md ran the parser without `--`, so
// the CLI parsed nothing.
// #102: Phase 1.5 looked for the master file at "<plugin-root>", where nothing
// writes it; the master file is at the repo root. The consistency call now
// passes the state file, so the state-anchored staleness check runs.
const CONTINUE = 'commands/trekcontinue.md';
const CONTINUE_PARSER_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekcontinue -- $ARGUMENTS';
const CONSISTENCY_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/next-session-prompt-validator.mjs --json --consistency <RESOLVED-PATH-A> <RESOLVED-PATH-B> --state-file <RESOLVED-PATH-STATE>';
/** The Phase 0 parser line run through bash() with `args` in place of $ARGUMENTS. */
function runContinueParser(cmd, args) {
return bash(cmd.split('$ARGUMENTS').join(args), ROOT);
}
test('[#66] /trekcontinue Phase 0 has no parseArgs($ARGUMENTS call, and its parser line run with --help sets the help flag', () => {
const phase0 = section(read(CONTINUE), '## Phase 0 — `--help` handling');
assert.doesNotMatch(phase0, /parseArgs\(\$ARGUMENTS/, 'Phase 0 still calls parseArgs($ARGUMENTS, ...) as a function');
const cmd = line(phase0, 'parsers/arg-parser.mjs');
assert.equal(cmd, CONTINUE_PARSER_LINE, 'the Phase 0 parser line is not the trekcontinue parser call on $ARGUMENTS');
const block = phase0.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0]).find((b) => b.includes(cmd));
assert.ok(block, 'the parser line is not in a runtime ```bash block');
assert.match(flat(phase0), /Run first, exactly like this:/);
assert.match(flat(phase0), /Read `flags` and `positional` from the JSON it prints/);
assert.match(flat(phase0), /`flags\['--help'\] === true`/, 'Phase 0 does not dispatch on the parsed --help flag');
assert.match(flat(phase0), /`positional\[0\] === '-h'`/, 'Phase 0 does not dispatch on a lone -h as the first positional');
const help = runContinueParser(cmd, '--help');
assert.equal(help.status, 0, help.stdout + help.stderr);
assert.equal(JSON.parse(help.stdout).flags['--help'], true, 'the parser line run with --help does not set the help flag');
const dir = runContinueParser(cmd, '.claude/projects/demo');
assert.equal(dir.status, 0, dir.stdout + dir.stderr);
const r = JSON.parse(dir.stdout);
assert.equal(r.flags['--help'], undefined, 'a project dir set the --help flag');
assert.deepEqual(r.positional, ['.claude/projects/demo']);
const h = runContinueParser(cmd, '-h');
assert.equal(h.status, 0, h.stdout + h.stderr);
assert.equal(JSON.parse(h.stdout).positional[0], '-h');
const p05 = flat(section(read(CONTINUE), '## Phase 0.5 — Cleanup mode dispatch'));
assert.doesNotMatch(p05, /After `parseArgs` has resolved/);
assert.match(p05, /After the parser has printed/);
});
test('[#66] sibling: agents/review-orchestrator.md passes the arguments to the parser after --', () => {
const p1 = section(read('agents/review-orchestrator.md'), '### Phase 1 — Parse mode and validate input');
const cmd = line(p1, 'parsers/arg-parser.mjs');
assert.equal(cmd, 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekreview -- "$@"');
const r = bash(`set -- --project .claude/projects/demo; ${cmd}`, ROOT);
assert.equal(r.status, 0, r.stdout + r.stderr);
assert.equal(JSON.parse(r.stdout).flags['--project'], '.claude/projects/demo', 'the parser did not see --project');
});
test('[#102] /trekcontinue Phase 1.5 finds the master file at the repo root and passes the state file to the consistency call', () => {
assert.doesNotMatch(read(CONTINUE), /<plugin-root>/, 'commands/trekcontinue.md still names <plugin-root>');
const p15 = section(read(CONTINUE), '## Phase 1.5 — Frontmatter consistency check');
assert.ok(flat(p15).includes('a. `NEXT-SESSION-PROMPT.local.md` at the repo root (`git rev-parse --show-toplevel`) — operator-managed master file, written by the operator or an external producer, never by Voyage'),
'candidate a is not the repo-root master file');
const cmd = line(p15, 'validators/next-session-prompt-validator.mjs');
assert.equal(cmd, CONSISTENCY_LINE, 'the consistency call does not pass --state-file <RESOLVED-PATH-STATE>');
assert.match(flat(p15), /When the project's `\.session-state\.local\.json` exists, pass it with `--state-file` \(else run the same line without the option\)/);
assert.match(flat(p15), /`<RESOLVED-PATH-STATE>` with the concrete path of the project's `\.session-state\.local\.json`/);
// The line, with the tokens filled in, runs the state-anchored check.
const dir = mkdtempSync(join(tmpdir(), 'v-102-'));
try {
const a = join(dir, 'a.md');
const b = join(dir, 'b.md');
const s = join(dir, '.session-state.local.json');
writeFileSync(a, '---\nproduced_by: graceful-handoff\nproduced_at: 2026-05-03T10:00:00.000Z\n---\n\nbody\n');
writeFileSync(b, '---\nproduced_by: trekexecute\nproduced_at: 2026-05-04T16:05:00.000Z\n---\n\nbody\n');
writeFileSync(s, JSON.stringify({ schema_version: 1, updated_at: '2026-05-04T16:00:00.000Z' }));
const filled = cmd.replace('<RESOLVED-PATH-A>', a).replace('<RESOLVED-PATH-B>', b).replace('<RESOLVED-PATH-STATE>', s);
const r = bash(filled, ROOT);
assert.equal(r.status, 0, r.stdout + r.stderr);
assert.ok(JSON.parse(r.stdout).warnings.some((w) => w.code === 'NEXT_SESSION_PROMPT_STALE_IGNORED'));
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('[#102] sibling: commands/trekexecute.md names project-dir and repo-root candidates, never plugin-root candidates', () => {
const text = flat(read(EXEC));
assert.doesNotMatch(text, /plugin-root candidates/);
assert.equal((text.match(/project-dir and repo-root candidates/g) || []).length, 2);
});
// --- #113: the contrarian gets a conclusion to attack -----------------------
// contrarian-researcher was launched in the same parallel message as the agents
// whose conclusion it is meant to attack, and standard effort pointed at a
// "conditional trigger" that nothing defined. It now runs as wave 2, after wave
// 1 returns, given wave 1's leading answer from the external findings only,
// with the local parts stripped (the de-localising rule of External agents), so
// no local file name or project term reaches its web queries.
const RESEARCH = 'commands/trekresearch.md';
const RESEARCH_ORCH = 'agents/research-orchestrator.md';
test('[#113] Phase 4 launches contrarian-researcher as wave 2 after wave 1, with the leading answer and claims from external findings only, and names the deep-research case', () => {
const text = read(RESEARCH);
const ext = partAt(RESEARCH, '### External agents (scope = both or external)', (l) => /^## /.test(l));
assert.doesNotMatch(ext, /Launch ALL selected agents \*\*in parallel\*\*/, 'Launch rules still start every selected agent, the contrarian too, in one parallel message');
assert.match(ext, /Launch in two waves, both in Phase 4 \(before Phase 4\.5 and Phase 5/);
assert.match(ext, /\*\*Wave 1:\*\* ALL selected agents except `contrarian-researcher` \*\*in parallel\*\* in a single message/);
assert.match(ext, /\*\*Wave 2:\*\* `contrarian-researcher`, when selected, after wave 1 returns/);
assert.match(ext, /the leading answer \(one or two sentences\), the claims for it and the alternatives wave 1 dismissed/);
assert.match(ext, /taken from wave 1's external findings only, never from the local agents' findings/, 'wave 2 does not say external findings only');
assert.match(ext, /stripped of local parts like the research question above \(absolute paths, user names, internal project or repository names, ticket IDs\)/, 'wave 2 does not apply the stripping rule');
assert.match(ext, /Pass the research question to them with the local parts \(absolute paths, user names, internal project or repository names, ticket IDs\) already removed/);
assert.match(ext, /With `--engine deep-research` \(`effective_engine = deep-research`\), wave 2 runs after the engine returns, from the leading answer of its report/);
const p4 = partAt(RESEARCH, '## Phase 4 — Parallel research (agent swarm)', (l) => /^## /.test(l));
assert.match(p4, /then run wave 2 \(`contrarian-researcher`, see Launch rules\) from the report's leading answer/, 'the deep-research step 5 does not hand over to wave 2');
const w2 = text.indexOf('**Wave 2:**');
assert.ok(w2 > text.indexOf('## Phase 4 —') && w2 < text.indexOf('## Phase 4.5 —'), 'wave 2 is not in Phase 4, before Phase 4.5');
});
test('[#113] no conditional trigger is left, and high effort keeps contrarian-researcher unconditional, as wave 2', () => {
const text = flat(read(RESEARCH));
assert.doesNotMatch(text, /triggers conditionally when a leading recommendation is emerging/);
assert.doesNotMatch(text, /use the existing conditional triggers/);
const high = flat(section(read(RESEARCH), '### High-effort behavior (v5.1.1)'));
assert.ok(high.includes('force `contrarian-researcher` to always-on: in high-effort mode `contrarian-researcher` runs unconditionally, as wave 2 (see Launch rules), so the final brief always carries an adversarial counter-evidence pass'),
'high effort does not keep the contrarian unconditional as wave 2');
});
test('[#113] sibling: README.md', () => {
const text = flat(read('README.md'));
assert.doesNotMatch(text, /contrarian-researcher when a leading recommendation forms/);
assert.ok(text.includes('contrarian-researcher, when selected, runs in wave 2 after the other external agents return, with their leading answer to attack (always at `effort=high`)'),
'README.md does not describe wave 2');
});
test('[#113] sibling: agents/research-orchestrator.md', () => {
const p2 = flat(section(read(RESEARCH_ORCH), '### Phase 2 — Parallel research'));
assert.doesNotMatch(p2, /Launch ALL selected agents \*\*in parallel\*\*/);
assert.match(p2, /\*\*Wave 1:\*\* ALL selected agents except `contrarian-researcher` \*\*in parallel\*\* using the Agent tool — one message, multiple tool calls/);
assert.match(p2, /\*\*Wave 2:\*\* `contrarian-researcher`, after wave 1 returns/);
assert.match(p2, /the leading answer \(one or two sentences\), the claims for it and the alternatives wave 1 dismissed/);
assert.match(p2, /taken from wave 1's external findings only, never from the local agents' findings/);
assert.match(p2, /with the local parts \(absolute paths, user names, internal project or repository names, ticket IDs\) removed/);
assert.ok(flat(read(RESEARCH_ORCH)).includes('| `contrarian-researcher` | Counter-evidence, overlooked alternatives, reasons to reconsider; runs in wave 2, against wave 1\'s leading answer |'),
'the agent table does not say the contrarian runs in wave 2');
});
// --- F9: changed code and instruction files get deep review by default ------
// The triage table made `summary-only` the default treatment for everything
// else, so changed code outside auth/crypto/security/hooks got a structural
// pass only. `deep-review` is now the default for every changed file not in
// the other two rows, the instruction files CLAUDE.md, AGENTS.md and
// .claude/rules/** among them; `summary-only` is for docs/**, root Markdown
// other than CLAUDE.md and AGENTS.md, and tests/fixtures/**. The four copies
// (command, orchestrator, README, workflow fallback, template rows) say the
// same; each is pinned by its own test against the phrase set below.
const REVIEW = 'commands/trekreview.md';
const F9_DEEP = 'for every changed file not in the other two rows — code, tests, `commands/`, `agents/`, `skills/` Markdown, and the instruction files `CLAUDE.md`, `AGENTS.md` and `.claude/rules/**`';
const F9_SUMMARY = '`docs/**`, Markdown files at the repo root other than `CLAUDE.md` and `AGENTS.md` (README, CHANGELOG), `tests/fixtures/**`';
const F9_OLD = /Default treatment for everything else/;
/** The one table row in `text` whose first cell is `cell`. */
function f9Row(text, cell) {
const hits = text.split('\n').filter((l) => l.startsWith(`| ${cell} |`));
assert.equal(hits.length, 1, `expected exactly one "${cell}" row; found ${hits.length}`);
return hits[0];
}
/** The deep-review and summary-only rows hold the phrase set; the old default is gone. */
function f9Table(text, where) {
assert.doesNotMatch(text, F9_OLD, `${where}: summary-only is still the default treatment for everything else`);
const deep = f9Row(text, '`deep-review`');
assert.match(deep, /\| Default for every changed file/, `${where}: the deep-review row does not name the default`);
assert.ok(deep.includes(F9_DEEP), `${where}: the deep-review row does not name code, tests and CLAUDE.md, AGENTS.md, .claude/rules/**`);
const summary = f9Row(text, '`summary-only`');
assert.ok(summary.includes(F9_SUMMARY), `${where}: the summary-only row is not docs/**, root Markdown, tests/fixtures/**`);
assert.doesNotMatch(summary, /[Dd]efault/, `${where}: the summary-only row still claims the default`);
assert.match(f9Row(text, '`skip`'), /Matches `\*\.lock`, `\*\.svg`, `dist\/\*\*`, `build\/\*\*`, `node_modules\/\*\*`/, `${where}: the skip row changed`);
}
test('[F9] the trekreview triage table makes deep-review the default, naming CLAUDE.md, AGENTS.md and .claude/rules/**; summary-only is for docs; the size gate is unchanged', () => {
const p4 = section(read(REVIEW), '## Phase 4 — Triage gate (deterministic path-pattern classifier)');
f9Table(p4, REVIEW);
assert.ok(p4.includes('if (reviewed_files_count > 100) → ask user'), 'the file-count gate changed');
assert.ok(p4.includes('if (estimated_diff_tokens > 100000) → ask user'), 'the diff-token gate changed');
});
test('[F9] sibling: agents/review-orchestrator.md', () => {
f9Table(section(read('agents/review-orchestrator.md'), '### Phase 4 — Triage gate (path-pattern classifier)'), 'agents/review-orchestrator.md');
});
test('[F9] sibling: README.md', () => {
const tri = flat(section(read('README.md'), '### Triage gate (deterministic)'));
assert.doesNotMatch(tri, /Deep-review patterns: `auth\/\*\*`, `crypto\/\*\*`, `\*\*\/security\/\*\*`\./, 'README.md still limits deep-review to auth/crypto/security');
assert.ok(tri.includes(`\`deep-review\`: default ${F9_DEEP}.`), 'README.md does not make deep-review the default with the instruction files');
assert.ok(tri.includes(`\`summary-only\`: ${F9_SUMMARY}.`), 'README.md does not limit summary-only to docs/**, root Markdown, tests/fixtures/**');
assert.ok(tri.includes('Hard refuse-with-suggestion above 100 files / 100K diff tokens.'), 'README.md size gate changed');
});
test('[F9] sibling: scripts/trekreview-armB.workflow.mjs', () => {
const text = read('scripts/trekreview-armB.workflow.mjs');
assert.doesNotMatch(text, /treat all changed files as summary-only/, 'the workflow fallback still treats every file as summary-only');
assert.ok(text.includes("a.triage || '(no triage map supplied — deep-review every changed file except docs/**, root Markdown other than CLAUDE.md and AGENTS.md, and tests/fixtures/**)',"),
'the workflow fallback does not deep-review every changed file except docs/**, root Markdown other than CLAUDE.md and AGENTS.md, and tests/fixtures/**');
});
test('[F9] sibling: templates/trekreview-template.md', () => {
const cov = section(read('templates/trekreview-template.md'), '## Coverage');
assert.doesNotMatch(cov, /matched deep-review pattern|no test patterns matched/, 'the template rows still show the old pattern reasons');
assert.ok(cov.includes('| lib/foo.mjs | deep-review | default for changed code |'), 'the template has no deep-review default row');
assert.ok(cov.includes('| docs/guide.md | summary-only | docs/** is summary-only |'), 'the template has no docs summary-only row');
});
// --- CL: a line number alone never refutes a finding ------------------------
// Pass 2's Accuracy row dropped a finding as refuted (`accuracy:refuted`) for an
// "impossibly large line number", so a finding whose claim was never checked
// weighed nothing and could not hold ALLOW back. Only a citation that escapes
// the repo root or a negative line refutes; a line past the end of the file
// makes the finding `unverified` (`accuracy:line-out-of-range`): the citation
// is wrong, the claim is unchecked. The lib side is pinned in
// tests/lib/coordinator-contract.test.mjs.
//
// --- DJ: the Declined to Judge copy-through is held -------------------------
// A reviewer's own `### Declined to Judge` lines reach review.md only through
// three sentences: /trekreview Phase 6 passes them to the coordinator, the
// coordinator's Input names them, and body section 9 copies them. Each is
// pinned by its own flattened regex. These pass at ab6b32c by design (no text
// changed); the round's red script proves them red by deleting the sentences.
const COORD = 'agents/review-coordinator.md';
/** The one Pass 2 table row whose first cell is `Accuracy`. */
function accuracyRow() {
const p2 = section(read(COORD), '### Pass 2 — HubSpot Judge filters (3 criteria)');
const hits = p2.split('\n').filter((l) => /^\| Accuracy \|/.test(l));
assert.equal(hits.length, 1, `expected exactly one Accuracy row in Pass 2; found ${hits.length}`);
return hits[0];
}
test('[CL] the Accuracy row refutes only a root escape or a negative line; a line past the end is unverified (accuracy:line-out-of-range)', () => {
const row = accuracyRow();
assert.match(row, /dropped|accuracy:refuted/, 'control: the Accuracy row still drops something as refuted');
for (const phrase of [/impossibly large line number/, /≤ file line count/]) {
assert.doesNotMatch(row, phrase, `the Accuracy row still pairs ${phrase} with dropped / accuracy:refuted`);
}
assert.match(row, /Path traversal escape or negative line \| \*\*dropped\*\* \(`accuracy:refuted`\)/,
'the Accuracy row does not drop exactly a root escape or a negative line as refuted');
assert.match(row, /A line past the end of the file[^|]*the citation is wrong, the claim is unchecked[^|]*`unverified` \(`accuracy:line-out-of-range`\)/,
'the Accuracy row does not send a line past the end of the file to unverified (accuracy:line-out-of-range)');
const why = flat(section(read(COORD), '### Pass 2 — HubSpot Judge filters (3 criteria)'));
assert.match(why, /Accuracy does: a citation that escapes the repo root, or a negative line, refutes the finding as a claim about this codebase\. A line past the end of the file does not: the citation is wrong, the claim is unchecked\./,
'the paragraph after the table still lets a line number alone refute');
});
test('[DJ] /trekreview Phase 6 passes each reviewer\'s Declined to Judge lines verbatim for the coordinator to copy', () => {
const p6 = flat(section(read(REVIEW), '## Phase 6 — Coordinator dedup + verdict'));
assert.match(p6, /pass each reviewer's `### Declined to Judge` lines verbatim from its saved `\{project_dir\}\/\.review-out-\{reviewer\}\.md`: the coordinator copies them into `## Declined to Judge`, so nothing a reviewer set aside disappears\./,
'Phase 6 no longer hands the reviewers\' Declined to Judge lines to the coordinator');
});
test('[DJ] review-coordinator Input: the reviewers\' Declined to Judge lines are copied into body section 9, not ruled on', () => {
const input = flat(section(read(COORD), '## Input'));
assert.match(input, /\*\*Reviewers' Declined to Judge lines\*\* — each reviewer's own `### Declined to Judge` list, verbatim\. You copy them into `## Declined to Judge` \(body section 9\); you do not rule on them\./,
'the coordinator\'s Input no longer names the reviewers\' Declined to Judge lines to copy');
});
test('[DJ] review-coordinator body section 9 copies every reviewer Declined to Judge line as {reviewer}: {line}, outside the JSON array and the verdict', () => {
const body = flat(section(read(COORD), '### Body sections (in order)'));
assert.match(body, /then every line a reviewer wrote under its own `### Declined to Judge`, copied as `\{reviewer\}: \{line\}` \(they are not findings: they stay out of the JSON `declined_to_judge` array and do not change the verdict\)/,
'body section 9 no longer copies the reviewers\' Declined to Judge lines');
});
test('[ORD] sibling: agents/review-coordinator.md — Pass 1 groups in two passes, title first, with no "else the first one"', () => {
const pass1 = flat(section(read(COORD), '### Pass 1 — Dedup by `(file, line, rule_key)` triplet'));
assert.doesNotMatch(pass1, /else the first one/, 'Pass 1 still lets a finding fall back to the first row in arrival order');
assert.doesNotMatch(pass1, /keep both rows, in input order/, 'Pass 1 still keeps the rows in input order');
assert.match(pass1, /Group in two passes, so the order the findings arrive in never changes a row: first every finding joins the row of its title/,
'Pass 1 does not name the two passes, title first');
});
// --- F4b sibling: the writer of step-{N}-upward.diff ------------------------
// The park tool reads the diff "Back to planning" saves; with color.diff=always
// or core.quotepath on in the user's config, its headers were coloured or
// quoted. The save line pins both off, on the same line (no line_ref move).
const PARK_WRITER = 'git -c core.quotepath=off diff --no-ext-diff --no-color --no-renames --src-prefix=a/ --dst-prefix=b/ {start_sha}';
test('[F4b] sibling: commands/trekexecute.md "Back to planning" saves the diff with core.quotepath=off and --no-color', () => {
const lines = read(EXEC).split('\n').filter((l) => l.startsWith('**Back to planning** (`fix_rounds = 4`): save `'));
assert.equal(lines.length, 1, `expected one "Back to planning" save line; found ${lines.length}`);
assert.ok(lines[0].includes(`save \`${PARK_WRITER}\` (one form whatever the user's git config) to`),
`the upward diff is not saved as ${PARK_WRITER}: ${lines[0]}`);
});
// --- e2e script: tests/e2e-fix-round-4-5a.sh --------------------------------
// The round run end to end (the plan's End-to-end:). Static pins only: the
// script itself is the End-to-end: command.
const E2E_ROUND = 'tests/e2e-fix-round-4-5a.sh';
test('e2e script: tests/e2e-fix-round-4-5a.sh exists, is ASCII only and passes /bin/bash -n', () => {
assert.ok(existsSync(join(ROOT, E2E_ROUND)), `missing file ${E2E_ROUND}`);
const buf = readFileSync(join(ROOT, E2E_ROUND));
assert.equal(buf.findIndex((b) => b > 127), -1, 'the e2e script is not ASCII only');
const r = spawnSync('/bin/bash', ['-n', join(ROOT, E2E_ROUND)], { encoding: 'utf8', env: childEnv() });
assert.equal(r.status, 0, r.stderr);
const text = buf.toString('utf8');
assert.match(text, /e2e: ok/, 'the script does not print e2e: ok');
assert.match(text, /^set -u$/m, 'the script does not set -u');
for (const bash4 of [/\bdeclare -A\b/, /\bmapfile\b/, /\breadarray\b/, /\|&/]) {
assert.doesNotMatch(text, bash4, `the script uses ${bash4}, which bash 3.2 lacks`);
}
});
test('e2e script: it makes its temp directory with mktemp -d and an EXIT trap removes it', () => {
const text = read(E2E_ROUND);
assert.match(text, /^TMP="\$\(mktemp -d [^\n]*\)"/m, 'the script does not make $TMP with mktemp -d');
assert.match(text, /^trap 'rm -rf "\$TMP"' EXIT$/m, 'no EXIT trap removes $TMP');
});
test('e2e script: it names each of the four CLIs it runs', () => {
const text = read(E2E_ROUND);
for (const cli of ['lib/util/park-attempt-files.mjs', 'lib/review/verdict-gate.mjs', 'lib/parsers/arg-parser.mjs', 'lib/util/autonomy-gate.mjs']) {
assert.ok(text.includes(cli), `the e2e script does not run ${cli}`);
}
});
// --- red script: tests/fix-round-4-5a-red.sh --------------------------------
// The round's red proof (the brief's last criterion). Static pins only: the
// script runs itself as the step's Regression:. Its judge is tested to go red
// in tests/lib/red-judge.test.mjs. The 30 ids are the brief's (brief.md:134
// and the 4.5a rows), written here as a constant so the list in the script
// cannot shrink without this test noticing.
const RED_SCRIPT = 'tests/fix-round-4-5a-red.sh';
const RED_IDS = ['#52', '#47', '#48', '#46', '#2', '#26', '#33', '#1', '#30', '#28', 'R6', 'D67', '#15', '#12', '#66',
'#102', '#57', '#67', '#113', '#111', '#24', '#23', 'F9', 'F4a', 'F4b', 'F4c', 'WF', 'CL', 'ORD', 'DJ'];
test('red script: tests/fix-round-4-5a-red.sh exists, is ASCII only, passes /bin/bash -n and uses no bash 4 form', () => {
assert.ok(existsSync(join(ROOT, RED_SCRIPT)), `missing file ${RED_SCRIPT}`);
const buf = readFileSync(join(ROOT, RED_SCRIPT));
assert.equal(buf.findIndex((b) => b > 127), -1, 'the red script is not ASCII only');
const r = spawnSync('/bin/bash', ['-n', join(ROOT, RED_SCRIPT)], { encoding: 'utf8', env: childEnv() });
assert.equal(r.status, 0, r.stderr);
const text = buf.toString('utf8');
assert.match(text, /^set -u$/m, 'the red script does not set -u');
for (const bash4 of [/\bdeclare -A\b/, /\bmapfile\b/, /\breadarray\b/, /\|&/, /^\s*[A-Za-z_]+=\(/m, /\$\{[A-Za-z_]+,,\}/]) {
assert.doesNotMatch(text, bash4, `the red script uses ${bash4}, which bash 3.2 lacks or the round forbids`);
}
});
test('red script: an EXIT trap removes $TMP, and INT, TERM and HUP remove it too', () => {
const text = read(RED_SCRIPT);
assert.match(text, /^TMP="\$\(mktemp -d [^\n]*\)"/m, 'the red script does not make $TMP with mktemp -d');
assert.match(text, /^trap 'rm -rf "\$TMP"' EXIT$/m, 'no EXIT trap removes $TMP');
assert.match(text, /^trap '[^'\n]*rm -rf "\$TMP"[^'\n]*' INT TERM HUP$/m, 'no INT/TERM/HUP trap removes $TMP');
});
test('red script: BASE is the full ab6b32c sha', () => {
const text = read(RED_SCRIPT);
const lines = text.split('\n').filter((l) => /^BASE=/.test(l));
assert.deepEqual(lines, ['BASE=ab6b32c9304534b77c7ca2b36e7c993682c9efde'], 'BASE is not the one full ab6b32c sha');
});
test('red script: the id list is the round\'s 30 ids, in the brief\'s order', () => {
const text = read(RED_SCRIPT);
const lines = text.split('\n').filter((l) => /^IDS=/.test(l));
assert.equal(lines.length, 1, `expected one IDS= line; found ${lines.length}`);
const m = lines[0].match(/^IDS='([^']*)'$/);
assert.ok(m, `the IDS= line is not one single-quoted list: ${lines[0]}`);
assert.deepEqual(m[1].trim().split(/\s+/), RED_IDS);
});
// --- CHANGELOG: what the round fixed ----------------------------------------
// Untagged: the CHANGELOG text is the round's report, not one id's fix. The
// round's section is found by its title, as tests/helpers/changelog-section.mjs
// finds the 6.4.0 delivery's: `## Unreleased — <title>` before the release,
// `## vX.Y.Z — <date> — <title>` after it, exactly once, up to the next `## `
// heading. The released 6.5.0 section must stay byte-equal: its sha256 (the
// heading line through the line before the next `## ` heading, joined with
// `\n`) was computed once from `git show ab6b32c:CHANGELOG.md` with this
// slice; the test reads no git history.
const CHANGELOG = 'CHANGELOG.md';
const ROUND_TITLE = "The annotating skill, the run coupling and P10's model-free part";
const ROUND_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${ROUND_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
const V650_SHA256 = '1df8fd0fe7730204740ca509c8fceb6f9d331859b98ce5c1f8e62b9551ce3274';
/** The CHANGELOG lines from the one heading `isHeading` accepts to the line before the next `## ` heading. */
function changelogPart(isHeading, what) {
const lines = read(CHANGELOG).split('\n');
const at = [];
lines.forEach((l, i) => { if (isHeading(l.replace(/\s+$/, ''))) at.push(i); });
assert.equal(at.length, 1, `${CHANGELOG} has the ${what} section exactly once (found ${at.length})`);
let end = lines.length;
for (let i = at[0] + 1; i < lines.length; i++) if (/^## /.test(lines[i])) { end = i; break; }
return lines.slice(at[0], end).join('\n');
}
const roundPart = () => changelogPart((l) => ROUND_HEADING.test(l), `"${ROUND_TITLE}"`);
/** The `### ` subsection headings of the round's section, in order. */
const roundSubsections = () => roundPart().split('\n').filter((l) => /^### /.test(l)).map((l) => l.replace(/\s+$/, ''));
/** The bullets of one `### ` subsection of the round's section, each flattened to one line. */
function roundBullets(heading) {
const lines = section(roundPart(), heading).split('\n').slice(1);
const out = [];
for (const l of lines) {
if (/^- /.test(l)) out.push(l);
else if (out.length && l.trim() !== '') out[out.length - 1] += ` ${l}`;
}
return out.map(flat);
}
const FIXED_KNOWN_ISSUES = [
['trial-5 finding 9 (core code never deep-reviewed)', /trial-5 finding 9/],
['the park step', /park step/],
['the workflow floor and put-back', /`--workflow`.*floor.*put-back|`--workflow`.*put-back.*floor/],
['the row order (which row a finding joins)', /which row a finding joins/i],
['the coordinator\'s line numbers (a line past the end refuted)', /line (?:number )?past the end of the file.*(?:refuted|refutes)/],
['the Declined to Judge copy-through', /Declined to Judge.*`review\.md`/],
];
test('CHANGELOG: the round\'s section has ### Fixed between ### Changed and ### Doors, and ### Known issues after ### Fixed', () => {
const subs = roundSubsections();
for (const h of ['### Changed', '### Fixed', '### Known issues', '### Doors']) {
assert.equal(subs.filter((s) => s === h).length, 1, `the round's section must have "${h}" once; it has ${JSON.stringify(subs)}`);
}
const at = (h) => subs.indexOf(h);
assert.ok(at('### Changed') < at('### Fixed') && at('### Fixed') < at('### Doors'),
`### Fixed must stand between ### Changed and ### Doors: ${JSON.stringify(subs)}`);
assert.ok(at('### Fixed') < at('### Known issues'), `### Known issues must come after ### Fixed: ${JSON.stringify(subs)}`);
});
test('CHANGELOG: ### Fixed names each of the six 6.5.0 Known issues the round fixed, each as a 6.5.0 Known issue', () => {
const bullets = roundBullets('### Fixed');
const known = bullets.filter((b) => /6\.5\.0 Known issue/.test(b));
const missing = FIXED_KNOWN_ISSUES.filter(([, re]) => !known.some((b) => re.test(b))).map(([name]) => name);
assert.deepEqual(missing, [], `### Fixed names no entry marked "6.5.0 Known issue" for: ${missing.join('; ')}`);
});
test('CHANGELOG: ### Known issues names that a plan\'s profile: field has no writer, and names /trekexecute and /trekcontinue', () => {
const hits = roundBullets('### Known issues').filter((b) => b.includes("**A plan's `profile:` field has no writer.**"));
assert.equal(hits.length, 1, `expected one Known issues line on the plan's profile: field; found ${hits.length}`);
assert.ok(hits[0].includes('`/trekexecute` and `/trekcontinue`'), `the line does not name \`/trekexecute\` and \`/trekcontinue\`: ${hits[0]}`);
});
test('CHANGELOG: ### Doors has the two-way line for fix round 4.5a', () => {
const hits = roundBullets('### Doors').filter((b) => b.startsWith('- two-way: fix round 4.5a'));
assert.deepEqual(hits, ['- two-way: fix round 4.5a (decisions 57, 67, 68) — a revert restores the old texts and checks'],
'the Doors part has no single two-way line for fix round 4.5a');
});
const SIBLINGS_ENTRY = '- **The texts next to the fixes say the same** (siblings found during the round): `/trekreview`, the review '
+ 'reference and the Arm B coordinator prompt no longer drop a finding whose rule key is unknown; `/trekbrief` writes its stats '
+ 'record on every exit of its manual and auto paths, says that `--gates` adds one pause before its auto chain, and that the chain '
+ 'ends at the plan; `/trekexecute` says `--gates` changes nothing there, checks the wave marker before a low-effort single session, '
+ 'and names no validator check that does not exist; plan mode goes through the plan approval before it prints the `/trekexecute` '
+ 'line; the gates shim reads `--gates=true` as on.';
test('CHANGELOG: ### Fixed holds the siblings entry, exactly', () => {
assert.ok(roundBullets('### Fixed').includes(SIBLINGS_ENTRY), '### Fixed has no siblings entry with the exact text');
});
test('CHANGELOG: the released v6.5.0 section is byte-equal to its text at ab6b32c (sha256)', () => {
const part = changelogPart((l) => l.startsWith('## v6.5.0 — '), 'v6.5.0');
assert.equal(createHash('sha256').update(part, 'utf8').digest('hex'), V650_SHA256,
'the released v6.5.0 section changed; edit only the round\'s section');
});