fix: close the six defects an independent review found in the trial fixes

Independent read-only review of c56f852..2d29966 (2026-10-08), each defect
reproduced by the reviewer; each new test fails on that code.

- test-lock green ran ANY allowlisted command: `green --cmd "git status"`
  marked a red step green and the stop guard let it stop. The lock now
  stores the step's test command (`lock --cmd`); `green` runs only that
  one (a different --cmd, or a lock without one, exits 2). trekexecute D1
  calls `green` with no command, which also removes the quoting hazard.
- plan-gate --accept-open worked at round 1 and passed the gate; it now
  refuses while rounds are left.
- findings-schema --repo-root swallowed a following flag, or took a root
  that does not exist, and silently skipped the line check: exit 2.
- brief-review-stamp hashed only the body, so a changed `framing` or
  `research_topics` stayed "current"; it now covers the reviewed
  frontmatter too (only the post-review stamps are left out).
- red-proof red_kind read an assertion message naming SyntaxError as a
  load error; the signatures are now specific.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-08 07:17:45 +02:00
commit 5f9faeed4a
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
12 changed files with 192 additions and 35 deletions

View file

@ -22,7 +22,7 @@
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1124-1139 | profile, phase_models, profile_source | none |
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:782-808 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1115-1135 | profile, phase_models, parallel_agents, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2341-2363 | profile, phase_models, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2343-2365 | profile, phase_models, profile_source | none |
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none |
| trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) |
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:628-632 | profile, phase_models, profile_source | none |

View file

@ -22,6 +22,7 @@ const CLI = join(ROOT, 'lib', 'util', 'test-lock.mjs');
const STOP = join(ROOT, 'hooks', 'scripts', 'stop-test-lock.mjs');
const PRE_WRITE = join(ROOT, 'hooks', 'scripts', 'pre-write-executor.mjs');
const SESS = `sess-green-${process.pid}-${Date.now()}`;
const CMD = 'node --test tests/step.test.mjs';
function cli(repo, args) {
const r = spawnSync(process.execPath, [CLI, ...args], { cwd: repo, encoding: 'utf8', env: { ...process.env, CLAUDE_CODE_SESSION_ID: SESS } });
@ -37,11 +38,9 @@ function lockedRepo(testBody) {
`test('step', () => { ${testBody} });`,
'',
].join('\n'));
assert.equal(cli(repo, ['lock', '--files', 'tests/step.test.mjs', '--step', '3']).code, 0);
assert.equal(cli(repo, ['lock', '--files', 'tests/step.test.mjs', '--step', '3', '--cmd', CMD]).code, 0);
return repo;
}
const CMD = 'node --test tests/step.test.mjs';
const stop = (repo) => ({ hook_event_name: 'Stop', session_id: SESS, cwd: repo, stop_hook_active: false });
const edit = (file_path) => ({ hook_event_name: 'PreToolUse', tool_name: 'Edit', tool_input: { file_path, old_string: 'x', new_string: 'z' } });
@ -62,7 +61,7 @@ test('green: the test fails -> exit 1, no mark, Stop is still refused', async ()
assert.equal(parseJsonOut((await runHook(STOP, stop(repo))).stdout)?.decision, 'block');
});
test('green: a command outside the test-runner allowlist is never run -> exit 2, no mark', () => {
test('green: a --cmd that is not the stored test command is never run -> exit 2, no mark', () => {
const repo = lockedRepo('assert.equal(1, 1);');
const g = cli(repo, ['green', '--cmd', 'echo ok']);
assert.equal(g.code, 2, g.stderr);
@ -85,12 +84,13 @@ test('pending: a fix round clears the mark, and Stop is refused again', async ()
test('re-lock of the same step starts without a mark', () => {
const repo = lockedRepo('assert.equal(1, 1);');
assert.equal(cli(repo, ['green', '--cmd', CMD]).code, 0);
assert.equal(cli(repo, ['lock', '--files', 'tests/step.test.mjs', '--step', '3']).code, 0);
assert.equal(cli(repo, ['lock', '--files', 'tests/step.test.mjs', '--step', '3', '--cmd', CMD]).code, 0);
assert.equal(JSON.parse(cli(repo, ['status']).stdout).green_at, null);
});
test('trekexecute: D1 marks the lock green through the command, a fix round marks it pending', () => {
const md = readFileSync(join(ROOT, 'commands', 'trekexecute.md'), 'utf8');
assert.match(md, /test-lock\.mjs green --cmd "\{Verify command\}"/);
assert.match(md, /test-lock\.mjs green\n/);
assert.match(md, /test-lock\.mjs lock --files \{test files\} --step \{N\} --cmd "\{Verify command\}"/);
assert.match(md, /test-lock\.mjs pending/);
});

View file

@ -0,0 +1,122 @@
// tests/lib/independent-review-fixes.test.mjs
// Findings of the independent review of c56f852..2d29966 (2026-10-08), each
// verified by the reviewer by running it; each test fails on that code.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { runHook } from '../helpers/hook-helper.mjs';
import { makeRepo, parseJsonOut } from '../hooks/guard-helpers.mjs';
import { recordVerdict, planHash, acceptOpen, gatePlan } from '../../lib/validators/plan-gate.mjs';
import { stampBriefReview, checkBriefReview } from '../../lib/validators/brief-review-stamp.mjs';
import { stampIntentApproval } from '../../lib/validators/intent-approval.mjs';
import { buildPlan } from '../helpers/plan-v18-builder.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const LOCK = join(ROOT, 'lib', 'util', 'test-lock.mjs');
const STOP = join(ROOT, 'hooks', 'scripts', 'stop-test-lock.mjs');
const SESS = `sess-review-${process.pid}-${Date.now()}`;
// --- 1 + 5: the green mark runs the step's OWN test command, stored at lock time
function lockRepo(withCmd) {
const repo = makeRepo('main');
mkdirSync(join(repo, 'tests'), { recursive: true });
writeFileSync(join(repo, 'tests', 'a.test.mjs'), "import { test } from 'node:test';\nimport assert from 'node:assert';\ntest('a', () => assert.equal(1, 2));\n");
const args = ['lock', '--files', 'tests/a.test.mjs', '--step', '1'];
if (withCmd) args.push('--cmd', 'node --test tests/a.test.mjs');
const r = spawnSync(process.execPath, [LOCK, ...args], { cwd: repo, encoding: 'utf8', env: { ...process.env, CLAUDE_CODE_SESSION_ID: SESS } });
assert.equal(r.status, 0, r.stderr);
return repo;
}
const lockCli = (repo, args) => spawnSync(process.execPath, [LOCK, ...args], { cwd: repo, encoding: 'utf8', env: { ...process.env, CLAUDE_CODE_SESSION_ID: SESS } });
test('green: another allowed command (git status) can never mark a red step green', async () => {
const repo = lockRepo(true);
assert.notEqual(lockCli(repo, ['green', '--cmd', 'git status']).status, 0);
assert.equal(lockCli(repo, ['green']).status, 1, 'the stored test command is red');
assert.equal(JSON.parse(lockCli(repo, ['status']).stdout).green_at, null);
const stop = await runHook(STOP, { hook_event_name: 'Stop', session_id: SESS, cwd: repo, stop_hook_active: false });
assert.equal(parseJsonOut(stop.stdout)?.decision, 'block');
});
test('green: a lock that names no test command is never marked green (exit 2)', () => {
const repo = lockRepo(false);
assert.equal(lockCli(repo, ['green', '--cmd', 'node --test tests/a.test.mjs']).status, 2);
});
test('green without --cmd runs the stored command; status shows it', () => {
const repo = lockRepo(true);
writeFileSync(join(repo, 'tests', 'a.test.mjs'), "import { test } from 'node:test';\ntest('a', () => {});\n");
assert.equal(lockCli(repo, ['green']).status, 0);
assert.equal(JSON.parse(lockCli(repo, ['status']).stdout).cmd, 'node --test tests/a.test.mjs');
});
test('prose: the helper locks with --cmd, and D1 calls green without re-quoting the command', () => {
assert.match(readFileSync(join(ROOT, 'agents', 'step-implementer.md'), 'utf8'), /test-lock\.mjs lock --files <test files> --step \{step\} --cmd "\{Verify command\}"/);
const ex = readFileSync(join(ROOT, 'commands', 'trekexecute.md'), 'utf8');
assert.match(ex, /test-lock\.mjs green`?\n/);
assert.doesNotMatch(ex, /test-lock\.mjs green --cmd/);
});
// --- 2: --accept-open only after the last round -------------------------------
const BOTH = [{ agent: 'plan-critic', present: true }, { agent: 'scope-guardian', present: true }];
test('--accept-open refuses while rounds are left: revise instead', () => {
const t = buildPlan();
const r = recordVerdict(t, { findings: [{ id: 'm', severity: 'major', text: 'weak test' }], sources: BOTH }, { reviewed: planHash(t) });
const a = acceptOpen(r.text);
assert.equal(a.code, 1);
assert.match(a.message, /round\(s\) left/);
assert.equal(gatePlan(r.text).code, 1);
});
// --- 3: --repo-root never swallows a flag or accepts a missing root ----------
test('findings-schema: --repo-root followed by a flag, or naming no directory, is a usage error (exit 2)', () => {
const dir = mkdtempSync(join(tmpdir(), 'voyage-root-'));
try {
const out = join(dir, 'r.md');
writeFileSync(out, '```json\n{"reviewer":"x","findings":[]}\n```\n');
const cli = (...a) => spawnSync(process.execPath, [join(ROOT, 'lib', 'review', 'findings-schema.mjs'), ...a], { encoding: 'utf8' });
assert.equal(cli('--repo-root', '--json', out).status, 2);
assert.equal(cli('--repo-root', join(dir, 'nope'), out).status, 2);
assert.equal(cli('--repo-root', dir, out).status, 0);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
// --- 4: the stamp covers the frontmatter the reviewer judges -----------------
const BRIEF = ['---', 'type: trekbrief', 'framing: preserve', 'research_topics: 2', '---', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', ''].join('\n');
test('brief stamp: changing framing or research_topics after the review makes it stale', () => {
const s = stampBriefReview(BRIEF).text;
assert.equal(checkBriefReview(s.replace('framing: preserve', 'framing: replace')).state, 'stale');
assert.equal(checkBriefReview(s.replace('research_topics: 2', 'research_topics: 0')).state, 'stale');
assert.equal(checkBriefReview(stampIntentApproval(s).text).state, 'current', 'the intent stamp still keeps it current');
});
// --- 6: red_kind does not read an assertion message as a load error ----------
test('red proof: a named test whose assertion mentions SyntaxError is a test-failure', async () => {
const { proveRed } = await import('../../lib/verification/red-proof.mjs');
const repo = makeRepo('main');
mkdirSync(join(repo, 'lib'));
writeFileSync(join(repo, 'lib', 'p.mjs'), 'export function parse(s) { return s; }\n');
spawnSync('git', ['add', 'lib/p.mjs'], { cwd: repo });
spawnSync('git', ['commit', '-q', '-m', 'base'], { cwd: repo });
const base = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).stdout.trim();
mkdirSync(join(repo, 'tests'));
writeFileSync(join(repo, 'tests', 'p.test.mjs'), "import { test } from 'node:test';\nimport assert from 'node:assert';\nimport { parse } from '../lib/p.mjs';\ntest('throws on bad input', () => { assert.throws(() => parse('{'), SyntaxError); });\n");
writeFileSync(join(repo, 'lib', 'p.mjs'), 'export function parse(s) { return JSON.parse(s); }\n');
const r = proveRed({ cwd: repo, base, cmd: 'node --test tests/p.test.mjs', tests: ['tests/p.test.mjs'] });
assert.equal(r.status, 'proven', JSON.stringify(r).slice(0, 400));
assert.equal(r.red_kind, 'test-failure');
});