voyage/tests/lib/doc-consistency.test.mjs
Kjell Tore Guttormsen fd0e5775eb docs(voyage): harmonize fan-out hedge + add banned-phrase guard
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:35:40 +02:00

1268 lines
60 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// tests/lib/doc-consistency.test.mjs
// Pin invariants between prose (CLAUDE.md, README.md) and source files
// (agents/*.md, commands/*.md, templates/, settings.json).
//
// 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 count as "doc-consistency pins" in the suite census
// (lib/util/test-census.mjs) so the cited test total stays honest about how much
// is behavior coverage vs documentation pinning.
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';
import { STATES } from '../../lib/util/autonomy-gate.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.7', () => {
const tpl = read('templates/plan-template.md');
assert.match(tpl, /plan_version:\s*['"]?1\.7['"]?/);
});
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.7"
// (above) + the plan-validator / plan-schema behavior tests.
test('settings.json has only known top-level scopes after Spor 0 cleanup', () => {
const cfg = JSON.parse(read('settings.json'));
const known = ['trekplan', 'trekresearch'];
for (const k of Object.keys(cfg)) {
assert.ok(known.includes(k), `Unknown top-level scope in settings.json: ${k}`);
}
});
test('settings.json no longer carries vestigial exploration block', () => {
const cfg = JSON.parse(read('settings.json'));
assert.equal(cfg.trekplan?.exploration, undefined,
'exploration block was vestigial — should be deleted in v3.1.0 Spor 0');
assert.equal(cfg.trekplan?.agentTeam, undefined,
'agentTeam block was vestigial — should be deleted in v3.1.0 Spor 0');
});
// 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', () => {
const text = read('lib/validators/review-validator.mjs');
assert.ok(
text.includes('import.meta.url === '),
'lib/validators/review-validator.mjs should expose the standard CLI shim ' +
'(if (import.meta.url === `file://${process.argv[1]}`)) 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('import.meta.url === '),
'lib/validators/session-state-validator.mjs should expose the standard CLI shim ' +
'(if (import.meta.url === `file://${process.argv[1]}`)) so /trekcontinue can call it from Bash',
);
});
test('next-session-prompt-validator has CLI shim', () => {
const text = read('lib/validators/next-session-prompt-validator.mjs');
assert.ok(
text.includes('import.meta.url === '),
'lib/validators/next-session-prompt-validator.mjs should expose the standard CLI shim ' +
'(if (import.meta.url === `file://${process.argv[1]}`)) so /trekcontinue Phase 1.5 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 12 entries', async () => {
const mod = await import('../../lib/review/rule-catalogue.mjs');
assert.strictEqual(
mod.RULE_CATALOGUE.length,
12,
'lib/review/rule-catalogue.mjs RULE_CATALOGUE size invariant: must be 12 (v1.0 baseline)',
);
});
test('headless-launch-template.md mirrors Phase 2.6 hardenings', () => {
const tpl = read('templates/headless-launch-template.md');
for (const needle of [
'GIT_OPTIONAL_LOCKS',
'--max-turns',
'--max-budget-usd',
'--append-system-prompt-file',
'SHARED_CONTEXT_FILE',
'SAFETY_PREAMBLE',
'git push origin',
'GH #36071',
'push-before-cleanup',
]) {
assert.ok(
tpl.includes(needle),
`templates/headless-launch-template.md should include "${needle}" (Step 10 mirrors Phase 2.6)`,
);
}
});
test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', () => {
const cmd = read('commands/trekplan.md');
const orch = read('agents/planning-orchestrator.md');
// Single-message reinforcement appears in both (command + orchestrator)
assert.ok(
cmd.includes('single assistant message turn'),
'commands/trekplan.md Phase 9 should reinforce single-message parallel dispatch',
);
assert.ok(
orch.includes('single assistant message turn'),
'agents/planning-orchestrator.md Phase 6 should mirror the single-message parallel-dispatch contract',
);
// Dedup CLI shim is wired in both
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',
);
assert.ok(
orch.includes('plan-review-dedup.mjs'),
'agents/planning-orchestrator.md Phase 6 should reference the dedup helper',
);
});
// --- 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. Phase-9 prose in BOTH docs 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');
const orch = read('agents/planning-orchestrator.md');
for (const [name, doc] of [['commands/trekplan.md', cmd], ['agents/planning-orchestrator.md', orch]]) {
assert.ok(
doc.includes('--stdin'),
`${name} Phase 9 must pipe reviewer findings into plan-review-dedup.mjs via --stdin (reviewers are read-only)`,
);
assert.ok(
!doc.includes('/tmp/plan-critic-out.json') && !doc.includes('/tmp/scope-guardian-out.json'),
`${name} 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: orchestrator docs must not claim ALL sub-agents run on Sonnet ---
//
// All named swarm agents are pinned `model: opus` (operator decision 40d8742;
// structural source-of-truth = agent-frontmatter.test.mjs). The orchestrator
// reference docs carried a pre-pinning "Use Sonnet for all sub-agents" cost rule
// that contradicts that pinning. This anti-false-claim pin keeps the prose honest.
// Legitimate sonnet uses (economy profile; low-effort `phase_signals.model`) are
// per-phase overrides, NOT a blanket "all sub-agents" default — unaffected here.
test('orchestrator docs do not claim ALL sub-agents run on Sonnet (agents are model: opus) [S20]', () => {
const orchestrators = listMd('agents').filter(f => f.endsWith('orchestrator.md'));
assert.ok(orchestrators.length >= 3, `expected ≥ 3 orchestrator reference docs, got ${orchestrators.length}`);
for (const f of orchestrators) {
assert.ok(
!/Use Sonnet for all sub-agents/i.test(read(`agents/${f}`)),
`agents/${f}: stale "Use Sonnet for all sub-agents" contradicts the all-opus agent pinning — fix the prose, not this test`,
);
}
});
// --- 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 a BOOLEAN gate: --gates {true|false} → gates_on|auto_running.
// State machine: idle → gates_on|auto_running → paused_for_gate → completed.
// The old prose (operations.md:15 / command-modes.md gates rows) (1) fabricated a
// state machine `idle → approved → executing → merge-pending → main-merged` — only
// `idle` exists in autonomy-gate.mjs STATES; (2) claimed event-emit.mjs "records
// each transition" — it emits 3 named lifecycle events and is decoupled from the
// (pure, no-I/O) autonomy-gate; (3) advertised `--gates {open|closed|adaptive}` —
// open/closed/adaptive is a DERIVED gates_mode policy in the command layer, not a
// flag value (the CLI shim + all 4 command docs take boolean `--gates {true|false}`).
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 — fabricated state names (merge-pending/main-merged are unique to the false claim)
for (const bad of ['merge-pending', 'main-merged']) {
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 real transient states (derived from autonomy-gate.mjs STATES) must be documented
for (const real of [STATES.GATES_ON, STATES.AUTO_RUNNING, STATES.PAUSED_FOR_GATE]) {
assert.ok(
ops.includes(real),
`docs/operations.md must name the real autonomy-gate state "${real}" (from autonomy-gate.mjs STATES)`,
);
}
// F2 — event-emit does NOT record each autonomy-gate transition
assert.ok(
!ops.includes('records each transition'),
'docs/operations.md: event-emit records 3 named lifecycle events, not "each transition" of the (decoupled) autonomy-gate; fix the prose',
);
// F3 — --gates is boolean; open/closed/adaptive is a derived gates_mode policy, not a flag 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 — flag is boolean {true|false}; open/closed/adaptive is a derived gates_mode; 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 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 + NW1NW3');
});
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/orchestrator files, settings.json, 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 = [
'settings.json',
'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',
'agents/planning-orchestrator.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 (Decision B + gemini-bridge)');
});
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 + gemini-bridge 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 (gates_mode = closed)');
});
// --- 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.25.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'].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']) {
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`,
);
}
});
// NOTE (S17): the "reference-only orchestrators must not grant the Agent tool"
// invariant (Survivor #5) is guarded in tests/lib/agent-frontmatter.test.mjs
// (its canonical frontmatter home), not duplicated here.
test('S17: NW2 bake-off verdict is not labeled unqualified POSITIVE (raw data un-archived)', () => {
// Survivor #3: the per-run JSON (a1..b3.json) was never committed
// (git log --diff-filter=A is empty), so the medians/jaccards cannot be
// re-derived, audited, or falsified. The verdict must be qualified as
// opt-in-defensible, and the un-reproducibility must be disclosed.
const doc = read('docs/T2-bakeoff-results.md');
assert.ok(
!/VERDICT:\s*\*\*POSITIVE\*\*/.test(doc),
'T2-bakeoff-results.md still labels the verdict bare POSITIVE — downgrade to opt-in-defensible (Survivor #3).',
);
assert.ok(
/un-archived|not committed|cannot be re-derived/i.test(doc),
'T2-bakeoff-results.md must disclose the raw per-run data is un-archived / un-reproducible.',
);
});
test('S21: T2-bakeoff §3 marks the auto/bypass classifier check as an OPEN RESIDUAL, not satisfied (Survivor #18)', () => {
// The W1 charter made an auto/bypass-mode re-run an explicit guard (trekreview
// runs headless under those modes). It was never performed and was footnoted
// rather than gating. Per recommendation #9, the classifier-interference
// result must read as an open residual / untested — not "metric satisfied".
const doc = read('docs/T2-bakeoff-results.md');
assert.ok(
/open residual/i.test(doc) && /untested/i.test(doc),
'T2-bakeoff §3 must mark the auto/bypass classifier check as an OPEN RESIDUAL / untested (Survivor #18 / S21).',
);
});
// ── 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('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.',
);
});
// ── S33 — documentation consolidation: record the considered-and-kept decisions ──
//
// The D1D3 forks (balance-backlog-plan.md) resolved conservatively, collapsing
// the model + observability work into a documentation record. These pins make
// the three deliverables enforced doc-truth:
// • V35 (doc half) — the "24 agents" framing is reconciled to its honest split
// (21 spawnable + 3 reference orchestrators, of which synthesis-agent is the
// one dormant spawnable). Counts are DERIVED from agents/ so they survive
// reword and track new files.
// • D2 (V32 rationale) — observability.md documents why direct export rather
// than a native collector (preserves the S21 in-process guards).
// • D3 (kept-opus) — the analysis doc carries a decision record citing the pin
// commit; the model frontmatter is NOT changed (agent-frontmatter.test.mjs
// stays the structural source-of-truth and is untouched by S33).
// Fix the SOURCE a pin guards, not the test.
test('S33: README + CLAUDE.md reconcile the agent inventory (spawnable + reference orchestrators = file count)', () => {
const agentFiles = listMd('agents');
const total = agentFiles.length;
const orchestrators = agentFiles.filter((f) => /-orchestrator\.md$/.test(f));
const refCount = orchestrators.length;
const spawnable = total - refCount;
assert.equal(refCount, 3, `expected exactly 3 *-orchestrator reference docs, got ${refCount}`);
for (const [name, body] of [['README.md', read('README.md')], ['CLAUDE.md', read('CLAUDE.md')]]) {
assert.ok(
body.includes(`${spawnable} spawnable`),
`${name} must state "${spawnable} spawnable" agents (derived: ${total} files ${refCount} reference orchestrators)`,
);
assert.ok(
body.includes(`${refCount} orchestrator reference doc`),
`${name} must label the ${refCount} orchestrators as reference docs (not spawnable capabilities)`,
);
}
});
test('S33: the inventory framing names synthesis-agent as the one dormant spawnable', () => {
// 1 of the spawnable agents ships dormant (Δ≈0, wired to nothing). The framing
// must say so, so the headline count is honest about live vs. dormant capability.
for (const [name, body] of [['README.md', read('README.md')], ['CLAUDE.md', read('CLAUDE.md')]]) {
assert.ok(
/synthesis-agent/.test(body) && /dormant/i.test(body),
`${name} inventory framing must name synthesis-agent as the dormant spawnable`,
);
}
});
test('S33: the 3 orchestrator files declare themselves reference docs, not spawnable capabilities (V35 relabel)', () => {
for (const f of listMd('agents').filter((x) => /-orchestrator\.md$/.test(x))) {
assert.ok(
/reference document, not a spawnable capability/i.test(read(`agents/${f}`)),
`agents/${f} must declare "reference document, not a spawnable capability" (V35 doc-half relabel)`,
);
}
});
test('S33: docs/observability.md documents the direct-export-vs-collector rationale (D2)', () => {
const t = read('docs/observability.md');
assert.ok(
/direct[- ]export/i.test(t) && /collector/i.test(t),
'observability.md must document why Voyage exports directly rather than via a native collector (D2)',
);
// The rationale must anchor on the in-process security boundary it preserves
// (the S21 hardening), not read as an unjustified re-host.
assert.ok(
/allowlist/i.test(t) && /(SSRF|CWE-918|CWE-212)/.test(t),
'the direct-export rationale must cite the in-process guards it preserves (field allowlist + SSRF/path)',
);
});
test('S33: the analysis doc records the D1D3 resolved forks (considered-and-kept, pin firm)', () => {
const t = read('docs/voyage-vs-cc-balance-analysis.md');
assert.ok(/Decision record/i.test(t), 'analysis doc must carry a "Decision record" addendum for the resolved forks');
for (const d of ['D1', 'D2', 'D3']) {
assert.ok(new RegExp(`\\b${d}\\b`).test(t), `analysis-doc decision record must name fork ${d}`);
}
// D3 kept the model pin firm — the record must cite the pin commit so a future
// reader sees the downgrade was reconsidered-and-declined, not overlooked.
assert.ok(/40d8742/.test(t), 'decision record must cite the model-pin commit 40d8742 (D3 kept firm, document-only)');
});
// ── 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
// docs/T2-bakeoff-results.md "frees the main context" lines are 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'),
);
});