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
|
|
@ -268,6 +268,13 @@ const SHELL_OPERATORS = /[;&|<>]|\$\(|`/;
|
|||
|
||||
// Read-only git subcommands: they report, they do not write a repository.
|
||||
const READONLY_GIT = new Set(['status', 'log', 'diff', 'show', 'ls-files']);
|
||||
// A word /bin/sh hands to git unchanged (refs like HEAD~1 and HEAD^, formats like %h, paths).
|
||||
const GIT_LITERAL_WORD = /^[A-Za-z0-9_@%^~:.,=+/-]+$/;
|
||||
// `--output`, `--output=<file>`, or an abbreviation of it git may accept.
|
||||
function writesOutput(word) {
|
||||
const name = word.split('=')[0];
|
||||
return name.length >= 5 && '--output'.startsWith(name);
|
||||
}
|
||||
|
||||
const deny = (reason) => ({ allowed: false, reason });
|
||||
const permit = () => ({ allowed: true, reason: '' });
|
||||
|
|
@ -317,9 +324,15 @@ const RUNNERS = {
|
|||
'/bin/bash': (t) => (isTestScript(t[1]) ? permit() : deny('/bin/bash, but not a script under tests/')),
|
||||
git: (t) => {
|
||||
if (!READONLY_GIT.has(t[1])) return deny(`git, but not a read-only subcommand first (${[...READONLY_GIT].join('|')})`);
|
||||
// `--output` makes diff/log/show write their output to a file (release
|
||||
// review of 6.3.0, 2026-10-10: a plan's check could overwrite any file).
|
||||
if (t.some((w) => w === '--output' || w.startsWith('--output='))) return deny('git with --output writes a file, so it is not a read-only check');
|
||||
// The command runs through /bin/sh, which unquotes and expands what it is
|
||||
// given, so a check on the words here holds only for words the shell passes
|
||||
// through unchanged: no quote, backslash, `$`, glob or brace. Then
|
||||
// `--output` (and an abbreviation of it) is refused: it makes diff/log/show
|
||||
// write to a file (release review of 6.3.0, 2026-10-10: a plan's check
|
||||
// could overwrite any file, first plainly, then through quoting).
|
||||
const odd = t.find((w) => !GIT_LITERAL_WORD.test(w));
|
||||
if (odd !== undefined) return deny(`git with a word the shell would rewrite (${odd}): only plain words are checked`);
|
||||
if (t.some(writesOutput)) return deny('git with --output writes a file, so it is not a read-only check');
|
||||
return permit();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@
|
|||
// (no command, or a command outside the test-runner allowlist): the
|
||||
// step is not done, and that is a finding for the fix rounds
|
||||
// 2 = could not run: bad usage, a plan that cannot be read, no step N
|
||||
// JSON on stdout: {step, status, command, exit_code, output}
|
||||
// JSON on stdout: {step, status, progress_status, command, exit_code, output}
|
||||
// status: passed | failed | blocked | unrunnable | absent | error
|
||||
// progress_status: the value progress.json takes for steps.N.regression
|
||||
// (blocked counts as unrunnable, error as not-run), so the executor
|
||||
// copies one field and never translates
|
||||
//
|
||||
// The command is the field's first backticked span — the same reading as the
|
||||
// runner's `## Verification` bullets — and it goes through the same two
|
||||
|
|
@ -44,6 +47,8 @@ function exec(command, { cwd, timeoutMs }) {
|
|||
|
||||
export const REGRESSION_FIELD = 'Regression';
|
||||
const EXIT = Object.freeze({ passed: 0, absent: 0, failed: 1, blocked: 1, unrunnable: 1, error: 2 });
|
||||
const PROGRESS = Object.freeze({ passed: 'passed', absent: 'absent', failed: 'failed', blocked: 'unrunnable', unrunnable: 'unrunnable', error: 'not-run' });
|
||||
const withProgress = (r) => ({ ...r, progress_status: PROGRESS[r.status] ?? 'not-run' });
|
||||
|
||||
/**
|
||||
* @param {{ planText: string, step: number, cwd?: string, run?: Function }} o
|
||||
|
|
@ -52,19 +57,19 @@ const EXIT = Object.freeze({ passed: 0, absent: 0, failed: 1, blocked: 1, unrunn
|
|||
export function checkRegression({ planText, step, cwd = process.cwd(), run = (c, o) => runCriteria(c, { ...o, exec }) }) {
|
||||
const base = { step, command: null, exit_code: null, output: '' };
|
||||
const found = sliceSteps(planText).find((s) => Number(s.n) === step);
|
||||
if (!found) return { ...base, status: 'error', output: `no "### Step ${step}:" in the plan`, exit: EXIT.error };
|
||||
if (!found) return withProgress({ ...base, status: 'error', output: `no "### Step ${step}:" in the plan`, exit: EXIT.error });
|
||||
const field = parseStepFields(found.body).get(REGRESSION_FIELD);
|
||||
if (!field) return { ...base, status: 'absent', output: 'no Regression: field — nothing to run', exit: EXIT.absent };
|
||||
if (!field) return withProgress({ ...base, status: 'absent', output: 'no Regression: field — nothing to run', exit: EXIT.absent });
|
||||
const { command, reason } = firstCommand(field.text);
|
||||
const [r] = run([{ label: `R${step}`, text: field.text, command, reason }], { cwd });
|
||||
return {
|
||||
return withProgress({
|
||||
step,
|
||||
status: r.status,
|
||||
command: r.command ?? null,
|
||||
exit_code: r.exitCode ?? null,
|
||||
output: r.output ?? '',
|
||||
exit: EXIT[r.status] ?? EXIT.error,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function main(argv) {
|
||||
|
|
@ -92,7 +97,7 @@ export function main(argv) {
|
|||
}
|
||||
let planText;
|
||||
try { planText = readFileSync(plan, 'utf8'); } catch (e) {
|
||||
return say({ step, status: 'error', command: null, exit_code: null, output: `cannot read ${plan}: ${e.message}`, exit: 2 });
|
||||
return say(withProgress({ step, status: 'error', command: null, exit_code: null, output: `cannot read ${plan}: ${e.message}`, exit: 2 }));
|
||||
}
|
||||
return say(checkRegression({ planText, step, cwd }));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue