fix(plan): an open MAJOR stops the plan; after the last round the operator decides
voyage-lekerepo trial (2026-10-06) point 6, operator decision 2026-10-08 («samme skala, og du spør»): the critics rated a non-discriminating test minor in round 3, there was no way to fix it after the last round, it was built, and /trekreview called the same weakness MAJOR (MISSING_TEST). - plan-critic, scope-guardian: rate on the review catalogue's tiers; a test that cannot fail on a wrong implementation is major, never minor. - plan-gate: an open MAJOR stops (PLAN_GATE_OPEN_MAJOR: revise and run the next round; PLAN_GATE_OPEN_MAJOR_FINAL after the last round: ask the operator). --accept-open records "build as is" after their yes, bound to the round and the plan hash; never for a blocker. - trekplan Phase 9: REVISE after the last round asks fix now (one extra round) or build as is, instead of carrying the majors silently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1b5218e8f4
commit
2d29966f51
7 changed files with 243 additions and 16 deletions
|
|
@ -251,9 +251,19 @@ Rate each finding:
|
|||
- **Major** — high risk of bugs, rework, or failure
|
||||
- **Minor** — worth fixing but won't derail the implementation
|
||||
|
||||
**The review's scale, not a softer one** (operator decision 2026-10-08).
|
||||
`/trekreview` judges the built code with the tiers in
|
||||
`lib/review/rule-catalogue.mjs`; a weakness rated lower here comes back MAJOR
|
||||
there, after it is built. So: a step's test that cannot fail on a wrong
|
||||
implementation (it passes for the wrong cut, the wrong value, an empty
|
||||
result), or a behaviour the brief names with no test, is **major** — the tier
|
||||
of `MISSING_TEST` — never minor. Whenever a finding is the same class as a
|
||||
catalogue rule, use that rule's tier.
|
||||
|
||||
Your rating decides what happens next: `/trekplan` records your findings with
|
||||
`plan-gate.mjs --record`, and the plan gate stops the plan while any BLOCKER
|
||||
is open. A finding with no valid severity counts as a blocker. You may be run
|
||||
or MAJOR is open — a MAJOR left after the last round goes to the operator
|
||||
(fix now, or build as is). A finding with no valid severity counts as a blocker. You may be run
|
||||
up to 3 times on successive revisions of the same plan; judge the plan in
|
||||
front of you, and do not repeat a finding the revision has fixed.
|
||||
|
||||
|
|
|
|||
|
|
@ -85,8 +85,17 @@ Evaluate:
|
|||
Intent, Goal or Success Criteria? That cannot be settled in the plan: it is a
|
||||
**blocker** until the brief is re-run through `/trekbrief`.
|
||||
|
||||
**The review's scale, not a softer one** (operator decision 2026-10-08).
|
||||
`/trekreview` judges the built code with the tiers in
|
||||
`lib/review/rule-catalogue.mjs`; a weakness rated lower here comes back MAJOR
|
||||
there, after it is built. So: a step's test that cannot fail on a wrong
|
||||
implementation (it passes for the wrong cut, the wrong value, an empty
|
||||
result), or a behaviour the brief names with no test, is **major** — the tier
|
||||
of `MISSING_TEST` — never minor. Whenever a finding is the same class as a
|
||||
catalogue rule, use that rule's tier.
|
||||
|
||||
Your findings go through `plan-gate.mjs --record` with plan-critic's; an open
|
||||
blocker stops the plan. `severity` is exactly `blocker`, `major` or `minor`;
|
||||
blocker or major stops the plan. `severity` is exactly `blocker`, `major` or `minor`;
|
||||
an unrated finding counts as a blocker.
|
||||
|
||||
## Output format
|
||||
|
|
|
|||
|
|
@ -910,8 +910,16 @@ duplicates. Source: research/05 R1 + R2.
|
|||
- **PASS** (minors only, or clean): fill in the quality header and
|
||||
`## Plan Quality Score` from plan-critic's score. These and `## Revisions`
|
||||
are the review log and do not stale the verdict. Go to Phase 10.
|
||||
- **REVISE after the last round**: do not edit the plan again (that would
|
||||
stale the verdict); carry the open majors into the Phase 10 summary as known.
|
||||
- **REVISE after the last round**: the gate reports
|
||||
`PLAN_GATE_OPEN_MAJOR_FINAL`. Stop and ask the operator with
|
||||
AskUserQuestion — each open major in plain words, recommendation first
|
||||
(operator decision 2026-10-08):
|
||||
- **Fix now** — revise the plan for the listed majors, then record one more
|
||||
round with `--extra-round` (only after this yes; one yes = one round).
|
||||
- **Build as is** — after this yes, record it:
|
||||
`node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-gate.mjs --accept-open "{plan_path}"`.
|
||||
The acceptance is bound to this round and this plan text; the majors stay
|
||||
listed in the Phase 10 summary. A blocker is never accepted.
|
||||
|
||||
**After round 3 with a BLOCKER open**, `plan-gate.mjs` reports
|
||||
`PLAN_GATE_ROUNDS_EXHAUSTED` and `--record` refuses a fourth round. Stop and ask
|
||||
|
|
@ -929,8 +937,9 @@ Then check the plan with the gate (K3), which every later step relies on:
|
|||
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-gate.mjs --json "{plan_path}"
|
||||
```
|
||||
|
||||
Exit 0 = the plan validates, the verdict matches it, and no BLOCKER is open.
|
||||
Exit 1 = stop; its `reasons` say why. Exit 2 = it could not read the plan.
|
||||
Exit 0 = the plan validates, the verdict matches it, no BLOCKER is open, and
|
||||
every open MAJOR was accepted by the operator. Exit 1 = stop; its `reasons`
|
||||
say why. Exit 2 = it could not read the plan.
|
||||
|
||||
Borrowed, with credit: the bounded review loop that ends only on a recorded
|
||||
verdict, never on a claim in prose, from Kiro's `design-loop` workflow pattern
|
||||
|
|
@ -974,7 +983,7 @@ Present a summary to the user:
|
|||
**Critic rounds:** {N} of {max} — last verdict {BLOCK | REVISE | PASS}
|
||||
**Plan critic:** {Summary — blockers/majors/minors found, how addressed}
|
||||
**Scope guardian:** {Summary — creep/gaps found, how addressed}
|
||||
**Open majors:** {list, or "none"}
|
||||
**Open majors:** {list with "accepted by the operator" for each, or "none"}
|
||||
**Plan gate (K3):** {PASS | STOP — the reasons}
|
||||
|
||||
You can:
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ The strongest validation in the entire pipeline. Phase 5.5 (planning-orchestrato
|
|||
| `PLAN_MODULE_MAP_UNPARSED` | 1.8 architectural: a module-map line that looks like a link but gives no edge | Write it as `a --> b` |
|
||||
| `PLAN_ARCH_RULE_NO_ENFORCER` / `PLAN_ARCH_RULE_NO_PROOF` | 1.8 architectural: a rule without an enforcer, or a checked rule without an existing proving step | Name the enforcer; name the step |
|
||||
|
||||
**The plan gate (K3).** `node lib/validators/plan-gate.mjs <plan.md> [--json]` decides whether a plan may go on: **0** = it validates strict, its `## Critic Verdict` matches the current text, and no BLOCKER is open (open majors are warnings); **1** = stop, with `reasons` (`PLAN_GATE_INVALID_PLAN`, `PLAN_GATE_VERSION` — below plan_version 1.8, `PLAN_GATE_NO_VERDICT`, `PLAN_GATE_VERDICT_MALFORMED`, `PLAN_GATE_VERDICT_STALE`, `PLAN_GATE_OPEN_BLOCKER`, `PLAN_GATE_ROUNDS_EXHAUSTED`) and `warnings` (`PLAN_GATE_OPEN_MAJOR`); **2** = could not read the plan. `--hash <plan.md>` prints the operative hash; Phase 9 takes it before the reviewers read the plan. `--record <plan.md> --reviewed <hash>` reads plan-review-dedup output on stdin (which lists the reviewers that delivered, `sources`) and writes the next round; exit 2 when a reviewer is missing or the plan changed since `--hash`; it refuses round 4 (exit 1, stop and ask) unless `--extra-round` follows the operator's yes, and refuses that flag before the cap is spent. Unrated findings count as blockers. The review log is excluded from the hash only up to its first other heading, so a step placed under it is hashed. The verdict is a trace of the review, not a signature.
|
||||
**The plan gate (K3).** `node lib/validators/plan-gate.mjs <plan.md> [--json]` decides whether a plan may go on: **0** = it validates strict, its `## Critic Verdict` matches the current text, no BLOCKER is open, and every open MAJOR was accepted by the operator; **1** = stop, with `reasons` (`PLAN_GATE_INVALID_PLAN`, `PLAN_GATE_VERSION` — below plan_version 1.8, `PLAN_GATE_NO_VERDICT`, `PLAN_GATE_VERDICT_MALFORMED`, `PLAN_GATE_VERDICT_STALE`, `PLAN_GATE_OPEN_BLOCKER`, `PLAN_GATE_ROUNDS_EXHAUSTED`, `PLAN_GATE_OPEN_MAJOR` — rounds left: revise, `PLAN_GATE_OPEN_MAJOR_FINAL` — after the last round: ask the operator) and `warnings` (`PLAN_GATE_OPEN_MAJOR` for majors the operator accepted); **2** = could not read the plan. `--hash <plan.md>` prints the operative hash; Phase 9 takes it before the reviewers read the plan. `--record <plan.md> --reviewed <hash>` reads plan-review-dedup output on stdin (which lists the reviewers that delivered, `sources`) and writes the next round; exit 2 when a reviewer is missing or the plan changed since `--hash`; it refuses round 4 (exit 1, stop and ask) unless `--extra-round` follows the operator's yes, and refuses that flag before the cap is spent. Unrated findings count as blockers. The review log is excluded from the hash only up to its first other heading, so a step placed under it is hashed. `--accept-open <plan.md>` records the operator's "build as is" for the current round's open MAJORs (`accepted_open` in the verdict block, bound to the round and the plan hash; never for a BLOCKER; exit 1 when stale, a blocker is open or no major is). Operator decision 2026-10-08: open majors used to be warnings, and a weakness the critics named was built and came back from `/trekreview` as MAJOR. plan-critic and scope-guardian rate on the review catalogue's tiers. The verdict is a trace of the review, not a signature.
|
||||
|
||||
**Verification CLIs that read a plan** (0/1/2, JSON on stdout, 2 is never green): `lib/verification/arch-rules.mjs --repo <dir>` runs the target repo's own dependency-cruiser or import-linter (installs nothing; `--prove --rule <name> --plant <file>=<dest>` shows pass → fail on that rule → pass, refusing a destination that exists or resolves outside the repo); `lib/verification/plan-scope-sweep.mjs --plan <plan.md> --base <ref>` lists changed files no step names (`Files:`, `Test first:` File, manifest `expected_paths`) and no process path covers.
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
// 0 = the plan may go on
|
||||
// 1 = stop: invalid plan (plan-validator --strict, plan_version >= 1.8),
|
||||
// no critic verdict, a malformed verdict, a verdict the plan has
|
||||
// moved away from (stale), or an open BLOCKER
|
||||
// moved away from (stale), an open BLOCKER, or an open MAJOR the
|
||||
// operator has not accepted
|
||||
// 2 = could not run (missing/unreadable plan, bad arguments)
|
||||
//
|
||||
// node plan-gate.mjs --hash <plan.md>
|
||||
|
|
@ -20,6 +21,20 @@
|
|||
// the plan changed since --hash (the verdict would bless unreviewed text),
|
||||
// an unreadable plan or a failed write. Nothing is written on 1 or 2.
|
||||
//
|
||||
// node plan-gate.mjs --accept-open <plan.md>
|
||||
// After the operator's explicit yes to "build as is": records the open
|
||||
// MAJORs of the current round as accepted (`accepted_open` in the verdict
|
||||
// block, bound to the round and the plan hash). 0 = written; 1 = refused
|
||||
// (a BLOCKER is open, no MAJOR is open, or the verdict is stale); 2 = could
|
||||
// not run.
|
||||
//
|
||||
// Open MAJORs (operator decision 2026-10-08, voyage-lekerepo trial point 6):
|
||||
// they used to pass as warnings, and after the last round there was no way to
|
||||
// fix one — a weakness the critics had named was built and came back from the
|
||||
// review as MAJOR. Now an open MAJOR stops: with rounds left, revise and run
|
||||
// the next round; after the last one, ask the operator — fix now (one extra
|
||||
// round) or build as is (`--accept-open`).
|
||||
//
|
||||
// Where the verdict lives: INSIDE the plan, as the `## Critic Verdict` section
|
||||
// with one fenced json block. Claude Code's plan approval hands a hook the
|
||||
// plan file's text (`tool_input.plan`, `planFilePath`) and nothing next to
|
||||
|
|
@ -231,6 +246,43 @@ export function recordVerdict(text, dedup, opts = {}) {
|
|||
return { code: 0, text: placeSection(text, renderSection(verdict)), verdict, message: `round ${round} recorded: ${verdict.verdict}` };
|
||||
}
|
||||
|
||||
const sevOf = f => normalizeSeverity(f.severity);
|
||||
const majorKey = f => `${f.id ?? ''}\u0000${f.text ?? ''}`;
|
||||
|
||||
/** The verdict's acceptance names this round, this plan text and every open MAJOR. */
|
||||
function acceptanceCovers(v, majors) {
|
||||
const a = v.accepted_open;
|
||||
if (!a || typeof a !== 'object' || a.round !== v.round || a.plan_sha256 !== v.plan_sha256 || !Array.isArray(a.findings)) return false;
|
||||
const accepted = new Set(a.findings.map(majorKey));
|
||||
return majors.every(m => accepted.has(majorKey(m)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the operator's "build as is" for the current round's open MAJORs.
|
||||
* Only after their explicit yes; never for a BLOCKER.
|
||||
* @returns {{code: 0|1|2, text?: string, message: string}}
|
||||
*/
|
||||
export function acceptOpen(rawText, opts = {}) {
|
||||
const text = norm(rawText);
|
||||
const ex = extractVerdict(text);
|
||||
if (!ex.found || ex.error) return { code: 2, message: `no readable verdict: ${ex.error ?? 'no ## Critic Verdict'}` };
|
||||
const v = ex.verdict;
|
||||
if (v.plan_sha256 !== planHash(text)) return { code: 1, message: 'the verdict is stale: the plan changed after the last round; run the next round instead' };
|
||||
if (v.findings.some(f => sevOf(f) !== 'major' && sevOf(f) !== 'minor')) return { code: 1, message: 'a BLOCKER is open; a blocker is never accepted' };
|
||||
const majors = v.findings.filter(f => sevOf(f) === 'major');
|
||||
if (majors.length === 0) return { code: 1, message: 'no MAJOR is open; nothing to accept' };
|
||||
const accepted = {
|
||||
...v,
|
||||
accepted_open: {
|
||||
round: v.round,
|
||||
plan_sha256: v.plan_sha256,
|
||||
accepted_at: opts.now ?? new Date().toISOString(),
|
||||
findings: majors.map(m => ({ id: m.id ?? null, text: m.text ?? '' })),
|
||||
},
|
||||
};
|
||||
return { code: 0, text: placeSection(text, renderSection(accepted)), message: `${majors.length} open MAJOR(s) of round ${v.round} accepted` };
|
||||
}
|
||||
|
||||
/**
|
||||
* K3 — decide whether the plan may go on.
|
||||
* @returns {{code: 0|1, status: 'pass'|'stop', reasons: object[], warnings: object[], verdict: object|null, validation: object}}
|
||||
|
|
@ -267,7 +319,19 @@ export function gatePlan(rawText) {
|
|||
}
|
||||
}
|
||||
if (majors.length > 0) {
|
||||
warnings.push({ code: 'PLAN_GATE_OPEN_MAJOR', message: `${majors.length} MAJOR finding(s) left open in round ${v.round}; show them to the operator.` });
|
||||
const exhausted = v.round >= v.max_rounds + (v.extra_rounds ?? 0);
|
||||
const list = majors.map(m => m.text).join(' | ');
|
||||
if (acceptanceCovers(v, majors) && v.plan_sha256 === planHash(text)) {
|
||||
warnings.push({ code: 'PLAN_GATE_OPEN_MAJOR', message: `${majors.length} MAJOR finding(s) open in round ${v.round}, accepted by the operator (${v.accepted_open.accepted_at}): ${list}` });
|
||||
} else if (exhausted) {
|
||||
reasons.push({
|
||||
code: 'PLAN_GATE_OPEN_MAJOR_FINAL',
|
||||
message: `${majors.length} MAJOR finding(s) open after the last critic round (${v.round}): ${list}. ` +
|
||||
'Stop and ask the operator: fix now (revise, then --record --extra-round after their yes) or build as is (--accept-open after their yes).',
|
||||
});
|
||||
} else {
|
||||
reasons.push({ code: 'PLAN_GATE_OPEN_MAJOR', message: `${majors.length} MAJOR finding(s) open in round ${v.round}: ${list}. Revise the plan and run the next round.` });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { code: reasons.length === 0 ? 0 : 1, status: reasons.length === 0 ? 'pass' : 'stop', reasons, warnings, verdict: v ?? null, validation };
|
||||
|
|
@ -289,8 +353,8 @@ function out(obj) {
|
|||
|
||||
if (isMain()) {
|
||||
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';
|
||||
const known = new Set(['--json', '--hash', '--record', '--reviewed', '--extra-round']);
|
||||
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']);
|
||||
const rIdx = args.indexOf('--reviewed');
|
||||
const reviewed = rIdx >= 0 ? args[rIdx + 1] : undefined;
|
||||
const positional = args.filter((a, i) => !a.startsWith('--') && !(rIdx >= 0 && i === rIdx + 1));
|
||||
|
|
@ -313,6 +377,22 @@ if (isMain()) {
|
|||
process.stdout.write(`${planHash(text)}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
if (args.includes('--accept-open')) {
|
||||
const r = acceptOpen(text);
|
||||
if (r.code === 0) {
|
||||
const tmp = `${planPath}.plan-gate.tmp`;
|
||||
try {
|
||||
writeFileSync(tmp, r.text);
|
||||
renameSync(tmp, planPath);
|
||||
} catch (e) {
|
||||
rmSync(tmp, { force: true });
|
||||
out({ code: 2, accepted: false, message: `cannot write the plan: ${e.message}` });
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
out({ code: r.code, accepted: r.code === 0, message: r.message });
|
||||
process.exit(r.code);
|
||||
}
|
||||
if (args.includes('--record')) {
|
||||
let dedup;
|
||||
try { dedup = JSON.parse(readFileSync(0, 'utf-8')); } catch (e) {
|
||||
|
|
|
|||
109
tests/validators/plan-gate-open-major.test.mjs
Normal file
109
tests/validators/plan-gate-open-major.test.mjs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// tests/validators/plan-gate-open-major.test.mjs
|
||||
// Operator decision 2026-10-08 (voyage-lekerepo trial point 6, «samme skala,
|
||||
// og du spør»). In the trial the critics marked a non-discriminating test as
|
||||
// minor in round 3; with no round left there was no way to fix it, it was
|
||||
// built, and the review called the same weakness MAJOR (MISSING_TEST). Now:
|
||||
// - plan-critic and scope-guardian rate on the review's scale (a test that
|
||||
// cannot fail on a wrong implementation is MAJOR, as MISSING_TEST is);
|
||||
// - an open MAJOR stops the gate: with rounds left the loop revises; after
|
||||
// the last round the operator is asked — fix now (one extra round) or
|
||||
// build as is, recorded by `--accept-open` after their yes;
|
||||
// - an acceptance is bound to the round and the plan text it was given for.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gatePlan, recordVerdict, planHash, acceptOpen, MAX_ROUNDS } from '../../lib/validators/plan-gate.mjs';
|
||||
import { buildPlan } from '../helpers/plan-v18-builder.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const GATE = join(ROOT, 'lib', 'validators', 'plan-gate.mjs');
|
||||
const BOTH = [{ agent: 'plan-critic', present: true }, { agent: 'scope-guardian', present: true }];
|
||||
const finding = (severity, text) => ({ id: `${severity}-${text.length}`, file: 'plan.md', line: 3, rule_key: 'test-quality', severity, text, raised_by: ['plan-critic'] });
|
||||
const dedup = (...f) => ({ findings: f, sources: BOTH });
|
||||
const rec = (text, d, opts = {}) => recordVerdict(text, d, { reviewed: planHash(text), ...opts });
|
||||
const codes = (list) => list.map(x => x.code);
|
||||
|
||||
function lastRound(...f) {
|
||||
let text = buildPlan();
|
||||
for (let i = 1; i < MAX_ROUNDS; i++) text = rec(text, dedup()).text;
|
||||
const r = rec(text, dedup(...f));
|
||||
assert.equal(r.code, 0, r.message);
|
||||
assert.equal(r.verdict.round, MAX_ROUNDS);
|
||||
return r.text;
|
||||
}
|
||||
|
||||
const WEAK = 'Step 2 test passes for any truncation at a hyphen: it cannot fail on the wrong cut';
|
||||
|
||||
test('an open MAJOR with rounds left stops and says: revise and run the next round', () => {
|
||||
const g = gatePlan(rec(buildPlan(), dedup(finding('major', WEAK))).text);
|
||||
assert.equal(g.code, 1);
|
||||
const r = g.reasons.find(x => x.code === 'PLAN_GATE_OPEN_MAJOR');
|
||||
assert.ok(r, codes(g.reasons).join(','));
|
||||
assert.match(r.message, /next round/);
|
||||
});
|
||||
|
||||
test('an open MAJOR after the last round stops and says: ask the operator, fix now or build as is', () => {
|
||||
const g = gatePlan(lastRound(finding('major', WEAK)));
|
||||
assert.equal(g.code, 1);
|
||||
const r = g.reasons.find(x => x.code === 'PLAN_GATE_OPEN_MAJOR_FINAL');
|
||||
assert.ok(r, codes(g.reasons).join(','));
|
||||
assert.match(r.message, /ask the operator/);
|
||||
assert.match(r.message, /--extra-round/);
|
||||
assert.match(r.message, /--accept-open/);
|
||||
});
|
||||
|
||||
test('--accept-open after the operator\'s yes: the gate passes, the accepted majors stay listed as a warning', () => {
|
||||
const text = lastRound(finding('major', WEAK), finding('minor', 'typo'));
|
||||
const a = acceptOpen(text, { now: '2026-10-08T07:00:00.000Z' });
|
||||
assert.equal(a.code, 0, a.message);
|
||||
assert.equal(planHash(a.text), planHash(text), 'the acceptance is review log: it does not stale the verdict');
|
||||
const g = gatePlan(a.text);
|
||||
assert.equal(g.code, 0, JSON.stringify(g.reasons));
|
||||
const w = g.warnings.find(x => x.code === 'PLAN_GATE_OPEN_MAJOR');
|
||||
assert.ok(w);
|
||||
assert.match(w.message, /accepted by the operator/);
|
||||
});
|
||||
|
||||
test('--accept-open never accepts a BLOCKER, and refuses when no MAJOR is open', () => {
|
||||
assert.equal(acceptOpen(lastRound(finding('blocker', 'Step 2 reads nothing'), finding('major', WEAK))).code, 1);
|
||||
assert.equal(acceptOpen(lastRound(finding('minor', 'typo'))).code, 1);
|
||||
});
|
||||
|
||||
test('an acceptance does not carry over to a later round', () => {
|
||||
const accepted = acceptOpen(lastRound(finding('major', WEAK))).text;
|
||||
const revised = accepted.replace('Add `run1()`', 'Add `run1()` and its error path');
|
||||
const next = rec(revised, dedup(finding('major', 'Step 1 error path untested')), { extraRound: true });
|
||||
assert.equal(next.code, 0, next.message);
|
||||
const g = gatePlan(next.text);
|
||||
assert.ok(codes(g.reasons).includes('PLAN_GATE_OPEN_MAJOR_FINAL'), codes(g.reasons).join(','));
|
||||
});
|
||||
|
||||
test('CLI: --accept-open writes the acceptance into the plan and exits 0', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'plan-accept-'));
|
||||
try {
|
||||
const p = join(dir, 'plan.md');
|
||||
writeFileSync(p, lastRound(finding('major', WEAK)));
|
||||
assert.equal(spawnSync(process.execPath, [GATE, p]).status, 1);
|
||||
const a = spawnSync(process.execPath, [GATE, '--accept-open', p], { encoding: 'utf8' });
|
||||
assert.equal(a.status, 0, a.stdout + a.stderr);
|
||||
assert.match(readFileSync(p, 'utf8'), /"accepted_open"/);
|
||||
assert.equal(spawnSync(process.execPath, [GATE, p]).status, 0);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('prose: critics rate a test weakness on the review\'s scale; Phase 9 asks after the last round', () => {
|
||||
for (const a of ['agents/plan-critic.md', 'agents/scope-guardian.md']) {
|
||||
assert.match(readFileSync(join(ROOT, a), 'utf8'), /cannot fail on a wrong\s+implementation[\s\S]{0,200}\*\*major\*\*/, a);
|
||||
}
|
||||
const plan = readFileSync(join(ROOT, 'commands', 'trekplan.md'), 'utf8');
|
||||
assert.match(plan, /plan-gate\.mjs --accept-open/);
|
||||
assert.doesNotMatch(plan, /carry the open majors into the Phase 10 summary as known/);
|
||||
});
|
||||
|
|
@ -55,14 +55,24 @@ test('K3: a plan with an open critic BLOCKER exits 1', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('K3: a valid plan whose latest verdict has no blocker exits 0 (majors are warnings)', () => {
|
||||
// Operator decision 2026-10-08 (voyage-lekerepo trial point 6): an open MAJOR
|
||||
// no longer passes as a warning. It stops until it is fixed or the operator
|
||||
// accepts it (tests/validators/plan-gate-open-major.test.mjs).
|
||||
test('K3: a valid plan whose latest verdict has an open MAJOR stops (fix it, or the operator accepts it)', () => {
|
||||
const r0 = rec(buildPlan(), dedupOut(finding('major', 'Step 1 lacks an error path'), finding('minor', 'typo')));
|
||||
withPlan(r0.text, (p) => {
|
||||
const r = run([p, '--json']);
|
||||
assert.equal(r.code, 0, r.out + r.err);
|
||||
assert.equal(r.code, 1, r.out + r.err);
|
||||
const j = JSON.parse(r.out);
|
||||
assert.equal(j.verdict.verdict, 'REVISE');
|
||||
assert.ok(j.warnings.some(w => w.code === 'PLAN_GATE_OPEN_MAJOR'));
|
||||
assert.ok(j.reasons.some(w => w.code === 'PLAN_GATE_OPEN_MAJOR'));
|
||||
});
|
||||
});
|
||||
|
||||
test('K3: a valid plan whose latest verdict has only minors exits 0', () => {
|
||||
const r0 = rec(buildPlan(), dedupOut(finding('minor', 'typo')));
|
||||
withPlan(r0.text, (p) => {
|
||||
assert.equal(run([p, '--json']).code, 0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -219,7 +229,7 @@ test('CLI: --hash, then --record --reviewed writes in place; malformed stdin or
|
|||
const ok = run(['--record', p, '--reviewed', hash], JSON.stringify(dedupOut(finding('major', 'm'))));
|
||||
assert.equal(ok.code, 0, ok.out + ok.err);
|
||||
assert.equal(JSON.parse(ok.out).verdict, 'REVISE');
|
||||
assert.equal(run([p]).code, 0);
|
||||
assert.equal(run([p]).code, 1, 'the recorded open MAJOR stops the gate (operator decision 2026-10-08)');
|
||||
assert.ok(!existsSync(join(dir, 'plan.md.plan-gate.tmp')));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue