wip(GSTA-131): reviewers stay at full effort; audits light

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-27 22:22:48 +00:00
1 parent fc5ee582e3
commit 3925e9b56d
9 files changed
+68 -63

No files matched your search

+3 -3
View File
@@ -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 ────────────────────────────────────────────────────────
+9 -7
View File
@@ -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 <fp> 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<string, string> = {};
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;
}
+12 -9
View File
@@ -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?
+12 -11
View File
@@ -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
+11 -14
View File
@@ -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 };
+2 -1
View File
@@ -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 ?? '');
+13 -12
View File
@@ -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([]));
});
+5 -5
View File
@@ -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);
+1 -1
View File
@@ -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);
});