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:
parent
0d044f6b6f
commit
ed87cc668d
10 changed files with 263 additions and 70 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue