fix: the findings of the release review of 6.3.0

The review of v6.2.0..f669b87 (BLOCK: 1 BLOCKER, 12 MAJOR, 1 MINOR), each
fixed behind a test that failed first, or, for the two missing tests, a test
shown to fail on a stub of the branch it covers.

- Review steps appended by /trekexecute Phase 7.2 are held to the step rules
  on any plan version: plan-validator --steps-from <N> (on a plan below 1.8 a
  template review step validated and was executed).
- The guard refuses where no one can be asked also when its rules will not
  load; the wave merge path names that refusal instead of MERGE_REFUSED.
  Tests for the classifier-error refusal and post-approval-log's own check.
- release-check: the version files may change only in their version after
  the review (JSON compared without it), README diff lines are read inside
  hunks only, an unreadable diff is exit 2, and npm test runs without
  NODE_TEST_CONTEXT.
- What counts as a step of only tests: a note in parentheses is not a path,
  and a data file under specs/ is not a test. End-to-end: rides on the last
  step that builds behaviour (validator hint, trekplan, template).
- A removed line is cited as <rev>:<path>:<line> (findings-schema hint,
  trekreview re-ask).
- skill-lint counts a trailing newline as no line; regression-check takes
  commandEnv from lib/util/command-env.mjs; CLAUDE.md names four mode-read
  exemptions.
- /security-review, older than v6.2.0: the criteria runner refuses git
  --output, which writes a file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-10 22:12:12 +02:00
commit bef9c5b3ac
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
18 changed files with 450 additions and 43 deletions

View file

@ -265,7 +265,7 @@ test('P11: a review step in the template form is caught by the plan validator, s
assert.ok(errors.some((c) => /VERIFY_NOT_RUNNABLE|TEST_FIRST/.test(c)), errors.join(','));
const p = section(CMD, '## Phase 7.2 — Simplify, review, verdict (P11)');
const check = p.indexOf('plan-validator.mjs --strict --json "{plan_path}"');
const check = p.indexOf('plan-validator.mjs --strict --json --steps-from {total + 1} "{plan_path}"');
const ask = p.indexOf('Approve the plan as it is now?');
assert.ok(check > 0 && ask > check, 'the validator runs before the approval question');
assert.match(p, /restore the plan from the copy/, 'a stop leaves the plan matching its approval');
@ -332,6 +332,56 @@ test('P12: the guard asks before a merge into main; in the mode with no prompts
} finally { rmSync(dir, { recursive: true, force: true }); }
});
// Release review of 6.3.0 (2026-10-10): the refusal covers the guard's two
// fallbacks too — rules that will not load, and a command it could not read.
function guardIn(plugin, cwd, command, permission_mode) {
return node([join(plugin, 'hooks', 'scripts', 'pre-bash-executor.mjs')], {
input: JSON.stringify({ tool_name: 'Bash', tool_input: { command }, cwd, permission_mode, session_id: 'coupling', tool_use_id: `t${Math.random()}` }),
});
}
test('P12 / review 630: a guard whose rules will not load refuses where no one is asked', async () => {
const { pluginCopy } = await import('../hooks/guard-helpers.mjs');
const plugin = pluginCopy({ 'hooks/lib/git-approval.mjs': 'missing' });
const dir = repo();
try {
const asked = guardIn(plugin, dir, 'git status', 'default');
assert.equal(asked.status, 0, asked.stderr);
assert.match(asked.stdout, /"permissionDecision":"ask"/, 'the control: where someone can answer, the broken guard asks');
const r = guardIn(plugin, dir, 'git push origin main', NO_PROMPT_MODE);
assert.equal(r.status, 2, `refused, not asked: ${r.stdout}${r.stderr}`);
assert.match(r.stderr, /no one to ask/);
assert.match(r.stderr, /could not load/);
} finally {
rmSync(dir, { recursive: true, force: true });
rmSync(plugin, { recursive: true, force: true });
}
});
test('P12 / review 630: a command the classifier could not read is asked, and refused where no one is asked', async () => {
const { pluginCopy } = await import('../hooks/guard-helpers.mjs');
const plugin = pluginCopy();
const lib = join(plugin, 'hooks', 'lib', 'git-approval.mjs');
const src = readFileSync(lib, 'utf8');
assert.ok(src.includes('export function classifyGitApproval('), 'the copy carries the classifier');
writeFileSync(lib, src.replace('export function classifyGitApproval(',
'export function classifyGitApproval() { throw new Error(\'classifier broke\'); }\nfunction classifyGitApprovalUnused('));
const dir = repo();
try {
const asked = guardIn(plugin, dir, 'git push origin feature', 'default');
assert.equal(asked.status, 0, asked.stderr);
assert.match(asked.stdout, /"permissionDecision":"ask"/, 'where someone can answer it is asked');
assert.match(asked.stdout, /could not read/);
const r = guardIn(plugin, dir, 'git push origin feature', NO_PROMPT_MODE);
assert.equal(r.status, 2, `refused: ${r.stdout}${r.stderr}`);
assert.match(r.stderr, /could not read/);
assert.match(r.stderr, /no one to ask/);
} finally {
rmSync(dir, { recursive: true, force: true });
rmSync(plugin, { recursive: true, force: true });
}
});
// ---- the other new progress fields (Handover 5, additive-optional) -----------
test('H5: the wave-2 fields are shape-checked when present and optional when absent', () => {