fix: round 7 of the release review of 6.3.0, for 6.3.1
Round 7 (516dffa..45aa9e1, WARN: 12 MAJOR, 0 BLOCKER) and the fence defect
found while writing its review.md. Each fix began with a test that failed on
0f400d3 (v6.3.0); suite 2208 (2206/0/2), 14 tests more.
- Tests-only (operator 2026-10-11): the plan's Files: alone decides, and the
text says so (red-proof, trekexecute Sub-step B, step-implementer,
HANDOVER-CONTRACTS). red-proof no longer reads the step's diff, so a change
already in the tree when the step began no longer counts against it, and a
plan placed outside .claude/ reads the same. The round-6 test that had the
diff overrule the plan is rewritten to the new rule.
- The Files: reader keeps a plain file beside a backticked path
(", Dockerfile", "the CLI entry src/cli.mjs", "and Makefile"). Measured on
the 520 Files: fields in 182 plans: no field reads differently.
- Red proof reports a command its timeout killed as killed after the timeout,
not "could not start"; the release check tells a timeout from output past
its buffer and from another signal. One reading of a stop
(criteria-runner stoppedBy) for red proof, the criteria runner and the
release check.
- A trailing `# comment` is dropped in a backticked check too, and in a
command passed as written, so the words screened are the words run.
- The release check reads a relative --review in --repo.
- The 15-minute timeout is defined once (criteria-runner.mjs).
- The findings check reads each cited file once per validation.
- One isMain helper (lib/util/is-main.mjs) for every CLI: the 31 copies of
the realpath guard and the 20 modules still on the file:// guard, which
exited 0 having done nothing under a symlink or a path with a space.
- The Bash guard writes its no-one-to-ask refusal once, on a direct condition.
- The findings check ends a ```json block only at a fence on its own line, so
three backticks inside a JSON string no longer end it (also in 6.2.0).
- Brief-conformance notes: a test for test-lock green on exit 127 (it
passed at once; it fails when 127 is read as could-not-run); the prose now
names the git revision `@~1` as the one leading-@ exception.
Not changed: the example plan's steps 1-2 keep their tests-only warning
(operator decision 2026-10-09; the r5 test records it). Brief note 4 (a
Regression: whose runner is not installed goes to the fix rounds) is a
routing question for the operator.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0f400d3461
commit
29b4ac91b3
65 changed files with 582 additions and 485 deletions
|
|
@ -10,6 +10,7 @@
|
|||
import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { issue } from '../util/result.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
const CANONICAL_OVERVIEW = 'overview.md';
|
||||
const CANONICAL_GAPS = 'gaps.md';
|
||||
|
|
@ -76,7 +77,7 @@ export function discoverArchitecture(projectDir) {
|
|||
return result;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const projectDir = process.argv[2];
|
||||
const wantJson = process.argv.includes('--json');
|
||||
if (!projectDir) {
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@
|
|||
// exit 0 = current (skip the review); 1 = stale or absent (review); 2 = usage / unreadable
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const REVIEWED_HASH_FIELD = 'brief_reviewed_hash';
|
||||
export const REVIEWED_AT_FIELD = 'brief_reviewed_at';
|
||||
|
|
@ -83,15 +83,7 @@ export function checkBriefReview(text) {
|
|||
return { state: stamped === actual ? 'current' : 'stale', stamped, actual };
|
||||
}
|
||||
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const mode = args.includes('--stamp') ? 'stamp' : args.includes('--check') ? 'check' : null;
|
||||
const path = args.find((a) => !a.startsWith('--'));
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
// optional fields (jf. the source_findings precedent on trekreview) can be
|
||||
// added without a brief_version bump.
|
||||
|
||||
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
import { issue, ok, fail } from '../util/result.mjs';
|
||||
import { BASE_ALLOWED_MODELS } from './profile-validator.mjs';
|
||||
import { parseSuccessCriteria, runnableCheck } from '../verification/criteria-runner.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const BRIEF_REQUIRED_FRONTMATTER = ['type', 'brief_version', 'task', 'slug', 'research_topics', 'research_status'];
|
||||
export const REVIEW_AS_BRIEF_REQUIRED_FRONTMATTER = ['type', 'task', 'slug', 'project_dir', 'findings'];
|
||||
|
|
@ -328,15 +328,7 @@ export function validateBrief(filePath, opts = {}) {
|
|||
return { ...r, parsed: { ...r.parsed, filePath } };
|
||||
}
|
||||
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const strict = !args.includes('--soft');
|
||||
const minIdx = args.indexOf('--min-version');
|
||||
|
|
|
|||
|
|
@ -39,13 +39,13 @@
|
|||
// what `/trekbrief --approve` approves: {brief_path, project_dir, plan_command}; exit 1 = no brief
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
import { issue } from '../util/result.mjs';
|
||||
import { emit } from '../stats/event-emit.mjs';
|
||||
import { resolveStatsDir } from '../stats/stats-append.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const INTENT_HASH_FIELD = 'intent_approved_hash';
|
||||
export const INTENT_APPROVED_AT_FIELD = 'intent_approved_at';
|
||||
|
|
@ -234,18 +234,7 @@ export function resolveApproveTarget(arg) {
|
|||
return { brief_path: briefPath, project_dir: projectDir, plan_command: planCommand };
|
||||
}
|
||||
|
||||
// Main-module guard on real paths: comparing against `file://` + argv[1] fails
|
||||
// when the plugin sits behind a symlink or in a path with a space; the CLI then
|
||||
// exits 0 having checked nothing.
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find((a) => !a.startsWith('--'));
|
||||
if (args.includes('--resolve')) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { issue, fail } from '../util/result.mjs';
|
||||
import { splitFrontmatter, parseFrontmatter } from '../util/frontmatter.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const NEXT_SESSION_PROMPT_REQUIRED_FIELDS = Object.freeze(['produced_by', 'produced_at']);
|
||||
|
||||
|
|
@ -150,7 +151,7 @@ export function validateNextSessionPromptConsistency(a, b, opts = {}) {
|
|||
return { valid: errors.length === 0, errors, warnings, parsed: { a: aFm || null, b: bFm || null } };
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const positionals = args.filter(a => !a.startsWith('--'));
|
||||
const wantJson = args.includes('--json');
|
||||
|
|
|
|||
|
|
@ -54,12 +54,12 @@
|
|||
// The verdict is a trace of a process, not a signature: anything that can
|
||||
// write the plan can write the block.
|
||||
|
||||
import { readFileSync, existsSync, realpathSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
||||
import { readFileSync, existsSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { validatePlanContent } from './plan-validator.mjs';
|
||||
import { splitSections, fenceTracker, extractPlanVersion, versionAtLeast } from '../parsers/plan-schema.mjs';
|
||||
import { normalizeSeverity } from '../review/plan-review-dedup.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const MAX_ROUNDS = 3;
|
||||
export const VERDICT_SCHEMA = 'voyage.plan-verdict/1';
|
||||
|
|
@ -343,19 +343,11 @@ export function gatePlan(rawText) {
|
|||
|
||||
// ---- CLI --------------------------------------------------------------------
|
||||
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function out(obj) {
|
||||
process.stdout.write(JSON.stringify(obj) + '\n');
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const usage = 'Usage: plan-gate.mjs <plan.md> [--json] | --hash <plan.md> | --record <plan.md> --reviewed <sha256> [--extra-round] < dedup.json | --accept-open <plan.md>';
|
||||
const known = new Set(['--json', '--hash', '--record', '--reviewed', '--extra-round', '--accept-open']);
|
||||
|
|
|
|||
|
|
@ -34,8 +34,7 @@
|
|||
// warning PLAN_STEP_TESTS_ONLY: its tests pass at its start point, so it can
|
||||
// never be proven red (voyage-lekerepo trial 3, 2026-10-09).
|
||||
|
||||
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import {
|
||||
sliceSteps, validatePlanHeadings, extractPlanVersion, extractPlanCeremony,
|
||||
parseStepFields, findSection, splitSections, findModuleCycle, parseMermaid, fencedBlocks,
|
||||
|
|
@ -46,6 +45,7 @@ import { validateAllManifests } from '../parsers/manifest-yaml.mjs';
|
|||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
import { issue, fail } from '../util/result.mjs';
|
||||
import { parsePlanVerification, firstCommand, runnableCheck } from '../verification/criteria-runner.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const OPTIONAL_STEP_FIELDS = Object.freeze(['Interface in', 'Interface out', 'Review focus']);
|
||||
export const DESIGN_SUBSECTIONS = Object.freeze([
|
||||
|
|
@ -371,17 +371,7 @@ export function validatePlan(filePath, opts = {}) {
|
|||
return { ...r, parsed: { ...r.parsed, filePath } };
|
||||
}
|
||||
|
||||
// realpath on both sides: `file://${argv[1]}` silently no-ops under a symlink
|
||||
// or a path with a space (the CLI then exits 0 having checked nothing).
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const strict = !args.includes('--soft');
|
||||
const at = args.indexOf('--steps-from');
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
import { issue, ok, fail } from '../util/result.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const PROFILE_REQUIRED_FIELDS = Object.freeze([
|
||||
'name',
|
||||
|
|
@ -175,7 +176,7 @@ export function validateProfile(filePath, opts = {}) {
|
|||
return { ...r, parsed: { ...(r.parsed || {}), filePath } };
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const filePath = args.find(a => !a.startsWith('--'));
|
||||
if (!filePath) {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@
|
|||
// simplify, review) and the key of plan_verification.failed_criteria: the runner
|
||||
// says `exitCode`, progress says `exit_code`, and only `exit_code` is accepted (C15).
|
||||
|
||||
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { issue, fail } from '../util/result.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const PROGRESS_REQUIRED_TOP = ['schema_version', 'plan', 'plan_version', 'mode', 'status', 'total_steps', 'current_step', 'steps'];
|
||||
export const PROGRESS_VALID_STATUSES = ['pending', 'in_progress', 'completed', 'failed', 'partial', 'stopped'];
|
||||
|
|
@ -212,17 +212,7 @@ export function validateProgress(filePath, opts = {}) {
|
|||
return validateProgressContent(text, opts);
|
||||
}
|
||||
|
||||
// The real paths on both sides: a `file://` comparison fails when the path holds a
|
||||
// space or runs through a symlink, and the CLI then exits 0 having checked nothing.
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const filePath = args.find(a => !a.startsWith('--'));
|
||||
if (!filePath) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
// → JSON {valid, errors, warnings}; exit 0 valid, 1 invalid.
|
||||
|
||||
import { issue } from '../util/result.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
// WARN tier — absolute filesystem paths (leaks local directory layout).
|
||||
export const ABSOLUTE_PATH_PATTERNS = Object.freeze([
|
||||
|
|
@ -105,7 +106,7 @@ export function validateOutboundQuery(text, opts = {}) {
|
|||
|
||||
// ---- CLI shim ----------------------------------------------------------------
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const strict = !args.includes('--soft');
|
||||
const text = args.find(a => !a.startsWith('--'));
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|||
import { join } from 'node:path';
|
||||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
import { issue, fail } from '../util/result.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const RESEARCH_REQUIRED_FRONTMATTER = ['type', 'created', 'question'];
|
||||
export const RESEARCH_BODY_SECTIONS = ['Executive Summary', 'Dimensions'];
|
||||
|
|
@ -77,7 +78,7 @@ export function validateResearchDir(dirPath, opts = {}) {
|
|||
return { valid: errors.length === 0, errors, warnings, parsed: { files: results } };
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const strict = !args.includes('--soft');
|
||||
const dirIdx = args.indexOf('--dir');
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@
|
|||
// gate that decides on the verdict is lib/review/verdict-gate.mjs, which never
|
||||
// reads a missing verdict as ALLOW.
|
||||
|
||||
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
import { issue, ok, fail } from '../util/result.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const REVIEW_REQUIRED_FRONTMATTER = [
|
||||
'type',
|
||||
|
|
@ -180,15 +180,7 @@ export function validateReview(filePath, opts = {}) {
|
|||
return { ...r, parsed: { ...r.parsed, filePath } };
|
||||
}
|
||||
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const strict = !args.includes('--soft');
|
||||
const filePath = args.find(a => !a.startsWith('--'));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { issue, fail } from '../util/result.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
export const SESSION_STATE_REQUIRED_TOP = [
|
||||
'schema_version',
|
||||
|
|
@ -98,7 +99,7 @@ export function validateSessionState(filePath, opts = {}) {
|
|||
return validateSessionStateContent(text, opts);
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const filePath = args.find(a => !a.startsWith('--'));
|
||||
if (!filePath) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue