fix: round 2 of the release review of 6.3.0

Round 2 (v6.2.0..0d044f6, BLOCK: 2 BLOCKER, 11 MAJOR, 1 MINOR), each fixed
behind a test that failed first.

- The git runner checks only words /bin/sh passes through unchanged (no
  quote, backslash, $, glob or brace) and refuses --output and its
  abbreviations: round 1's --output refusal was bypassed by quoting.
- fieldPaths: backticked paths are kept as written, parentheses and all; in
  plain text every path-like word counts, and a parenthesised note does not.
- plan-validator --steps-from reads the plan's ## Verification for an
  appended End-to-end step, and warns instead of failing under --soft.
- regression-check prints progress_status, the value progress.json takes;
  /trekexecute copies it.
- release-check: a plugin in a subdirectory is checked in its own directory,
  the suite is not run when a cheaper check already makes the answer exit 2,
  and a bad argument or a crash is exit 2.
- Skill routing looks a skill up among skills only, and builds the corpus
  vectors once.
- /trekexecute Phase 7.2 says plainly that remediation steps in the review
  template's own form stop with a re-plan; making them runnable is a later
  order (operator 2026-10-10).

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

View file

@ -131,7 +131,7 @@ export function changedLines(diff) {
}
function checkTree(repo) {
const r = git(repo, ['status', '--porcelain']);
const r = git(repo, ['status', '--porcelain', '--', '.']);
if (r.status !== 0) return { id: 'tree', status: 'error', detail: `git status failed: ${r.stderr.trim()}` };
const changed = r.stdout.split('\n').filter(Boolean);
if (changed.length > 0) {
@ -177,13 +177,15 @@ function checkCoverage(repo, reviewText) {
const anc = git(repo, ['merge-base', '--is-ancestor', full, 'HEAD']);
if (anc.status !== 0) return { id: 'coverage', status: 'fail', detail: `the reviewed commit ${full.slice(0, 12)} is not in the history of HEAD` };
const names = git(repo, ['diff', '--name-only', full, 'HEAD']);
// --relative: the paths, and the files that count, are those under the plugin's
// own directory, also when it sits in a subdirectory of a larger repo.
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));
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}`]);
const before = git(repo, ['show', `${full}:./${file}`]);
const after = git(repo, ['show', `HEAD:./${file}`]);
if (before.status !== 0 || after.status !== 0) { outside.push(`${file} (added or removed after the review)`); continue; }
let same;
try { same = withoutVersion(before.stdout) === withoutVersion(after.stdout); } catch (e) {
@ -192,7 +194,7 @@ function checkCoverage(repo, reviewText) {
if (!same) outside.push(`${file} (lines other than the version)`);
}
if (changed.includes('README.md')) {
const d = git(repo, ['diff', '-U0', full, 'HEAD', '--', 'README.md']);
const d = git(repo, ['diff', '--relative', '-U0', full, 'HEAD', '--', 'README.md']);
if (d.status !== 0) return { id: 'coverage', status: 'error', detail: `git diff of README.md failed: ${d.stderr.trim()} — not measured` };
if (changedLines(d.stdout).some((l) => !BADGE_RE.test(l))) outside.push('README.md (lines other than the version badge)');
}
@ -235,15 +237,22 @@ function resolveVersion(repo, explicit) {
export function releaseCheck({ repo, review, version: explicitVersion }) {
const top = git(repo, ['rev-parse', '--show-toplevel']);
if (top.error || top.status !== 0) return { exit: EXIT.UNDECIDED, version: null, repo, checks: [], reason: `${repo} is not a git repository` };
const root = top.stdout.trim();
// The plugin's own directory, not the git top level: a plugin may sit in a
// subdirectory of a larger repo (release review of 6.3.0, round 2).
const root = resolve(repo);
const version = resolveVersion(root, explicitVersion);
if (!version) return { exit: EXIT.UNDECIDED, version: null, repo: root, checks: [], reason: 'no version to release: pass --version or set .claude-plugin/plugin.json version' };
// The cheap checks first: when one of them cannot be measured the answer is
// already exit 2, and the suite (minutes) is not run for nothing.
const tree = checkTree(root);
const suite = checkSuite(root, tree.status === 'pass');
const { check: rev, text } = checkReview(review ? resolve(review) : null);
const coverage = checkCoverage(root, text);
const door = checkDoor(root, version);
const undecided = [tree, rev, coverage, door].filter((c) => c.status === 'error');
const suite = undecided.length > 0
? { id: 'suite', status: 'skip', detail: `not run: ${undecided.map((c) => c.id).join(', ')} could not be measured, so the answer is exit 2 already` }
: checkSuite(root, tree.status === 'pass');
const checks = [tree, suite, rev, coverage, door];
let exit = EXIT.READY;
@ -284,10 +293,11 @@ function parseArgs(argv) {
const out = { repo: process.cwd(), review: null, version: null, json: false };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--repo') out.repo = argv[++i];
else if (a === '--review') out.review = argv[++i];
else if (a === '--version') out.version = argv[++i];
else if (a === '--json') out.json = true;
if (a === '--repo' || a === '--review' || a === '--version') {
const v = argv[++i];
if (v === undefined || v.startsWith('--')) throw new Error(`${a} needs a value`);
out[a.slice(2)] = v;
} else if (a === '--json') out.json = true;
}
return out;
}
@ -299,8 +309,17 @@ function isMain() {
}
if (isMain()) {
const args = parseArgs(process.argv.slice(2));
const result = releaseCheck({ repo: resolve(args.repo), review: args.review, version: args.version });
process.stdout.write((args.json ? JSON.stringify(result, null, 2) : render(result)) + '\n');
// Anything that throws is "could not run" (2), never "not ready" (1): exit 1
// promises a list of failed checks, and none ran.
let result;
let json = process.argv.includes('--json');
try {
const args = parseArgs(process.argv.slice(2));
json = args.json;
result = releaseCheck({ repo: resolve(args.repo), review: args.review, version: args.version });
} catch (e) {
result = { exit: EXIT.UNDECIDED, version: null, repo: null, checks: [], reason: e.message };
}
process.stdout.write((json ? JSON.stringify(result, null, 2) : render(result)) + '\n');
process.exit(result.exit);
}