fix: round 3 of the release review of 6.3.0

Round 3 (v6.2.0..96cb977, BLOCK: 1 BLOCKER, 8 MAJOR, 2 MINOR), each fixed
behind a test that failed first.

- Every allowlisted runner, not only git, takes plain words and a short list
  of options that only read: an option that writes a file or loads code
  (--test-reporter-destination, --outputFile, --junitxml, --basetemp,
  --import, --config, -p, -o, -exec, a reporter path) is on no list; after
  `npm test --` only paths follow; `make test` takes no VAR=value. A built-in
  node test reporter (tap, spec, dot, junit, lcov) stays allowed.
- /trekexecute: a restore the guard refuses (destructive git, in the mode
  that shows no prompts) or the operator declines is never recorded as
  reverted: simplify = "revert-refused" and the run stops before review; a
  refused step revert is reported with its paths.
- stepTestsOnly: a fixture document under tests/ is a test file, "e.g." is
  not a path, ./ is dropped; the plan-file sweep reads Files: with the same
  reader (fieldPaths); the validator hands its parsed fields over.
- plan-validator --steps-from asks for Verify: on a 1.8 plan too.
- skill frontmatter: a value starting with # is a hazard; a quote after an
  escaped backslash closes the value. Routing reads kind tags with hasOwn.
- release-check reads scope_sha_end as written (an all-digit sha keeps its
  leading zero), and Voyage's own artifacts under .claude/projects/ and
  .claude/voyage/ committed after the review do not count as unreviewed.

Suite 2170 (2168/0/2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-10 23:25:24 +02:00
commit 0e612c88ac
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
14 changed files with 284 additions and 40 deletions

View file

@ -39,7 +39,6 @@ import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { decideVerdict } from '../lib/review/verdict-gate.mjs';
import { commandEnv } from '../lib/util/command-env.mjs';
import { parseDocument } from '../lib/util/frontmatter.mjs';
const PLUGIN_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@ -54,6 +53,11 @@ export const CHECK_IDS = Object.freeze(['tree', 'suite', 'review', 'coverage', '
export const RELEASE_BOOKKEEPING = Object.freeze([
'CHANGELOG.md', 'package.json', 'package-lock.json', '.claude-plugin/plugin.json', 'README.md',
]);
// Voyage's own pipeline artifacts, committed by a repo that tracks them
// (`artifact_store: git`, `.claude/voyage/approvals.jsonl`): the review itself
// and the approval log land after the reviewed commit by design (release
// review of 6.3.0, round 3). The plan-file sweep treats them the same way.
const PROCESS_PREFIXES = Object.freeze(['.claude/projects/', '.claude/voyage/']);
const BADGE_RE = /badge\/version-/;
const VERSION_FILES = Object.freeze(['package.json', 'package-lock.json', '.claude-plugin/plugin.json']);
@ -165,11 +169,20 @@ function checkReview(reviewPath) {
return { check: { id: 'review', status: 'pass', detail: `review verdict ${v.verdict}` }, text };
}
/**
* The review's `scope_sha_end` as written. Read from the frontmatter text, not
* through the YAML reader, which turns an all-digit value into a number and
* drops a leading zero (`0123456` → 123456; release review of 6.3.0, round 3).
*/
export function reviewedSha(reviewText) {
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/.exec(String(reviewText ?? ''));
const m = fm && /^scope_sha_end:[ \t]*(["']?)([^"'\s#]*)\1[ \t]*(?:#.*)?$/m.exec(fm[1]);
return m ? m[2] : '';
}
function checkCoverage(repo, reviewText) {
if (reviewText === null) return { id: 'coverage', status: 'skip', detail: 'not checked: no readable review' };
let sha;
try { sha = parseDocument(reviewText).parsed?.frontmatter?.scope_sha_end; } catch { sha = undefined; }
sha = sha === undefined || sha === null ? '' : String(sha).trim();
const sha = reviewedSha(reviewText);
if (!sha) return { id: 'coverage', status: 'fail', detail: 'review.md has no scope_sha_end — cannot tell what it reviewed' };
const resolved = git(repo, ['rev-parse', '--verify', '--quiet', `${sha}^{commit}`]);
if (resolved.status !== 0) return { id: 'coverage', status: 'fail', detail: `the reviewed commit ${sha} is not in the history of HEAD (unknown commit)` };
@ -182,7 +195,7 @@ function checkCoverage(repo, reviewText) {
const names = git(repo, ['diff', '--relative', '--name-only', full, 'HEAD']);
if (names.status !== 0) return { id: 'coverage', status: 'error', detail: `git diff failed: ${names.stderr.trim()}` };
const changed = names.stdout.split('\n').filter(Boolean);
const outside = changed.filter((f) => !RELEASE_BOOKKEEPING.includes(f));
const outside = changed.filter((f) => !RELEASE_BOOKKEEPING.includes(f) && !PROCESS_PREFIXES.some((p) => f.startsWith(p)));
for (const file of changed.filter((f) => VERSION_FILES.includes(f))) {
const before = git(repo, ['show', `${full}:./${file}`]);
const after = git(repo, ['show', `HEAD:./${file}`]);