/trekresearch loses Phase 4.5, the bounded Phase 5 loop (Phase 5 keeps its single pass of at most 2 follow-ups), the --engine flag, the deep-research adapter and its output self-check. hooks.json loses the pre-agent-cap PreToolUse entry (README: 9 hooks = 9 scripts). The STORM env vars leave the docs; query-privacy-gate.mjs stays, called by no command, and says so. Files removed: 13 (4 docs, 1 hook, 1 lib module, 1 script, 5 tests, 1 fixture). Files edited: 17. Tests: 2600 (2598 pass / 0 fail / 2 skipped) before, 2467 (2466 pass / 0 fail / 1 skipped, R3 pending) after. Durable check: group R2 green and no longer pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1568 lines
76 KiB
JavaScript
1568 lines
76 KiB
JavaScript
// tests/lib/doc-consistency.test.mjs
|
||
// Pin invariants between prose (CLAUDE.md, README.md) and source files
|
||
// (agents/*.md, commands/*.md, templates/).
|
||
//
|
||
// When this test fails, fix the source-of-truth — do NOT rewrite the test to
|
||
// hide drift. Borrowed pattern from llm-security commit 97c5c9d.
|
||
//
|
||
// Two kinds of pin live here (devil's-advocate audit §Top changes #8, S19):
|
||
// • STRUCTURAL cross-file invariants — derive one side from a source-of-truth
|
||
// (file counts, dimension counts from `### N.` headers, profile tables from
|
||
// the yaml, version-sync) and survive harmless rewording. These are the
|
||
// load-bearing ones; keep and grow them.
|
||
// • PROSE / EXISTENCE pins — assert a literal string or file fact. Most still
|
||
// guard a real wiring (CLI shims, annotate.mjs, anti-stale CHANGELOG,
|
||
// anti-false-claim), but they break on reword. Prefer a structural form
|
||
// when one exists; do not add a prose-pin that a structural test subsumes.
|
||
// All tests in this file are documentation pins, not behavior coverage.
|
||
|
||
import { test } from 'node:test';
|
||
import { strict as assert } from 'node:assert';
|
||
import { readFileSync, readdirSync } from 'node:fs';
|
||
import { join, dirname } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||
import { resolveProfile, loadProfile } from '../../lib/profiles/resolver.mjs';
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = join(HERE, '..', '..');
|
||
|
||
function read(rel) { return readFileSync(join(ROOT, rel), 'utf-8'); }
|
||
function listMd(rel) { return readdirSync(join(ROOT, rel)).filter(f => f.endsWith('.md')); }
|
||
function listMjs(rel) { return readdirSync(join(ROOT, rel)).filter(f => f.endsWith('.mjs')); }
|
||
|
||
// Normalize a "<N|word> dimensions" descriptor to an integer (four..ten + digits).
|
||
const NUMWORDS = { four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 };
|
||
function dimCounts(text, anchor) {
|
||
// Every dimension count stated on a line mentioning `anchor`.
|
||
return text.split('\n')
|
||
.filter(l => l.includes(anchor) && /dimensions/.test(l))
|
||
.map(l => l.match(/(\d+|four|five|six|seven|eight|nine|ten)\s+(?:numbered\s+)?dimensions/i))
|
||
.filter(Boolean)
|
||
.map(m => /^\d+$/.test(m[1]) ? Number(m[1]) : NUMWORDS[m[1].toLowerCase()]);
|
||
}
|
||
|
||
test('CLAUDE.md agents table row count == agents/*.md file count', () => {
|
||
const md = read('CLAUDE.md');
|
||
const agentFiles = listMd('agents');
|
||
const agentTable = md.split('## Agents')[1] || '';
|
||
const tableSection = agentTable.split('\n## ')[0];
|
||
const dataRows = tableSection
|
||
.split('\n')
|
||
.filter(l => l.startsWith('|') && !l.match(/^\|[\s-]+\|/) && !l.match(/^\|\s*Agent\s*\|/));
|
||
assert.equal(
|
||
dataRows.length,
|
||
agentFiles.length,
|
||
`Drift: ${agentFiles.length} agent files vs ${dataRows.length} CLAUDE.md table rows. ` +
|
||
`Sync agents/ ↔ CLAUDE.md.`,
|
||
);
|
||
});
|
||
|
||
test('README architecture block agent-count == agents/*.md file count', () => {
|
||
const md = read('README.md');
|
||
const m = md.match(/agents\/\s+(\d+)\s+specialized agents/);
|
||
assert.ok(m, 'README architecture block must state "N specialized agents"');
|
||
assert.equal(
|
||
Number(m[1]),
|
||
listMd('agents').length,
|
||
`Drift: README says ${m[1]} specialized agents vs ${listMd('agents').length} agents/*.md files.`,
|
||
);
|
||
});
|
||
|
||
test('README architecture block hook-count == hooks/scripts/*.mjs file count', () => {
|
||
const md = read('README.md');
|
||
const m = md.match(/hooks\/\s+(\d+)\s+hooks\s*\(/);
|
||
assert.ok(m, 'README architecture block must state "N hooks (...)"');
|
||
assert.equal(
|
||
Number(m[1]),
|
||
listMjs('hooks/scripts').length,
|
||
`Drift: README says ${m[1]} hooks vs ${listMjs('hooks/scripts').length} hooks/scripts/*.mjs files.`,
|
||
);
|
||
});
|
||
|
||
test('plan-critic dimension count: README + CLAUDE.md match the agent definition', () => {
|
||
const dims = (read('agents/plan-critic.md').match(/^### \d+\./gm) || []).length;
|
||
assert.ok(dims > 0, 'agents/plan-critic.md must define numbered "### N." dimensions');
|
||
const stated = [...dimCounts(read('README.md'), 'plan-critic'), ...dimCounts(read('CLAUDE.md'), 'plan-critic')];
|
||
assert.ok(stated.length >= 2, 'README + CLAUDE.md must cite plan-critic (N dimensions)');
|
||
for (const n of stated) {
|
||
assert.equal(n, dims, `Drift: a doc cites plan-critic (${n} dimensions) vs ${dims} numbered dims in agents/plan-critic.md.`);
|
||
}
|
||
});
|
||
|
||
test('brief-reviewer dimension count: README matches the agent definition', () => {
|
||
const agentDims = dimCounts(read('agents/brief-reviewer.md'), 'dimensions');
|
||
assert.ok(agentDims.length >= 1, 'agents/brief-reviewer.md must state "N dimensions"');
|
||
const expected = agentDims[0];
|
||
// Only the line stating brief-reviewer's OWN count — exclude agent-list lines
|
||
// that co-mention `plan-critic (N dimensions)` (those carry plan-critic's count).
|
||
const readmeDims = read('README.md').split('\n')
|
||
.filter(l => l.includes('brief-reviewer') && /dimensions/.test(l) && !l.includes('plan-critic'))
|
||
.map(l => l.match(/(\d+|four|five|six|seven|eight|nine|ten)\s+dimensions/i))
|
||
.filter(Boolean)
|
||
.map(m => /^\d+$/.test(m[1]) ? Number(m[1]) : NUMWORDS[m[1].toLowerCase()]);
|
||
assert.ok(readmeDims.length >= 1, 'README must cite brief-reviewer (N dimensions)');
|
||
for (const n of readmeDims) {
|
||
assert.equal(n, expected, `Drift: README cites brief-reviewer (${n} dimensions) vs ${expected} in agents/brief-reviewer.md.`);
|
||
}
|
||
});
|
||
|
||
test('CLAUDE.md commands table mentions every commands/*.md file', () => {
|
||
const md = read('CLAUDE.md');
|
||
const commandFiles = listMd('commands');
|
||
for (const f of commandFiles) {
|
||
const cmdName = `/${f.replace(/\.md$/, '')}`;
|
||
assert.ok(
|
||
md.includes(cmdName),
|
||
`commands/${f} not mentioned in CLAUDE.md (looked for ${cmdName})`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('every command frontmatter name matches its filename', () => {
|
||
for (const f of listMd('commands')) {
|
||
const text = read(`commands/${f}`);
|
||
const doc = parseDocument(text);
|
||
if (!doc.valid) continue;
|
||
const expected = f.replace(/\.md$/, '');
|
||
if (doc.parsed.frontmatter && doc.parsed.frontmatter.name !== undefined) {
|
||
assert.equal(
|
||
doc.parsed.frontmatter.name,
|
||
expected,
|
||
`commands/${f} frontmatter.name="${doc.parsed.frontmatter.name}" should be "${expected}"`,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('templates/plan-template.md declares plan_version: 1.9', () => {
|
||
const tpl = read('templates/plan-template.md');
|
||
assert.match(tpl, /plan_version:\s*['"]?1\.9['"]?/);
|
||
});
|
||
|
||
test('HANDOVER-CONTRACTS.md locates plan_version in the plan header line, not frontmatter (S28)', () => {
|
||
// The plan template emits plan_version only in the prose "Generated by"
|
||
// header line; brief-generated plans carry no frontmatter at all (the sole
|
||
// frontmatter block, `source_findings`, is trekreview-only and additive).
|
||
// The contract must therefore not label plan_version a frontmatter field.
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.doesNotMatch(t, /`plan_version`\s*\(frontmatter\)/,
|
||
'HANDOVER-CONTRACTS.md must not call plan_version a (frontmatter) field — it lives in the plan header line');
|
||
assert.doesNotMatch(t, /Mirrors plan's frontmatter/,
|
||
"progress.json's plan_version mirrors the plan header line, not frontmatter");
|
||
assert.match(t, /`plan_version`\s*\(plan header line\)/,
|
||
'HANDOVER-CONTRACTS.md versioning table must locate plan_version in the plan header line');
|
||
});
|
||
|
||
// S19 prune (audit §Top changes #8): removed a tautological prose-pin —
|
||
// "commands/trekexecute.md still parses v1.7 plan schema" was an OR-chain
|
||
// (`includes('plan_version') || includes('Step N:') || includes('### Step ')`)
|
||
// that is ~always true and asserts almost nothing. The real v1.7 coverage lives
|
||
// in the structural pin "templates/plan-template.md declares plan_version: 1.9"
|
||
// (above; 1.8 and 1.7 plans stay valid) + the plan-validator / plan-schema behavior tests.
|
||
|
||
// S19 prune (audit §Top changes #8): removed "CLAUDE.md mentions all six
|
||
// pipeline commands" — a hardcoded six-string list fully subsumed by the
|
||
// structural pin "CLAUDE.md commands table mentions every commands/*.md file"
|
||
// above, which derives the command set from the filesystem and auto-covers any
|
||
// new command. The hardcoded list was the weaker, reword-fragile form.
|
||
|
||
test('HANDOVER-CONTRACTS.md contains Handover 6 section', () => {
|
||
const text = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(
|
||
text.includes('## Handover 6'),
|
||
'docs/HANDOVER-CONTRACTS.md should document Handover 6 (review → plan)',
|
||
);
|
||
});
|
||
|
||
test('HANDOVER-CONTRACTS.md contains Handover 7 section (session-state)', () => {
|
||
const text = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(
|
||
text.includes('## Handover 7'),
|
||
'docs/HANDOVER-CONTRACTS.md should document Handover 7 (.session-state.local.json) ' +
|
||
'consumed by /trekcontinue',
|
||
);
|
||
assert.ok(
|
||
text.includes('.session-state.local.json'),
|
||
'Handover 7 section should name the artifact path',
|
||
);
|
||
});
|
||
|
||
test('review-validator has CLI shim', () => {
|
||
// Ordre 5: the realpath form (no-op-free under a symlink or a space), shared
|
||
// since 6.3.1 (lib/util/is-main.mjs); the run through a symlink is proven in
|
||
// tests/validators/review-validator.test.mjs.
|
||
const text = read('lib/validators/review-validator.mjs');
|
||
assert.ok(
|
||
text.includes('if (isMain(import.meta.url))'),
|
||
'lib/validators/review-validator.mjs should expose the realpath CLI shim so commands can call it from Bash',
|
||
);
|
||
});
|
||
|
||
test('session-state-validator has CLI shim', () => {
|
||
const text = read('lib/validators/session-state-validator.mjs');
|
||
assert.ok(
|
||
text.includes('if (isMain(import.meta.url))'),
|
||
'lib/validators/session-state-validator.mjs should expose the standard CLI shim ' +
|
||
'(if (isMain(import.meta.url))) so /trekcontinue can call it from Bash',
|
||
);
|
||
});
|
||
|
||
test('HANDOVER-CONTRACTS.md Handover 7 documents § Lifecycle subsection', () => {
|
||
const text = read('docs/HANDOVER-CONTRACTS.md');
|
||
const h7Start = text.indexOf('## Handover 7');
|
||
assert.ok(h7Start >= 0, 'Handover 7 heading missing');
|
||
const h7End = text.indexOf('## Stability summary', h7Start);
|
||
assert.ok(h7End > h7Start, 'Stability summary heading missing — could not bound Handover 7');
|
||
const h7 = text.slice(h7Start, h7End);
|
||
assert.ok(
|
||
h7.includes('Lifecycle'),
|
||
'Handover 7 section should include a § Lifecycle subsection (SC-5 stale-file principle)',
|
||
);
|
||
});
|
||
|
||
test('HANDOVER-CONTRACTS.md Handover 7 § Lifecycle names --cleanup and produced_by contract', () => {
|
||
const text = read('docs/HANDOVER-CONTRACTS.md');
|
||
const h7Start = text.indexOf('## Handover 7');
|
||
const h7End = text.indexOf('## Stability summary', h7Start);
|
||
const h7 = text.slice(h7Start, h7End);
|
||
assert.ok(
|
||
h7.includes('--cleanup'),
|
||
'Handover 7 § Lifecycle should mention --cleanup as the operator-invoked stale-file remover',
|
||
);
|
||
assert.ok(
|
||
h7.includes('produced_by'),
|
||
'Handover 7 § Lifecycle should document the produced_by frontmatter contract for NEXT-SESSION-PROMPT.local.md',
|
||
);
|
||
});
|
||
|
||
// S19 prune (audit §Top changes #8): removed "CLAUDE.md mentions /trekcontinue
|
||
// command" — subsumed by "CLAUDE.md commands table mentions every commands/*.md
|
||
// file" (commands/trekcontinue.md exists, so that loop already asserts the
|
||
// /trekcontinue reference). Single-command existence pin → redundant.
|
||
|
||
test('rule-catalogue has exactly 17 entries', async () => {
|
||
const mod = await import('../../lib/review/rule-catalogue.mjs');
|
||
assert.strictEqual(
|
||
mod.RULE_CATALOGUE.length,
|
||
17,
|
||
'lib/review/rule-catalogue.mjs RULE_CATALOGUE size invariant: must be 17 (12 v1.0 baseline + 2 engine keys, ordre 5, + CODE_REVIEW_IMPROVEMENT, lekerepo trial 2, + CLAUDE_MD_DEVIATION and CLAUDE_MD_STALE, decision 98)',
|
||
);
|
||
});
|
||
|
||
test('Phase 2.6 hands waves over as session orders; no launch template ships', () => {
|
||
// The coupling pin for the PM way: the prose must call the order writer and
|
||
// the status reader (tests/lib/session-orders.test.mjs proves they work), keep
|
||
// push-before-cleanup, and carry the never-start rule.
|
||
assert.ok(!existsSync(join(ROOT, 'templates', 'headless-launch-template.md')),
|
||
'templates/headless-launch-template.md must stay removed');
|
||
const exec = read('commands/trekexecute.md');
|
||
const p26 = exec.slice(exec.indexOf('## Phase 2.6'), exec.indexOf('## Phase 3 '));
|
||
for (const needle of [
|
||
'lib/util/session-orders.mjs" write',
|
||
'lib/util/session-orders.mjs" status',
|
||
'GIT_OPTIONAL_LOCKS',
|
||
'git push origin',
|
||
'Voyage never starts a Claude Code',
|
||
]) {
|
||
assert.ok(p26.includes(needle), `trekexecute.md Phase 2.6 must include "${needle}"`);
|
||
}
|
||
assert.match(exec, /21\. \*\*Never start Claude Code\.\*\*/, 'trekexecute.md Hard rule 21 missing');
|
||
});
|
||
|
||
test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', () => {
|
||
const cmd = read('commands/trekplan.md');
|
||
assert.ok(
|
||
cmd.includes('single assistant message turn'),
|
||
'commands/trekplan.md Phase 9 should reinforce single-message parallel dispatch',
|
||
);
|
||
assert.ok(
|
||
cmd.includes('plan-review-dedup.mjs'),
|
||
'commands/trekplan.md Phase 9 should call lib/review/plan-review-dedup.mjs after both reviewers complete',
|
||
);
|
||
});
|
||
|
||
// --- S23 — Phase-9 dedup hand-off is executable (defect #1 fix) ---
|
||
//
|
||
// The read-only reviewers (plan-critic, scope-guardian: Read/Glob/Grep, no Write)
|
||
// cannot write the temp files the old prose told them to. Pin the A′ fix so the
|
||
// broken /tmp-write instruction cannot creep back:
|
||
// 1. The Phase-9 prose of commands/trekplan.md must use --stdin, not the dead
|
||
// /tmp/*-out.json file path the reviewers can't produce.
|
||
// 2. BOTH reviewer agents must declare a machine-readable json findings block
|
||
// with the keys the dedup helper consumes (agent + findings + rule_key).
|
||
test('S23: Phase-9 dedup hand-off uses --stdin and the reviewers emit a json findings block', () => {
|
||
const cmd = read('commands/trekplan.md');
|
||
assert.ok(
|
||
cmd.includes('--stdin'),
|
||
'commands/trekplan.md Phase 9 must pipe reviewer findings into plan-review-dedup.mjs via --stdin (reviewers are read-only)',
|
||
);
|
||
assert.ok(
|
||
!cmd.includes('/tmp/plan-critic-out.json') && !cmd.includes('/tmp/scope-guardian-out.json'),
|
||
'commands/trekplan.md must NOT instruct the read-only reviewers to Write /tmp/*-out.json — they have no Write tool (defect #1)',
|
||
);
|
||
for (const agentFile of ['agents/plan-critic.md', 'agents/scope-guardian.md']) {
|
||
const a = read(agentFile);
|
||
assert.ok(
|
||
a.includes('"findings"') && a.includes('"rule_key"') && a.includes('"agent"'),
|
||
`${agentFile} must declare a machine-readable json findings block (agent + findings + rule_key) for the Phase-9 dedup helper`,
|
||
);
|
||
}
|
||
});
|
||
|
||
// --- S20 — anti-false-claim: operations.md custom-profile prose must match findProfilePath ---
|
||
//
|
||
// findProfilePath (lib/profiles/resolver.mjs:57) resolves built-in FIRST, then
|
||
// repo-root voyage-profiles/, then ~/.claude/voyage-profiles/. Custom profiles
|
||
// live in voyage-profiles/ (NOT lib/profiles/), must use NEW names, and CANNOT
|
||
// shadow a built-in. The old prose claimed the opposite (lib/profiles/<custom>;
|
||
// "override built-ins of the same name"; "alphabetical … precedence") — guard it.
|
||
test('operations.md custom-profile prose matches findProfilePath resolution order [S20]', () => {
|
||
const ops = read('docs/operations.md');
|
||
for (const bad of ['lib/profiles/<custom>', 'alphabetical', 'override built-ins of the same name']) {
|
||
assert.ok(
|
||
!ops.includes(bad),
|
||
`docs/operations.md: false custom-profile claim "${bad}" contradicts findProfilePath — fix the prose, not this test`,
|
||
);
|
||
}
|
||
assert.ok(
|
||
ops.includes('voyage-profiles/'),
|
||
'docs/operations.md must point custom profiles at voyage-profiles/ (not lib/profiles/)',
|
||
);
|
||
});
|
||
|
||
// --- S24 — anti-false-claim: operations.md + command-modes.md autonomy-gate truth-pass ---
|
||
//
|
||
// autonomy-gate.mjs is the --gates flag parser: --gates [true|false|closed|open]
|
||
// → {"ok":true,"gates":true|false}. It has no state machine (R9).
|
||
// The old prose (operations.md:15 / command-modes.md gates rows) (1) fabricated a
|
||
// state machine `idle → approved → executing → merge-pending → main-merged`, and
|
||
// later named the removed one (gates_on, auto_running, paused_for_gate); (3) advertised `--gates {open|closed|adaptive}` —
|
||
// there is no strictness policy and no `adaptive` value: the flag is boolean, and
|
||
// the CLI shim + all 4 command docs take `--gates [true|false|closed|open]`
|
||
// (bare, true, closed = on; false, open = off; fix round 4.5a, #1).
|
||
test('operations.md + command-modes.md describe the real autonomy-gate, not a fabricated one [S24]', () => {
|
||
const ops = read('docs/operations.md');
|
||
const modes = read('docs/command-modes.md');
|
||
|
||
// F1 — state names: fabricated (merge-pending/main-merged) or removed with the state machine (R9)
|
||
for (const bad of ['merge-pending', 'main-merged', 'gates_on', 'auto_running', 'paused_for_gate']) {
|
||
assert.ok(
|
||
!ops.includes(bad),
|
||
`docs/operations.md: fabricated autonomy state "${bad}" — autonomy-gate.mjs has no such state; fix the prose, not this test`,
|
||
);
|
||
}
|
||
|
||
// F1 — the module is documented as what it is: the flag parser
|
||
assert.ok(
|
||
ops.includes('`lib/util/autonomy-gate.mjs` is the `--gates` flag parser'),
|
||
'docs/operations.md must describe lib/util/autonomy-gate.mjs as the --gates flag parser',
|
||
);
|
||
|
||
// F3 — --gates is boolean; there is no open/closed/adaptive strictness enum
|
||
const GATES_ENUM_FORMS = ['{open|closed|adaptive}', '{open\\|closed\\|adaptive}'];
|
||
for (const [label, txt] of [['operations.md', ops], ['command-modes.md', modes]]) {
|
||
for (const bad of GATES_ENUM_FORMS) {
|
||
assert.ok(
|
||
!txt.includes(bad),
|
||
`docs/${label}: --gates flag enum "${bad}" is false — the flag is boolean: bare, true or closed = on, false or open = off, and there is no adaptive; fix the prose, not this test`,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
|
||
// --- v4.1 Step 21 — pin --profile + phase_models on the 6 commands ---
|
||
//
|
||
// CLAUDE.md / README.md pinning is deferred to Step 22 (post-write of
|
||
// those documents). Step 21 only verifies command-file content, which
|
||
// was written in Step 7 (Wave 3).
|
||
|
||
const PIPELINE_COMMANDS = [
|
||
'trekbrief.md',
|
||
'trekresearch.md',
|
||
'trekplan.md',
|
||
'trekexecute.md',
|
||
'trekreview.md',
|
||
'trekcontinue.md',
|
||
];
|
||
|
||
test('every pipeline command-file documents the --profile flag (SC #20)', () => {
|
||
for (const f of PIPELINE_COMMANDS) {
|
||
const text = read(`commands/${f}`);
|
||
assert.match(
|
||
text,
|
||
/--profile\b/,
|
||
`commands/${f}: --profile flag is required documentation in v4.1`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('command-files mentioning model profiles use canonical name `phase_models`', () => {
|
||
// Reject legacy / brainstormed alternatives that would confuse readers.
|
||
const FORBIDDEN = ['model_per_phase', 'phase_to_model', 'profile_phase_models'];
|
||
for (const f of PIPELINE_COMMANDS) {
|
||
const text = read(`commands/${f}`);
|
||
for (const bad of FORBIDDEN) {
|
||
assert.ok(
|
||
!text.includes(bad),
|
||
`commands/${f}: forbidden alias "${bad}" — canonical name is "phase_models"`,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('at least one pipeline command-file references `phase_models` canonical name', () => {
|
||
// Sanity: not every command has to enumerate phase_models inline (e.g.
|
||
// trekbrief and trekcontinue may only mention --profile), but ≥ 1
|
||
// command-file must spell out the canonical name so the regression test
|
||
// pins drift.
|
||
let mentioned = 0;
|
||
for (const f of PIPELINE_COMMANDS) {
|
||
if (read(`commands/${f}`).includes('phase_models')) mentioned += 1;
|
||
}
|
||
assert.ok(
|
||
mentioned >= 1,
|
||
`expected ≥ 1 command-file to mention canonical name "phase_models", got ${mentioned}`,
|
||
);
|
||
});
|
||
|
||
// --- v4.1 Step 22 — post-write CLAUDE.md / README.md pinning ---
|
||
//
|
||
// Plan-critic Blocker 2 fix: Step 21 only pinned commands/*.md (which
|
||
// are written in Step 7 / Wave 3). Step 22 writes the top-level docs
|
||
// and extends pinning here so doc-consistency stays green AFTER Step 22.
|
||
|
||
test('CLAUDE.md documents --profile flag', () => {
|
||
const md = read('CLAUDE.md');
|
||
assert.match(
|
||
md,
|
||
/--profile\b/,
|
||
'CLAUDE.md must document the --profile flag (v4.1 SC #20)',
|
||
);
|
||
});
|
||
|
||
test('canonical name `phase_models` documented in docs/profiles.md', () => {
|
||
// v5.x doc consolidation (67f6dd5) moved profile/phase detail OUT of
|
||
// CLAUDE.md and into docs/. docs/profiles.md is now the canonical home for
|
||
// phase_models; CLAUDE.md only links to it via "Reference docs". Assert the
|
||
// canonical name lives in the canonical doc and no legacy alias crept in.
|
||
// (was: read('CLAUDE.md') — v4.1 SC #20.)
|
||
const md = read('docs/profiles.md');
|
||
assert.match(
|
||
md,
|
||
/phase_models/,
|
||
'docs/profiles.md must use canonical name "phase_models" (v4.1 SC #20)',
|
||
);
|
||
for (const bad of ['model_per_phase', 'phase_to_model', 'profile_phase_models']) {
|
||
assert.ok(
|
||
!md.includes(bad),
|
||
`docs/profiles.md must NOT use legacy alias "${bad}"`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('README.md documents --profile flag for all 6 commands', () => {
|
||
// SG1: README flag-table coverage is gating for SC #20. README is the
|
||
// primary discovery surface for new users.
|
||
const md = read('README.md');
|
||
// Top-level Profile system section is required so the flag is
|
||
// discoverable independent of per-command tables.
|
||
assert.match(md, /## Profile system/, 'README.md missing top-level "## Profile system" section');
|
||
// Every per-command Modes table must include --profile (count of
|
||
// --profile occurrences should be ≥ 6 — one per command + Profile
|
||
// system section).
|
||
const profileMentions = (md.match(/--profile\b/g) || []).length;
|
||
assert.ok(
|
||
profileMentions >= 6,
|
||
`README.md must mention --profile ≥ 6 times (one per command + section), got ${profileMentions}`,
|
||
);
|
||
});
|
||
|
||
test('CHANGELOG.md has v4.1.0 entry', () => {
|
||
const cl = read('CHANGELOG.md');
|
||
assert.match(
|
||
cl,
|
||
/## v4\.1\.0\b/,
|
||
'CHANGELOG.md must include "## v4.1.0" entry per Keep-a-Changelog 1.1.0',
|
||
);
|
||
});
|
||
|
||
test('docs/profiles.md exists and documents Custom.yaml authoring', () => {
|
||
const dp = read('docs/profiles.md');
|
||
assert.ok(dp.length > 1000, 'docs/profiles.md must be substantive (> 1000 chars)');
|
||
// Must document custom-profile authoring (Step 22 manifest must_contain
|
||
// pattern: "Custom.yaml" — case-insensitive match handled here as
|
||
// /[Cc]ustom[. ]/ to allow either "custom.yaml" or "Custom profile" prose).
|
||
assert.match(
|
||
dp,
|
||
/[Cc]ustom\.yaml|[Cc]ustom profile|<custom>\.yaml/,
|
||
'docs/profiles.md must document custom profile authoring',
|
||
);
|
||
});
|
||
|
||
test('commands/trekplan.md Phase 8 seals Opus-4.7 schema-drift defense', () => {
|
||
const cmd = read('commands/trekplan.md');
|
||
// Locate Phase 8 section
|
||
const phase8Start = cmd.indexOf('## Phase 8');
|
||
assert.ok(phase8Start >= 0, 'Phase 8 heading missing');
|
||
const phase8End = cmd.indexOf('## Phase 9', phase8Start);
|
||
assert.ok(phase8End > phase8Start, 'Phase 9 heading missing — could not bound Phase 8');
|
||
const phase8 = cmd.slice(phase8Start, phase8End);
|
||
// Required regex source-of-truth references
|
||
assert.ok(
|
||
phase8.includes('STEP_HEADING_REGEX'),
|
||
'Phase 8 should inline STEP_HEADING_REGEX so format contract survives without orchestrator-doc loading',
|
||
);
|
||
assert.ok(
|
||
phase8.includes('FORBIDDEN_HEADING_REGEX'),
|
||
'Phase 8 should inline FORBIDDEN_HEADING_REGEX (Step 7 — schema-drift seal)',
|
||
);
|
||
// Required validator self-check
|
||
assert.ok(
|
||
phase8.includes('plan-validator.mjs --strict'),
|
||
'Phase 8 should mandate post-write `plan-validator.mjs --strict` self-check',
|
||
);
|
||
// Forbidden-headings list (literal "FORBIDDEN" appears more than once: in regex const + in human-readable list)
|
||
assert.ok(
|
||
/FORBIDDEN/.test(phase8),
|
||
'Phase 8 should explicitly enumerate FORBIDDEN headings',
|
||
);
|
||
});
|
||
|
||
// --- v5.0.0 / v5.0.1 — bespoke playground removed; /playground invocation explicit ---
|
||
//
|
||
// v5.0.0 removed the bespoke playground SPA, /trekrevise, and Handover 8.
|
||
// v5.0.1 dropped the v5.0.0 stop-gap (scripts/render-artifact.mjs) and made
|
||
// the producing commands print a literal, copy-paste-ready /playground
|
||
// document-critique invocation instead. These pins lock both removals in
|
||
// AND pin the new copy-paste invocation as the operator-facing contract.
|
||
|
||
import { existsSync } from 'node:fs';
|
||
|
||
test('playground/ directory no longer exists (removed in v5.0.0)', () => {
|
||
assert.ok(
|
||
!existsSync(join(ROOT, 'playground')),
|
||
'plugins/voyage/playground/ should be deleted — the bespoke playground was retired in v5.0.0',
|
||
);
|
||
});
|
||
|
||
test('commands/trekrevise.md no longer exists (removed in v5.0.0)', () => {
|
||
assert.ok(
|
||
!existsSync(join(ROOT, 'commands/trekrevise.md')),
|
||
'/trekrevise was removed in v5.0.0 — its command file should be gone',
|
||
);
|
||
});
|
||
|
||
test('Handover 8 deleted from HANDOVER-CONTRACTS.md (back to seven handovers)', () => {
|
||
const text = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(!text.includes('## Handover 8'), 'Handover 8 section should be removed in v5.0.0');
|
||
assert.ok(text.includes('## Handover 7'), 'Handover 7 must remain');
|
||
});
|
||
|
||
test('scripts/render-artifact.mjs is still removed (v5.0.1 + v5.0.2)', () => {
|
||
assert.ok(
|
||
!existsSync(join(ROOT, 'scripts/render-artifact.mjs')),
|
||
'scripts/render-artifact.mjs should be deleted — v5.0.1 dropped the standalone HTML render; v5.0.2 kept it removed (annotate.mjs is the replacement)',
|
||
);
|
||
});
|
||
|
||
test('scripts/annotate.mjs exists (v5.0.2 operator-annotation HTML generator)', () => {
|
||
assert.ok(
|
||
existsSync(join(ROOT, 'scripts/annotate.mjs')),
|
||
'scripts/annotate.mjs is required — producing commands call it to build the operator-annotation HTML',
|
||
);
|
||
});
|
||
|
||
test('producing commands reference scripts/annotate.mjs (v5.0.2 render-and-link step)', () => {
|
||
// v5.0.0 → v5.0.1 → v5.0.2 chain: v5.0.0 added an HTML render that didn't
|
||
// afford annotation; v5.0.1 pointed at /playground document-critique (which
|
||
// pre-generates Claude's suggestions, not operator-driven annotation); v5.0.2
|
||
// ships scripts/annotate.mjs — an operator-driven annotation surface where
|
||
// the OPERATOR clicks lines and writes their own notes. Pin the wiring.
|
||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||
assert.ok(
|
||
read(`commands/${f}`).includes('scripts/annotate.mjs'),
|
||
`commands/${f} must invoke scripts/annotate.mjs to build the operator-annotation HTML (v5.0.2)`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('producing commands no longer print the v5.0.1 /playground document-critique line', () => {
|
||
// v5.0.1 told operators to copy-paste "/playground build a document-critique
|
||
// playground for X" — but that flow pre-generates Claude's suggestions. The
|
||
// operator asked for their own annotations, not a critique of Claude's.
|
||
// v5.0.2 removes that line from the producing commands' final report.
|
||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||
assert.ok(
|
||
!read(`commands/${f}`).includes('/playground build a document-critique'),
|
||
`commands/${f} must not print the v5.0.1 /playground document-critique invocation — v5.0.2 replaces it with annotate.mjs`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('producing commands tell the operator the flow is THEIR own annotations', () => {
|
||
// Pin language: every producing command's prose must mention that the
|
||
// OPERATOR drives annotation, not Claude. Phrase variants are allowed
|
||
// ("YOUR OWN note", "operator drives", etc.) — we look for the operator-
|
||
// ownership signal.
|
||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||
const text = read(`commands/${f}`);
|
||
assert.ok(
|
||
/YOUR OWN|operator drives|your own/i.test(text),
|
||
`commands/${f} must signal that the operator drives annotation (v5.0.2 contract)`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('producing commands emit file:// link in final report (operator-UX contract, 2026-05-13)', () => {
|
||
// Operator runs Ghostty / iTerm2 / modern Terminal.app — all support cmd+click
|
||
// on file:// URLs. Producing commands MUST emit both forms: (a) plain file://
|
||
// line in the report block, (b) `open file://...` copy-pasteable command.
|
||
// Both must reference $ANNOT_HTML (absolute path from scripts/annotate.mjs).
|
||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||
const text = read(`commands/${f}`);
|
||
assert.ok(
|
||
/file:\/\/\{\$ANNOT_HTML\}/.test(text),
|
||
`commands/${f} must include "file://{$ANNOT_HTML}" plain URL in the final report block`,
|
||
);
|
||
assert.ok(
|
||
/open file:\/\/\{\$ANNOT_HTML\}/.test(text),
|
||
`commands/${f} must include "open file://{$ANNOT_HTML}" copy-pasteable command in the final report block`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('package.json is marked private — the npm name `voyage` belongs to a third party', () => {
|
||
// D-census 2026-08-20/21: registry.npmjs.org/voyage answers 200, but that is
|
||
// NOT this package — it is "Advanced HTTP Routing System for Node.js"
|
||
// (NEURS/voyage, v0.0.1, 2022, maintainer kevin.martin). This manifest is test
|
||
// tooling for a Claude Code plugin distributed through the marketplace catalogue
|
||
// (`ref: vX.Y.Z`), never through npm. Without `private`, nothing in the manifest
|
||
// stops an accidental `npm publish` against a name someone else owns — a one-way
|
||
// action. Sibling non-package manifests (okr, repo-mailbox, repo-standard) all
|
||
// carry the flag; voyage was the exception. Operator decision (S93): option A.
|
||
const pkg = JSON.parse(read('package.json'));
|
||
assert.equal(
|
||
pkg.private,
|
||
true,
|
||
'package.json must declare "private": true — the npm name `voyage` is owned by a third party',
|
||
);
|
||
});
|
||
|
||
test('package.json still has no "npm run render" script (removed in v5.0.1)', () => {
|
||
const pkg = JSON.parse(read('package.json'));
|
||
assert.equal(
|
||
pkg.scripts && pkg.scripts.render,
|
||
undefined,
|
||
'package.json scripts.render should remain gone',
|
||
);
|
||
});
|
||
|
||
test('CHANGELOG.md has v5.0.0 entry', () => {
|
||
const cl = read('CHANGELOG.md');
|
||
assert.match(cl, /## v5\.0\.0\b/, 'CHANGELOG.md must include "## v5.0.0" entry');
|
||
});
|
||
|
||
test('CHANGELOG.md has v5.0.1 entry', () => {
|
||
const cl = read('CHANGELOG.md');
|
||
assert.match(cl, /## v5\.0\.1\b/, 'CHANGELOG.md must include "## v5.0.1" entry');
|
||
});
|
||
|
||
test('CHANGELOG.md has v5.0.2 entry', () => {
|
||
const cl = read('CHANGELOG.md');
|
||
assert.match(cl, /## v5\.0\.2\b/, 'CHANGELOG.md must include "## v5.0.2" entry');
|
||
});
|
||
|
||
test('CHANGELOG.md retains v4.2.0 entry (history is not rewritten)', () => {
|
||
const cl = read('CHANGELOG.md');
|
||
assert.match(cl, /## v4\.2\.0\b/, 'CHANGELOG.md must keep the historical "## v4.2.0" entry');
|
||
});
|
||
|
||
test('CHANGELOG.md has v5.5.0 entry (S13 coordinated release)', () => {
|
||
const cl = read('CHANGELOG.md');
|
||
assert.match(cl, /## v5\.5\.0\b/, 'CHANGELOG.md must include "## v5.5.0" entry — the coordinated release that lands brief_version 2.2 + NW1–NW3');
|
||
});
|
||
|
||
test('version is consistent across plugin.json, package.json, README badge, and CHANGELOG top entry', () => {
|
||
// versjonssync invariant (CLAUDE.md rule): every file that names the plugin
|
||
// version must agree. Bumping one without the others is a release defect.
|
||
const pluginVersion = JSON.parse(read('.claude-plugin/plugin.json')).version;
|
||
const pkgVersion = JSON.parse(read('package.json')).version;
|
||
const badgeVersion = (read('README.md').match(/badge\/version-([\d.]+)-/) || [])[1];
|
||
const changelogTop = (read('CHANGELOG.md').match(/## v(\d+\.\d+\.\d+)\b/) || [])[1];
|
||
assert.ok(pluginVersion, 'plugin.json missing version');
|
||
assert.equal(pkgVersion, pluginVersion, `package.json version ${pkgVersion} != plugin.json ${pluginVersion}`);
|
||
assert.equal(badgeVersion, pluginVersion, `README badge ${badgeVersion} != plugin.json ${pluginVersion}`);
|
||
assert.equal(changelogTop, pluginVersion, `CHANGELOG top entry v${changelogTop} != plugin.json ${pluginVersion}`);
|
||
});
|
||
|
||
test('operational files no longer reference trekrevise (v5.0.0 removal)', () => {
|
||
// Templates, the touched command files, and the
|
||
// handover-contracts doc must be fully scrubbed. CLAUDE.md / README.md are
|
||
// intentionally allowed to mention /trekrevise in their "removed in v5.0.0"
|
||
// prose — those are historical notes, not live references.
|
||
const targets = [
|
||
'docs/HANDOVER-CONTRACTS.md',
|
||
'templates/plan-template.md', 'templates/trekbrief-template.md', 'templates/trekreview-template.md',
|
||
'commands/trekplan.md', 'commands/trekbrief.md', 'commands/trekreview.md',
|
||
];
|
||
for (const t of targets) {
|
||
assert.ok(
|
||
!read(t).includes('trekrevise'),
|
||
`${t} still references trekrevise — it was removed in v5.0.0`,
|
||
);
|
||
}
|
||
});
|
||
|
||
// --- v5.1 — phase_signals + brief_version 2.1 ---
|
||
|
||
test('v5.5 — templates/trekbrief-template.md declares brief_version: "2.2" (quoted)', () => {
|
||
const t = read('templates/trekbrief-template.md');
|
||
assert.match(t, /^brief_version: "2\.2"$/m,
|
||
'trekbrief-template.md must declare brief_version: "2.2" (quoted) — unquoted parses as Number and bypasses sequencing gate; v5.5 bumped 2.1→2.2 for framing enforcement');
|
||
});
|
||
|
||
test('v5.1 — templates/trekbrief-template.md contains phase_signals: block', () => {
|
||
const t = read('templates/trekbrief-template.md');
|
||
assert.match(t, /^phase_signals:$/m,
|
||
'trekbrief-template.md must contain a phase_signals: block in frontmatter');
|
||
});
|
||
|
||
test('v5.1 — HANDOVER-CONTRACTS.md schema row includes phase_signals + phase_signals_partial', () => {
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(t.includes('| `phase_signals` |'),
|
||
'HANDOVER-CONTRACTS must add a phase_signals row to the Handover 1 schema table');
|
||
assert.ok(t.includes('| `phase_signals_partial` |'),
|
||
'HANDOVER-CONTRACTS must add a phase_signals_partial row to the Handover 1 schema table');
|
||
});
|
||
|
||
test('v5.1 — phase_signals documented in docs/HANDOVER-CONTRACTS.md', () => {
|
||
// v5.x doc consolidation (67f6dd5) moved phase_signals out of CLAUDE.md.
|
||
// phase_signals is a Handover 1 (brief) field, so docs/HANDOVER-CONTRACTS.md
|
||
// is its canonical home (the schema rows are pinned separately above).
|
||
// CLAUDE.md only links to it. (was: read('CLAUDE.md') — v5.1.)
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(t.includes('phase_signals'),
|
||
'docs/HANDOVER-CONTRACTS.md must document phase_signals (v5.1)');
|
||
});
|
||
|
||
test('v5.1 — voyage README.md mentions phase_signals', () => {
|
||
const t = read('README.md');
|
||
assert.ok(t.includes('phase_signals'),
|
||
'voyage README.md must mention phase_signals (v5.1 "What\'s new" bullet)');
|
||
});
|
||
|
||
// --- v5.1.1 — High-effort behavior sub-section per command (Step 10) ---
|
||
|
||
test('v5.1.1 — commands/trekplan.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
|
||
const t = read('commands/trekplan.md');
|
||
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
|
||
'trekplan.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (Adversarial Pass 2, unavailable/skipped since v5.10.1)');
|
||
});
|
||
|
||
test('v5.1.1 — commands/trekresearch.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
|
||
const t = read('commands/trekresearch.md');
|
||
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
|
||
'trekresearch.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (contrarian-researcher always-on)');
|
||
});
|
||
|
||
test('v5.1.1 — commands/trekreview.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
|
||
const t = read('commands/trekreview.md');
|
||
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
|
||
'trekreview.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (skip Pass 3 + coordinator normalization)');
|
||
});
|
||
|
||
test('v5.1.1 — commands/trekexecute.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
|
||
const t = read('commands/trekexecute.md');
|
||
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
|
||
'trekexecute.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (high effort changes nothing in /trekexecute)');
|
||
});
|
||
|
||
// --- v5.4 — brief.md formalized as PUBLIC CONTRACT (Handover 1) ---
|
||
// S5: the brief schema is the only public producer↔Voyage integration boundary
|
||
// (Trinity asymmetry invariant). These pin the formalization as enforced doc-truth,
|
||
// not prose — same pattern as the v5.1 Handover-1 pins above. Freeze, not promote:
|
||
// phase_signals stays optional; brief_version 2.1 is the frozen baseline (S3 decision).
|
||
|
||
test('v5.4 — HANDOVER-CONTRACTS.md Handover 1 heading is labeled (PUBLIC CONTRACT)', () => {
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.match(t, /^## Handover 1 —.*\(PUBLIC CONTRACT\)\s*$/m,
|
||
'Handover 1 heading must carry the (PUBLIC CONTRACT) label — symmetric with Handover 3 (EXTERNAL CONTRACT)');
|
||
});
|
||
|
||
test('v5.4 — Handover 1 declares the PUBLIC CONTRACT callout + breaking-for-downstream guarantee', () => {
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(t.includes('Stability tier: PUBLIC CONTRACT'),
|
||
'Handover 1 must open with a "Stability tier: PUBLIC CONTRACT" callout');
|
||
assert.ok(t.includes('breaking change for every downstream consumer'),
|
||
'the callout must state that a brief-schema change is a breaking change for every downstream consumer');
|
||
});
|
||
|
||
test('v5.5 — brief_version 2.2 is current in the public-contract schema table (no stale 2.0/2.1)', () => {
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(!t.includes('`"2.0"` (current)'),
|
||
'Handover 1 schema table must not still mark brief_version "2.0" as current (stale)');
|
||
assert.ok(!t.includes('`"2.1"` (current)'),
|
||
'Handover 1 schema table must not still mark brief_version "2.1" as current (stale — v5.5 bumped to 2.2)');
|
||
assert.ok(t.includes('`"2.2"` (current)'),
|
||
'Handover 1 schema table must mark brief_version "2.2" as current');
|
||
});
|
||
|
||
test('v5.5 — phase_signals stays optional: not promoted to required', () => {
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(!t.includes('v5.4 may promote'),
|
||
'the speculative "v5.4 may promote phase_signals to required" line must be resolved (S3 froze the shape)');
|
||
assert.ok(!/v5\.4/.test(t),
|
||
'the contract doc must not cite the phantom v5.4 release (v5.2–5.4 never shipped; S16 → v5.5.0)');
|
||
assert.ok(t.includes('established `2.1` as the public-contract baseline'),
|
||
'the Versioning note must record the v5.5.0 contract formalization establishing 2.1 as the baseline');
|
||
});
|
||
|
||
// --- v5.5 — framing enforcement: brief_version 2.2 (Handover 1 contract evolution) ---
|
||
// S6: the framing-alignment invariant (CLAUDE.md) ships as a controlled brief_version
|
||
// bump 2.1→2.2 adding two required-at-2.2 elements (framing field + ## TL;DR section),
|
||
// version-gated so 2.0/2.1 briefs stay valid. These pin the contract-doc evolution.
|
||
|
||
test('v5.5 — Handover 1 schema table adds the framing field row', () => {
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(t.includes('| `framing` |'),
|
||
'HANDOVER-CONTRACTS must add a framing row to the Handover 1 schema table');
|
||
assert.ok(t.includes('preserve \\| refine \\| replace \\| new-direction'),
|
||
'the framing row must list the four canonical enum values');
|
||
});
|
||
|
||
test('v5.5 — Handover 1 documents the framing gate + required-at-2.2 TL;DR section', () => {
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(t.includes('v5.5 framing gate'),
|
||
'validation-strategy table must add a v5.5 framing gate row');
|
||
assert.ok(/##\s*TL;DR.*required at `brief_version ≥ 2\.2`/.test(t) || t.includes('`## TL;DR` — **required at `brief_version ≥ 2.2`**'),
|
||
'body-invariants must mark ## TL;DR as required at brief_version ≥ 2.2');
|
||
});
|
||
|
||
test('v5.5 — Versioning note records the 2.2 framing-enforcement evolution', () => {
|
||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(t.includes('v5.5 → `2.2` (framing enforcement)'),
|
||
'the Versioning note must document the v5.5 → 2.2 framing-enforcement evolution');
|
||
});
|
||
|
||
test('v5.5 — voyage README.md mentions framing enforcement / brief_version 2.2', () => {
|
||
const t = read('README.md');
|
||
assert.ok(t.includes('framing') && t.includes('brief_version 2.2'),
|
||
'voyage README.md must carry a "What\'s new" note for framing enforcement (brief_version 2.2)');
|
||
});
|
||
|
||
// --- S15 — default-profile name + per-phase model claims (cost-claim truth-pass) ---
|
||
// The resolver's hardcoded default (lib/profiles/resolver.mjs) is the single
|
||
// source of truth for which built-in profile a fresh user gets. README,
|
||
// docs/profiles.md, and docs/operations.md must all mark THAT profile as the
|
||
// default and must not mark any other built-in as default. Guards the
|
||
// code-vs-docs contradiction the S14 audit surfaced (Addendum finding #3).
|
||
const PROFILE_DOCS = ['README.md', 'docs/profiles.md', 'docs/operations.md'];
|
||
|
||
test('S15: default-profile name is consistent across resolver + all profile docs', () => {
|
||
// Empty env so the operator's VOYAGE_PROFILE does not mask the hardcoded default.
|
||
const { profile: def, profile_source } = resolveProfile({}, {});
|
||
assert.equal(profile_source, 'default', 'resolveProfile({}, {}) must report source=default');
|
||
assert.equal(def, 'premium', 'resolver hardcoded default is premium (operator decision 2026-05-13, commit 40d8742)');
|
||
|
||
const OTHERS = ['economy', 'balanced', 'premium', 'fable'].filter((p) => p !== def);
|
||
for (const doc of PROFILE_DOCS) {
|
||
const body = read(doc);
|
||
assert.ok(
|
||
body.includes(`\`${def}\` (default)`),
|
||
`${doc}: profile table must mark \`${def}\` as "(default)" to match resolver`,
|
||
);
|
||
for (const other of OTHERS) {
|
||
assert.ok(
|
||
!body.includes(`\`${other}\` (default)`),
|
||
`${doc}: must NOT mark \`${other}\` as "(default)" — resolver default is ${def}`,
|
||
);
|
||
assert.ok(
|
||
!body.includes(`Default \`${other}\``),
|
||
`${doc}: lookup-order must not say "Default \`${other}\`" — resolver default is ${def}`,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('S15: profile tables encode each built-in yaml phase_models exactly', () => {
|
||
// Column order in every profile table: Profile | Brief | Research | Plan | Execute | Review | Continue | Use case
|
||
const PHASES = ['brief', 'research', 'plan', 'execute', 'review', 'continue'];
|
||
for (const name of ['economy', 'balanced', 'premium', 'fable']) {
|
||
const pm = loadProfile(name).phase_models; // {brief:'opus', ...}
|
||
const expected = PHASES.map((ph) => pm[ph]);
|
||
for (const doc of PROFILE_DOCS) {
|
||
const row = read(doc)
|
||
.split('\n')
|
||
.find((l) => new RegExp(`^\\|\\s*\`${name}\``).test(l));
|
||
assert.ok(row, `${doc}: profile table is missing a \`${name}\` row`);
|
||
const cells = row.split('|').map((c) => c.trim());
|
||
// cells[0] is '' (leading pipe), cells[1] is the name, cells[2..7] are the 6 models
|
||
const docModels = cells.slice(2, 8);
|
||
assert.deepEqual(
|
||
docModels,
|
||
expected,
|
||
`${doc}: \`${name}\` row models ${JSON.stringify(docModels)} must equal lib/profiles/${name}.yaml ${JSON.stringify(expected)}`,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
|
||
// --- S34 (V30) — economy is self-declared experimental until the cross-tier
|
||
// Jaccard floor (0.55) is empirically calibrated (Step-17 calibration deferred
|
||
// to v4.2). The status must be visible in BOTH the profile data
|
||
// (lib/profiles/economy.yaml `experimental: true`) and every profile doc's
|
||
// economy row. Fix the SOURCE doc/profile, not these pins.
|
||
test('S34: economy profile row is flagged experimental across all profile docs', () => {
|
||
for (const doc of PROFILE_DOCS) {
|
||
const body = read(doc);
|
||
const row = body.split('\n').find((l) => /^\|\s*`economy`/.test(l));
|
||
assert.ok(row, `${doc}: profile table missing an \`economy\` row`);
|
||
assert.match(
|
||
row,
|
||
/experimental/i,
|
||
`${doc}: the \`economy\` table row must flag it experimental ` +
|
||
`(uncalibrated Jaccard floor, S34/V30) — matches lib/profiles/economy.yaml experimental: true`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('S34: economy experimental flag agrees with parked-synthetic calibration status', () => {
|
||
// economy.yaml experimental:true is honest ONLY while calibration is still
|
||
// parked-synthetic. When empirical calibration lands (status: empirical),
|
||
// drop experimental:true in the SAME change — this pin enforces the linkage.
|
||
const eco = parseDocument(read('lib/profiles/economy.yaml')).parsed.frontmatter;
|
||
const cal = parseDocument(read('tests/synthetic/profile-jaccard-calibration.md')).parsed.frontmatter;
|
||
if (cal.status === 'parked-synthetic') {
|
||
assert.equal(
|
||
eco.experimental,
|
||
true,
|
||
'while calibration is parked-synthetic, economy.yaml must stay experimental: true',
|
||
);
|
||
} else {
|
||
assert.notEqual(
|
||
eco.experimental,
|
||
true,
|
||
'calibration is no longer parked-synthetic — drop economy experimental: true in the same change',
|
||
);
|
||
}
|
||
});
|
||
|
||
test('S15: README cost prose does not resurrect the false Sonnet-swarm claim', () => {
|
||
// All 24 sub-agents are model: opus (operator-pinned, commit 40d8742) and the
|
||
// model is uniform per phase — there is no "Opus orchestrates / Sonnet runs
|
||
// the swarms" split. These phrases asserted the false split and must stay out.
|
||
const md = read('README.md');
|
||
const BANNED = [
|
||
'Sonnet exploration',
|
||
'Sonnet runs the exploration',
|
||
'front-loads cheap Sonnet',
|
||
'exploration agents stay on Sonnet',
|
||
];
|
||
for (const phrase of BANNED) {
|
||
assert.ok(
|
||
!md.includes(phrase),
|
||
`README.md must not claim "${phrase}" — sub-agents are opus-pinned; default profile runs Opus on every phase`,
|
||
);
|
||
}
|
||
});
|
||
|
||
// ── S18: framing-hardening cross-file invariants ───────────────────────────
|
||
|
||
test('S18: --min-brief-version is documented at the trekplan + trekresearch boundary', () => {
|
||
// The opt-in version floor must be discoverable wherever flags live: both
|
||
// consuming commands and the central flag reference.
|
||
for (const f of ['commands/trekplan.md', 'commands/trekresearch.md', 'docs/command-modes.md']) {
|
||
assert.ok(
|
||
read(f).includes('--min-brief-version'),
|
||
`${f} must document the --min-brief-version flag (S18 framing-hardening)`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('README research-dimension prose stays at the existing 3–8 ceiling', () => {
|
||
// /trekresearch Phase 2 determines 3-8 research dimensions; the README must not raise it.
|
||
assert.ok(
|
||
read('README.md').includes('3–8 research dimensions'),
|
||
'README.md must keep the "3–8 research dimensions" prose — it matches the Phase 2 ceiling, it does not raise it',
|
||
);
|
||
});
|
||
|
||
test('no banned Sonnet-swarm phrase on the reference surfaces', () => {
|
||
const BANNED = [
|
||
'Sonnet exploration',
|
||
'Sonnet runs the exploration',
|
||
'front-loads cheap Sonnet',
|
||
'exploration agents stay on Sonnet',
|
||
];
|
||
for (const f of ['docs/command-modes.md', 'CLAUDE.md', 'README.md', 'docs/architecture.md']) {
|
||
const t = read(f);
|
||
for (const phrase of BANNED) {
|
||
assert.ok(!t.includes(phrase), `${f} must not claim "${phrase}" — sub-agents are opus-pinned`);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('S18: HANDOVER-CONTRACTS documents the pre-2.2 zero-framing-enforcement hole', () => {
|
||
// The framing defense is producer-elective: a brief declaring ≤ 2.1 sidesteps
|
||
// it entirely. Handover 1 (PUBLIC CONTRACT) must disclose this and name the remedy.
|
||
const h = read('docs/HANDOVER-CONTRACTS.md');
|
||
assert.ok(
|
||
/BRIEF_VERSION_BELOW_MINIMUM/.test(h),
|
||
'HANDOVER-CONTRACTS.md must name the BRIEF_VERSION_BELOW_MINIMUM gate as the pre-2.2 remedy',
|
||
);
|
||
assert.ok(
|
||
/zero framing enforcement|no framing enforcement|sidestep/i.test(h),
|
||
'HANDOVER-CONTRACTS.md must state that pre-2.2 briefs receive zero framing enforcement',
|
||
);
|
||
});
|
||
|
||
// ── S32 — "delegate the engine, keep the policy": native-delegation hygiene ──
|
||
//
|
||
// Balance-analysis V01 / V07 / V08 / V11 / V24 audit (2026-06-20): the brief
|
||
// interview and the research / exploration / reviewer swarms must ride NATIVE
|
||
// Claude Code primitives — parallel `Agent` spawn in a single message +
|
||
// `AskUserQuestion` for interviews — and re-implement no scheduler /
|
||
// concurrency-loop / menu engine of their own. The audit found all five ALREADY
|
||
// native (no code change), so these pins are the standing guard against an
|
||
// engine creeping back in, plus the documented principle in docs/architecture.md.
|
||
// Fix the SOURCE (command prose / the architecture.md note), not the test.
|
||
|
||
const SWARM_COMMANDS = ['commands/trekresearch.md', 'commands/trekplan.md', 'commands/trekreview.md'];
|
||
|
||
test('S32: docs/architecture.md records the "delegate the engine, keep the policy" principle', () => {
|
||
const t = read('docs/architecture.md');
|
||
assert.ok(
|
||
/delegate the engine, keep the policy/i.test(t),
|
||
'docs/architecture.md must name the "delegate the engine, keep the policy" principle (V01/V07/V08/V11/V24 audit, S32)',
|
||
);
|
||
// The note must name the native primitives it delegates to (so the principle is
|
||
// anchored to real CC tools, not abstract prose).
|
||
assert.ok(
|
||
/AskUserQuestion/.test(t) && /\bAgent\b/.test(t),
|
||
'the principle note must name the native primitives delegated to (Agent + AskUserQuestion)',
|
||
);
|
||
});
|
||
|
||
test('S32: each swarm command lists the Agent tool and mandates native parallel single-message spawn', () => {
|
||
for (const f of SWARM_COMMANDS) {
|
||
const text = read(f);
|
||
assert.ok(
|
||
/^allowed-tools:.*\bAgent\b/m.test(text),
|
||
`${f}: allowed-tools frontmatter must list Agent (the native swarm-spawn engine)`,
|
||
);
|
||
assert.ok(
|
||
/in parallel/i.test(text),
|
||
`${f}: must instruct parallel swarm spawn (native delegation, not a hand-rolled scheduler)`,
|
||
);
|
||
assert.ok(
|
||
/single (assistant )?message|one message|multiple tool calls/i.test(text),
|
||
`${f}: must mandate single-message multi-tool-call dispatch (the native parallel-Agent pattern)`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('S32: trekbrief Phase 3 delegates Q&A turn-taking to AskUserQuestion (V01 — selection rule is policy, asking is native)', () => {
|
||
const text = read('commands/trekbrief.md');
|
||
assert.ok(
|
||
/^allowed-tools:.*AskUserQuestion/m.test(text),
|
||
'commands/trekbrief.md: allowed-tools must list AskUserQuestion (the native Q&A engine)',
|
||
);
|
||
const p3Start = text.indexOf('## Phase 3 — Completeness loop');
|
||
assert.ok(p3Start >= 0, 'trekbrief.md must have a "## Phase 3 — Completeness loop" section');
|
||
const p3End = text.indexOf('## Phase 3.5', p3Start);
|
||
const p3 = text.slice(p3Start, p3End > p3Start ? p3End : undefined);
|
||
assert.ok(
|
||
/Use `AskUserQuestion` for every question/.test(p3),
|
||
'Phase 3 must delegate Q&A turn-taking to AskUserQuestion — the selection rule picks WHICH ' +
|
||
'question to ask (policy); AskUserQuestion does the asking (engine). No hand-rolled menu loop.',
|
||
);
|
||
});
|
||
|
||
// ── S38 — fan-out hedge harmonization + relief-assertion forward-guard (SC d) ──
|
||
//
|
||
// The brief's SC(d) premise — an unhedged "fan-out relieves main context" string
|
||
// present in ~23 files awaiting a removal diff — is factually false: plan-time
|
||
// enumeration shows the unhedged set is ∅. Every live relief mention is already
|
||
// hedged (the sole one is CLAUDE.md:5, hedged since S18), and the
|
||
// T2 bake-off record's "frees the main context" lines were a DIFFERENT, true
|
||
// background-execution claim (must NOT be touched). SC(d) therefore ships as:
|
||
// (1) one surgical harmonization at CLAUDE.md:5 making the replacement phrase
|
||
// "parallel wall-clock" grep-positive (the Δ≈0 / "not yet demonstrated"
|
||
// qualifier kept intact), and
|
||
// (2) this durable forward-guard — a PRECISE relief-assertion matcher (verb-of-
|
||
// relief + main-context, NOT a bare "main context" grep that would false-fail
|
||
// benign locatives like "runs inline in main context") scoped to product-
|
||
// facing docs, requiring an adjacent hedge token on any match.
|
||
// Fix the SOURCE prose (add the hedge / drop the claim), never this test.
|
||
|
||
test('S38: CLAUDE.md carries the harmonized fan-out phrase "parallel wall-clock" (SC d)', () => {
|
||
assert.ok(
|
||
read('CLAUDE.md').includes('parallel wall-clock'),
|
||
'CLAUDE.md:5 must state the replacement phrase "parallel wall-clock + structured artifact handoffs" (SC d harmonization)',
|
||
);
|
||
});
|
||
|
||
test('S38: forward-guard — no product-facing doc asserts main-context relief unhedged (SC d)', () => {
|
||
// Banned form: a verb of relief adjacent to "main context" (or "main-context …
|
||
// relief"), NOT a plain locative ("runs in main context"). Decision/research
|
||
// records (docs/*) are append-only history and out of scope by design.
|
||
const RELIEF = /(relieve|relieves|relieving|free|frees|offload|offloads)\b[^.\n]{0,40}\bmain[-\s]context\b|\bmain[-\s]context\b[^.\n]{0,20}\brelief\b|swarm\s+relieves\s+context/i;
|
||
const HEDGE = /Δ|not measured|asserted-by-design|not yet demonstrated|not demonstrated/i;
|
||
const productFacing = ['README.md', 'CLAUDE.md', ...listMd('commands').map((f) => `commands/${f}`)];
|
||
const offenders = [];
|
||
for (const rel of productFacing) {
|
||
read(rel).split('\n').forEach((ln, i) => {
|
||
if (RELIEF.test(ln) && !HEDGE.test(ln)) offenders.push(`${rel}:${i + 1} ${ln.trim().slice(0, 100)}`);
|
||
});
|
||
}
|
||
assert.equal(
|
||
offenders.length,
|
||
0,
|
||
'Unhedged main-context-relief assertion(s) in product-facing docs — add an adjacent hedge ' +
|
||
'(Δ / "not measured" / "asserted-by-design" / "not yet demonstrated") or drop the claim:\n' +
|
||
offenders.join('\n'),
|
||
);
|
||
});
|
||
|
||
// ── v5.9 (fable-tier step 6) — composed-resolver wiring pin ────────────────
|
||
// The four pipeline commands must resolve {effort, model} via the composed
|
||
// CLI (`resolver.mjs --resolve-phase-model`, brief > profile > default). A
|
||
// direct `phase-signal-resolver.mjs --brief` invocation in a command Bash
|
||
// block is the brief-only CLI: it silently drops the profile layer (the
|
||
// AP2-1 footgun — `--profile <x>` would never reach sub-agent spawns again).
|
||
// If this pin fails, re-wire the command to the composed CLI — do not relax
|
||
// the pin.
|
||
|
||
for (const cmd of ['trekresearch', 'trekplan', 'trekreview', 'trekexecute']) {
|
||
test(`v5.9: commands/${cmd}.md invokes the composed resolver, not the brief-only CLI`, () => {
|
||
const text = read(`commands/${cmd}.md`);
|
||
assert.ok(
|
||
text.includes('--resolve-phase-model'),
|
||
`commands/${cmd}.md must invoke resolver.mjs --resolve-phase-model (composed brief > profile > default)`,
|
||
);
|
||
assert.ok(
|
||
!/phase-signal-resolver\.mjs --brief/.test(text),
|
||
`commands/${cmd}.md must not invoke the brief-only phase-signal-resolver CLI — the profile layer would be silently dropped`,
|
||
);
|
||
});
|
||
}
|
||
|
||
// ── v5.9 (fable-tier step 7) — orchestrator model-pin ABSENCE pin ───────────
|
||
// Command frontmatter deliberately omits `model:` — omission is the only
|
||
// session-inheritance spelling documented by BOTH official surfaces (AP2-2:
|
||
// the `inherit` literal is disputed between skills.md and the plugin-dev
|
||
// command-frontmatter reference). Re-adding a pin locks the orchestrator to a
|
||
// fixed model in every session; if deterministic pinning is ever wanted again,
|
||
// do it consciously and update this pin's rationale.
|
||
|
||
// ── S85 — every spawn site must forbid the Agent tool's `name` parameter ────
|
||
// MEASURED 2026-08-17 (this repo, session c37b89f7), after akashic-intelligence
|
||
// lost a whole Phase 9 to it. Passing `name` does not rename a subagent — it
|
||
// changes its kind: the meta record flips from a real subagent
|
||
// (`spawnDepth: 1`) to `taskKind: "in_process_teammate"` (`spawnDepth: 0`).
|
||
// A teammate's final assistant text is NOT a return value; it reaches the
|
||
// parent only if the teammate itself calls SendMessage(to: "main").
|
||
// voyage's reviewers declare `tools: ["Read","Glob","Grep"]` — no SendMessage —
|
||
// so as teammates they are STRUCTURALLY incapable of returning, whatever the
|
||
// prompt says. Denominators: named 0/5 returned, named-with-explicit-
|
||
// SendMessage 1/1, unnamed 3/3 (plan-critic + scope-guardian both returned
|
||
// full findings + JSON block in ~110s). Full measurement:
|
||
// docs/agent-return-channel-defect.md.
|
||
//
|
||
// The derived set is asserted exactly, so the pin cannot go vacuous if a
|
||
// command stops spawning or a new spawning command is added.
|
||
|
||
// Wave 1 (P8): /trekexecute joined the set - one fresh step helper per plan step
|
||
// plus one controller - so the no-name rule now covers five commands.
|
||
const SPAWNING_COMMANDS = ['trekbrief', 'trekexecute', 'trekplan', 'trekresearch', 'trekreview'];
|
||
|
||
test('S85: the set of agent-spawning commands is exactly the five that carry the no-name rule', () => {
|
||
const spawns = listMd('commands')
|
||
.filter((f) => /^Launch\b/m.test(read(`commands/${f}`)))
|
||
.map((f) => f.replace(/\.md$/, ''))
|
||
.sort();
|
||
assert.deepEqual(spawns, [...SPAWNING_COMMANDS].sort(),
|
||
'a command started or stopped spawning agents — add/remove it from SPAWNING_COMMANDS and give it the no-name rule');
|
||
});
|
||
|
||
for (const cmd of SPAWNING_COMMANDS) {
|
||
test(`S85: commands/${cmd}.md forbids the Agent tool's name parameter at its spawn sites`, () => {
|
||
const text = read(`commands/${cmd}.md`);
|
||
assert.ok(
|
||
text.includes('in_process_teammate'),
|
||
`commands/${cmd}.md must name the mechanism (in_process_teammate) so the rule is not mistaken for style`,
|
||
);
|
||
assert.ok(
|
||
/never pass .*`name`/i.test(text),
|
||
`commands/${cmd}.md must state that the Agent tool's \`name\` parameter is never passed — a named agent completes its work but its result never reaches the orchestrator`,
|
||
);
|
||
});
|
||
}
|
||
|
||
test('v5.9: no commands/*.md frontmatter carries a model: key (session inheritance by omission)', () => {
|
||
const offenders = [];
|
||
for (const f of listMd('commands')) {
|
||
const doc = parseDocument(read(`commands/${f}`));
|
||
const fm = doc.parsed && doc.parsed.frontmatter;
|
||
if (fm && 'model' in fm) offenders.push(f);
|
||
}
|
||
assert.deepEqual(offenders, [],
|
||
`command frontmatter must omit model: (orchestrator follows the session model); offenders: ${offenders.join(', ')}`);
|
||
});
|
||
|
||
// --- S95 (③): gemini-bridge is DROPPED from the pipeline (operator directive) ---
|
||
//
|
||
// Operator directive, three repetitions: gemini-mcp and voyage:gemini-bridge are
|
||
// never to be used. The drop is permanent, not a wait-for-upgrade — so the pin is
|
||
// on the LIVE pipeline surfaces only. Historical records (CHANGELOG entries,
|
||
// measurement/decision docs) must KEEP their references: they say what a past
|
||
// version did or what was measured then, and rewriting them is the same defect
|
||
// class as bumping a version string inside a measurement doc.
|
||
//
|
||
// BLIND SPOT, stated rather than hidden: THIS file cannot be in the live-surface
|
||
// list, because the S95 block below is necessarily full of the word. So the two
|
||
// v5.1.1 assertion messages above (which describe the high-effort rules) are not
|
||
// covered by the scanner and were found stale by review, not by npm test — they
|
||
// match on the heading regex, so a wrong message never fails. When the high-effort
|
||
// rules change, re-read those messages by hand.
|
||
|
||
// Live = anything that can still steer a run. Explicit, so a new file forces a decision.
|
||
const GEMINI_LIVE_SURFACES = [
|
||
'README.md',
|
||
'CLAUDE.md',
|
||
'docs/architecture.md',
|
||
'docs/command-modes.md',
|
||
'templates/research-brief-template.md',
|
||
'templates/trekbrief-template.md',
|
||
...listMd('commands').map((f) => `commands/${f}`),
|
||
...listMd('agents').map((f) => `agents/${f}`),
|
||
];
|
||
|
||
// Historical/measurement records that MUST keep their gemini references.
|
||
const GEMINI_HISTORICAL = [
|
||
'CHANGELOG.md',
|
||
'examples/02-real-cli/REGENERATED.md',
|
||
];
|
||
|
||
test('S95: agents/gemini-bridge.md is removed from the agent inventory', () => {
|
||
assert.ok(
|
||
!listMd('agents').includes('gemini-bridge.md'),
|
||
'gemini-bridge is dropped by operator directive — agents/gemini-bridge.md must not exist',
|
||
);
|
||
});
|
||
|
||
test('S95: no live pipeline surface still names the gemini-bridge agent', () => {
|
||
const offenders = [];
|
||
for (const rel of GEMINI_LIVE_SURFACES) {
|
||
const hits = read(rel).match(/gemini[\w-]*/gi) || [];
|
||
if (hits.length) offenders.push(`${rel} (${[...new Set(hits)].join(', ')})`);
|
||
}
|
||
assert.deepEqual(
|
||
offenders, [],
|
||
`gemini-bridge is dropped — these live surfaces still reference it: ${offenders.join(' · ')}`,
|
||
);
|
||
});
|
||
|
||
test('S95: known-positive control — the historical records still DO carry gemini references', () => {
|
||
// Verifiseringsloven ansikt 4: a scanner that finds nothing anywhere proves
|
||
// nothing. This pins that the same query CAN find, so the empty result above
|
||
// is a measurement and not a broken query.
|
||
const silent = GEMINI_HISTORICAL.filter((rel) => !/gemini/i.test(read(rel)));
|
||
assert.deepEqual(
|
||
silent, [],
|
||
`these historical records lost their gemini references — history must not be rewritten: ${silent.join(', ')}`,
|
||
);
|
||
});
|
||
|
||
test('S95: trekplan high-effort Pass 2 degrades explicitly instead of failing', () => {
|
||
const t = read('commands/trekplan.md');
|
||
const section = t.split('\n### High-effort behavior (v5.1.1)\n')[1];
|
||
assert.ok(section, 'trekplan.md must still carry the High-effort behavior sub-section');
|
||
const head = section.split('\n## ')[0];
|
||
assert.match(
|
||
head, /unavailable, skipped/,
|
||
'the dropped Pass 2 must read "unavailable, skipped" explicitly — a removed pass that is silent is indistinguishable from one that failed',
|
||
);
|
||
});
|
||
|
||
// End-state defect D-07: since v5.9.0 no command pins `model:`, so the orchestrator follows
|
||
// the session model, and sub-agent models are resolved at the spawn sites (brief signal >
|
||
// profile > agent frontmatter). No user-facing doc may claim otherwise. Fix the SOURCE.
|
||
test('D-07: no doc claims an opus orchestrator or ships a recipe for model: lines no command has', () => {
|
||
const readme = read('README.md');
|
||
assert.ok(
|
||
!/\^model: opus\$/.test(readme),
|
||
'README.md must not ship a sed recipe for `model: opus` frontmatter lines — 0 of 7 command files have one',
|
||
);
|
||
assert.ok(
|
||
!/default for `\/trekbrief`[\s\S]{0,80}?is `opus`/.test(readme),
|
||
'README.md must not claim the command orchestrators default to opus — they follow the session model',
|
||
);
|
||
for (const f of ['README.md', 'CLAUDE.md', 'commands/trekresearch.md']) {
|
||
assert.ok(!/orchestrator runs on Opus/i.test(read(f)), `${f}: the orchestrator follows the session model`);
|
||
}
|
||
});
|
||
|
||
test('D-07: trekresearch launch rules inject the resolved model instead of a blanket opus', () => {
|
||
const t = read('commands/trekresearch.md');
|
||
const rules = (t.split('\n### Launch rules\n')[1] || '').split('\n## ')[0];
|
||
assert.ok(rules.length > 0, 'trekresearch.md must still carry the Launch rules sub-section');
|
||
assert.ok(
|
||
!/model: "opus" for all sub-agents/i.test(rules),
|
||
'a blanket opus rule contradicts the file\'s own resolution (brief > profile > agent frontmatter, wired in v5.9.0)',
|
||
);
|
||
assert.match(rules, /phase_signal_result\.model/, 'the launch rules must name the resolved model the spawn sites inject');
|
||
});
|
||
|
||
// End-state defect D-03: a trekplan's `## Verification` is where the brief's success
|
||
// criteria land. Phase 7 used to say "Skip for trekplans", so on the single-session path
|
||
// the criteria were never run and `completed` meant "the executor believed it". The
|
||
// behaviour lives in lib/verification/criteria-runner.mjs; this pin guards the WIRING —
|
||
// a capability no phase calls is the same defect wearing a lib/ file. Fix the SOURCE.
|
||
test('D-03: trekexecute Phase 7 runs the plan Verification on the single-session path', () => {
|
||
const t = read('commands/trekexecute.md');
|
||
const phase7 = (t.split('\n## Phase 7 — ')[1] || '').split('\n## ')[0];
|
||
assert.ok(phase7.length > 0, 'trekexecute.md must still carry a Phase 7 section');
|
||
assert.ok(
|
||
!/^\*\*Skip for trekplans\.\*\*/m.test(phase7),
|
||
'Phase 7 may no longer skip trekplans — that skip IS defect D-03',
|
||
);
|
||
assert.match(
|
||
phase7, /criteria-runner\.mjs" --plan/,
|
||
'Phase 7 must invoke the criteria runner over the plan file, not describe the check in prose',
|
||
);
|
||
assert.match(
|
||
phase7, /MUST NOT emit `result: completed`/,
|
||
'a failing criterion must fell the run, not be noted in the final report',
|
||
);
|
||
});
|
||
|
||
// End-state defect D-04: the rubric required brief-conformance-reviewer to decide whether a
|
||
// Success Criterion's verification command "exists and passes" — with Read/Glob/Grep. The
|
||
// command now runs the commands and hands over the result; the reviewer stays read-only.
|
||
// Fix the SOURCE.
|
||
// Re-measured 2026-09-18, minor: /trekreview passed --cwd and Phase 7 did not,
|
||
// so the same criterion could resolve two ways in the two phases. It is no longer
|
||
// cosmetic - --cwd is where every allowlisted command is executed and where a
|
||
// `bash tests/<script>.sh` criterion resolves, so an unset one silently moves
|
||
// the ground the criteria are measured on to the process cwd.
|
||
test('both phases pin the criteria runner to the working tree with --cwd', () => {
|
||
const phase7 = (read('commands/trekexecute.md').split('\n## Phase 7 — ')[1] || '').split('\n## ')[0];
|
||
const phase45 = (read('commands/trekreview.md').split('\n## Phase 4.5 — ')[1] || '').split('\n## ')[0];
|
||
for (const [label, phase] of [['trekexecute Phase 7', phase7], ['trekreview Phase 4.5', phase45]]) {
|
||
assert.ok(phase.length > 0, `${label} must still exist`);
|
||
assert.match(phase, /--cwd "\$CRITERIA_CWD"/, `${label} must pass --cwd, and the same resolved value`);
|
||
assert.match(
|
||
phase, /CRITERIA_CWD="\$\(git rev-parse --show-toplevel 2>\/dev\/null \|\| pwd\)"/,
|
||
`${label} must resolve the working tree with a fallback — an empty --cwd is a moved boundary, not an error`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('D-04: the conformance reviewer judges a supplied result, and never runs a command', () => {
|
||
const a = read('agents/brief-conformance-reviewer.md');
|
||
assert.ok(
|
||
!/exists and passes/.test(a),
|
||
'the rubric may no longer ask a Read/Glob/Grep agent whether a command passes — that ask IS defect D-04',
|
||
);
|
||
assert.match(a, /^tools: \["Read", "Glob", "Grep"\]$/m, 'the reviewer stays read-only: no Bash');
|
||
assert.match(a, /You never run a command/, 'the reviewer must be told in-band that it does not execute');
|
||
assert.match(a, /Success-criteria check results/, 'the reviewer must document the supplied evidence as an input');
|
||
});
|
||
|
||
// Follow-up to D-04 (re-measured 2026-09-18): the runner now reports a criterion
|
||
// whose sentence named a flag or a path as NOT RUN with reason `not-a-command`,
|
||
// instead of running it and reporting exit 2 / exit 126. The rubric must not turn
|
||
// that absent measurement into a finding of its own - it judges delivered code.
|
||
test('D-04b: a NOT RUN check result is never, on its own, a defect in the rubric', () => {
|
||
const a = read('agents/brief-conformance-reviewer.md');
|
||
assert.ok(
|
||
!/NOT RUN` because the criterion declares no command/.test(a),
|
||
'a criterion that declares no runnable command may no longer read as an incomplete verification path',
|
||
);
|
||
assert.match(
|
||
a, /A `NOT RUN` result is never on its own a finding/,
|
||
'the rubric must say in-band that an absent measurement is not a defect',
|
||
);
|
||
assert.match(
|
||
a, /not-a-command/,
|
||
'the reviewer must be told the reason string the runner emits for a sentence that named a flag or a path',
|
||
);
|
||
});
|
||
|
||
test('D-04: trekreview runs the success-criteria commands and hands the reviewer the result', () => {
|
||
const t = read('commands/trekreview.md');
|
||
const phase = (t.split("\n## Phase 4.5 — ")[1] || '').split('\n## ')[0];
|
||
assert.ok(phase.length > 0, 'trekreview.md must carry the phase that runs the success-criteria checks');
|
||
assert.match(phase, /criteria-runner\.mjs" \\\n --brief .* --evidence/, 'the phase must invoke the runner in --evidence mode');
|
||
assert.match(phase, /sc_evidence_block/, 'the phase must name the captured block the reviewer prompt carries');
|
||
assert.match(
|
||
t.split('\n## Phase 5 — ')[1] || '', /sc_evidence_block/,
|
||
'the reviewer-launch phase must hand the block over — a block nobody passes is not evidence',
|
||
);
|
||
});
|
||
|
||
// Re-measured 2026-09-18: the plan's `## Verification` section
|
||
// is the one artifact BOTH /trekplan (writer) and lib/verification/criteria-runner.mjs
|
||
// (reader) touch, and nothing pinned them to the same format. Measured: nine
|
||
// ordinary shapes of that section parsed to zero criteria, which fells the
|
||
// single-session run. The writer must pin the heading string and the two forms
|
||
// the reader reads. Fix the SOURCE: if the runner learns a new form, say so here.
|
||
test('trekplan pins the ## Verification heading and a format the criteria runner reads', () => {
|
||
const t = read('commands/trekplan.md');
|
||
const section = (t.split('8. **Verification**')[1] || '').split('\n9. ')[0];
|
||
assert.ok(section.length > 0, 'trekplan.md must still carry the Verification instruction');
|
||
assert.match(
|
||
section, /exactly `## Verification`/,
|
||
'the heading string is load-bearing: `## Verification (acceptance)` and `### Verification` parse to nothing',
|
||
);
|
||
assert.match(
|
||
section, /criteria runner|criteria-runner/,
|
||
'the instruction must name the reader it is writing for',
|
||
);
|
||
assert.match(
|
||
section, /- \[ \] `/,
|
||
'the bullet form the runner reads must be shown, not described',
|
||
);
|
||
assert.match(section, /```bash/, 'the shell-tagged fence form must be shown too');
|
||
});
|
||
|
||
test('trekexecute Phase 7 says NO_CRITERIA out loud instead of failing silently', () => {
|
||
const phase7 = (read('commands/trekexecute.md').split('\n## Phase 7 — ')[1] || '').split('\n## ')[0];
|
||
assert.match(
|
||
phase7, /NO_CRITERIA/,
|
||
'a section the runner could not read must be reported as such, not as a bare exit 1',
|
||
);
|
||
assert.match(
|
||
phase7, /source line|`\{plan_path\}:/,
|
||
'the report must carry the line the unreadable section starts on',
|
||
);
|
||
});
|
||
|
||
// Model-alias facts, verified 2026-10-02 against code.claude.com/docs/en/model-config
|
||
// (Anthropic API) and code.claude.com/docs/en/sub-agents: `opus` -> Opus 5.5 and
|
||
// `sonnet` -> Sonnet 5.5, both default effort `medium`; `fable` -> Fable 5.1, default
|
||
// `high`; a subagent without `effort:` inherits the session level. Live surfaces state
|
||
// what is true now; historical records keep what was true when written and are never
|
||
// rewritten. When an alias moves again, re-verify against the docs and fix the SOURCE.
|
||
const ALIAS_LIVE_SURFACES = [
|
||
'CLAUDE.md', 'README.md', 'docs/profiles.md', 'docs/operations.md',
|
||
...listMd('commands').map((f) => `commands/${f}`),
|
||
...listMd('agents').map((f) => `agents/${f}`),
|
||
];
|
||
const RETIRED_ALIAS_TARGETS = [
|
||
{ term: 'Opus 4.8', re: /Opus 4\.8/g },
|
||
{ term: 'Sonnet 4.6', re: /Sonnet 4\.6/g },
|
||
{ term: 'Fable 5', re: /Fable 5(?!\.\d)/g },
|
||
];
|
||
// History inside a live surface: an entry must hit EXACTLY its count (a stale one fails).
|
||
const ALIAS_HISTORY_EXEMPT = [
|
||
{ path: 'docs/operations.md', term: 'Sonnet 4.6', count: 1, reason: 'Path C measurement, closed 2026-05-05 on CC v2.1.128' },
|
||
];
|
||
const ALIAS_HISTORICAL = ['CHANGELOG.md'];
|
||
|
||
function aliasHits(rel, re) { return (read(rel).match(re) || []).length; }
|
||
|
||
test('model aliases: no live surface names a retired alias target as current', () => {
|
||
const offenders = [];
|
||
for (const rel of ALIAS_LIVE_SURFACES) {
|
||
for (const { term, re } of RETIRED_ALIAS_TARGETS) {
|
||
const n = aliasHits(rel, re);
|
||
const ex = ALIAS_HISTORY_EXEMPT.find((e) => e.path === rel && e.term === term);
|
||
const allowed = ex ? ex.count : 0;
|
||
if (n !== allowed) offenders.push(`${rel}: ${term} x${n} (allowed ${allowed})`);
|
||
}
|
||
}
|
||
assert.deepEqual(offenders, [], `retired alias targets on live surfaces: ${offenders.join(' · ')}`);
|
||
});
|
||
|
||
test('model aliases: known-positive control — the same query still finds every retired term in history', () => {
|
||
// Verifiseringsloven ansikt 4: the empty result above is a measurement only if the
|
||
// query CAN find. Every retired term must still hit in a historical record or an
|
||
// exact-count exemption.
|
||
const silent = RETIRED_ALIAS_TARGETS.filter(({ term, re }) =>
|
||
!ALIAS_HISTORICAL.some((rel) => aliasHits(rel, re) > 0)
|
||
&& !ALIAS_HISTORY_EXEMPT.some((e) => e.term === term)).map(({ term }) => term);
|
||
assert.deepEqual(silent, [], `no known-positive left for: ${silent.join(', ')}`);
|
||
});
|
||
|
||
test('model aliases: profiles.md states the current targets, defaults and session inheritance', () => {
|
||
const t = read('docs/profiles.md');
|
||
const axes = (t.split('\n## Model & effort axes\n')[1] || '').split('\n## ')[0];
|
||
assert.ok(axes.length > 0, 'docs/profiles.md must still carry the Model & effort axes section');
|
||
assert.match(axes, /Opus 5\.5[^\n]*\n?[^\n]*`medium`/, 'opus -> Opus 5.5, default effort `medium`');
|
||
assert.match(axes, /Sonnet 5\.5/, 'sonnet -> Sonnet 5.5');
|
||
assert.match(axes, /Fable 5\.1[\s\S]{0,120}`high`/, 'fable -> Fable 5.1, default effort `high`');
|
||
assert.match(axes, /inherits? the session/i, 'a subagent without `effort:` inherits the session level');
|
||
assert.ok(!/inherit the model default/.test(axes), 'unset `effort:` inherits the session, not the model default');
|
||
assert.ok(!/overrides effort entirely/.test(axes),
|
||
'`MAX_THINKING_TOKENS=0` has no effect on Opus 5.5, Sonnet 5.5 or the Fable models');
|
||
assert.match(axes, /maxEffortLevel/, 'effort is capped by `maxEffortLevel` / an organization cap, not by `availableModels`');
|
||
for (const f of ['CLAUDE.md', 'README.md']) {
|
||
for (const target of ['Opus 5.5', 'Sonnet 5.5', 'Fable 5.1']) {
|
||
assert.ok(read(f).includes(target), `${f} must name the current alias target ${target}`);
|
||
}
|
||
}
|
||
});
|
||
|
||
// A `## Brief Quality` section belongs to the exhausted and force-stopped
|
||
// branches only; the passed-gate branch must not tell the model to write one.
|
||
test('trekbrief: the passed-gate branch names no `## Brief Quality`, the exhausted branch still does', () => {
|
||
const text = read('commands/trekbrief.md');
|
||
const passStart = text.indexOf('**If gate passes:**');
|
||
const passEnd = text.indexOf('**If gate fails AND iteration count < the class cap:**');
|
||
assert.ok(passStart >= 0 && passEnd > passStart, 'passed-gate branch markers');
|
||
assert.ok(!text.slice(passStart, passEnd).includes('## Brief Quality'), 'the passed-gate branch names `## Brief Quality`');
|
||
const exStart = text.indexOf('**If gate fails AND iteration count == the class cap (loop exhausted):**');
|
||
const exEnd = text.indexOf('### Step 4f', exStart);
|
||
assert.ok(exStart > passEnd && exEnd > exStart, 'exhausted branch markers');
|
||
assert.ok(text.slice(exStart, exEnd).includes('## Brief Quality'), 'the exhausted branch must still name `## Brief Quality`');
|
||
});
|
||
|
||
// --- effort floor: no live doc claims retrieval agents run at medium ------------
|
||
// Operator rule (2026-10-05): no agent is pinned below `high`. The model DEFAULT
|
||
// sentence ("Opus 5.5 ... default reasoning effort `medium`") stays true and is not a pin.
|
||
const RETRIEVAL_MEDIUM = /retrieval(?: agents)?\s*(?:→|->|at)\s*`medium`/i;
|
||
|
||
test('effort floor: no live doc claims retrieval agents run at medium', () => {
|
||
const hits = [];
|
||
for (const f of ['docs/profiles.md', 'CLAUDE.md', 'README.md']) {
|
||
read(f).split('\n').forEach((l, i) => { if (RETRIEVAL_MEDIUM.test(l)) hits.push(`${f}:${i + 1}`); });
|
||
}
|
||
assert.deepEqual(hits, [], 'the four retrieval agents pin `high` now');
|
||
});
|
||
|
||
test('effort floor: known-positive control — the query finds the old phrases, not the model default', () => {
|
||
for (const phrase of ['**Retrieval agents → `medium`:**', '(retrieval → `medium`, adversarial', 'retrieval at `medium`, adversarial']) {
|
||
assert.match(phrase, RETRIEVAL_MEDIUM, phrase);
|
||
}
|
||
assert.doesNotMatch('both with default reasoning effort `medium`', RETRIEVAL_MEDIUM);
|
||
});
|
||
|
||
test('effort floor: profiles.md names the four retrieval agents as high', () => {
|
||
const axes = (read('docs/profiles.md').split('\n## Model & effort axes\n')[1] || '').split('\n## ')[0];
|
||
const bullet = axes.split('\n- ').find((b) => ['task-finder', 'git-historian', 'dependency-tracer', 'architecture-mapper'].every((a) => b.includes(a)));
|
||
assert.ok(bullet, 'one bullet in §Model & effort axes names all four retrieval agents');
|
||
assert.match(bullet, /`high`/);
|
||
assert.doesNotMatch(bullet, /`medium`/);
|
||
});
|
||
|
||
// The resolver returns `effort` only when the brief's phase signal sets one;
|
||
// no command says it always does.
|
||
const RESOLVER_COMMANDS = ['trekplan', 'trekexecute', 'trekresearch', 'trekreview'];
|
||
|
||
test('resolver: no command says the resolver always returns {effort, model, source}', () => {
|
||
for (const f of readdirSync(join(ROOT, 'commands')).filter((x) => x.endsWith('.md'))) {
|
||
assert.doesNotMatch(read(`commands/${f}`), /\{effort, model, source\}/, f);
|
||
}
|
||
});
|
||
|
||
test('resolver: each resolver-using command says `effort` comes only from the brief signal, else standard', () => {
|
||
for (const c of RESOLVER_COMMANDS) {
|
||
const t = read(`commands/${c}.md`);
|
||
const hits = t.match(/\{model, source\}[^\n]*\n?[^\n]*effort[^\n]*only when the brief's phase signal sets one/g) ?? [];
|
||
assert.ok(hits.length >= 1, `${c}: no statement that effort is only present with a brief signal`);
|
||
assert.match(t, /without it[^\n]*\n?[^\n]*`?standard`?/i, c);
|
||
}
|
||
});
|