Wave 2, order 6. /trekexecute now: - refuses a plan whose logged approval does not cover its current sha256 (K2, approval-log verify --kind plan), before step 1; - runs the step check (K9) and the step's optional Regression: (K10, new lib/verification/regression-check.mjs through the criteria runner) at step end, before the checkpoint; exit 2 is "not measured", never green; - runs the architecture rules (K5) and the plan-file sweep (K6) after the last step; a violation or a file outside the plan turns completed into partial; - reviews by itself in the closing run (Phase 7.2): run the thing, /simplify, /trekreview, verdict-gate; a BLOCK adds the review's remediation steps after the plan validator accepts them and the operator re-approves the plan, capped at TREKEXECUTE_MAX_REVIEW_ROUNDS (3); - makes the main-merge gate a lock on the verdict: BLOCK or undecided keeps the merge closed; when open, the executor merges and the guard's question is the one question (operator decision 2026-10-08). The --gates pause before the wave merge-back is gone for the same reason. The guard refuses a merge into main and a push in the permission mode that shows no prompts, since no one can be asked (operator decision 2026-10-09; two mode-read exemptions approved by the operator). progress.json gains optional coupling fields (Handover 5); the validator rejects the runner's exitCode in plan_verification.failed_criteria (C15). Pointer lines name the skills test-first and debugging (S2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
8.4 KiB
JavaScript
149 lines
8.4 KiB
JavaScript
// tests/lib/no-claude-code-launch.test.mjs
|
|
// Nothing Voyage publishes may start Claude Code. The published tree is every
|
|
// tracked file (the catalog pins a release tag, so the tag's tree is what a
|
|
// user installs). A hit is any line that carries a way to start a Claude Code
|
|
// session or model run:
|
|
// cli the `claude` CLI with a flag or a prompt (-p, --print, --bg,
|
|
// --resume, --version, "prompt", $(...)); a user opening their
|
|
// own session is the PM way and carries no command line
|
|
// plugin-eval `claude plugin eval` (it runs one child session per case)
|
|
// spawn spawn/exec/execFile/fork of the `claude` binary from code
|
|
// sdk the Agent SDK, the claude-code npm package, the claude-code
|
|
// GitHub Action
|
|
// bypass permission-bypass flags, which only exist for launched sessions
|
|
// Plugin management (`claude plugin install|validate`, `marketplace add`)
|
|
// starts no session and is not a hit.
|
|
//
|
|
// Classes: (a) starts Claude Code, (b) tells a user or a session to start it,
|
|
// (c) historical mention (changelog, measurement records, recorded fixtures),
|
|
// (operator-instrument) a documented tool a PERSON runs, kept by operator
|
|
// decision 2026-10-02: the fixed test set run through Anthropic's plugin eval
|
|
// tool; Voyage starts nothing.
|
|
// (a) and (b) must be zero. A (c) or operator-instrument file stands only in
|
|
// EXEMPT, with its reason and its EXACT hit count: a new hit in an exempt file
|
|
// fails, and so does an entry that no longer hits.
|
|
//
|
|
// When this test fails, remove the launch (work that needs another session
|
|
// becomes an order plus a starter the user runs in a session they open); do
|
|
// NOT widen EXEMPT to hide it.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(HERE, '..', '..');
|
|
const SELF = 'tests/lib/no-claude-code-launch.test.mjs';
|
|
|
|
const PATTERNS = [
|
|
['cli', /(^|[^A-Za-z0-9_./@-])claude[ \t]+(-{1,2}[A-Za-z]|["'$])/],
|
|
['plugin-eval', /(^|[^A-Za-z0-9_./@-])claude[ \t]+plugin[ \t]+eval\b/],
|
|
['spawn', /\b(spawn|spawnSync|exec|execSync|execFile|execFileSync|fork)\(\s*['"`]claude\b/],
|
|
['sdk', /claude-agent-sdk|claude_agent_sdk|@anthropic-ai\/claude-code|claude-code-sdk|claude_code_sdk|claude-code-action/],
|
|
['bypass', /bypassPermissions|--dangerously-skip-permissions/],
|
|
];
|
|
|
|
// path -> [class, exact hit count, reason]
|
|
const EXEMPT = new Map([
|
|
[SELF, ['self', 17, "the gate's own pattern table and known-positive fixture"]],
|
|
['CHANGELOG.md', ['c', 15, 'release history; never rewritten']],
|
|
['docs/T1-cc26-delegated-orchestration.md', ['c', 4, 'measurement record (T1/CC-26)']],
|
|
['docs/T1-synthesis-poc-results.md', ['c', 1, 'measurement record (synthesis PoC)']],
|
|
['docs/cc-upgrade-2.1.181-decision-matrix.md', ['c', 3, 'decision record (CC 2.1.181 upgrade)']],
|
|
['docs/deep-research-engine-research.md', ['c', 6, 'research record (deep-research engine)']],
|
|
['docs/operations.md', ['c', 1, 'Path A decision record: why cache-first was rejected']],
|
|
['docs/spike-pretooluse-subagent-reach.md', ['c', 2, 'spike record (PreToolUse reach)']],
|
|
['docs/voyage-vs-cc-balance-analysis.md', ['c', 1, 'decision record (balance analysis)']],
|
|
['scripts/synthesis-measure.mjs', ['c', 2, 'measurement script; says why a child-session instrument was not used, launches nothing']],
|
|
['tests/fixtures/research-deep-research-adapted.md', ['c', 1, 'recorded research fixture']],
|
|
['tests/fixtures/synthesis/digest.json', ['c', 1, 'recorded synthesis fixture']],
|
|
['tests/fixtures/synthesis/exploration/architecture-mapper.md', ['c', 3, 'recorded exploration fixture']],
|
|
['tests/fixtures/synthesis/exploration/dependency-tracer.md', ['c', 2, 'recorded exploration fixture']],
|
|
['docs/proevesett.md', ['operator-instrument', 3, "the operator's own eval run through Anthropic's `claude plugin eval`; Voyage starts nothing - kept by operator decision 2026-10-02"]],
|
|
['.gitignore', ['operator-instrument', 1, 'ignores the output of that same operator-run eval']],
|
|
['hooks/scripts/post-approval-log.mjs', ['mode-read', 3, "reads the session's permission_mode from hook input so an approval is never logged when no person was asked; starts nothing - operator decision 2026-10-06"]],
|
|
['tests/hooks/guards-review-regressions.test.mjs', ['mode-read', 1, 'feeds that same permission_mode value to the hook under test; starts nothing']],
|
|
['hooks/scripts/pre-bash-executor.mjs', ['mode-read', 1, "reads the session's permission_mode from hook input so a merge into main or a push is refused when no person can be asked; starts nothing - operator decision 2026-10-09"]],
|
|
['tests/commands/trekexecute-coupling.test.mjs', ['mode-read', 1, 'feeds that same permission_mode value to the guard under test; starts nothing - operator decision 2026-10-09']],
|
|
]);
|
|
|
|
function trackedFiles(root) {
|
|
const out = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf-8' });
|
|
return out.split('\0').filter(Boolean);
|
|
}
|
|
|
|
// One "path:line:pattern" entry per hit line (first matching pattern wins).
|
|
function scan(root, files) {
|
|
const hits = [];
|
|
for (const rel of files) {
|
|
const buf = readFileSync(join(root, rel));
|
|
if (buf.includes(0)) continue; // binary
|
|
buf.toString('utf-8').split('\n').forEach((line, i) => {
|
|
const p = PATTERNS.find(([, re]) => re.test(line));
|
|
if (p) hits.push(`${rel}:${i + 1}:${p[0]}`);
|
|
});
|
|
}
|
|
return hits;
|
|
}
|
|
|
|
test('scan finds every launch form and passes the look-alikes (known-positive + known-negative)', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'cc-launch-'));
|
|
try {
|
|
const positives = [
|
|
'claude -p "/trekexecute --session 1 plan.md"',
|
|
' GIT_OPTIONAL_LOCKS=0 claude --print "x"',
|
|
'run `claude --bg` in a pane',
|
|
'claude "$(cat session-1.md)"',
|
|
'claude --resume',
|
|
'claude plugin eval . --runs 1',
|
|
"const child = spawn('claude', argv);",
|
|
'execFileSync("claude", ["--version"])',
|
|
"import { query } from '@anthropic-ai/claude-agent-sdk';",
|
|
'npx @anthropic-ai/claude-code',
|
|
' --permission-mode bypassPermissions \\',
|
|
'claude --dangerously-skip-permissions',
|
|
'uses: anthropics/claude-code-action@v1',
|
|
];
|
|
const negatives = [
|
|
'Claude Code runs the command inline.',
|
|
'model: claude-opus-5-5',
|
|
'paths under .claude/projects/ and ${CLAUDE_PLUGIN_ROOT}',
|
|
'claude plugin install voyage@ktg-plugin-marketplace',
|
|
'claude plugin validate',
|
|
'Open a Claude Code session in the worktree and run `/trekexecute --session 1 plan.md`.',
|
|
'const bin = "claude";',
|
|
'the claude binary is not on PATH',
|
|
];
|
|
writeFileSync(join(dir, 'pos.md'), positives.join('\n') + '\n');
|
|
writeFileSync(join(dir, 'neg.md'), negatives.join('\n') + '\n');
|
|
const pos = scan(dir, ['pos.md']);
|
|
assert.equal(pos.length, positives.length, `every positive must hit:\n${pos.join('\n')}`);
|
|
assert.deepEqual(scan(dir, ['neg.md']), []);
|
|
assert.deepEqual(new Set(pos.map(h => h.split(':')[2])), new Set(PATTERNS.map(([k]) => k)),
|
|
'the known-positive fixture must exercise every pattern');
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('no tracked file outside EXEMPT starts Claude Code or tells anyone to', () => {
|
|
const files = trackedFiles(ROOT);
|
|
assert.ok(files.length > 100, `git ls-files returned ${files.length} files; refusing to pass on an empty scan`);
|
|
const hits = scan(ROOT, files.filter(f => !EXEMPT.has(f)));
|
|
assert.deepEqual(hits, [], `${hits.length} launch hit(s) in (a)/(b):\n${hits.join('\n')}`);
|
|
});
|
|
|
|
test('every EXEMPT entry is tracked, has a reason, and hits exactly its recorded count', () => {
|
|
const files = new Set(trackedFiles(ROOT));
|
|
for (const [rel, [cls, count, reason]] of EXEMPT) {
|
|
assert.ok(files.has(rel), `EXEMPT names untracked ${rel}`);
|
|
assert.ok(['c', 'self', 'operator-instrument', 'mode-read'].includes(cls), `${rel}: unknown class ${cls}`);
|
|
assert.ok(reason && reason.length > 10, `${rel}: an exemption needs a reason`);
|
|
const n = scan(ROOT, [rel]).length;
|
|
assert.equal(n, count, `${rel}: ${n} hit(s), EXEMPT records ${count} - a new hit is a launch, a lost one is a stale entry`);
|
|
}
|
|
});
|