mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
v1.91.323.0 ship: proportional (scoped) second review pass after a blocking fix (GSTA-152 PR B) (#3596)
* ship: token cap with real meters and fresh-context delegation (GSTA-152 PR A) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ship: scoped second review pass after a blocking fix (GSTA-152 PR B) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (stopped or lost capped run no longer blocks uncapped review starts; short child return reconciles incomplete) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: carry estimate spend across a meter epoch change; calibration docs match their evidence Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: load the committed calibration file by its repo path so pregate maps it to a lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ship: fix fence placeholders and register claude-usage fixture (GSTA-152 PR A) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: run ship-budget and review-log bins through their interpreters so Windows can launch them Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: plan the full pass 2 when the fix range touches a gate path or is empty (GSTA-152 PR B review) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: classify the fix range and the diff at the range base with their own scope and gate scan (GSTA-152 PR B pass 2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: a full pass 1 always gets a full pass 2; drop the at-base reconstruction (GSTA-152 PR B pass 3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a8d60be09f
commit
0732890091
17 files changed
+392
-46
No files matched your search
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.323.0] - 2026-10-29
|
||||
|
||||
### Tests
|
||||
- `test/review-depth.test.ts`: the #19 shape plans fix-check, Red Team and native adversarial; full depth, a fail-closed classification, `--depth-floor full`, a 151-line, missing or unread range and a gate-touching fix keep the full pass; a forced specialist and changed numbers keep their reviewers; 150, 151 and a docs-only range; the scoped pass counts toward the cap and a blocker it leaves stops; `DEPTH_RULES` moves with the constant.
|
||||
- `test/review-plan-cli.test.ts`: `--range-base` plans `NEXT: scoped`; a 151-line range, no range, an unknown base and uncommitted fixes plan full.
|
||||
- `test/review-scope.test.ts`: `raised_by` from the explicit field, the specialist tag or `unknown`.
|
||||
- `test/ship-receipt-depth.test.ts`: the queue binds a receipt whose pass 2 was scoped.
|
||||
|
||||
### Gate integrity
|
||||
- Addition to the 1.91.316.0 entry below: **Pass 2 after a blocking fix now reviews the fix in proportion to its size.** When pass 1 fixed a blocking finding on a diff below full depth and the fix range has at most 150 runtime lines, /ship runs a scoped pass: one fix-check, the Red Team and the native adversarial pass on the range, instead of the specialist army. On gstack-evals#19 a 3-file fix range (+18/-6) was planned for nine reviewers. Full depth, `--depth-floor full`, a fix that touched a gate path, a larger range, or a range it cannot read keeps the full pass 2. The two-pass cap and the stop for the owner are unchanged.
|
||||
- Addition to the 1.91.316.0 entry below: **`gstack-review-plan --range-base <sha>`** (`lib/review-depth.ts`): counts the committed fix range `<sha>..HEAD` the way the depth counts runtime lines, prints `FIX_RANGE: runtime_lines=<n>|unread (<why>) base=<sha>` (`fix_range` in `--json`), and `NEXT: scoped` when the scoped pass applies. `nextStep` takes the depth and `History.rangeRuntimeLines`; `SCOPED_RANGE_LINES` (150) joins `DEPTH_RULES`, so `rules=` changes. `REVIEW_PLAN:` ends with the step's `reason="…"`. Uncommitted or untracked edits, a range base that is not an ancestor of HEAD, an empty range, and a range the classifier cannot count all plan the full pass. A full pass 1 always gets a full pass 2: `--pass1-depth` carries pass 1's `REVIEW_DEPTH` (absent counts as full), so a fix that reverts a gate, auth, migration or gate-tagged edit cannot lower pass 2. The fix range is classified with its own `gstack-diff-scope` and `gstack-gate-diff` input, and a gate-class range plans the full pass and sets `fix_range.touched_gate`.
|
||||
- Addition to the 1.91.316.0 entry below: **Scoped dispatches**: the fix-check, every force-included specialist, the Red Team, the native adversarial pass when the range changes runtime lines, and the outside voice when published numbers changed. Step 11's voices that have not run yet stay listed. A light diff's raise to standard applies to the scoped set only.
|
||||
- Addition to the 1.91.316.0 entry below: **Step 9** (`ship/sections/review-army.md`): the repeat decision passes `--range-base $PASS_HEAD` and maps `NEXT: scoped` to decision 3; a scoped pass logs a full-pass `review` row with `"scope":"scoped"`, so it counts toward the cap and its roles land in `review_loop.dispatches` with no receipt schema change.
|
||||
- Addition to the 1.91.316.0 entry below: **`raised_by` on classified findings** (`gstack-review-scope classify`, `lib/review-scope.ts`): the reviewer that raised each finding (`core`, `qa`, a specialist, `red-team`, `adversarial-native`, `outside-voice`, `fix-check`), from the finding's `raised_by` or `specialist` tag, else `unknown`. Step 9 copies it into the review-log row. Older rows without it still summarize.
|
||||
- Addition to the 1.91.316.0 entry below: **Docs**: docs/review-policy.md (rule 3, the transitions table, a scoped-pass section and Behavior changes) and docs/ship-policy.md rule 3.
|
||||
|
||||
## [1.91.322.0] - 2026-10-29
|
||||
|
||||
**A PR screen no longer runs the whole paid gate just because a changed file sits on every case's import path.** Editing a file like `lib/restamp.ts` reached all 113 gate cases through the derived-reference closure, so 19 of the 41 full screens between Oct 20 and 29 ran the full $33 gate for that reason alone. A narrow screen (`gstack-ship-queue ready --screen`, and `bun run eval:bg:pr`) now leaves such a file's cases to the train gate's full census, which runs every case before anything merges. Replayed on those 41 screens with today's maps, this saves about $457 a week, and the screens that caught real PR-caused reds (#3434, #3453) still run the full gate.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.322.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.323.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+46
-18
@@ -4,14 +4,14 @@
|
||||
*
|
||||
* gstack-review-plan --base <ref> [--pass <full passes run>] [--last none|full|fix-check] [--fixed-blocking] [--fixed-below]
|
||||
* [--blocking-open] [--touched-gate] [--critical-found] [--adversarial-ran] [--gate-tagged <n>] [--gated a,b] [--force a,b|all]
|
||||
* [--reviewed-numbers <fingerprint>] [--depth-floor standard|full] [--no-tiers] [--json]
|
||||
* [--reviewed-numbers <fingerprint>] [--depth-floor standard|full] [--range-base <sha>] [--pass1-depth light|standard|full] [--no-tiers] [--json]
|
||||
* gstack-review-plan --fix-check-prompt --fix-list <file> --hunks <file> --tests <file> [--callers <file>]
|
||||
*
|
||||
* Reads the diff against the merge base with <ref> (committed, tracked uncommitted
|
||||
* edits and untracked files, through lib/review-collect.ts collectLive), runs
|
||||
* gstack-diff-scope and, unless --gate-tagged is given, gstack-gate-diff; prints
|
||||
* REVIEW_CLASS:, REVIEW_DEPTH: (depth, runtime lines, the rules hash, reasons and
|
||||
* result codes), GATE_SCAN:, REVIEW_PLAN:, one DEPTH_ERROR: per input that failed
|
||||
* result codes), GATE_SCAN:, FIX_RANGE: (with --range-base), REVIEW_PLAN:, one DEPTH_ERROR: per input that failed
|
||||
* the depth closed, one DISPATCH: plus DIFF_CMD: (and DIFF_EXCLUDE: none) per
|
||||
* reviewer and one SKIP: per reviewer left out. A plan that fails closed to full
|
||||
* exits 0; a diff git cannot list exits 1. Logic: lib/review-depth.ts.
|
||||
@@ -21,8 +21,8 @@ import * as path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
|
||||
import { runScriptSync } from '../lib/script-invocation';
|
||||
import { collectLive } from '../lib/review-collect';
|
||||
import { diffCommand, planReview, reviewDepthLine, reviewPlanLine, scanNumbers, type Depth, type History } from '../lib/review-depth';
|
||||
import { collectCommit, collectLive } from '../lib/review-collect';
|
||||
import { classifyDiff, diffCommand, planReview, reviewDepthLine, reviewPlanLine, scanNumbers, SCOPED_RANGE_LINES, type Depth, type History } from '../lib/review-depth';
|
||||
import { renderFixCheckPrompt } from '../scripts/resolvers/fix-check';
|
||||
|
||||
const HELP = `gstack-review-plan — the review depth of the next /ship step (docs/review-policy.md#review-depth)
|
||||
@@ -41,11 +41,15 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do
|
||||
--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
|
||||
--depth-floor standard|full raise the depth to at least this (never lowers it); /ship passes its prompt flag on every run
|
||||
--range-base <sha> the fix range's base (this pass's PASS_HEAD): after a fixed blocker on a diff below full depth, a
|
||||
committed range of at most ${SCOPED_RANGE_LINES} runtime lines plans a scoped pass; anything unread plans the full pass
|
||||
--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 (depth, runtime_lines, rules, errors with input and repair, gate_scan, per-dispatch exclude)
|
||||
--pass1-depth <d> pass 1's REVIEW_DEPTH depth: full, or absent, keeps pass 2 full
|
||||
--fix-check-prompt print the fix-check reviewer's prompt from --fix-list, --hunks, --tests and --callers files
|
||||
|
||||
Depth: light (runtime lines <= 50), standard (above 50), full (gate class or any DEPTH_ERROR). A full plan
|
||||
Depth: light (runtime lines <= 50), standard (above 50), full (gate class or any DEPTH_ERROR). NEXT: full, scoped
|
||||
(a full pass on the range: fix-check, Red Team, native adversarial), fix-check, stop or done. A full plan
|
||||
from an error exits 0; exit 1 means no plan could be built (git could not list the diff).
|
||||
${exitTableHelp()}`;
|
||||
|
||||
@@ -72,10 +76,14 @@ function main(): number {
|
||||
if (!base) { process.stderr.write(`gstack-review-plan: --base <ref> is required\n${HELP}\n`); return EXIT.usage; }
|
||||
const last = (flag('--last') ?? 'none') as History['last'];
|
||||
if (!['none', 'full', 'fix-check'].includes(last)) { process.stderr.write('gstack-review-plan: --last must be none, full or fix-check\n'); return EXIT.usage; }
|
||||
const pass1Depth = (flag('--pass1-depth') ?? 'full') as Depth;
|
||||
if (!['light', 'standard', 'full'].includes(pass1Depth)) { process.stderr.write('gstack-review-plan: --pass1-depth must be light, standard or full\n'); return EXIT.usage; }
|
||||
const pass = Number(flag('--pass') ?? '0');
|
||||
if (!Number.isInteger(pass) || pass < 0) { process.stderr.write('gstack-review-plan: --pass needs an integer >= 0\n'); return EXIT.usage; }
|
||||
const floor = flag('--depth-floor') as Depth | undefined;
|
||||
if (has('--depth-floor') && floor !== 'standard' && floor !== 'full') { process.stderr.write('gstack-review-plan: --depth-floor must be standard or full (a floor only raises the depth)\n'); return EXIT.usage; }
|
||||
const rangeBase = flag('--range-base');
|
||||
if (has('--range-base') && (!rangeBase || rangeBase.startsWith('-'))) { process.stderr.write('gstack-review-plan: --range-base needs the fix range\'s base commit (this pass\'s PASS_HEAD)\n'); return EXIT.usage; }
|
||||
const root = git(['rev-parse', '--show-toplevel'])?.trim();
|
||||
if (!root) { process.stderr.write('gstack-review-plan: run it inside a git checkout\n'); return EXIT.fail; }
|
||||
process.chdir(root);
|
||||
@@ -86,34 +94,54 @@ function main(): number {
|
||||
}
|
||||
const head = git(['rev-parse', 'HEAD'])?.trim() ?? 'unknown';
|
||||
const binDir = path.dirname(Bun.fileURLToPath(import.meta.url));
|
||||
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 = runScriptSync(path.join(binDir, 'gstack-gate-diff'), [base], { encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
|
||||
const scopeOf = (args: string[]): Record<string, string> => {
|
||||
const run = runScriptSync(path.join(binDir, 'gstack-diff-scope'), args, { encoding: 'utf8', timeout: 60_000 });
|
||||
const s: Record<string, string> = {};
|
||||
for (const l of (run.stdout ?? '').split('\n')) { const m = /^(SCOPE_[A-Z_]+)=(.*)$/.exec(l); if (m) s[m[1]!] = m[2]!; }
|
||||
if (run.status !== 0 && !s.SCOPE_ERROR) s.SCOPE_ERROR = `exit ${run.status}`;
|
||||
return s;
|
||||
};
|
||||
const gateTagsOf = (args: string[]): number | null => {
|
||||
const g = runScriptSync(path.join(binDir, 'gstack-gate-diff'), args, { 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;
|
||||
}
|
||||
return g.status === 0 && m ? m[1]!.split(',').reduce((n, kv) => n + (Number(kv.split(':')[1]) || 0), 0) : null;
|
||||
};
|
||||
const scope = scopeOf([base]);
|
||||
let gateTagged = flag('--gate-tagged') !== undefined ? Number(flag('--gate-tagged')) : gateTagsOf([base]);
|
||||
if (gateTagged !== null && !Number.isFinite(gateTagged)) gateTagged = null;
|
||||
const force = flag('--force') === 'all' ? 'all' as const : list('--force');
|
||||
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 { added, diffLines, paths, deleted, stampOnly, perPath, registry } = collected;
|
||||
let fixRange: { base: string; runtime_lines: number | null; error?: string; touched_gate?: true } | undefined;
|
||||
if (rangeBase) {
|
||||
const unread = (error: string) => ({ base: rangeBase, runtime_lines: null, error });
|
||||
const resolved = git(['rev-parse', '--verify', '--quiet', `${rangeBase}^{commit}`])?.trim();
|
||||
if (!resolved) fixRange = unread('not a commit in this repository');
|
||||
else if (git(['merge-base', '--is-ancestor', resolved, 'HEAD']) === null) fixRange = unread('not an ancestor of HEAD');
|
||||
else if (git(['diff', '--quiet', 'HEAD', '--']) === null || collected.untracked.length) fixRange = unread('uncommitted or untracked edits: commit the fixes first');
|
||||
else {
|
||||
const commit = collectCommit(root, resolved, 'HEAD');
|
||||
const range = classifyDiff({ ...commit, registry, scope: scopeOf([resolved, '--head', 'HEAD']), gateTagged: gateTagsOf([resolved]) });
|
||||
fixRange = range.errors.length ? unread(range.errors.map(e => `${e.code} ${e.input}`).join('; '))
|
||||
: !commit.paths.length ? unread('the fix range is empty: commit the fix after the range base')
|
||||
: { base: rangeBase, runtime_lines: range.runtimeLines, ...(range.class === 'gate' ? { touched_gate: true as const } : {}) };
|
||||
}
|
||||
}
|
||||
const plan = planReview({
|
||||
...collected, scope, gateTagged, numbers: scanNumbers(added), force, gated: list('--gated'), floor,
|
||||
criticalFound: has('--critical-found'), adversarialRan: has('--adversarial-ran'), reviewedNumbers: flag('--reviewed-numbers'),
|
||||
history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: has('--fixed-below'), blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') },
|
||||
history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: has('--fixed-below'), blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') || !!fixRange?.touched_gate, pass1Depth, rangeRuntimeLines: fixRange?.runtime_lines },
|
||||
});
|
||||
const gateScan = { tagged: gateTagged, head };
|
||||
if (has('--json')) {
|
||||
const { runtimeLines, stepDepth, depthReasons, ...rest } = plan;
|
||||
process.stdout.write(JSON.stringify({ ...rest, step_depth: stepDepth, runtime_lines: runtimeLines, depth_reasons: depthReasons, depth_floor: floor ?? null, gate_scan: gateScan, registry: { sha: registry.sha, outputs: registry.outputs }, per_path: perPath, diff_lines: diffLines, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly }) + '\n');
|
||||
process.stdout.write(JSON.stringify({ ...rest, step_depth: stepDepth, runtime_lines: runtimeLines, depth_reasons: depthReasons, depth_floor: floor ?? null, gate_scan: gateScan, fix_range: fixRange ?? null, registry: { sha: registry.sha, outputs: registry.outputs }, per_path: perPath, 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'})`, reviewDepthLine(plan), `GATE_SCAN: ${gateTagged === null ? 'unavailable' : `tagged=${gateTagged}`} head=${head}`, reviewPlanLine(plan, pass + (plan.next.step === 'full' ? 1 : 0)), `NEXT: ${plan.next.step} (${plan.next.reason})`];
|
||||
const out = [`REVIEW_CLASS: ${plan.class} (${plan.reasons.slice(0, 3).join('; ') || 'no signals'})`, reviewDepthLine(plan), `GATE_SCAN: ${gateTagged === null ? 'unavailable' : `tagged=${gateTagged}`} head=${head}`];
|
||||
if (fixRange) out.push(`FIX_RANGE: ${fixRange.runtime_lines === null ? `unread (${fixRange.error})` : `runtime_lines=${fixRange.runtime_lines}`} base=${fixRange.base}`);
|
||||
out.push(reviewPlanLine(plan, pass + (plan.next.step === 'full' || plan.next.step === 'scoped' ? 1 : 0)), `NEXT: ${plan.next.step} (${plan.next.reason})`);
|
||||
for (const e of plan.errors) out.push(`DEPTH_ERROR: ${e.code} input=${e.input} detail=${e.detail}; fix: ${e.repair}${e.result ? ` (${e.result})` : ''}`);
|
||||
for (const d of plan.dispatches) {
|
||||
const key = d.name.split(' ')[0];
|
||||
|
||||
+30
-4
@@ -93,7 +93,8 @@ incomplete coverage, which blocks until the reviewer is rerun.
|
||||
for every reviewer (the gate scan and QA probes keep the whole candidate) and
|
||||
classifies with `--range-base`. Only a blocking finding in that range or in
|
||||
the diff is `fix`; every below-floor finding is a `note`.
|
||||
3. **Two full passes is the cap**, plus at most one fix-check. A blocking
|
||||
3. **Two full passes is the cap**, plus at most one fix-check; a scoped pass
|
||||
([below](#scoped-pass)) is a full pass. A blocking
|
||||
finding still open after the last allowed step stops the run and names the
|
||||
finding (path, severity, problem) for the owner; a third full pass is the
|
||||
owner's call, never the loop's. The cap approves nothing.
|
||||
@@ -108,7 +109,8 @@ incomplete coverage, which blocks until the reviewer is rerun.
|
||||
|---|---|
|
||||
| none | full pass 1 on the whole diff |
|
||||
| a full pass with no edits | done |
|
||||
| a full pass that fixed a blocking finding, below the cap | full pass on the range |
|
||||
| a full pass that fixed a blocking finding, below the cap, on a diff below full depth with a fix range of at most 150 runtime lines | scoped pass on the range |
|
||||
| a full pass that fixed a blocking finding, below the cap (full depth, a larger range, or a range or depth it could not read) | 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 |
|
||||
@@ -121,6 +123,25 @@ 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.
|
||||
|
||||
<a id="scoped-pass"></a>
|
||||
A **scoped pass** is pass 2 after a blocking fix when pass 1 and the diff both
|
||||
ran below full depth (no gate class, no depth error, no `--depth-floor full`).
|
||||
A full pass 1 always gets a full pass 2: `--pass1-depth` carries pass 1's
|
||||
`REVIEW_DEPTH`, and an absent flag counts as full. The fix range
|
||||
`RANGE_BASE..HEAD` must have at most `SCOPED_RANGE_LINES` (150) runtime lines, counted
|
||||
as the depth counts them (`gstack-review-plan --range-base $PASS_HEAD` prints
|
||||
`FIX_RANGE:`). It dispatches one fix-check on the range, the Red Team on the
|
||||
range, the native adversarial pass when the range changes runtime lines, every
|
||||
force-included specialist and the outside voice when published numbers changed.
|
||||
Step 11's voices that have not run yet stay listed, as on a full pass 2. It
|
||||
writes a full-pass `review` row with `"scope":"scoped"`, so it counts toward
|
||||
the cap, and a blocker it leaves open stops for the owner as before. A fix that
|
||||
touched a gate-class path (read from the range with its own scope and gate
|
||||
scan), a larger range, an empty range, or a
|
||||
range, classifier or depth it cannot read keeps the full pass 2. The 150-line figure is a starting value from
|
||||
a 24-PR backtest (GSTA-152 D6), to revisit once classified findings carry
|
||||
`raised_by`.
|
||||
|
||||
The same floor and the same `NO FINDINGS` rule apply to `/review` on its own;
|
||||
its re-review loop keeps its three-cycle fix cap (`CYCLES`), and a pass that
|
||||
applies no edits is its exit too.
|
||||
@@ -156,7 +177,8 @@ runtime; a `.tmpl` edit plus its regenerated `SKILL.md` counts the template once
|
||||
|
||||
A fixed blocking finding, a CRITICAL or an open blocker raises a light diff to
|
||||
standard for the next step (`step=standard`): its specialists read at least 50 lines
|
||||
and the Red Team runs, so a raise never dispatches less than light did. A fix-check
|
||||
and the Red Team runs, so a raise never dispatches less than light did. When the
|
||||
next step is a scoped pass, the raise applies to the scoped set only. A fix-check
|
||||
is the same at every depth.
|
||||
|
||||
**Fail closed.** Each of these makes the depth full and is listed in `errors[]` with
|
||||
@@ -217,6 +239,9 @@ the audits and watchers at the host default too
|
||||
|
||||
### Behavior changes
|
||||
|
||||
- Pass 2 after a blocking fix on a diff below full depth, with a fix range of at
|
||||
most 150 runtime lines, is a [scoped pass](#scoped-pass): a fix-check, the Red
|
||||
Team and the native adversarial pass on the range instead of the specialist army.
|
||||
- A tests-only diff (runtime 0) no longer dispatches the Testing specialist: the
|
||||
core checklist and the gate scan review it.
|
||||
- Light no longer runs a separate Red Team: the batched specialist carries its
|
||||
@@ -243,7 +268,8 @@ puts in the receipt:
|
||||
| `review_loop.est_input_tokens` | prompt plus diff bytes the dispatches read, divided by 4: a relative estimate for comparing runs, not a bill |
|
||||
|
||||
Each review row may carry `dispatches: [{role, effort, prompt_bytes,
|
||||
diff_bytes, status}]`; `summary` adds them up (older rows fall back to the
|
||||
diff_bytes, status}]`, and each finding `raised_by` (the reviewer that raised it,
|
||||
stamped by `gstack-review-scope classify`; `unknown` when untagged, absent on older rows); `summary` adds them up (older rows fall back to the
|
||||
specialist stats they record). The summary line appends `fix_checks=`,
|
||||
`dispatches=` and `est_input_tokens=` only when they were recorded, so an old
|
||||
loop prints the old line.
|
||||
|
||||
+2
-1
@@ -920,7 +920,8 @@ construction:
|
||||
pass. `NO FINDINGS` is the expected reviewer result for a clean range, never a
|
||||
failed review.
|
||||
3. **Two full passes plus one fix-check is the cap**, stepped by the
|
||||
[transition table](review-policy.md#transitions). A pass that applied no edits
|
||||
[transition table](review-policy.md#transitions); a
|
||||
[scoped pass](review-policy.md#scoped-pass) counts as a full pass. A pass that applied no edits
|
||||
continues to Step 10: the loop ships with its deferred and noted items named in
|
||||
the PR body. A blocking finding still open after the last allowed step stops
|
||||
the run and names the finding (path, severity, problem) for the owner instead
|
||||
|
||||
+34
-9
@@ -19,7 +19,7 @@ export type PathClass = 'docs' | 'tests' | 'code';
|
||||
export type Depth = 'light' | 'standard' | 'full';
|
||||
export type Effort = 'light' | 'full' | 'host';
|
||||
export type Role = 'specialist' | 'red-team' | 'adversarial-native' | 'outside-voice' | 'fix-check';
|
||||
export type Step = 'full' | 'fix-check' | 'done' | 'stop';
|
||||
export type Step = 'full' | 'scoped' | 'fix-check' | 'done' | 'stop';
|
||||
|
||||
export const SPECIALISTS = ['testing', 'maintainability', 'security', 'performance', 'data-migration', 'api-contract', 'design', 'simplification', 'state-machines'] as const;
|
||||
export type Specialist = (typeof SPECIALISTS)[number];
|
||||
@@ -27,6 +27,8 @@ export const FULL_PASS_CAP = 2;
|
||||
export const SMALL_DIFF_LINES = 50;
|
||||
export const RED_TEAM_LINES = 200;
|
||||
export const LARGE_DIFF_LINES = 100;
|
||||
/** The largest fix range, in runtime lines, a pass 2 after a blocking fix reviews as a scoped pass (GSTA-152 D6); a starting value. */
|
||||
export const SCOPED_RANGE_LINES = 150;
|
||||
/** The checklists light's batched specialist always reads, whatever the scope. */
|
||||
export const LIGHT_FIXED: readonly Specialist[] = ['testing', 'maintainability', 'security'];
|
||||
/** Why light with runtime above 0 keeps the native adversarial pass as its own dispatch. */
|
||||
@@ -48,7 +50,7 @@ const GATE_PATH = [
|
||||
|
||||
/** A hash of the constants and patterns that decide a depth, not of this file's bytes: a comment edit keeps it, a threshold edit changes it. */
|
||||
export const DEPTH_RULES = createHash('sha256').update(JSON.stringify({
|
||||
SMALL_DIFF_LINES, RED_TEAM_LINES, LARGE_DIFF_LINES, FULL_PASS_CAP, LIGHT_FIXED,
|
||||
SMALL_DIFF_LINES, RED_TEAM_LINES, LARGE_DIFF_LINES, FULL_PASS_CAP, LIGHT_FIXED, SCOPED_RANGE_LINES,
|
||||
patterns: Object.entries({ GATE_PATH, AGENT_PROSE, TEST_PATH, RELEASE_METADATA, DOC_PATH: [DOC_PATH] }).map(([k, v]) => [k, v.map(r => `/${r.source}/${r.flags}`)]),
|
||||
})).digest('hex').slice(0, 16);
|
||||
|
||||
@@ -199,23 +201,34 @@ export interface History {
|
||||
blockingOpen?: boolean;
|
||||
/** A fix in the finished step touched a gate-class path. */
|
||||
touchedGate?: boolean;
|
||||
/** Runtime lines of the fix range (RANGE_BASE..HEAD) as classifyDiff counts them, 0 when it touches none; undefined or null when the range was not read. */
|
||||
rangeRuntimeLines?: number | null;
|
||||
pass1Depth?: Depth;
|
||||
}
|
||||
export interface NextStep { step: Step; reason: string }
|
||||
|
||||
/**
|
||||
* The transition table (docs/review-policy.md#transitions). Pass 1 is full. A fixed
|
||||
* blocking finding earns a second full pass on the range; any other fix earns one
|
||||
* fix-check; two full passes is the cap; a blocker still open after any step
|
||||
* stops for the owner.
|
||||
* blocking finding earns a second full pass on the range, scoped when `depth` (known
|
||||
* only to planReview) is below full and the fix range has at most SCOPED_RANGE_LINES
|
||||
* runtime lines; any other fix earns one fix-check; two full passes is the cap (a
|
||||
* scoped pass is one); a blocker still open after any step stops for the owner.
|
||||
*/
|
||||
export function nextStep(h: History): NextStep {
|
||||
export function nextStep(h: History, depth?: Depth): NextStep {
|
||||
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' } : { 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 (h.blockingOpen) return { step: 'stop', reason: 'a blocking finding is still open in fix scope: name it for the owner' };
|
||||
if (!fixed) return { 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' };
|
||||
if ((h.fixedBlocking || h.touchedGate) && h.fullPasses < FULL_PASS_CAP) {
|
||||
if (!h.fixedBlocking) return { step: 'full', reason: 'a fix touched a gate-class path: full pass on the range' };
|
||||
const n = h.rangeRuntimeLines;
|
||||
const keep = h.touchedGate ? 'a fix touched a gate-class path' : depth === undefined ? 'the depth is unknown' : h.pass1Depth === 'full' ? 'pass 1 ran at full depth' : depth === 'full' ? 'the diff is at full depth'
|
||||
: typeof n !== 'number' || !Number.isFinite(n) || n < 0 ? 'the fix range was not read' : n > SCOPED_RANGE_LINES ? `the fix range has ${n} runtime lines (> ${SCOPED_RANGE_LINES})` : null;
|
||||
if (keep) return { step: 'full', reason: `a blocking finding was fixed and ${keep}: full pass on the range` };
|
||||
return { step: 'scoped', reason: `a blocking finding was fixed on a ${depth} diff with ${n} runtime lines in the fix range (<= ${SCOPED_RANGE_LINES}): scoped 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' };
|
||||
}
|
||||
|
||||
@@ -295,7 +308,7 @@ export function planReview(i: PlanInput): ReviewPlan {
|
||||
const c = classifyDiff(i);
|
||||
const depth = decideDepth(c, c.runtimeLines ?? (CLASS_RANK[c.class] >= CLASS_RANK.code ? i.diffLines : 0), i.floor);
|
||||
const { class: cls, runtimeLines } = depth;
|
||||
const next = nextStep(i.history);
|
||||
const next = nextStep(i.history, depth.depth);
|
||||
const h = i.history;
|
||||
const raise = depth.depth === 'light' && (h.fixedBlocking || h.blockingOpen || i.criticalFound);
|
||||
const stepDepth: Depth = raise ? 'standard' : depth.depth;
|
||||
@@ -340,6 +353,18 @@ export function planReview(i: PlanInput): ReviewPlan {
|
||||
skipped.push('adversarial-native and outside-voice (run once per invocation)');
|
||||
if (numbersChanged) add('outside-voice', 'outside-voice (numbers changed)', 'host');
|
||||
}
|
||||
} else if (next.step === 'scoped') {
|
||||
add('fix-check', 'fix-check', 'full');
|
||||
const forced = SPECIALISTS.filter(forcedBy(i));
|
||||
for (const n of forced) add('specialist', n, 'full');
|
||||
skipped.push(forced.length ? 'specialists (scoped pass; forced ones dispatched)' : 'specialists (scoped pass)');
|
||||
add('red-team', 'red-team', 'full');
|
||||
const stepElevenPending = !i.adversarialRan && cls === 'code';
|
||||
if ((h.rangeRuntimeLines ?? 0) > 0 || stepElevenPending) add('adversarial-native', 'adversarial', 'full');
|
||||
else skipped.push('adversarial-native (scoped pass: the fix range changes no runtime lines)');
|
||||
if (stepElevenPending) add('outside-voice', 'outside-voice', 'host');
|
||||
else if (numbersChanged) add('outside-voice', 'outside-voice (numbers changed)', 'host');
|
||||
else skipped.push('outside-voice (scoped pass)');
|
||||
} else if (next.step === 'fix-check') {
|
||||
add('fix-check', 'fix-check', 'full');
|
||||
if (numbersChanged) add('outside-voice', 'outside-voice (numbers changed)', 'host');
|
||||
@@ -348,7 +373,7 @@ export function planReview(i: PlanInput): ReviewPlan {
|
||||
}
|
||||
|
||||
export function reviewPlanLine(p: ReviewPlan, pass: number): string {
|
||||
return `REVIEW_PLAN: class=${p.class} pass=${pass} next=${p.next.step} dispatches=${p.dispatches.length} numbers=${p.numbers.publishes ? `yes(${p.numbers.claims})` : 'no'} fingerprint=${p.numbers.fingerprint}`;
|
||||
return `REVIEW_PLAN: class=${p.class} pass=${pass} next=${p.next.step} dispatches=${p.dispatches.length} numbers=${p.numbers.publishes ? `yes(${p.numbers.claims})` : 'no'} fingerprint=${p.numbers.fingerprint} reason=${JSON.stringify(p.next.reason)}`;
|
||||
}
|
||||
|
||||
export function reviewDepthLine(p: ReviewPlan): string {
|
||||
|
||||
+18
-2
@@ -13,6 +13,7 @@
|
||||
*
|
||||
* Pure functions here; bin/gstack-review-scope runs git and gh.
|
||||
*/
|
||||
import { SPECIALISTS } from './review-depth';
|
||||
|
||||
export type Locus = 'diff' | 'fix-range' | 'outside';
|
||||
export type Kind = 'defect' | 'test-gap' | 'advisory';
|
||||
@@ -30,7 +31,22 @@ export interface ScopeFinding {
|
||||
cause?: { path?: string; line?: number };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface ScopedFinding extends ScopeFinding { locus: Locus; kind: Kind; blocking: boolean; scope: ScopeAction }
|
||||
export interface ScopedFinding extends ScopeFinding { locus: Locus; kind: Kind; blocking: boolean; scope: ScopeAction; raised_by?: string }
|
||||
|
||||
/** Who can raise a finding: the core checklist, QA probes, a specialist by name and the review-depth roles; anything else is `unknown`. */
|
||||
const RAISERS: readonly string[] = ['core', 'qa', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check', ...SPECIALISTS];
|
||||
const RAISER_ALIASES: Record<string, string> = { adversarial: 'adversarial-native' };
|
||||
|
||||
/** The reviewer that raised a finding, for attributing passes later (GSTA-152 D6): its `raised_by` when known, else its `specialist` tag, else `unknown`. */
|
||||
export function raisedBy(finding: ScopeFinding): string {
|
||||
for (const v of [finding.raised_by, finding.specialist]) {
|
||||
if (typeof v !== 'string') continue;
|
||||
const k = v.trim().toLowerCase().replace(/[\s_]+/g, '-');
|
||||
const name = RAISER_ALIASES[k] ?? k;
|
||||
if (RAISERS.includes(name)) return name;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/** Per path, the new-side line ranges a diff touched; `'whole'` for an added or untracked file. */
|
||||
export type HunkMap = Record<string, Array<[number, number]> | 'whole'>;
|
||||
@@ -152,7 +168,7 @@ export function scopeFindings(findings: readonly ScopeFinding[], input: ScopeInp
|
||||
else if (locus === 'outside') scope = !blocking ? 'note' : input.policy === 'diff-plus-blocking' || !placed ? 'fix' : 'defer';
|
||||
else if (input.pass <= 1) scope = 'fix';
|
||||
else scope = blocking ? 'fix' : 'note';
|
||||
return { ...finding, locus, kind, blocking, scope };
|
||||
return { ...finding, locus, kind, blocking, scope, raised_by: raisedBy(finding) };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.322",
|
||||
"version": "1.91.323",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -86,7 +86,8 @@ function newShip(f: Fixture, sc: Scenario): { calls: Call[]; ending: string } {
|
||||
for (const d of plan.dispatches) calls.push({ role: d.role, name: d.name, effort: d.effort, diffBytes: d.role === 'outside-voice' ? f.diff_bytes : diffBytes });
|
||||
adversarialRan ||= plan.dispatches.some(d => d.role === 'adversarial-native');
|
||||
const o = outcomes[sc][Math.min(i, outcomes[sc].length - 1)] ?? {};
|
||||
h = { fullPasses: h.fullPasses + (plan.next.step === 'full' ? 1 : 0), last: plan.next.step, ...o };
|
||||
const last = plan.next.step === 'scoped' ? 'full' : plan.next.step;
|
||||
h = { fullPasses: h.fullPasses + (last === 'full' ? 1 : 0), last, ...o };
|
||||
}
|
||||
for (const a of FIXED_AUDITS) calls.push({ role: 'audit', effort: 'light', diffBytes: f.diff_bytes / 2 });
|
||||
return { calls, ending };
|
||||
|
||||
@@ -16,6 +16,8 @@ Finish the complete review and QA before applying any fix in Step 9.4.
|
||||
**Review plan.** Pass 1 runs `~/.claude/skills/gstack/bin/gstack-review-plan
|
||||
--base <base>`, plus `--depth-floor <d>` on every plan run when the /ship prompt
|
||||
gave `--depth-floor <d>` (it only raises); a repeat uses the plan run that picked it.
|
||||
A `scoped` repeat is a full pass on the range: its `fix-check` is decision 3.5's
|
||||
reviewer and prompt with `git diff $RANGE_BASE` as the hunks, inside this pass.
|
||||
Save its `REVIEW_PLAN:` line and fingerprint, `REVIEW_DEPTH:` (depth,
|
||||
`runtime_lines`, `rules`) and `GATE_SCAN:`. Dispatch exactly its `DISPATCH:` lines
|
||||
(specialists, Red Team, Step 11's voices), each at its `effort=` on the same model
|
||||
@@ -675,7 +677,9 @@ but missing dispatched output still blocks continuation, even with a QA exceptio
|
||||
|
||||
0. **Scope the findings.** Write the unmatched or reopened findings from Step 9.3
|
||||
(all sources, queued Steps 10–11 included) as a JSON array to
|
||||
`.gstack/tmp/review-findings-p$PASS.json` with your file-write tool, then:
|
||||
`.gstack/tmp/review-findings-p$PASS.json` with your file-write tool, each with
|
||||
`raised_by` (`core`, `qa`, a specialist, `red-team`, `adversarial-native`,
|
||||
`outside-voice` or `fix-check`), then:
|
||||
`~/.claude/skills/gstack/bin/gstack-review-scope classify --base <base> --pass $PASS --range-base $RANGE_BASE --findings .gstack/tmp/review-findings-p$PASS.json --json > .gstack/tmp/review-classified-p$PASS.json`
|
||||
(omit `--range-base` on pass 1). Every Step 9.4 scratch file lives under
|
||||
`.gstack/tmp/` (gitignored): `<classified>` below is this pass's classified
|
||||
@@ -776,6 +780,7 @@ but missing dispatched output still blocks continuation, even with a QA exceptio
|
||||
else `issues_found`. N counts unresolved defects now, not original totals; missing
|
||||
coverage, deferred and noted findings are not defects. `SCOPE` is the policy
|
||||
(`frozen`, `diff-plus-blocking` or `all`); `RANGE_BASE_OR_NULL` is `"<sha>"` from pass 2, else `null`.
|
||||
- A `scoped` pass adds `"scope":"scoped"`; it is a full-pass row and counts toward the cap.
|
||||
- `REVIEW_START`: this pass's Step 9 token captured before reading the diff;
|
||||
never recapture at persistence to certify unreviewed fixes.
|
||||
- `COMPLETED`: checklist and dispatched specialists/Red Team finish, and all required probes pass.
|
||||
@@ -791,7 +796,7 @@ but missing dispatched output still blocks continuation, even with a QA exceptio
|
||||
`{"fingerprint":"path:line:category","severity":"CRITICAL|INFORMATIONAL","action":"ACTION"}`.
|
||||
ACTION: `"auto-fixed"`, `"fixed"` (approved), `"skipped"` (explicit Skip),
|
||||
`"deferred"` (with `issue`, the filed number or `null`) or `"noted"`. Copy
|
||||
`locus`, `kind` and `blocking` from the classified output onto every finding.
|
||||
`locus`, `kind`, `blocking` and `raised_by` from the classified output onto every finding.
|
||||
Merge revalidated invocation decisions by identity and advisory/defect kind;
|
||||
preserve `advisory`, `evidence_paths` and `helper_target`.
|
||||
Gate findings add `gate` (RH tag), `gate_id` (the tool's hunk identity), `reason`
|
||||
@@ -809,8 +814,9 @@ Decision 1 always applies first. Then, under `frozen`, run `gstack-review-plan -
|
||||
with each flag that holds (`--fixed-blocking`, `--fixed-below`, `--blocking-open`,
|
||||
`--touched-gate` for a fix to an item 2.5 path, `--critical-found` when this pass
|
||||
logged a CRITICAL, `--adversarial-ran` once Step 11's native pass ran, `--reviewed-numbers <fp>`,
|
||||
`--depth-floor <d>` when given);
|
||||
`--depth-floor <d>` when given), `--range-base $PASS_HEAD` and `--pass1-depth <pass 1's REVIEW_DEPTH>`;
|
||||
its `NEXT:` alone picks the next decision: `stop` 2, `full` 3, `fix-check` 3.5, `done` 4.
|
||||
`scoped` is 3 too. A full pass 1 always gets a full pass 2.
|
||||
Other scopes apply decisions 2–4 in order.
|
||||
|
||||
1. **Dispatched reviewer output missing:** STOP and name each failed specialist or
|
||||
|
||||
@@ -14,6 +14,8 @@ Finish the complete review and QA before applying any fix in Step 9.4.
|
||||
**Review plan.** Pass 1 runs `~/.claude/skills/gstack/bin/gstack-review-plan
|
||||
--base <base>`, plus `--depth-floor <d>` on every plan run when the /ship prompt
|
||||
gave `--depth-floor <d>` (it only raises); a repeat uses the plan run that picked it.
|
||||
A `scoped` repeat is a full pass on the range: its `fix-check` is decision 3.5's
|
||||
reviewer and prompt with `git diff $RANGE_BASE` as the hunks, inside this pass.
|
||||
Save its `REVIEW_PLAN:` line and fingerprint, `REVIEW_DEPTH:` (depth,
|
||||
`runtime_lines`, `rules`) and `GATE_SCAN:`. Dispatch exactly its `DISPATCH:` lines
|
||||
(specialists, Red Team, Step 11's voices), each at its `effort=` on the same model
|
||||
@@ -89,7 +91,9 @@ but missing dispatched output still blocks continuation, even with a QA exceptio
|
||||
|
||||
0. **Scope the findings.** Write the unmatched or reopened findings from Step 9.3
|
||||
(all sources, queued Steps 10–11 included) as a JSON array to
|
||||
`.gstack/tmp/review-findings-p$PASS.json` with your file-write tool, then:
|
||||
`.gstack/tmp/review-findings-p$PASS.json` with your file-write tool, each with
|
||||
`raised_by` (`core`, `qa`, a specialist, `red-team`, `adversarial-native`,
|
||||
`outside-voice` or `fix-check`), then:
|
||||
`~/.claude/skills/gstack/bin/gstack-review-scope classify --base <base> --pass $PASS --range-base $RANGE_BASE --findings .gstack/tmp/review-findings-p$PASS.json --json > .gstack/tmp/review-classified-p$PASS.json`
|
||||
(omit `--range-base` on pass 1). Every Step 9.4 scratch file lives under
|
||||
`.gstack/tmp/` (gitignored): `<classified>` below is this pass's classified
|
||||
@@ -190,6 +194,7 @@ but missing dispatched output still blocks continuation, even with a QA exceptio
|
||||
else `issues_found`. N counts unresolved defects now, not original totals; missing
|
||||
coverage, deferred and noted findings are not defects. `SCOPE` is the policy
|
||||
(`frozen`, `diff-plus-blocking` or `all`); `RANGE_BASE_OR_NULL` is `"<sha>"` from pass 2, else `null`.
|
||||
- A `scoped` pass adds `"scope":"scoped"`; it is a full-pass row and counts toward the cap.
|
||||
- `REVIEW_START`: this pass's Step 9 token captured before reading the diff;
|
||||
never recapture at persistence to certify unreviewed fixes.
|
||||
- `COMPLETED`: checklist and dispatched specialists/Red Team finish, and all required probes pass.
|
||||
@@ -205,7 +210,7 @@ but missing dispatched output still blocks continuation, even with a QA exceptio
|
||||
`{"fingerprint":"path:line:category","severity":"CRITICAL|INFORMATIONAL","action":"ACTION"}`.
|
||||
ACTION: `"auto-fixed"`, `"fixed"` (approved), `"skipped"` (explicit Skip),
|
||||
`"deferred"` (with `issue`, the filed number or `null`) or `"noted"`. Copy
|
||||
`locus`, `kind` and `blocking` from the classified output onto every finding.
|
||||
`locus`, `kind`, `blocking` and `raised_by` from the classified output onto every finding.
|
||||
Merge revalidated invocation decisions by identity and advisory/defect kind;
|
||||
preserve `advisory`, `evidence_paths` and `helper_target`.
|
||||
Gate findings add `gate` (RH tag), `gate_id` (the tool's hunk identity), `reason`
|
||||
@@ -223,8 +228,9 @@ Decision 1 always applies first. Then, under `frozen`, run `gstack-review-plan -
|
||||
with each flag that holds (`--fixed-blocking`, `--fixed-below`, `--blocking-open`,
|
||||
`--touched-gate` for a fix to an item 2.5 path, `--critical-found` when this pass
|
||||
logged a CRITICAL, `--adversarial-ran` once Step 11's native pass ran, `--reviewed-numbers <fp>`,
|
||||
`--depth-floor <d>` when given);
|
||||
`--depth-floor <d>` when given), `--range-base $PASS_HEAD` and `--pass1-depth <pass 1's REVIEW_DEPTH>`;
|
||||
its `NEXT:` alone picks the next decision: `stop` 2, `full` 3, `fix-check` 3.5, `done` 4.
|
||||
`scoped` is 3 too. A full pass 1 always gets a full pass 2.
|
||||
Other scopes apply decisions 2–4 in order.
|
||||
|
||||
1. **Dispatched reviewer output missing:** STOP and name each failed specialist or
|
||||
|
||||
+103
-1
@@ -3,7 +3,8 @@ 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 { classifyDiff, classifyPath, depthFor, depthOfCommitInput, diffCommand, nextStep, planReview, scanNumbers, DEPTH_RULES, FULL_PASS_CAP, type DepthError, type PlanInput } from '../lib/review-depth';
|
||||
import { classifyDiff, classifyPath, depthFor, depthOfCommitInput, diffCommand, nextStep, planReview, reviewPlanLine, scanNumbers, DEPTH_RULES, FULL_PASS_CAP, SCOPED_RANGE_LINES, type DepthError, type PlanInput } from '../lib/review-depth';
|
||||
import { summarizeReviewLoop } from '../lib/review-scope';
|
||||
|
||||
const ROOT = path.join(import.meta.dir, '..');
|
||||
const none = scanNumbers({});
|
||||
@@ -289,3 +290,104 @@ describe('review depth: diff commands and the rules hash', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('D6: the scoped second pass after a blocking fix (GSTA-152 PR B)', () => {
|
||||
const r = (p: ReturnType<typeof planReview>) => p.dispatches.map(d => `${d.role}:${d.name}`);
|
||||
const fixed = (rangeRuntimeLines: number | null | undefined, o: Partial<PlanInput['history']> = {}): PlanInput['history'] => ({ fullPasses: 1, last: 'full', fixedBlocking: true, rangeRuntimeLines, ...o });
|
||||
// gstack-evals#19: a standard diff, pass 1 fixed one CRITICAL from a QA probe, the fix range was 3 files +18/-6 of runtime.
|
||||
const n19 = (o: Partial<PlanInput> = {}) => mixed({ perPath: per({ 'src/a.ts': 300, 'src/b.ts': 100, 'src/c.ts': 60, 'test/a.test.ts': 80 }), diffLines: 540, scope: { SCOPE_BACKEND: 'true' }, history: fixed(24), adversarialRan: true, ...o });
|
||||
|
||||
test('the #19 shape plans one fix-check, the Red Team and the native adversarial pass, not the specialist army', () => {
|
||||
const before = planReview(n19({ history: { fullPasses: 1, last: 'full', fixedBlocking: true } }));
|
||||
expect(before.next.step).toBe('full');
|
||||
expect(before.dispatches.length).toBeGreaterThanOrEqual(6);
|
||||
const p = planReview(n19());
|
||||
expect(p.next.step).toBe('scoped');
|
||||
expect(p.next.reason).toContain('24 runtime lines');
|
||||
expect(r(p)).toEqual(['fix-check:fix-check', 'red-team:red-team', 'adversarial-native:adversarial']);
|
||||
expect(p.dispatches.every(d => d.effort === 'full' && d.exclude.length === 0)).toBe(true);
|
||||
});
|
||||
test('Step 11 voices that have not run yet stay listed, as on a full pass 2', () => {
|
||||
const p = planReview(n19({ adversarialRan: false }));
|
||||
expect(r(p)).toEqual(['fix-check:fix-check', 'red-team:red-team', 'adversarial-native:adversarial', 'outside-voice:outside-voice']);
|
||||
expect(p.skipped).toContain('specialists (scoped pass)');
|
||||
expect(planReview(n19()).skipped).toEqual(expect.arrayContaining(['specialists (scoped pass)', 'outside-voice (scoped pass)']));
|
||||
expect(planReview(n19({ force: ['security'] })).skipped).toContain('specialists (scoped pass; forced ones dispatched)');
|
||||
});
|
||||
test.each([
|
||||
['full depth (a gate tag)', { gateTagged: 1 }],
|
||||
['a fail-closed classification', { gateTagged: null }],
|
||||
['--depth-floor full', { floor: 'full' as const }],
|
||||
['a 151-line range', { history: fixed(151) }],
|
||||
['a missing range', { history: fixed(undefined) }],
|
||||
['an unread range', { history: fixed(null) }],
|
||||
['a fix that touched a gate-class path', { history: fixed(10, { touchedGate: true }) }],
|
||||
])('%s keeps the full pass 2', (_n, o) => {
|
||||
const p = planReview(n19(o as Partial<PlanInput>));
|
||||
expect(p.next.step).toBe('full');
|
||||
expect(p.dispatches.some(d => d.role === 'specialist')).toBe(true);
|
||||
});
|
||||
test('a force-included specialist and changed published numbers keep their reviewers', () => {
|
||||
expect(r(planReview(n19({ force: ['security'] })))).toEqual(['fix-check:fix-check', 'specialist:security', 'red-team:red-team', 'adversarial-native:adversarial']);
|
||||
const numbers = scanNumbers({ 'docs/bench.md': ['3x faster'] });
|
||||
expect(r(planReview(n19({ numbers, reviewedNumbers: 'old' })))).toContain('outside-voice:outside-voice (numbers changed)');
|
||||
expect(r(planReview(n19({ numbers, reviewedNumbers: numbers.fingerprint })))).not.toContain('outside-voice:outside-voice (numbers changed)');
|
||||
});
|
||||
test('150 is scoped, 151 is full, and a docs-only range is scoped without the native adversarial pass', () => {
|
||||
expect(SCOPED_RANGE_LINES).toBe(150);
|
||||
expect(planReview(n19({ history: fixed(150) })).next.step).toBe('scoped');
|
||||
expect(planReview(n19({ history: fixed(151) })).next.step).toBe('full');
|
||||
const docs = planReview(n19({ history: fixed(0) }));
|
||||
expect(docs.next.step).toBe('scoped');
|
||||
expect(r(docs)).toEqual(['fix-check:fix-check', 'red-team:red-team']);
|
||||
});
|
||||
test('a light diff with a fixed blocker gets the scoped set: the raise adds the Red Team, never the standard army', () => {
|
||||
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 30 }), history: fixed(12), adversarialRan: true }));
|
||||
expect(p.next.step).toBe('scoped');
|
||||
expect(r(p)).toEqual(['fix-check:fix-check', 'red-team:red-team', 'adversarial-native:adversarial']);
|
||||
});
|
||||
test('the scoped pass counts toward the cap, and a blocker it leaves stops for the owner', () => {
|
||||
expect(nextStep(fixed(10, { fullPasses: FULL_PASS_CAP }), 'standard').step).toBe('fix-check');
|
||||
expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', blockingOpen: true, rangeRuntimeLines: 10 }, 'light').step).toBe('stop');
|
||||
const at = (m: number) => new Date(Date.UTC(2026, 9, 28, 10, m)).toISOString();
|
||||
const rows = [
|
||||
{ skill: 'review', via: 'ship', timestamp: at(1), pass: 1, dispatches: [{ role: 'specialist' }, { role: 'adversarial-native' }] },
|
||||
{ skill: 'review', via: 'ship', timestamp: at(2), pass: 2, scope: 'scoped', dispatches: [{ role: 'fix-check' }, { role: 'red-team' }, { role: 'adversarial-native' }] },
|
||||
];
|
||||
const s = summarizeReviewLoop(rows, at(0));
|
||||
expect(s.passes).toBe(2);
|
||||
expect(s.fix_checks).toBeUndefined();
|
||||
expect(s.dispatches).toEqual({ specialist: 1, 'adversarial-native': 2, 'fix-check': 1, 'red-team': 1 });
|
||||
});
|
||||
test('nextStep without a depth keeps the full pass (planReview is where depth is known)', () => {
|
||||
expect(nextStep(fixed(10)).step).toBe('full');
|
||||
expect(nextStep(fixed(10), 'full').step).toBe('full');
|
||||
expect(nextStep(fixed(10), 'standard').step).toBe('scoped');
|
||||
});
|
||||
test('REVIEW_PLAN prints the chosen step and its reason', () => {
|
||||
const line = reviewPlanLine(planReview(n19()), 2);
|
||||
expect(line).toMatch(/^REVIEW_PLAN: class=code pass=2 next=scoped dispatches=3 numbers=no fingerprint=none reason="[^"]*24 runtime lines[^"]*"$/);
|
||||
expect(reviewPlanLine(planReview(n19({ history: fixed(151) })), 2)).toContain('reason="a blocking finding was fixed');
|
||||
});
|
||||
test('DEPTH_RULES changes when the scoped-range constant does', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'depth-rules-scoped-'));
|
||||
try {
|
||||
const src = fs.readFileSync(path.join(ROOT, 'lib/review-depth.ts'), 'utf8');
|
||||
expect(src).toContain('SCOPED_RANGE_LINES = 150');
|
||||
fs.symlinkSync(path.join(ROOT, 'lib/ship-policy.ts'), path.join(dir, 'ship-policy.ts'));
|
||||
fs.writeFileSync(path.join(dir, 'review-depth.ts'), src.replace('SCOPED_RANGE_LINES = 150', 'SCOPED_RANGE_LINES = 151'));
|
||||
fs.writeFileSync(path.join(dir, 'print.ts'), "import { DEPTH_RULES } from './review-depth';\nconsole.log(DEPTH_RULES);\n");
|
||||
const res = spawnSync(process.execPath, ['--no-env-file', '--config=/dev/null', path.join(dir, 'print.ts')], { encoding: 'utf8', timeout: 30_000 });
|
||||
expect(res.status, res.stderr).toBe(0);
|
||||
expect(res.stdout.trim()).not.toBe(DEPTH_RULES);
|
||||
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
test('consumers name the scoped step: the Step 9 template and the transitions table', () => {
|
||||
const tmpl = fs.readFileSync(path.join(ROOT, 'ship/sections/review-army.md.tmpl'), 'utf8');
|
||||
expect(tmpl).toContain('--range-base $PASS_HEAD');
|
||||
expect(tmpl).toContain('`scoped` is 3 too.');
|
||||
expect(tmpl).toContain('"scope":"scoped"');
|
||||
expect(tmpl).toContain('raised_by');
|
||||
const policy = fs.readFileSync(path.join(ROOT, 'docs/review-policy.md'), 'utf8');
|
||||
expect(policy).toContain('| a full pass that fixed a blocking finding, below the cap, on a diff below full depth with a fix range of at most 150 runtime lines | scoped pass on the range |');
|
||||
});
|
||||
});
|
||||
@@ -157,3 +157,88 @@ describe('gstack-review-plan: review depth', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-review-plan: the scoped second pass (GSTA-152 D6)', () => {
|
||||
const code = (n: number, tag = 'v') => Array.from({ length: n }, (_, i) => `export const ${tag}${i} = ${i};`).join('\n') + '\n';
|
||||
function fixed(lines: number): { r: { dir: string; state: string }; rangeBase: string } {
|
||||
const r = repo({ 'src/a.ts': code(80) });
|
||||
const git = (...a: string[]) => spawnSync('git', a, { cwd: r.dir, encoding: 'utf8', timeout: 10_000 });
|
||||
const rangeBase = git('rev-parse', 'HEAD').stdout.trim();
|
||||
fs.writeFileSync(path.join(r.dir, 'src', 'fix.ts'), code(lines, 'f'));
|
||||
git('add', '.'); git('commit', '-qm', 'fix: pre-landing review fixes');
|
||||
return { r, rangeBase };
|
||||
}
|
||||
const pass2 = ['--pass', '1', '--last', 'full', '--fixed-blocking', '--adversarial-ran', '--pass1-depth', 'standard'];
|
||||
const full1 = ['--pass', '1', '--last', 'full', '--fixed-blocking', '--adversarial-ran', '--pass1-depth', 'full'];
|
||||
test('--range-base with a small fix range plans NEXT: scoped with a fix-check and the Red Team, and counts it as pass 2', () => {
|
||||
const { r, rangeBase } = fixed(24);
|
||||
const out = run(r, [...pass2, '--range-base', rangeBase]).stdout;
|
||||
expect(out).toMatch(/^REVIEW_PLAN: class=code pass=2 next=scoped dispatches=3 /m);
|
||||
expect(out).toContain(`FIX_RANGE: runtime_lines=24 base=${rangeBase}`);
|
||||
expect(out).toMatch(/^NEXT: scoped \(a blocking finding was fixed on a standard diff with 24 runtime lines in the fix range/m);
|
||||
expect(out).toContain('DISPATCH: fix-check fix-check effort=full');
|
||||
expect(out).toContain('DISPATCH: red-team red-team effort=full');
|
||||
expect(out).toContain('DISPATCH: adversarial-native adversarial effort=full');
|
||||
expect(out).not.toContain('DISPATCH: specialist');
|
||||
expect(JSON.parse(run(r, [...pass2, '--range-base', rangeBase, '--json']).stdout)).toMatchObject({ next: { step: 'scoped' }, fix_range: { base: rangeBase, runtime_lines: 24 } });
|
||||
});
|
||||
test('a 151-line range, no --range-base, an unknown range base and uncommitted fixes keep the full pass 2', () => {
|
||||
const big = fixed(151);
|
||||
expect(run(big.r, [...pass2, '--range-base', big.rangeBase]).stdout).toMatch(/^NEXT: full \(a blocking finding was fixed and the fix range has 151 runtime lines \(> 150\)/m);
|
||||
const { r, rangeBase } = fixed(10);
|
||||
expect(run(r, [...pass2.slice(0, -2), '--range-base', rangeBase]).stdout).toMatch(/^NEXT: full \(a blocking finding was fixed and pass 1 ran at full depth/m);
|
||||
expect(run(r, pass2).stdout).toMatch(/^NEXT: full \(a blocking finding was fixed and the fix range was not read/m);
|
||||
const unknown = run(r, [...pass2, '--range-base', 'f'.repeat(40)]).stdout;
|
||||
expect(unknown).toMatch(/^FIX_RANGE: unread \(.*\) base=f{40}$/m);
|
||||
expect(unknown).toMatch(/^NEXT: full /m);
|
||||
fs.writeFileSync(path.join(r.dir, 'src', 'fix.ts'), code(12, 'g'));
|
||||
const dirty = run(r, [...pass2, '--range-base', rangeBase]).stdout;
|
||||
expect(dirty).toMatch(/^FIX_RANGE: unread \(uncommitted or untracked edits.*\)/m);
|
||||
expect(dirty).toMatch(/^NEXT: full /m);
|
||||
expect(run(r, [...pass2, '--range-base']).status).toBe(2);
|
||||
});
|
||||
test('a fix range that touches a gate path keeps the full pass 2 without --touched-gate, and an empty range is unread', () => {
|
||||
const r = repo({ 'src/a.ts': code(80), '.github/workflows/ci.yml': 'on: push\n' });
|
||||
const git = (...a: string[]) => spawnSync('git', a, { cwd: r.dir, encoding: 'utf8', timeout: 10_000 });
|
||||
const rangeBase = git('rev-parse', 'HEAD').stdout.trim();
|
||||
expect(run(r, [...pass2, '--range-base', rangeBase]).stdout).toMatch(/^FIX_RANGE: unread \(the fix range is empty: .*\) base=/m);
|
||||
git('rm', '-q', '.github/workflows/ci.yml');
|
||||
fs.writeFileSync(path.join(r.dir, 'src', 'b.ts'), code(2, 'b'));
|
||||
git('add', '.'); git('commit', '-qm', 'fix: revert the workflow edit');
|
||||
const out = run(r, [...pass2, '--range-base', rangeBase]).stdout;
|
||||
expect(out).toMatch(/^REVIEW_DEPTH: standard /m);
|
||||
expect(out).toMatch(/^NEXT: full \(a blocking finding was fixed and a fix touched a gate-class path/m);
|
||||
expect(JSON.parse(run(r, [...pass2, '--range-base', rangeBase, '--json']).stdout)).toMatchObject({ next: { step: 'full' }, fix_range: { touched_gate: true } });
|
||||
}, 30_000);
|
||||
test.each([
|
||||
['an auth revert', 'src/auth/login.ts', 'export const login = (u: string) => u.length > 0;\n'],
|
||||
['a migration revert', 'db/migrate/001_a.sql', 'ALTER TABLE users ADD COLUMN email text;\n'],
|
||||
])('%s on a full-depth PR plans the full pass 2', (_n, file, text) => {
|
||||
const r = repo({ 'src/a.ts': code(80) });
|
||||
const git = (...a: string[]) => spawnSync('git', a, { cwd: r.dir, encoding: 'utf8', timeout: 10_000 });
|
||||
const write = (body: string) => { fs.mkdirSync(path.dirname(path.join(r.dir, file)), { recursive: true }); fs.writeFileSync(path.join(r.dir, file), body); };
|
||||
git('checkout', '-q', 'main'); write(text); git('add', '.'); git('commit', '-qm', 'base file');
|
||||
git('checkout', '-q', 'feat'); git('merge', '-q', '--no-edit', 'main');
|
||||
write(text + '-- edited\n'); git('add', '.'); git('commit', '-qm', 'feat edit');
|
||||
const rangeBase = git('rev-parse', 'HEAD').stdout.trim();
|
||||
write(text);
|
||||
fs.writeFileSync(path.join(r.dir, 'src', 'b.ts'), code(2, 'b'));
|
||||
git('add', '.'); git('commit', '-qm', 'fix: revert the edit');
|
||||
const out = run(r, [...full1, '--range-base', rangeBase]).stdout;
|
||||
expect(out).not.toMatch(/^REVIEW_CLASS: gate/m);
|
||||
expect(out).toMatch(/^NEXT: full /m);
|
||||
}, 30_000);
|
||||
test('a fix that un-skips a gate tag added in an earlier pass-1 commit plans the full pass 2 on a full-depth PR', () => {
|
||||
const r = repo({ 'src/a.ts': code(80), 'test/a.test.ts': "test.skip('x', () => {});\n" });
|
||||
const git = (...a: string[]) => spawnSync('git', a, { cwd: r.dir, encoding: 'utf8', timeout: 10_000 });
|
||||
fs.writeFileSync(path.join(r.dir, 'src', 'c.ts'), code(3, 'c')); git('add', '.'); git('commit', '-qm', 'later pass-1 commit');
|
||||
const rangeBase = git('rev-parse', 'HEAD').stdout.trim();
|
||||
fs.writeFileSync(path.join(r.dir, 'test', 'a.test.ts'), "test('x', () => {});\n");
|
||||
fs.writeFileSync(path.join(r.dir, 'src', 'y.ts'), code(1, 'y'));
|
||||
git('add', '.'); git('commit', '-qm', 'fix: un-skip');
|
||||
expect(run(r, [...full1, '--range-base', rangeBase]).stdout).toMatch(/^NEXT: full \(a blocking finding was fixed and pass 1 ran at full depth/m);
|
||||
}, 30_000);
|
||||
test('--depth-floor full opts out of the scoped pass', () => {
|
||||
const { r, rangeBase } = fixed(10);
|
||||
expect(run(r, [...pass2, '--range-base', rangeBase, '--depth-floor', 'full']).stdout).toMatch(/^NEXT: full \(a blocking finding was fixed and the diff is at full depth/m);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import {
|
||||
isBlocking, kindOf, locusOf, parseHunks, renderDeferredIssue, renderDeferredSection, reviewLoopLine, scopeFindings, scopeLine, summarizeReviewLoop,
|
||||
isBlocking, kindOf, locusOf, parseHunks, raisedBy, renderDeferredIssue, renderDeferredSection, reviewLoopLine, scopeFindings, scopeLine, summarizeReviewLoop,
|
||||
type ScopedFinding,
|
||||
} from '../lib/review-scope';
|
||||
import { cleanup, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
||||
@@ -421,3 +421,17 @@ describe('gstack-review-scope (bin)', () => {
|
||||
expect(late.status).toBe(EXIT.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('raised_by: which reviewer raised a classified finding (GSTA-152 D6)', () => {
|
||||
test.each([
|
||||
[{ raised_by: 'red-team' }, 'red-team'], [{ raised_by: 'QA' }, 'qa'], [{ raised_by: 'core' }, 'core'], [{ raised_by: 'fix-check' }, 'fix-check'],
|
||||
[{ specialist: 'State Machines' }, 'state-machines'], [{ specialist: 'red-team' }, 'red-team'], [{ specialist: 'adversarial' }, 'adversarial-native'],
|
||||
[{ specialist: 'outside-voice' }, 'outside-voice'], [{ raised_by: 'nonsense\n', specialist: 'testing' }, 'testing'], [{ specialist: 42 }, 'unknown'], [{}, 'unknown'],
|
||||
])('%j -> %s', (f, want) => expect(raisedBy(f as any)).toBe(want));
|
||||
test('classify stamps raised_by on every finding, and an older row without it still summarizes', () => {
|
||||
const scoped = scopeFindings([{ fingerprint: 'src/a.ts:3:logic', path: 'src/a.ts', line: 3, severity: 'CRITICAL', specialist: 'security' }, { fingerprint: 'src/a.ts:9:logic', path: 'src/a.ts', line: 9, severity: 'CRITICAL' }], { policy: 'frozen', pass: 1, diffHunks: { 'src/a.ts': 'whole' }, rangeHunks: null });
|
||||
expect(scoped.map(f => f.raised_by)).toEqual(['security', 'unknown']);
|
||||
const old = { skill: 'review', via: 'ship', timestamp: '2026-10-28T10:00:00Z', findings: [{ fingerprint: 'src/a.ts:3:logic', blocking: true, action: 'fixed' }] };
|
||||
expect(summarizeReviewLoop([old], '2026-10-28T09:00:00Z')).toMatchObject({ passes: 1, blocking_found: 1, fixed: 1 });
|
||||
});
|
||||
});
|
||||
@@ -205,3 +205,19 @@ describe('the review-loop exception (#3559)', () => {
|
||||
expect(!offBranch.ok && offBranch.message).toContain('is not the receipt head or an ancestor of it');
|
||||
}, 90_000);
|
||||
});
|
||||
|
||||
describe('the queue and a scoped second pass (GSTA-152 D6)', () => {
|
||||
// Value: protects=a receipt whose pass 2 was scoped (fix-check, Red Team, native adversarial; no specialists) READYs at the depth
|
||||
// its head needs, with no schema change; fails_when=the queue reads review_loop.dispatches or passes as a depth signal; seam=bindShippedReceipt
|
||||
test('a scoped pass 2 receipt at the required depth binds, its dispatch roles recorded as written', () => {
|
||||
const repo = makeFixtureRepo(); repos.push(repo);
|
||||
const head = commit(repo, { 'lib/big.js': lines(80) });
|
||||
const body = path.join(repo.root, 'body.md');
|
||||
const loop = { passes: 2, blocking_found: 1, deferred: 0, fixed: 1, scope: 'frozen', dispatches: { specialist: 4, 'adversarial-native': 2, 'outside-voice': 1, 'fix-check': 1, 'red-team': 1 } };
|
||||
expect(writeReceipt(repo, body, ['--review-loop', JSON.stringify(loop)]).status).toBe(EXIT.ok);
|
||||
expect(receiptIn(body).review_loop).toMatchObject({ passes: 2, dispatches: loop.dispatches });
|
||||
const bound = bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8'), '7', [head]);
|
||||
expect(bound.ok).toBe(true);
|
||||
expect(bound.ok && bound.depthLine).toContain('REVIEW_DEPTH: receipt=standard required=standard');
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"bytes": 521,
|
||||
"why": "Step 9 passes --pass1-depth: a full pass 1 always gets a full pass 2 (GSTA-152 PR B)"
|
||||
}
|
||||
Reference in new issue
Block a user