From 3925e9b56db1bd8ab67a96800911459a3d2b1640 Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 27 Oct 2026 22:22:48 +0000 Subject: [PATCH] wip(GSTA-131): reviewers stay at full effort; audits light Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- bin/gstack-config | 6 +++--- bin/gstack-review-plan | 16 +++++++++------- docs/model-policy.md | 21 ++++++++++++--------- docs/review-policy.md | 23 ++++++++++++----------- lib/review-depth.ts | 25 +++++++++++-------------- scripts/review-dispatch-compare.ts | 3 ++- test/review-depth.test.ts | 25 +++++++++++++------------ test/review-plan-cli.test.ts | 10 +++++----- test/ship-receipt-carry-pregate.test.ts | 2 +- 9 files changed, 68 insertions(+), 63 deletions(-) diff --git a/bin/gstack-config b/bin/gstack-config index 187807d0d..6fff279f9 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -200,9 +200,9 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne # # Absent = every gate keeps its effort. GSTACK_CODEX_EFFORT # # overrides it per run, /codex --effort per request; # # /spec keeps medium (120 s budget). -# review_effort_tiers: on # on | off. on: read-and-report reviewers (specialists, -# # audits, fix-checks, watchers) run at light effort on -# # the same model; core and adversarial review keep full. +# review_effort_tiers: on # on | off. on: audits, trail reviewers and watchers +# # (nothing that can block a ship) run at light effort on +# # the same model; every reviewer keeps full. # Docs: docs/model-policy.md # # ─── Advanced ──────────────────────────────────────────────────────── diff --git a/bin/gstack-review-plan b/bin/gstack-review-plan index a51c6b787..16e557e4f 100755 --- a/bin/gstack-review-plan +++ b/bin/gstack-review-plan @@ -15,6 +15,7 @@ import { spawnSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { EXIT, exitTableHelp } from '../lib/headless-artifacts'; +import { runScriptSync } from '../lib/script-invocation'; import { planReview, reviewPlanLine, scanNumbers, type History } from '../lib/review-depth'; const HELP = `gstack-review-plan — the review depth of the next /ship step (docs/review-policy.md#review-depth) @@ -31,7 +32,7 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do --gated a,b specialists the hit-rate stats tag GATE_CANDIDATE --force a,b|all specialists forced by prompt flags (--all-specialists is all) --reviewed-numbers the numeric-claim fingerprint the outside voice already reviewed - --no-tiers every role at the host's default effort (as gstack-config review_effort_tiers off does) + --no-tiers audits and watchers at the host's default effort too (as gstack-config review_effort_tiers off does) --json the plan as JSON ${exitTableHelp()}`; @@ -70,30 +71,31 @@ function main(): number { } for (const p of untracked) { try { added[p] = fs.readFileSync(p, 'utf8').split('\n'); } catch { /* unreadable: no claims */ } } const binDir = path.dirname(Bun.fileURLToPath(import.meta.url)); - const scopeRun = spawnSync('bash', [path.join(binDir, 'gstack-diff-scope'), base], { encoding: 'utf8', timeout: 60_000 }); + const scopeRun = runScriptSync(path.join(binDir, 'gstack-diff-scope'), [base], { encoding: 'utf8', timeout: 60_000 }); const scope: Record = {}; for (const l of (scopeRun.stdout ?? '').split('\n')) { const m = /^(SCOPE_[A-Z_]+)=(.*)$/.exec(l); if (m) scope[m[1]!] = m[2]!; } if (scopeRun.status !== 0 && !scope.SCOPE_ERROR) scope.SCOPE_ERROR = `exit ${scopeRun.status}`; let gateTagged: number | null = null; if (flag('--gate-tagged') !== undefined) gateTagged = Number(flag('--gate-tagged')); else { - const g = spawnSync(path.join(binDir, 'gstack-gate-diff'), [base], { encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }); + const g = runScriptSync(path.join(binDir, 'gstack-gate-diff'), [base], { encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }); const m = /GATE_SUMMARY: .*?tagged=\{([^}]*)\}/.exec(g.stdout ?? ''); gateTagged = g.status === 0 && m ? m[1]!.split(',').reduce((n, kv) => n + (Number(kv.split(':')[1]) || 0), 0) : null; } if (gateTagged !== null && !Number.isFinite(gateTagged)) gateTagged = null; const force = flag('--force') === 'all' ? 'all' as const : list('--force'); - const tiersSetting = spawnSync(path.join(binDir, 'gstack-config'), ['get', 'review_effort_tiers'], { encoding: 'utf8', timeout: 15_000 }); - const tiers = !has('--no-tiers') && (tiersSetting.stdout ?? '').trim() !== 'off'; + const tiersSetting = runScriptSync(path.join(binDir, 'gstack-config'), ['get', 'review_effort_tiers'], { encoding: 'utf8', timeout: 15_000 }); + const auditEffort = has('--no-tiers') || (tiersSetting.stdout ?? '').trim() === 'off' ? 'host' : 'light'; const plan = planReview({ paths, deleted, stampOnly, scope, gateTagged, diffLines, numbers: scanNumbers(added), force, gated: list('--gated'), - criticalFound: has('--critical-found'), reviewedNumbers: flag('--reviewed-numbers'), tiers, + criticalFound: has('--critical-found'), reviewedNumbers: flag('--reviewed-numbers'), history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: has('--fixed-below'), blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') }, }); - if (has('--json')) { process.stdout.write(JSON.stringify({ ...plan, diff_lines: diffLines, pass, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly }) + '\n'); return EXIT.ok; } + if (has('--json')) { process.stdout.write(JSON.stringify({ ...plan, diff_lines: diffLines, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly }) + '\n'); return EXIT.ok; } const out = [`REVIEW_CLASS: ${plan.class} (${plan.reasons.slice(0, 3).join('; ') || 'no signals'})`, reviewPlanLine(plan, pass + (plan.next.step === 'full' ? 1 : 0)), `NEXT: ${plan.next.step} (${plan.next.reason})`]; for (const d of plan.dispatches) out.push(`DISPATCH: ${d.role} ${d.name} effort=${d.effort}`); for (const s of plan.skipped) out.push(`SKIP: ${s}`); + out.push(`AUDIT_EFFORT: ${auditEffort} (coverage, plan-completion and docs audits, decision-trail reviewers, watchers; reviewers above stay full)`); process.stdout.write(out.join('\n') + '\n'); return EXIT.ok; } diff --git a/docs/model-policy.md b/docs/model-policy.md index 459c338fb..2c40f0ba4 100644 --- a/docs/model-policy.md +++ b/docs/model-policy.md @@ -156,15 +156,18 @@ set, because a higher effort would spend the time the gate is sized for. ### Review effort tiers -Subagents that only read and report run at a lighter reasoning effort on the -same model; gstack never swaps in a cheaper model family for them. -`gstack-review-plan` names each dispatch's tier: `light` for specialists, -audits, fix-checks, decision-trail reviewers and watchers, `full` for the core -checklist, the Red Team and the native adversarial pass, `host` for the outside -voice (its provider and `codex_effort` above). A host that can set a -subagent's effort maps `light` to a medium or low setting; one that cannot runs -it at its default. `gstack-config set review_effort_tiers off` (default `on`) -renders every role at the host default +Every reviewer that can block a ship runs at `full` effort on the lane's +model: the core checklist, each specialist, the Red Team, the native +adversarial pass and the fix-check. `gstack-review-plan` names each +dispatch's effort, and `host` for the outside voice (its provider and +`codex_effort` above). Only work that cannot block runs `light`, the same +model at a medium or low setting and never a cheaper model family: the coverage, +plan-completion and docs audits, decision-trail reviewers and watchers +(`AUDIT_EFFORT:` in the plan's output). A paid replay showed why reviewers stay +at full: a medium-effort fix-check missed a fail-open in gate code that the same +check at full effort caught. A host that cannot set a subagent's effort runs it +at its default. `gstack-config set review_effort_tiers off` (default `on`) +runs the audits and watchers at the host default too ([review-policy.md](review-policy.md#review-depth)). ## Which workflows use the tiers? diff --git a/docs/review-policy.md b/docs/review-policy.md index 5eabc1618..81095755a 100644 --- a/docs/review-policy.md +++ b/docs/review-policy.md @@ -111,13 +111,13 @@ incomplete coverage, which blocks until the reviewer is rerun. | a full pass that fixed a blocking finding, below the cap | full pass on the range | | a full pass whose fix touched a gate-class path, below the cap | full pass on the range | | a full pass that fixed only below-floor findings | one fix-check | -| a full pass at the cap that fixed a blocking finding | one fix-check (full effort when the fix touched gate code) | +| a full pass at the cap that fixed a blocking finding | one fix-check | | a full pass at the cap, or with no fix, leaving a blocking finding open | stop: name it for the owner | | a fix-check that found nothing blocking | done | | a fix-check that found a blocking finding | stop: name it for the owner | A **fix-check** is one reviewer reading the touched hunks, the fix list, the -test results and the callers of every changed symbol, at light effort. It is +test results and the callers of every changed symbol, at full effort. It is logged as a `review-fix-check` row, so it never counts as a pass. No army, no Red Team and no outside voice run on it. @@ -148,15 +148,16 @@ outside voice reruns, on the numbers. **The adversarial and outside voices run once**, on pass 1's diff (Step 11). Later full passes rerun the specialists on the range only. -**Effort tiers.** Reviewers that only read and report (specialists, the Step 7 -coverage audit, the Step 8 plan-completion audit, the Step 14.5 docs audit, -fix-checks, decision-trail reviewers, watchers) run at `effort: light`: the -same model at a lower reasoning effort, never a cheaper model family. The core -checklist, the Red Team and the native adversarial pass stay at `full`. The -outside voice keeps its own provider and effort (`codex_effort`). A host that -cannot set a subagent's effort runs it at its default. -`gstack-config set review_effort_tiers off` (or `--no-tiers`) renders every -role at the host default ([model-policy.md](model-policy.md#reasoning-effort)). +**Effort.** Every reviewer that can block a ship (the core checklist, +specialists, the Red Team, the native adversarial pass, fix-checks) runs at +`full` effort. Work that cannot block (the Step 7 coverage audit, the Step 8 +plan-completion audit, the Step 14.5 docs audit, decision-trail reviewers, +watchers) runs at `light`: the same model at a lower reasoning effort, never a +cheaper model family. The outside voice keeps its own provider and effort +(`codex_effort`). A host that cannot set a subagent's effort runs it at its +default. `gstack-config set review_effort_tiers off` (or `--no-tiers`) runs +the audits and watchers at the host default too +([model-policy.md](model-policy.md#reasoning-effort)). ## What the receipt records diff --git a/lib/review-depth.ts b/lib/review-depth.ts index 0e1621c5b..10f510093 100644 --- a/lib/review-depth.ts +++ b/lib/review-depth.ts @@ -112,7 +112,7 @@ export interface History { /** A fix in the finished step touched a gate-class path. */ touchedGate?: boolean; } -export interface NextStep { step: Step; reason: string; effort: Effort } +export interface NextStep { step: Step; reason: string } /** * The transition table (docs/review-policy.md#transitions). Pass 1 is full. A fixed @@ -121,13 +121,13 @@ export interface NextStep { step: Step; reason: string; effort: Effort } * allowed step stops for the owner. */ export function nextStep(h: History): NextStep { - if (h.last === 'none') return { step: 'full', reason: 'pass 1 reviews the whole diff', effort: 'full' }; + if (h.last === 'none') return { step: 'full', reason: 'pass 1 reviews the whole diff' }; const fixed = !!(h.fixedBlocking || h.fixedBelow); - if (h.last === 'fix-check') return h.blockingOpen || fixed ? { step: 'stop', reason: 'the fix-check found a blocking finding: name it for the owner, never a third full pass', effort: 'full' } : { step: 'done', reason: 'the fix-check found nothing blocking', effort: 'full' }; - if (h.blockingOpen && h.fullPasses >= FULL_PASS_CAP) return { step: 'stop', reason: `a blocking finding is open after ${FULL_PASS_CAP} full passes: name it for the owner`, effort: 'full' }; - if (!fixed) return h.blockingOpen ? { step: 'stop', reason: 'a blocking finding is open with no fix applied', effort: 'full' } : { step: 'done', reason: 'the step applied no edits', effort: 'full' }; - if ((h.fixedBlocking || h.touchedGate) && h.fullPasses < FULL_PASS_CAP) return { step: 'full', reason: h.fixedBlocking ? 'a blocking finding was fixed: full pass on the range' : 'a fix touched a gate-class path: full pass on the range', effort: 'full' }; - return { step: 'fix-check', reason: h.fixedBlocking ? `full-pass cap ${FULL_PASS_CAP} reached: one fix-check reads the fix` : 'only below-floor fixes: one fix-check reads the touched hunks', effort: h.touchedGate ? 'full' : 'light' }; + if (h.last === 'fix-check') return h.blockingOpen || fixed ? { step: 'stop', reason: 'the fix-check found a blocking finding: name it for the owner, never a third full pass' } : { step: 'done', reason: 'the fix-check found nothing blocking' }; + if (h.blockingOpen && h.fullPasses >= FULL_PASS_CAP) return { step: 'stop', reason: `a blocking finding is open after ${FULL_PASS_CAP} full passes: name it for the owner` }; + if (!fixed) return h.blockingOpen ? { step: 'stop', reason: 'a blocking finding is open with no fix applied' } : { step: 'done', reason: 'the step applied no edits' }; + if ((h.fixedBlocking || h.touchedGate) && h.fullPasses < FULL_PASS_CAP) return { step: 'full', reason: h.fixedBlocking ? 'a blocking finding was fixed: full pass on the range' : 'a fix touched a gate-class path: full pass on the range' }; + return { step: 'fix-check', reason: h.fixedBlocking ? `full-pass cap ${FULL_PASS_CAP} reached: one fix-check reads the fix` : 'only below-floor fixes: one fix-check reads the touched hunks' }; } // --------------------------------------------------------------------------- @@ -145,8 +145,6 @@ export interface PlanInput extends ClassInput { criticalFound?: boolean; /** The numeric-claim fingerprint the outside voice last reviewed; a change reruns it. */ reviewedNumbers?: string; - /** review_effort_tiers: off renders every role at the host's default effort. */ - tiers?: boolean; } export interface Dispatch { role: Role; name: string; effort: Effort } export interface ReviewPlan { class: DiffClass; reasons: string[]; next: NextStep; numbers: NumbersResult; dispatches: Dispatch[]; skipped: string[] } @@ -182,18 +180,17 @@ export function specialistsFor(cls: DiffClass, i: PlanInput, skipped: string[] = export function planReview(i: PlanInput): ReviewPlan { const { class: cls, reasons } = classifyDiff(i); const next = nextStep(i.history); - const tier = (e: Effort): Effort => (i.tiers === false ? 'host' : e); const skipped: string[] = []; const dispatches: Dispatch[] = []; const numbersChanged = i.numbers.publishes && i.reviewedNumbers !== undefined && i.reviewedNumbers !== i.numbers.fingerprint; if (next.step === 'full') { - for (const n of specialistsFor(cls, i, skipped)) dispatches.push({ role: 'specialist', name: n, effort: tier('light') }); + for (const n of specialistsFor(cls, i, skipped)) dispatches.push({ role: 'specialist', name: n, effort: 'full' }); const redTeam = cls === 'gate' || (cls === 'code' && (i.diffLines > RED_TEAM_LINES || !!i.criticalFound)); - if (redTeam) dispatches.push({ role: 'red-team', name: 'red-team', effort: tier('full') }); + if (redTeam) dispatches.push({ role: 'red-team', name: 'red-team', effort: 'full' }); else skipped.push(`red-team (${cls} class${cls === 'code' ? `, ${i.diffLines} lines, no critical` : ''})`); if (i.history.fullPasses === 0) { if (cls === 'docs' || cls === 'tests') skipped.push(`adversarial-native (${cls} class)`); - else dispatches.push({ role: 'adversarial-native', name: 'adversarial', effort: tier('full') }); + else dispatches.push({ role: 'adversarial-native', name: 'adversarial', effort: 'full' }); if (cls === 'code' || cls === 'gate' || i.numbers.publishes) dispatches.push({ role: 'outside-voice', name: 'outside-voice', effort: 'host' }); else skipped.push(`outside-voice (${cls} class, no numeric claims)`); } else { @@ -201,7 +198,7 @@ export function planReview(i: PlanInput): ReviewPlan { if (numbersChanged) dispatches.push({ role: 'outside-voice', name: 'outside-voice (numbers changed)', effort: 'host' }); } } else if (next.step === 'fix-check') { - dispatches.push({ role: 'fix-check', name: 'fix-check', effort: tier(next.effort) }); + dispatches.push({ role: 'fix-check', name: 'fix-check', effort: 'full' }); if (numbersChanged) dispatches.push({ role: 'outside-voice', name: 'outside-voice (numbers changed)', effort: 'host' }); } return { class: cls, reasons, next, numbers: i.numbers, dispatches, skipped }; diff --git a/scripts/review-dispatch-compare.ts b/scripts/review-dispatch-compare.ts index eaf3c73c8..8977aa832 100755 --- a/scripts/review-dispatch-compare.ts +++ b/scripts/review-dispatch-compare.ts @@ -16,6 +16,7 @@ import { spawnSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; +import { runScriptSync } from '../lib/script-invocation'; import { planReview, specialistsFor, type DiffClass, type Dispatch, type History, type NumbersResult } from '../lib/review-depth'; const ROOT = path.resolve(import.meta.dir, '..'); @@ -113,7 +114,7 @@ function capture(sha: string, name: string, out: string): number { try { if (git(['worktree', 'add', '-q', '--detach', dir, sha]).status !== 0) throw new Error(`cannot check out ${sha}`); git(['branch', '-f', 'gsta131-capture-base', `${sha}^`], dir); - const r = spawnSync(path.join(ROOT, 'bin', 'gstack-review-plan'), ['--base', 'gsta131-capture-base', '--json'], { cwd: dir, encoding: 'utf8', timeout: 300_000, maxBuffer: 64 * 1024 * 1024 }); + const r = runScriptSync(path.join(ROOT, 'bin', 'gstack-review-plan'), ['--base', 'gsta131-capture-base', '--json'], { cwd: dir, encoding: 'utf8', timeout: 300_000, maxBuffer: 64 * 1024 * 1024 }); if (r.status !== 0) throw new Error(`gstack-review-plan failed: ${r.stderr}`); const plan = JSON.parse(r.stdout); const diffBytes = Buffer.byteLength(git(['diff', `${sha}^`, sha], dir).stdout ?? ''); diff --git a/test/review-depth.test.ts b/test/review-depth.test.ts index 7206ed294..36db2571f 100644 --- a/test/review-depth.test.ts +++ b/test/review-depth.test.ts @@ -35,10 +35,10 @@ describe('nextStep: the transition table', () => { test('pass 1 is full', () => expect(nextStep({ fullPasses: 0, last: 'none' }).step).toBe('full')); test('no edits is done', () => expect(nextStep({ fullPasses: 1, last: 'full' }).step).toBe('done')); test('a fixed blocker earns full pass 2', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBlocking: true }).step).toBe('full')); - test('below-floor fixes earn one light fix-check', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true })).toMatchObject({ step: 'fix-check', effort: 'light' })); + test('below-floor fixes earn one fix-check', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true }).step).toBe('fix-check')); test('a fix touching gate code earns a full pass instead of a fix-check', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true, touchedGate: true }).step).toBe('full')); test('at the full-pass cap a fixed blocker gets a fix-check', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true }).step).toBe('fix-check')); - test('at the cap a gate-touching fix gets a full-effort fix-check', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true, touchedGate: true })).toMatchObject({ step: 'fix-check', effort: 'full' })); + test('at the cap a gate-touching fix still gets a fix-check, never a third full pass', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true, touchedGate: true }).step).toBe('fix-check')); test('a blocker open at the cap stops', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', blockingOpen: true }).step).toBe('stop')); test('a blocker open with no fix stops', () => expect(nextStep({ fullPasses: 1, last: 'full', blockingOpen: true }).step).toBe('stop')); test('a fix-check that finds a blocker stops, never a third full pass', () => { @@ -73,19 +73,19 @@ describe('planReview', () => { }); test('tests class: testing specialist only', () => { const p = planReview(base({ paths: ['test/a.test.ts'], scope: { SCOPE_TESTS: 'true' } })); - expect(roles(p)).toEqual(['specialist:testing:light']); + expect(roles(p)).toEqual(['specialist:testing:full']); }); - test('code class pass 1: specialists light, adversarial full, outside voice', () => { + test('code class pass 1: every reviewer at full effort, the outside voice at its own', () => { const p = planReview(base({})); - expect(roles(p)).toEqual(['specialist:testing:light', 'specialist:maintainability:light', 'specialist:security:light', 'specialist:performance:light', 'specialist:simplification:light', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']); + expect(roles(p)).toEqual(['specialist:testing:full', 'specialist:maintainability:full', 'specialist:security:full', 'specialist:performance:full', 'specialist:simplification:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']); }); test('gate class forces red team and ignores hit-rate gating', () => { const p = planReview(base({ gateTagged: 1, gated: ['security'] })); expect(roles(p)).toContain('red-team:red-team:full'); - expect(roles(p)).toContain('specialist:security:light'); + expect(roles(p)).toContain('specialist:security:full'); }); test('code class honours hit-rate gating and small-diff skip', () => { - expect(roles(planReview(base({ gated: ['simplification'] })))).not.toContain('specialist:simplification:light'); + expect(roles(planReview(base({ gated: ['simplification'] })))).not.toContain('specialist:simplification:full'); expect(planReview(base({ diffLines: 20 })).dispatches.filter(d => d.role === 'specialist')).toEqual([]); }); test('pass 2 reruns the army on the range but not the adversarial or outside voice', () => { @@ -93,16 +93,17 @@ describe('planReview', () => { expect(p.next.step).toBe('full'); expect(roles(p).some(r => r.startsWith('outside-voice') || r.startsWith('adversarial'))).toBe(false); }); - test('a fix-check is one light dispatch', () => { - expect(roles(planReview(base({ history: { fullPasses: 1, last: 'full', fixedBelow: true } })))).toEqual(['fix-check:fix-check:light']); + test('a fix-check is one full-effort dispatch', () => { + expect(roles(planReview(base({ history: { fullPasses: 1, last: 'full', fixedBelow: true } })))).toEqual(['fix-check:fix-check:full']); }); test('changed numeric claims rerun only the outside voice', () => { const numbers = scanNumbers({ 'docs/bench.md': ['3x faster'] }); const p = planReview(base({ numbers, reviewedNumbers: 'old', history: { fullPasses: 1, last: 'full', fixedBelow: true } })); - expect(roles(p)).toEqual(['fix-check:fix-check:light', 'outside-voice:outside-voice (numbers changed):host']); + expect(roles(p)).toEqual(['fix-check:fix-check:full', 'outside-voice:outside-voice (numbers changed):host']); }); - test('tiers off renders every role at the host default', () => { - expect(planReview(base({ tiers: false })).dispatches.every(d => d.effort === 'host')).toBe(true); + test('no reviewer runs below full effort (GSTA-131 eval: a light fix-check missed a fail-open a full one caught)', () => { + for (const h of [{ fullPasses: 0, last: 'none' }, { fullPasses: 1, last: 'full', fixedBelow: true }, { fullPasses: 1, last: 'full', fixedBlocking: true }] as const) + expect(planReview(base({ gateTagged: 1, history: h })).dispatches.filter(d => d.role !== 'outside-voice').every(d => d.effort === 'full')).toBe(true); }); test('done dispatches nothing', () => expect(planReview(base({ history: { fullPasses: 1, last: 'full' } })).dispatches).toEqual([])); }); diff --git a/test/review-plan-cli.test.ts b/test/review-plan-cli.test.ts index 02b031346..2544feee7 100644 --- a/test/review-plan-cli.test.ts +++ b/test/review-plan-cli.test.ts @@ -33,18 +33,18 @@ describe('gstack-review-plan', () => { const r = run(repo({ 'docs/bench.md': 'Median latency fell 42% to 180 ms.\n' }), []); expect(r.stdout).toContain('DISPATCH: outside-voice outside-voice effort=host'); }); - test('--json carries the class, the next step and every dispatch with its effort; --no-tiers renders host effort', () => { + test('--json carries the class, the next step, every dispatch with its effort and the audit effort; --no-tiers renders audits at host effort', () => { const r = repo({ 'src/a.ts': Array.from({ length: 80 }, (_, i) => `export const v${i} = ${i};`).join('\n') + '\n' }); const plan = JSON.parse(run(r, ['--json']).stdout); expect(plan).toMatchObject({ class: 'code', next: { step: 'full' }, pass: 0 }); - expect(plan.dispatches.find((d: any) => d.role === 'specialist').effort).toBe('light'); - const host = JSON.parse(run(r, ['--json', '--no-tiers']).stdout); - expect(host.dispatches.every((d: any) => d.effort === 'host')).toBe(true); + expect(plan.dispatches.find((d: any) => d.role === 'specialist').effort).toBe('full'); + expect(plan.audit_effort).toBe('light'); + expect(JSON.parse(run(r, ['--json', '--no-tiers']).stdout).audit_effort).toBe('host'); }); test('a below-floor fix after pass 1 plans one fix-check', () => { const r = run(repo({ 'src/a.ts': 'export const a = 1;\n' }), ['--pass', '1', '--last', 'full', '--fixed-below']); expect(r.stdout).toContain('next=fix-check dispatches=1'); - expect(r.stdout).toContain('DISPATCH: fix-check fix-check effort=light'); + expect(r.stdout).toContain('DISPATCH: fix-check fix-check effort=full'); }); test('usage errors exit 2', () => { expect(spawnSync(BIN, [], { encoding: 'utf8', timeout: 10_000 }).status).toBe(2); diff --git a/test/ship-receipt-carry-pregate.test.ts b/test/ship-receipt-carry-pregate.test.ts index 483453eff..fac6fc040 100644 --- a/test/ship-receipt-carry-pregate.test.ts +++ b/test/ship-receipt-carry-pregate.test.ts @@ -55,5 +55,5 @@ describe('carry with a recorded pregate', () => { const after = extractReceipt(fs.readFileSync(body, 'utf8')).receipt!; expect(after.head).toBe(head); expect(after.pregate).toContain(`current for ${head.slice(0, 12)}`); - }); + }, 60_000); });