Merge remote-tracking branch 'origin/gsta-167-calls-1' into gsta-167-calls-2

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

# Conflicts:
#	capy/skill/gstack/SKILL.md
This commit is contained in:
garrytan committed 2026-10-31 05:33:28 +00:00
commit 2996c1dfa8
70 files changed
+1327 -218

No files matched your search

+62
View File
@@ -1,5 +1,67 @@
# Changelog
## [1.91.350.0] - 2026-10-31
**`/ship` review costs less and catches more: one batched specialist, a blocking floor in both prose voices, and data files that no code reads review as docs (GSTA-167 variant G).** The no-loss replay certified the stack before merge. Round 5 replayed 14 PRs with 29 known blockers, and the new reviewers found 24 against the control's 21, non-inferior at p=0.035. Round 6 pooled 41 blockers (the 29 plus 12 confirmed units from GSTA-2): 33 against 27, non-inferior at p=0.0055. The free re-score for the classifier and state-matrix change lost 0 blockers.
### Changed
- **One batched specialist reads every checklist at each depth.** It always reads Testing and Security on code and gate diffs, and adds State-machines, Data-migration, API-contract and Design when scope asks for them. Maintainability, Performance and Simplification run only when forced, and `gstack-review-plan` names them on a `SKIP:` line. The Red Team reads the batched reader's merged findings.
- **Specialists run at medium reasoning effort.** The Red Team, the native adversarial pass and the fix-check stay at full effort. `--no-tiers` (or `review_effort_tiers off`) runs specialists at the host default.
- **Both prose voices carry the blocking floor.** The native adversarial and outside-voice prompts say a finding blocks only if it would fail a test, break production, or lose data or secrets, and end each finding with `Severity:` and `Blocks: yes|no`. A finding is CRITICAL when its old label rule says so or it says `Blocks: yes`, and `Blocks: no` never lowers one. `gstack-review-dispatch collect` records a `Blocks: yes` finding as CRITICAL, so `judge` refuses a lead judgment that keeps it below the floor.
- **A JSON, CSV, TSV or raster data file that no code names counts as docs, not runtime lines.** `classifyDiff` finds code consumers with `git grep`; YAML, TOML, SVG and config files stay code, and path-only callers (the receipt's gate scan, the release route) keep data files code. `gstack-ship-preflight`'s `RUNTIME_LINES` now comes from the same function.
- **An untested state-matrix cell is a test gap below the floor.** A wrong row or collapsed kinds stays CRITICAL, and the state-machines checklist itself is unchanged.
### Fixed
- **`gstack-review-dispatch render --checklists` takes the plan's checklist names** as well as paths, and refuses an unknown one. The outside-voice brief no longer adds a separate severity line, since the prompt now asks for Severity and Blocks itself.
- **`collect` keeps one finding per id in the layout the prompts ask for.** A `Severity:` or `Blocks:` line below a finding joins that finding, an inline `Blocks: yes` with no label still collects as CRITICAL, and the native voice's FIXABLE collects as CRITICAL even beside a lower `Severity:`. When the layout is unclear, `collect` merges findings rather than dropping one.
- **The data-file consumer scan reads `git grep` the same way under any git config.** `grep.lineNumber`, `grep.column` and `color.grep` no longer hide a consumer, and a failed scan keeps every data file code with `consumer_scan_failed`.
### Tests
- `test/prose-voice-floor.test.ts` renders both prose prompts for the claude and codex hosts and checks the floor sentence and the Blocks line. `test/review-depth.test.ts` covers the batched reader on the captured fixtures, effort per role and the data-file rule. `test/review-dispatch.test.ts` covers `Blocks: yes` collection and batched checklist names. `test/review-packets-removed.test.ts` repins the changed briefs to their no-loss replays (PR-a, G1, L).
## [1.91.349.0] - 2026-10-31
**Nine flaky tests from the 2026-10-30 flaky pass stop failing correct runs.** Two paid detectors missed correct replies, one paid case lost the evidence of its own failure, five free tests raced their own fixtures or paid a cold start inside a timed call, and two Windows failures came from real defects: a lost IPC receipt in the QA deadline worker and a Chromium feature that fails loopback connections.
### Fixed
- **`lib/qa-deadline.ts`**: the receipt worker waits for the parent to acknowledge each receipt before it exits. Bun can drop an IPC message the parent has not read yet when the child exits, so on Windows `gstack-qa-deadline run` and `gstack-qa-evidence` could return the exit code without the `finished` receipt (Windows Free Tests 40991662945, `test/qa-deadline.test.ts:431` received one of two receipts). A receipt still unacknowledged after the existing 5 s write wait makes the worker exit 2, as an unwritten receipt did before.
- **`patches/playwright-core@1.62.1.patch`**: backports microsoft/playwright#43605, which disables Chromium's `TcpPortRandomizationWin`. On Windows it fails loopback page loads with `net::ERR_NO_BUFFER_SPACE` even when ports are free (`browse/test/cookie-picker-ui.test.ts`, Windows Free Tests 40656299153 and 41056150287). gstack's own browser launches on Windows had the same exposure.
- **`test/helpers/qa-only-cleanup.ts`**: the cleanup check reads the daemon state file once and treats a file deleted during shutdown as absent, instead of checking that it exists and then reading it (Free Tests 40964422549: ENOENT on `browse.json`).
- **`test/claude-code-runner.test.ts`**: the liveness helper counts a process in the `X` (being reaped) state as dead, like `Z`, and the cleanup no longer kills a pid that is already gone (Free Tests 41034844588: ESRCH).
- **`test/helpers/free-owned-browser-settlement.ts`**: in the `cancel-vanishing-record` case the fake daemon waits for the observer to remove its record before it exits, so the race the case injects always happens (Free Tests 40924799742).
- **`test/investigate-freeze-path.test.ts`**: the E1 cases pay the first bash and JSON interpreter start once, in an unasserted warm-up, before the timed hook calls. In four Windows runs (40824965988, 40917695070, 40985163846, 40996480693) only the file's first hook call hit the 15 s kill, the second took 1.9 to 7.9 s and the rest 0.3 to 1 s. Each timed call keeps its 15 s kill.
### Gate integrity
- **ship-unmanaged-hook-consent** (`test/helpers/ship-hook-actor.ts`): the decline check also credits "no hook/change/file was installed, changed, modified, overwritten or replaced". Gate 41005113772 said "No hook was installed or changed" and "nothing was overwritten" after a correct decline; its installs, hook bytes and question checks all passed. The four negative controls still fail.
- **tpa-absent-darwin** (`test/helpers/third-party-actions.ts`): the Aside download pitch also counts in the passive voice at a clause start ("Aside ... can be downloaded at aside.com", gate 40987391676). Reported and mid-clause forms still count zero, and so does a clause with "not", "no", "never" or "n't" before the phrase.
### Changed
- **autoplan-guard-pty** (`test/helpers/autoplan-guard-pty.ts`, `test/skill-e2e-autoplan-guard-pty.test.ts`): a `dispatch_prompt` deny prints each Agent dispatch's length, first differing offset and context next to the prompt the actor had to copy, read before cleanup deletes it. Gate 40992022983 lost those bytes because the journal summary cuts inputs at 160 characters.
### Tests
- New captures: `test/fixtures/ship-hook-consent-2026-10-30.json` and a passive-voice capture in `test/fixtures/third-party-actions-pitch-captures.json`; both tests fail with their detector change reverted. `browse/test/playwright-core-patch.test.ts` checks the backport in the patch and the installed tree, and fails without it. `test/autoplan-guard-pty-fixture.test.ts` checks the dispatch delta on a synthetic journal. `test/qa-deadline.test.ts` plays the parent, so it now acknowledges each receipt; its assertions are unchanged, and a new case shows a worker whose parent never acknowledges exits 2 after sending both receipts.
## [1.91.348.0] - 2026-10-31
- **Pregate now maps tests to dotfiles they name.** A test that reads `../.gitleaks.toml`, `.gitleaks.toml` or `join('.github', 'workflows', 'ci.yml')` used to get no edge to that file, so a branch touching only the dotfile ended in `PREGATE_NO_LANE`. Quoted literals may now start with a dot, and a dot-led literal that isn't `./` or `../` resolves from both the repository root and the folder of the file that names it, so a fixture's own `.github/` keeps its edge. A literal still forms an edge only when it names a file in the repo, so strings like `'.ts'` add nothing. On gstack this adds 179 edges (`buildGraph` over this repository: 16,934 on main 2127729, 17,113 with this change, none removed), all to real dotfiles such as `.github/workflows/*.yml` and `.gstack/*.json`. (GST-246, reported from garrytan/truther#956.)
## [1.91.347.0] - 2026-10-31
**A Captain archives only its own crew's threads (R39, amended). A thread outside every crew belongs to the owner and stays as it is, even when the Captain's own automation created it.**
### Changed
- **docs/captain.md and the Capy factory context (`factory.md` point 12)** now say a Captain archives only the threads in its own crew, each when its plan for that thread says the work is done. No Captain archives, flags or lists a thread outside every crew for archiving unless the owner asks. The earlier wording also let a Captain archive threads its automations started outside the crew, and that emptied the owner's unattached threads.
## [1.91.346.0] - 2026-10-31
**`sealed.local_path` now flags a home path inside a shell default, `${UBI_RUNNER:-/home/<user>…}`, as it already flagged the same path after `?? "/home/<user>…"` in TypeScript.** gstack-evals `quick/laneb/runner/launch.sh:9` passed the scan because the `-` before the path counted as part of a word (#3568).
### Fixed
- **`sealed.local_path`** (`lib/sealed-guard.ts`): a home path directly after a `${…:-` or `${…-` default now matches, for a name, an array element (`${arr[0]:-`, a subscript of up to 64 characters, so a long line of unclosed `${a[` cannot make the scan quadratic), an indirect name (`${!ref:-`), a positional number or a special parameter (`${@:-`). `:=`, `:+` and `:?` already matched, because `=`, `+` and `?` were never word characters. A hyphen anywhere else still excludes the match, so a path glued to a word (`docs-…`, `$VAR-…`, `${VAR}-…`) stays unflagged; docs/pregate.md lists that as a known limit.
### Gate integrity
- This is a gate edit that only tightens. The guard now refuses lines it passed before. No existing test, threshold or allowlist changed. A new case in `test/sealed-guard.test.ts` covers all five operators, a positional parameter, `${@`, an array element, an indirect name, a Windows path and the TypeScript `??` form. It also checks six negative controls, which are two hyphenated paths, a hyphenated sentence, `$VAR-`, `${VAR}-` and a relative default. Without the fix it misses every `-` form.
## [1.91.345.0] - 2026-10-31
**A capped `/ship` on Capy now spends fewer agent turns and counts only its own tokens, while every reviewer still gets exactly the brief 1.91.331.0 gives it.** Two new commands do `/ship`'s fixed shell work, a short driver file replaces reading the whole skill when a parent agent runs `/ship`, and the budget measures each reviewer batch on its own instead of across the parent's work. Replays of gstack-evals #34 and #36 (GSTA-167) spent 12.19M tokens on Steps 12-17 and 0.25-0.4M on each parent turn; these changes cut turns, not reviewer context.
+1 -1
View File
@@ -1 +1 @@
1.91.345.0
1.91.350.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.345.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.350.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,
+4 -2
View File
@@ -31,7 +31,7 @@ const HELP = `gstack-review-dispatch — verbatim reviewer briefs and loss-free
roles: ${BRIEF_ROLES.join(', ')}
slots: --diff-base <sha> (every role but fix-check) --exclude '<DIFF_CMD tail>' --stack <s> --test-fw <s>
--learnings-file <f> --checklist <path> (default <root>/review/specialists/<name>.md; red-team.md for red-team)
--checklists <a,b> (specialist-batched) --findings-file <f> --specialists <n> (red-team)
--checklists <a,b> (specialist-batched: the plan's checklists= names, or paths) --findings-file <f> --specialists <n> (red-team)
--fix-list <f> --hunks <f> --tests <f> [--callers <f>] (fix-check)
writes <out>/<role>[-<name>].prompt.md, records it in the ledger (default <out>/review-ledger.jsonl) and prints
BRIEF: reader=<role/name> sha256=<h> bytes=<n> file=<path>. Pass that file's content as the reader's prompt, unchanged.
@@ -82,6 +82,8 @@ function render(): number {
const diffBase = flag('--diff-base');
const checklist = flag('--checklist') ?? (role === 'red-team' ? path.join(ROOT, 'review/specialists/red-team.md') : role === 'specialist' && name ? path.join(ROOT, 'review/specialists', `${name}.md`) : undefined);
if (checklist && (role === 'specialist' || role === 'red-team') && !fs.existsSync(checklist)) throw new BriefError(`checklist not found: ${checklist}`);
const checklists = flag('--checklists')?.split(',').map(s => s.trim()).filter(Boolean).map(c => (c.includes('/') ? c : path.join(ROOT, 'review/specialists', `${c}.md`)));
for (const c of checklists ?? []) if (!fs.existsSync(c)) throw new BriefError(`checklist not found: ${c}`);
let diff: string | undefined;
if (role === 'outside-voice' && diffBase) {
const r = spawnSync('git', ['-c', 'core.quotePath=false', 'diff', '--no-color', '--no-ext-diff', diffBase], { encoding: 'utf8', timeout: 60_000, maxBuffer: 256 * 1024 * 1024 });
@@ -90,7 +92,7 @@ function render(): number {
}
const slots: Slots = {
name, diff_base: diffBase, exclude: flag('--exclude'), stack: flag('--stack'), test_fw: flag('--test-fw'),
learnings: readFile(flag('--learnings-file'), 'learnings'), checklist, checklists: flag('--checklists'),
learnings: readFile(flag('--learnings-file'), 'learnings'), checklist, checklists: checklists?.join(','),
findings: readFile(flag('--findings-file'), 'findings'), specialists: flag('--specialists'), diff,
fix_list: readFile(flag('--fix-list'), 'fix list'), hunks: readFile(flag('--hunks'), 'hunks'), tests: readFile(flag('--tests'), 'tests'), callers: readFile(flag('--callers'), 'callers'),
};
+7 -5
View File
@@ -39,12 +39,14 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do
--adversarial-ran Step 11's native adversarial pass already ran (a repeat skips it)
--gate-tagged <n> gate hunks the gate scan tagged (default: run gstack-gate-diff; failure means unavailable)
--gated a,b specialists the hit-rate stats tag GATE_CANDIDATE
--force a,b|all specialists forced by prompt flags (--all-specialists is all)
--force a,b|all specialists forced by prompt flags (--all-specialists is all): at standard and full depth they join the
batched specialist's checklists, where maintainability, performance and simplification are read only when
forced; light runtime-0 and scoped passes dispatch them one reader each
--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)
--no-tiers specialists, 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
@@ -130,14 +132,14 @@ function main(): number {
}
}
const plan = planReview({
...collected, scope, gateTagged, numbers: scanNumbers(added), force, gated: list('--gated'), floor,
...collected, scope, gateTagged, numbers: scanNumbers(added), force, gated: list('--gated'), floor, tiers: auditEffort !== 'host',
criticalFound: has('--critical-found'), adversarialRan: has('--adversarial-ran'), reviewedNumbers: flag('--reviewed-numbers'),
history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: has('--fixed-below') && last !== 'fix-check', 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, 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');
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, consumed: collected.consumed }) + '\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}`];
@@ -150,7 +152,7 @@ function main(): number {
if (!d.exclude.length) out.push(`DIFF_EXCLUDE: ${key} none`);
}
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)`);
out.push(`AUDIT_EFFORT: ${auditEffort} (coverage, plan-completion and docs audits, decision-trail reviewers, watchers; reviewers above run at their effort=)`);
process.stdout.write(out.join('\n') + '\n');
return EXIT.ok;
}
+17 -1
View File
@@ -5,6 +5,7 @@
* gstack-state-matrix detect [--base <ref>] [--diff-base <sha>]
* gstack-state-matrix check <specialist-output|-> [--repo <dir>] [--expect <expectation.json>] [--json]
* gstack-state-matrix outfile
* gstack-state-matrix findings <reviewer-output|->
*
* `detect` prints `SCOPE_STATE_MACHINE=true|false` (shell-safe for
* `source <(...)`, with `# state-machine:` comment lines naming the hits) from
@@ -14,7 +15,10 @@
* last; exit 0 ok, 1 incomplete (STATE_MATRIX_INCOMPLETE). `outfile` creates and
* prints a new empty 0600 file in the repo's private `.gstack/tmp/` (0700, kept
* out of git through info/exclude) for the specialist's output, so a later
* review never checks or commits an earlier run's file. Logic: lib/state-matrix.ts.
* review never checks or commits an earlier run's file. `findings` prints the
* output for Step 9.3's merge with each untested cell mapped below the floor
* (lib/review-scope.ts mapStateMatrixOutput) and `STATE_MATRIX_FINDINGS:` on
* stderr. Logic: lib/state-matrix.ts.
*/
import { spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
@@ -24,6 +28,7 @@ import { addedLines } from '../lib/banned-terms';
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { resultLine } from '../lib/result-codes';
import { resolveBaseBranch } from '../lib/ship-policy';
import { mapStateMatrixOutput } from '../lib/review-scope';
import { checkStateMatrix, detectStateMachine, renderStateMatrixCheck, type Expectation } from '../lib/state-matrix';
const HELP = `gstack-state-matrix — kind × owner action × resulting row for proposal, hold and approval state machines
@@ -32,6 +37,7 @@ const HELP = `gstack-state-matrix — kind × owner action × resulting row for
check <file|-> [--repo <dir>] [--expect <json>] [--json]
gaps in the state-machines specialist's output (review/specialists/state-machines.md)
outfile print a new empty private file for that output
findings <file|-> that output for the Step 9.3 merge, each untested cell mapped below the floor
${exitTableHelp()}`;
@@ -71,6 +77,15 @@ function check(rest: string[]): number {
return EXIT.ok;
}
function findings(rest: string[]): number {
const input = rest.find(a => !a.startsWith('--'));
if (!input) { process.stderr.write(`gstack-state-matrix findings: a reviewer output file (or -) is required\n${HELP}\n`); return EXIT.usage; }
const r = mapStateMatrixOutput(input === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(input, 'utf8'));
process.stdout.write(r.text.endsWith('\n') ? r.text : `${r.text}\n`);
process.stderr.write(`STATE_MATRIX_FINDINGS: mapped=${r.mapped} (untested cells below the floor; docs/review-policy.md test gap)\n`);
return EXIT.ok;
}
function outfile(): number {
const root = git(['rev-parse', '--show-toplevel']);
const exclude = git(['rev-parse', '--git-path', 'info/exclude']);
@@ -94,6 +109,7 @@ function main(argv: string[]): number {
if (cmd === 'detect') return detect(rest);
if (cmd === 'check') return check(rest);
if (cmd === 'outfile') return outfile();
if (cmd === 'findings') return findings(rest);
process.stderr.write(`gstack-state-matrix: unknown command ${cmd}\n${HELP}\n`);
return EXIT.usage;
}
+13
View File
@@ -60,3 +60,16 @@ describe('playwright-core windowsHide patch (#2160, #1989)', () => {
expect(bundle).toContain('shell: true, windowsHide: true');
});
});
// Windows Free Tests 40656299153 and 41056150287 (cookie-picker-ui, serial retry passed): goto failed with
// net::ERR_NO_BUFFER_SPACE on 127.0.0.1. Chromium's TcpPortRandomizationWin raises WSAENOBUFS with ports free;
// microsoft/playwright#43605 disables it in 1.65, and the 1.62.1 patch backports it; drop this block at 1.65.
describe('playwright-core TcpPortRandomizationWin backport (microsoft/playwright#43605)', () => {
test('the patch and the installed tree disable the feature in the --disable-features list', () => {
expect(fs.readFileSync(path.join(ROOT, 'patches', 'playwright-core@1.62.1.patch'), 'utf-8')).toContain('+ "TcpPortRandomizationWin"');
const bundle = fs.readFileSync(path.join(ROOT, 'node_modules', 'playwright-core', 'lib', 'coreBundle.js'), 'utf-8');
const list = bundle.slice(bundle.indexOf('disabledFeatures = ['), bundle.indexOf('].filter(Boolean);', bundle.indexOf('disabledFeatures = [')));
expect(list).toContain('"TcpPortRandomizationWin"');
expect(bundle).toContain('"--disable-features=" + disabledFeatures.join(",")');
});
});
+1 -1
View File
@@ -46,7 +46,7 @@ Capy checklist for `/autoplan`: drive `gstack-autoplan`, never a hand adaptation
Token caps on /ship: pass a brief's cap as `--token-cap <n>`. /ship reads `cost.tokens` and `cost.credits` from `tools.thread.get({ thread_id: "<your own jam id>" })` (its own jam, subagents included; `"this"` names the root thread, so only a root run may pass it) inside the same execute program as the `gstack-ship-budget` command that takes them as `--reading <n> --credits <c>`, never in a call of its own. `ship/DRIVER.md` starts `--mode observe`: each program ends with `note`, and only a note past the cap stops (`docs/ship-budget.md`).
/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). Each review pass and Step 11 is one call to the checked-in pass program (fill only `input`): `const t0 = await now(); const p = (await tools.capy.bash({ command: ". '<launch file>' && gstack-review-dispatch program", description: "Pass" })).output.split("\n"); return p[0] + "\n" + (await new (Object.getPrototypeOf(async () => {}).constructor)("tools", "sleep", "now", "input", JSON.parse(p[1]))(tools, sleep, now, { startedAt: t0, launch: "<launch file>", base: "<base>", out: "<run dir>/p<n>", pass: "<run>-p<n>", planFlags: [], logStart: ["review"], jam: "<jam id>", record: "<rec>", step: "9.2" }));` It plans, logs `logStart` (admission mode: `reservation` from `RESERVATION_ID:`), sends each brief after `sent` on `machine: "shared"`, then polls: one `tools.subagent.list` per ~20 s tick, returning 200 s after the program's first line, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); it starts readers listed under `rejected` with `tools.subagent.start` as slots free and stops on one stuck 20 minutes. It ends on `NEXT: judge`, `NEXT: poll` (rerun) or `STOP:`. `models` maps a role to its model (`outside-voice`: another family); Step 11 adds `stage: "voices"`. Steps 7, 8 and 9.2.1 go in `extras`, a JSON file of `{name, prompt_file, first, model}` (prompts verbatim from their sections; Step 7 `first`); they cannot block, so `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits). Waits: never let 5 minutes pass between calls. While readers run, never end the turn on `wait`; rerun the loader. Otherwise re-issue `wait` with `duration_seconds: 200` and the operation ids. At Step 22 report the `CALLS:` line of `gstack-ship-budget calls --events <file>` over your jam's transcript since /ship started. A small waiting child saves nothing.
/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). Each review pass and Step 11 is one call to the checked-in pass program (fill only `input`): `const t0 = await now(); const p = (await tools.capy.bash({ command: ". '<launch file>' && gstack-review-dispatch program", description: "Pass" })).output.split("\n"); return p[0] + "\n" + (await new (Object.getPrototypeOf(async () => {}).constructor)("tools", "sleep", "now", "input", JSON.parse(p[1]))(tools, sleep, now, { startedAt: t0, launch: "<launch file>", base: "<base>", out: "<run dir>/p<n>", pass: "<run>-p<n>", laneModel: "<your model id>", planFlags: [], logStart: ["review"], jam: "<jam id>", record: "<rec>", step: "9.2" }));` It plans, logs `logStart` (admission mode: `reservation` from `RESERVATION_ID:`), sends each brief after `sent` on `machine: "shared"`, then polls: one `tools.subagent.list` per ~20 s tick, returning 200 s after the program's first line, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); it starts readers listed under `rejected` with `tools.subagent.start` as slots free and stops on one stuck 20 minutes. It ends on `NEXT: judge`, `NEXT: poll` (rerun) or `STOP:`. `effort=` medium and light run `laneModel` at that reasoning; `models` maps a role to its model (`outside-voice`: another family); Step 11 adds `stage: "voices"`. Steps 7, 8 and 9.2.1 go in `extras`, a JSON file of `{name, prompt_file, first, model}` (prompts verbatim from their sections; Step 7 `first`); they cannot block, so `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits). Waits: never let 5 minutes pass between calls. While readers run, never end the turn on `wait`; rerun the loader. Otherwise re-issue `wait` with `duration_seconds: 200` and the operation ids. At Step 22 report the `CALLS:` line of `gstack-ship-budget calls --events <file>` over your jam's transcript since /ship started. A small waiting child saves nothing.
## Plan, approve, ship
+8 -8
View File
@@ -3,15 +3,15 @@
What seven Captain crews converged on (2026-10-29). Your crew's CREW.md and recorded approvals decide who may merge, update a base, spend or touch production; urgency changes priority, never permissions or gates. With no Captain, ask the person and never merge without their word. Story: capy/README.md; examples: your org Drive's software-factory/README.md, if any.
1. **Plan, then a yes.** New functionality starts with /autoplan sized to the work; the build waits for "do it". [SKILL.md: Plan, approve, ship]
2. **Someone else merges.** Run /ship to a receipt on your final pushed head, send one READY quoting gstack's verify output word for word, and stop. The merge owner merges on green and assigns versions. [docs/captain.md: The READY contract]
3. **Block only on the floor.** A finding blocks if it would fail a test, break production, or lose data or secrets; the rest goes in the PR body or the next wave. [docs/review-policy.md]
4. **Fix waves.** When main goes red, hold /ship while one owner finds the root cause; bring main in once, as your rules allow. Fold deferred findings and test follow-ups into one wave PR. [docs/triage.md]
5. **Severe first, same gates.** A severe user-facing bug jumps the queue; the plan can be short, but receipt, checks and merge owner stay.
6. **Trade findings, not small talk.** A finding that belongs to another crew goes to its Captain, through yours, with the evidence.
2. **Someone else merges.** Run /ship to a receipt on your final pushed head, send one READY quoting gstack's verify output verbatim, and stop. The merge owner merges on green and assigns versions. [docs/captain.md: The READY contract]
3. **Block only on the floor.** A finding blocks if it would fail a test, break production, or lose data or secrets; the rest goes in the PR body or next wave. [docs/review-policy.md]
4. **Fix waves.** When main goes red, hold /ship while one owner finds the root cause; bring main in once, as your rules allow. Fold deferred findings and test follow-ups into one PR. [docs/triage.md]
5. **Severe first, same gates.** A severe user-facing bug jumps the queue; the plan can be short; receipt, checks and merge owner stay.
6. **Trade findings, not small talk.** Another crew's finding goes to its Captain, through yours, with the evidence.
7. **Measure before you act.** Run the real system on the merged tree you ship; a real run outranks a reviewed plan. Each number names its run, commit or uncapped query; say what you did not verify.
8. **Evals decide, and losses get reported.** Preregister bars, cap and stop rule before paying [/eval-plan]. Cheap proof (a keyless fake, a smoke) before the paid run. Use controls (no-skill and placebo arms, clean decoys). Report use and pass separately; quote spend from the meter.
8. **Evals decide, and losses get reported.** Preregister bars, cap and stop rule before paying [/eval-plan]. Cheap proof (a keyless fake, a smoke) before the paid run. Use controls (no-skill and placebo arms, clean decoys). Report use and pass apart; quote spend from the meter.
9. **Flakes: one rerun, a ledger, a daily fix.** Only the rerun the policy allows, never a harness or infra red; a second red is real; no test is loosened. [docs/captain.md: The daily flaky pass]
10. **Quiet.** Message on a decision, a blocker, a READY or a finished phase. A broadcast that needs no reply goes in the Captain's notes; never reply just to acknowledge.
10. **Quiet.** Message on a decision, a blocker, a READY or a finished phase. A broadcast needing no reply goes in the Captain's notes; never reply just to acknowledge.
11. **Ask once, plainly.** One ask per decision, explained like the owner is five: your Captain's name, then a lettered line each: "A: <code> hit problem p, which happens when h. We could do 1) x, 2) y or 3) z. Recommendation x because why. A1 to take recommendation." "do it" takes every recommendation; "A2" takes option 2. Owner tasks: the exact link (in Capy: path and field), numbered simple steps, the reply word. Never re-ask a standing rule; check what you can yourself. [docs/captain.md: The ask format]
12. **Fresh threads, closed out.** One thread per unit of work (crew threads ask their Captain); past the context threshold, hand off. Its Captain archives the threads it, its crew or automations start. Size reviews to the diff; read the token counter. [docs/ship-budget.md]
12. **Fresh threads, closed out.** One thread per unit of work (crew threads ask their Captain); past the context limit, hand off. Its Captain archives only its crew's threads; threads outside every crew are the owner's, even its automation's. Size reviews to the diff; read the token counter. [docs/ship-budget.md]
13. **The owner's model preference.** `gstack-models resolve --role agent` picks agent models, except cross-family voices and eval subjects. [docs/model-policy.md: Your model preference]
+2
View File
@@ -12,3 +12,5 @@
### Fixed
- **`gstack-ship-budget note --step 9.1`** records under step 9 with the sub-step as its label. Before, it was refused, and replays lost two notes to that error.
- **`gstack-review-scope render`** accepts `{"deferred": [...]}` as well as the bare array.
- **`gstack-review-dispatch judge`** treats a judged line with no severity, or `"severity": null`, as invalid and prints `JUDGMENT_REJECTED`. Before, it crashed with a TypeError, and a replay spent three calls recovering.
- **The Capy skill passes a rendered brief as bash `cat` prints it.** A file read cut the long lines of a 231,589-byte outside-voice brief, so `sent` reported `BRIEF_MISMATCH` and the reader had to be sent again.
+1 -1
View File
@@ -96,7 +96,7 @@ gstack/
├── extension/ # Chrome extension (side panel + activity feed + CSS inspector)
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-lint/ = the static and render tiers behind bin/gstack-design-lint, ported from hallmark's sloplint; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
│ └── diagram-render/ # Vendored mermaid + excalidraw runtimes, built into one offline bundle the renderer loads
├── patches/ # bun `patchedDependencies` patches (playwright-core windowsHide)
├── patches/ # bun `patchedDependencies` patches (playwright-core windowsHide, TcpPortRandomizationWin disabled on Windows)
├── docs/designs/ # Design documents (incl. IMPECCABLE_INTEROP.md = the design detector / catalog / open DESIGN.md record, and fork-port-residual-2026-09/ evaluation evidence)
├── setup-deploy/ # /setup-deploy skill (one-time deploy config)
├── .github/ # CI workflows + shared composite actions (.github/actions/) + Docker image (claude CLI pinned)
+5 -3
View File
@@ -35,9 +35,11 @@ by a daily pass; nothing is retried into green.
rules file, the digest and the notes, validates READYs, builds and merges trains, calls
slots, relays asks to the owner in the three shapes, runs the flaky pass, and decides
everything that needs no access, credentials, spend over cap or a cited approval. It
archives its own child threads, including ones its crew or its automations started
outside the crew, when its plan for each thread says that thread's work is done; it
knows each thread's next step, so nobody else archives them.
archives only its own crew threads, each when its plan for that thread says the work is
done; it knows each thread's next step, so nobody else archives them. A thread outside
every crew belongs to the owner and stays as it is: no Captain archives it, flags it or
lists it for archiving unless the owner asks, even when the Captain's own automation
created it.
- **A lane** (a crew thread) owns named files, builds on its own branch and machine,
runs /ship, sends one READY block, stamps on call (merging the base into its pin only
as part of that stamp commit, or when the controller names a real-code conflict),
+7 -6
View File
@@ -206,18 +206,19 @@ set, because a higher effort would spend the time the gate is sized for.
### Review effort tiers
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
The core checklist, the Red Team, the native adversarial pass and the
fix-check run at `full` effort, the lane's reasoning. Specialists run at
`medium`, reasoning medium on the same model (GSTA-167 part 3: the no-loss
controls C and C2 ran them there); light's batched specialist carries the Red
Team prompt and stays `full`. `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
(`AUDIT_EFFORT:` in the plan's output). A paid replay showed why the fix-check stays
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
runs the specialists, audits and watchers at the host default too
([review-policy.md](review-policy.md#review-depth)).
## Which workflows use the tiers?
+4 -2
View File
@@ -805,8 +805,10 @@ gstack-pregate sealed [--range <a..b>] [--remote origin] [--base <branch>] [--js
- **Rules.** `sealed.path` and `sealed.transcript` check every path a commit
adds, renames or copies, whatever its content (binary, empty, deleted again
in a later commit); `sealed.answer`, `sealed.seed` and `sealed.local_path`
(`/home/<user>/`, `/Users/<user>/`, `C:\Users\<user>\`) check each commit's added
lines. Findings print rule, `file[:line]` and commit, never the text.
(`/home/<user>/`, `/Users/<user>/`, `C:\Users\<user>\`, including after a shell
default such as `${VAR:-…}` or `${VAR-…}`) check each commit's added lines.
Known limit: a path glued to the text before it, by a hyphen (`docs-…`,
`$VAR-…`, `${VAR}-…`) or a letter (`-I…`, `-L…`), is not matched. Findings print rule, `file[:line]` and commit, never the text.
- **Globs.** `paths`, `transcripts` and `allow_paths` match the whole
repo-relative path from the repository root. `*` and `?` stay inside one
path segment; `**/` spans zero or more directories; a `**` not followed by
+53 -12
View File
@@ -58,7 +58,7 @@ whatever class it resembles.
| **bounded repeat work** | A re-dispatch on every call, a redundant screen after main moves, an orphaned scratch branch, a refusal that could come one step earlier in the sequence, a settle that could wait one more check. | The operation completes, nothing is lost, and the cost is one more bounded run; uncapped spend is the sideways sweep's CRITICAL, not this class. |
| **conservative refusal** | Stale state scoped wider than its cause: a RED verdict that keeps refusing a head after main moved, a pending result that keeps refusing after it settled. | A refusal costs a rerun and never passes something wrong; a wrong pass would be the floor. |
| **artifact presentation** | A string from an artifact printed verbatim into a parsed line (whitespace, control characters); a classifier that reads a non-zero exit code beside a non-RED verdict as RED. | Display and classification of the repository's own output; it forges no parsed field. |
| **test gap** | A state-matrix cell without a test, a coverage percentage, a weak or flaky test, a fixture that shares the coordinator's environment. | Category `testing`, below the floor by definition above; a gap is work for the test audit, not a defect in the diff. |
| **test gap** | A state-matrix cell without a test, a coverage percentage, a weak or flaky test, a fixture that shares the coordinator's environment. | Category `testing`, below the floor by definition above; a gap is work for the test audit, not a defect in the diff. Precedence for state machines: the specialist still reports each untested cell CRITICAL so `gstack-state-matrix check` proves the matrix complete, and `gstack-state-matrix findings` maps it to INFORMATIONAL per matrix cell before the merge; a wrong row or collapsed kinds stays CRITICAL, and a cell with a second finding keeps both. |
A reviewer that wants a class promoted argues it against the floor with a
reaching input, in the finding, and the owner rules; the table records the
@@ -168,12 +168,38 @@ or test path, CI workflows, credential, state-root and the paths below).
commit's `.gstack/generated.json` registers. Docs, tests and release metadata are never
runtime; a `.tmpl` edit plus its regenerated `SKILL.md` counts the template once.
**Data files no code names are docs.** A changed or deleted JSON, JSONL, NDJSON, CSV,
TSV or raster image (PNG, JPEG, GIF, WebP, BMP, ICO, AVIF) counts as docs when no
non-docs file names its basename at the head, at the merge base or, for live /ship, in
the working tree with untracked files; a data file named only by another data file
follows that file, four files deep. Precedence: agent prose (`.gstack/**`), a test or
fixture path, a gate path, a config file (`.github/**`, anything under a `config/`
directory, dotfiles, `*.config.*`, `package.json`, lockfiles, `tsconfig*.json`,
`manifest.json` and the other tool manifests `lib/review-depth.ts` lists) and a named
consumer each keep the path code, and YAML, TOML and SVG are always code. More than
2,000 data paths keep every one code; a `git grep` failure is `consumer_scan_failed`
and fails the depth closed. Only `classifyDiff` applies the rule (`lib/review-collect.ts`
finds the consumers, `gstack-ship-preflight`'s `RUNTIME_LINES` reads the same
function); every path-only caller (the receipt's gate-scan decision, the Step 16
post-review route, /autoplan's docs-only scope) keeps data files code. A numeric claim
in a data file still schedules the outside voice.
| Depth | When | Pass 1 dispatches |
|---|---|---|
| **light**, runtime 0 | docs and tests only, or only registered outputs | the core checklist only; the outside voice when the diff publishes numbers |
| **light**, runtime 1–50 | `SMALL_DIFF_LINES` or fewer runtime lines | core, one **batched** specialist (testing, maintainability and security checklists, the specialists scope selects and the Red Team prompt in one context), the native adversarial pass (kept: backtest inconclusive, GSTA-147 D5) and the outside voice on numbers |
| **standard** | more than 50 runtime lines | the code set with every threshold (specialists at 50, simplification at 100, Red Team at 200) read on runtime lines; specialists leave docs and registered outputs out of their diff, and every specialist but Testing leaves tests out too; adversarial and outside voice read everything |
| **full** | gate class, or any classifier error | the gate set, unchanged: every reviewer on the whole diff, the Red Team always, no hit-rate gating |
| **light**, runtime 1–50 | `SMALL_DIFF_LINES` or fewer runtime lines | core, one **batched** specialist (the batched checklists below plus the Red Team prompt in one context), the native adversarial pass (kept: backtest inconclusive, GSTA-147 D5) and the outside voice on numbers |
| **standard** | more than 50 runtime lines | one batched specialist at `medium` and the Red Team at 200 runtime lines (or on a CRITICAL); the batched reader leaves docs and registered outputs out of its diff; adversarial and outside voice read everything |
| **full** | gate class, or any classifier error | one batched specialist on the whole diff, the Red Team always, no hit-rate gating |
**Batched checklists (the lean ensemble, GSTA-167 part 2).** One specialist
subagent reads Testing and Security on every code or gate diff, plus
State-machines, Data-migration, API-contract and Design when scope asks (a
forced checklist always joins). Maintainability, Performance and Simplification
are read only when forced (`SKIP:` names them). The Red Team reads the batched
reader's merged findings. A free re-score of saved C and D findings lost 0 of
the 29 blockers (handoffs/GSTA-167/lean-ensemble-rescore.md); the live round 5
replay passed (L∘G1 found 24 of 29 blockers against the control's 21,
handoffs/GSTA-167/noloss5-results.md).
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
@@ -226,15 +252,20 @@ passes `--adversarial-ran` so a repeat lists them no more. Later full passes rer
specialists on the range only. Step 11.5 binds a planned skip through the Step 9
record's `CURRENT` freshness instead of a native record.
**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
**Effort.** The core checklist, the Red Team, the native adversarial pass and
fix-checks run at `full` effort (the lane's reasoning); specialists run at
`medium` (GSTA-167 part 3: the no-loss controls ran them there), except light's
batched specialist, which carries the Red Team prompt and stays `full`. The
outside voice and the native adversarial pass carry the floor sentence and end
each finding with `Severity:` and `Blocks: yes|no`; a finding is CRITICAL when
its label rule says so (native FIXABLE, outside Critical or High) or it says
`Blocks: yes`, and `Blocks: no` never lowers one (`gstack-review-dispatch collect`). 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
the specialists, audits and watchers at the host default too
([model-policy.md](model-policy.md#reasoning-effort)).
### Behavior changes
@@ -244,10 +275,15 @@ the audits and watchers at the host default too
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.
- Every depth dispatches one batched specialist instead of one reader per
checklist; Maintainability, Performance and Simplification run only when forced.
- Light no longer runs a separate Red Team: the batched specialist carries its
prompt. At runtime 0 light runs no adversarial pass either.
- Standard reads thresholds on runtime lines, not the whole diff, so a test-heavy
diff with few runtime lines dispatches fewer specialists.
- A JSON, CSV, TSV or raster data file no code names is docs, not runtime (GSTA-167
part 4a). A binary image no code names is no longer a `binary_code_path` error
that forces full depth; one that code names still is.
## What the receipt records
@@ -375,10 +411,15 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
prompt and runs `sent` on what it passed: a hash that differs from the rendered
one is `BRIEF_MISMATCH`, and that reader's review does not count.
3. `collect` hashes each reader's raw output and appends its findings to
`merged.jsonl` with the label exactly as written (a JSON line's `severity`, or a
prose line's Critical, High, Medium, Low, P0-P3, FIXABLE or INVESTIGATE wherever it
stands, headings and table rows included; a top-level list item under a label-only
heading takes that label). A phantom finding costs a fold; a lost one costs the gate.
`merged.jsonl` with the label as written (a JSON line's `severity`, or the most
severe of a prose line's Critical, High, Medium, Low, P0-P3, FIXABLE or INVESTIGATE
wherever they stand, headings and table rows included; a top-level list item under a
label-only heading takes the stronger of the two). A `Severity:` or `Blocks:` line
continues the finding above it until a list item, heading, bold paragraph opener or a
second line for the same field starts another: `Severity` raises its label,
`Blocks: yes` makes it CRITICAL and `Blocks: no` never lowers it; an inline
`Blocks: yes` is CRITICAL too, and with no finding above it is one. The native
adversarial voice's FIXABLE collects as CRITICAL (normalizer v2, GSTA-167). A phantom finding costs a fold; a lost one costs the gate.
4. Lead judgment writes `judged.jsonl`; `judge` refuses a set that drops a merged
finding, lowers its severity, folds it into a less severe duplicate, or adds or
promotes one without a reason (`JUDGMENT_REJECTED`). A merged file missing any id
+1 -1
View File
@@ -733,7 +733,7 @@ Every `/ship` run builds a code path map from your diff, searches for correspond
### Review gate
`/ship` displays historical review readiness in the [Review Readiness Dashboard](#review-readiness-dashboard) during preflight. A missing Eng Review is reported without an extra question; it does not replace or waive the current pre-landing review. Step 9 still runs the checklist, applicable specialists and bounded exploratory QA, with its existing approval and completion gates.
`/ship` displays historical review readiness in the [Review Readiness Dashboard](#review-readiness-dashboard) during preflight. A missing Eng Review is reported without an extra question; it does not replace or waive the current pre-landing review. Step 9 still runs the checklist, one batched specialist (Testing and Security, plus the checklists scope selects) and bounded exploratory QA, with its existing approval and completion gates.
A lot of branches die when the interesting work is done and only the boring release work is left. Humans procrastinate that part. AI should not.
+5 -1
View File
@@ -67,7 +67,9 @@ const briefText = async (r) => {
return text;
};
const roleArgs = (r) => ' --role ' + q(r.role) + (r.name ? ' --name ' + q(r.name) : '');
const modelOf = (r) => r.model || (input.models || {})[r.role === 'specialist-batched' ? 'specialist' : r.role];
const REASONING = { medium: 'medium', light: 'low' };
const modelOf = (r) => r.model || (input.models || {})[r.role === 'specialist-batched' ? 'specialist' : r.role]
|| (REASONING[r.effort] && input.laneModel ? { model_id: input.laneModel, reasoning_mode: REASONING[r.effort] } : undefined);
const safe = (s) => s.replace(/[^a-zA-Z0-9._-]/g, '_');
const active = input.stage === 'voices' ? ['voices'] : ['first', 'specialists', 'red-team'];
@@ -134,6 +136,8 @@ try {
m = await load();
}
if (m.readers.some((x) => x.stage === stage && x.role === 'outside-voice' && x.state === 'rendered' && !modelOf(x))) return stop("the outside voice needs input.models['outside-voice'] (a model of another family); none was given");
const unmapped = m.readers.find((x) => x.stage === stage && x.state === 'rendered' && REASONING[x.effort] && !modelOf(x));
if (unmapped) return stop(unmapped.reader + ' runs at effort=' + unmapped.effort + ', which needs input.laneModel (your model id) or input.models; none was given');
for (const r of m.readers.filter((x) => x.stage === stage && x.state === 'rendered')) {
const model = modelOf(r);
const text = await briefText(r);
+6 -2
View File
@@ -56,6 +56,9 @@ const scanner = new Bun.Transpiler({ loader: 'tsx' });
// names it, like a relative import; without it a test reading a template by
// relative path had no edge to the template (GSTA-121's test/ship-publication-gates.test.ts).
const QUOTED = /['"`]((?:\.\.?\/)*[A-Za-z0-9_@][A-Za-z0-9_./@()[\]+-]*)['"`]/g;
// A dot-led literal (`'.gitleaks.toml'`, `'../.github/x.yml'`) runs as its own pass and leaves its
// closing quote unconsumed, so a closing quote followed by `.method(` never eats the next literal's quote.
const DOTFILE = /['"`]((?:\.\.?\/)*\.[A-Za-z0-9_@][A-Za-z0-9_./@()[\]+-]*)(?=['"`])/g;
const JOIN = /\bjoin\(([^()]*)\)/g;
const RESOLVE_EXT = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts', '/index.ts', '/index.tsx', '/index.js', '/index.jsx', '/index.mjs'];
// A statement starts a line or follows `;` or a compound statement's `:`. The module spec is
@@ -291,11 +294,11 @@ function resolveCandidate(files: Set<string>, dirs: Set<string>, c: string, lite
*/
function resolveRefs(files: Set<string>, dirs: Set<string>, fromFile: string, ref: string, literal = false, config: PathAliases = NO_ALIASES): string[] {
if (!ref || ref.startsWith('node:') || ref.startsWith('bun:')) return [];
if (ref.startsWith('.')) { const t = resolveCandidate(files, dirs, normalize(path.posix.join(path.posix.dirname(fromFile), ref)), literal); return t ? [t] : []; }
if (/^\.\.?(?:\/|$)/.test(ref)) { const t = resolveCandidate(files, dirs, normalize(path.posix.join(path.posix.dirname(fromFile), ref)), literal); return t ? [t] : []; }
const { chosen, others } = matchAliases(config.aliases, ref);
for (const c of chosen ?? []) { const t = resolveCandidate(files, dirs, c, literal); if (t) return [t]; }
if (!chosen && others.length === 0 && ref.startsWith('@') && !ref.includes('/')) return [];
const plausible = [...others, ...(config.baseUrl ? [normalize(path.posix.join(config.baseUrl, ref))] : []), normalize(ref)];
const plausible = [...others, ...(config.baseUrl ? [normalize(path.posix.join(config.baseUrl, ref))] : []), normalize(ref), ...(literal && ref.startsWith('.') ? [normalize(path.posix.join(path.posix.dirname(fromFile), ref))] : [])];
return [...new Set(plausible.map(c => resolveCandidate(files, dirs, c, literal)).filter((t): t is string => t !== null))];
}
@@ -429,6 +432,7 @@ export function referencesIn(files: Set<string>, dirs: Set<string>, rel: string,
// sources (a test spawns a bin; a bash bin calls a sibling). Prose and
// manifests name everything and would make every file a hub.
if (!scansLiterals(rel)) return out;
for (const m of source.matchAll(DOTFILE)) for (const target of resolveRefs(files, dirs, rel, m[1]!, true, config)) out.push({ target, via: 'literal' });
for (const m of source.matchAll(QUOTED)) {
const targets = resolveRefs(files, dirs, rel, m[1]!, true, config);
for (const target of targets) out.push({ target, via: 'literal' });
+12 -1
View File
@@ -224,6 +224,9 @@ export async function runQaWindowsWorker(args: string[], emit: Emit, entrypoint:
worker.on('message', (message: any) => {
if (message?.type === messageType && ['stdout', 'stderr'].includes(message.stream)
&& message.receipt && typeof message.receipt === 'object') emit(message.stream, message.receipt, message.completion);
if (message?.type === messageType && Number.isSafeInteger(message.seq)) {
try { worker.send({ type: `${messageType}-ack`, seq: message.seq }, () => {}); } catch {}
}
});
worker.once('error', () => {
process.off('exit', kill);
@@ -244,6 +247,10 @@ export async function withQaReceiptOutput(receiptWorker: boolean, messageType: s
stderr: fs.createWriteStream('', { fd: 2, autoClose: false }),
};
const writes: Promise<void>[] = [];
const acks = new Map<number, () => void>();
if (receiptWorker) process.on('message', (message: any) => {
if (message?.type === `${messageType}-ack`) acks.get(message.seq)?.();
});
let writeFailed = false;
const failed = () => { writeFailed = true; };
output.stdout.on('error', failed);
@@ -252,7 +259,11 @@ export async function withQaReceiptOutput(receiptWorker: boolean, messageType: s
writes.push(new Promise<void>(resolve => {
const done = (error?: Error | null) => { if (error) writeFailed = true; resolve(); };
try {
if (receiptWorker) process.send!({ type: messageType, stream, receipt, ...(completion ? { completion } : {}) }, done);
if (receiptWorker) {
const seq = writes.length;
acks.set(seq, () => { acks.delete(seq); resolve(); });
process.send!({ type: messageType, seq, stream, receipt, ...(completion ? { completion } : {}) }, (error?: Error | null) => { if (error) done(error); });
}
else output[stream].write(format(receipt), done);
} catch { writeFailed = true; resolve(); }
}));
+1 -1
View File
@@ -117,7 +117,7 @@ function renderRole(role: BriefRole, slots: Slots, t: SectionTexts): string {
return fill(t.native, [['DIFF_BASE=$(git merge-base origin/<base> HEAD)', `DIFF_BASE=${diffBase}`]], role);
case 'outside-voice': {
const body = fill(t.outside, [['DIFF_BASE=$(git merge-base origin/<base> HEAD)', `DIFF_BASE=${diffBase}`]], role);
return [body, '', 'Give each finding a severity: Critical, High, Medium or Low.', '', `## Branch diff (git diff ${diffBase})`, FENCE + 'diff', (slots.diff ?? '').trim(), FENCE].join('\n');
return [body, '', `## Branch diff (git diff ${diffBase})`, FENCE + 'diff', (slots.diff ?? '').trim(), FENCE].join('\n');
}
case 'fix-check':
return renderFixCheckPrompt({ fixList: need(slots, 'fix_list'), touchedHunks: need(slots, 'hunks'), testResults: need(slots, 'tests'), callers: slots.callers ?? '' });
+64 -8
View File
@@ -12,7 +12,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { GENERATED_PATH, parseRegistry } from './regen';
import { RESULT_CODES } from './result-codes';
import type { ClassInput, DepthError, PathLines, RegistryInfo } from './review-depth';
import { classifyPath, dataCandidate, type ClassInput, type DepthError, type PathLines, type RegistryInfo } from './review-depth';
export interface Collected extends ClassInput {
perPath: PathLines[];
@@ -25,11 +25,12 @@ export interface Collected extends ClassInput {
diffLines: number;
mergeBase: string | null;
untracked: string[];
consumed: string[] | 'all';
}
function git(root: string, args: string[]): { ok: boolean; out: string; err: string } {
function git(root: string, args: string[]): { ok: boolean; status: number | null; out: string; err: string } {
const r = spawnSync('git', ['-c', 'core.quotePath=false', ...args], { cwd: root, encoding: 'utf8', timeout: 60_000, maxBuffer: 256 * 1024 * 1024 });
return { ok: r.status === 0, out: r.stdout ?? '', err: (r.stderr ?? '').trim() };
return { ok: r.status === 0, status: r.status, out: r.stdout ?? '', err: (r.stderr ?? '').trim() };
}
const scratch = (p: string) => p.startsWith('.gstack/tmp/');
const oneLine = (s: string) => s.replace(/[\0-\x1f]+/g, ' ').trim().slice(0, 200);
@@ -88,7 +89,60 @@ function readDiff(root: string, range: string[], errors: DepthError[]): Pick<Col
return { perPath, deleted, stampOnly, added };
}
function result(diff: ReturnType<typeof readDiff>, extra: Pick<Collected, 'root' | 'registry' | 'errors' | 'mergeBase' | 'untracked'>): Collected {
/** More data candidates than this keep every data file code: the consumer scan is not run. */
export const CONSUMER_SCAN_CAP = 2000;
/** How many data files deep a chain of data naming data is followed (a manifest naming a data file) before the rest count as consumed. */
export const CONSUMER_SCAN_DEPTH = 4;
/**
* The data candidates (lib/review-depth.ts dataCandidate) among the changed and deleted paths that a non-docs file names
* by basename, at each rev and, with `live`, in the working tree with untracked files. The merge base is one of the revs,
* so a consumer the change removes still counts. A data file named only by another data file is consumed when that one
* is, followed CONSUMER_SCAN_DEPTH files deep; anything still open after that counts as consumed. A git grep failure is
* a collector error, which fails the depth closed.
*/
export function scanConsumers(root: string, paths: readonly string[], revs: readonly string[], live: boolean, classRoot: string | readonly string[], errors: DepthError[]): string[] | 'all' {
const candidates = [...new Set(paths)].filter(p => dataCandidate(p, classRoot)).sort();
if (candidates.length > CONSUMER_SCAN_CAP) return 'all';
const base = (p: string) => p.slice(p.lastIndexOf('/') + 1);
const named = new Map<string, Set<string>>();
const direct = new Set<string>();
const probed = new Set<string>();
let probe = candidates;
for (let depth = 0; probe.length && depth < CONSUMER_SCAN_DEPTH; depth++) {
const patterns = [...new Set(probe.map(base))].flatMap(b => ['-e', b]);
const runs = [...revs.map(rev => ({ at: rev, args: [rev, '--'] })), ...(live ? [{ at: '', args: ['--untracked', '--'] }] : [])];
const next = new Set<string>();
for (const run of runs) {
const r = git(root, ['grep', '-I', '-F', '-o', '--null', '--no-line-number', '--no-column', '--no-color', ...patterns, ...run.args]);
if (r.status !== 0 && r.status !== 1) { errors.push({ code: 'consumer_scan_failed', detail: oneLine(r.err || `git grep exited ${r.status}`), input: `git grep -F <data basenames> ${run.at || '--untracked'}`, repair: 'run the git grep by hand and fix what git reports' }); return 'all'; }
for (const line of r.out.split('\n')) {
const parts = line.split('\0');
const where = parts[0];
const match = parts.length > 1 ? parts[parts.length - 1] : undefined;
if (!where || !match) continue;
const file = run.at ? where.slice(run.at.length + 1) : where;
if (scratch(file) || classifyPath(file, classRoot) === 'docs') continue;
for (const p of probe) {
if (file === p || !match.includes(base(p))) continue;
if (!dataCandidate(file, classRoot)) { direct.add(p); continue; }
(named.get(file) ?? named.set(file, new Set()).get(file)!).add(p);
if (!probed.has(file) && !probe.includes(file)) next.add(file);
}
}
}
for (const p of probe) probed.add(p);
probe = [...next];
}
const consumed = new Set([...direct, ...probe]);
for (let grew = true; grew;) {
grew = false;
for (const [file, targets] of named) if (consumed.has(file)) for (const p of targets) if (!consumed.has(p)) { consumed.add(p); grew = true; }
}
return candidates.filter(p => consumed.has(p));
}
function result(diff: ReturnType<typeof readDiff>, extra: Pick<Collected, 'root' | 'registry' | 'errors' | 'mergeBase' | 'untracked' | 'consumed'>): Collected {
const paths = diff.perPath.map(e => e.path).sort();
return { ...diff, ...extra, paths, gateTagged: null, diffLines: diff.perPath.reduce((n, e) => n + e.lines, 0) };
}
@@ -100,13 +154,14 @@ export function collectCommit(root: string, baseSha: string, headSha: string): C
const mb = git(root, ['merge-base', baseSha, headSha]);
if (!mb.ok) {
errors.push({ code: 'numstat_failed', detail: oneLine(mb.err || 'no merge base'), input: `git merge-base ${baseSha} ${headSha}`, repair: 'check that both commits exist locally (git fetch origin <base>) and rerun' });
return result({ perPath: [], deleted: [], stampOnly: [], added: {} }, { root: [], registry, errors, mergeBase: null, untracked: [] });
return result({ perPath: [], deleted: [], stampOnly: [], added: {} }, { root: [], registry, errors, mergeBase: null, untracked: [], consumed: 'all' });
}
const mergeBase = mb.out.trim();
const diff = readDiff(root, [mergeBase, headSha], errors);
const tree = git(root, ['ls-tree', '-r', '--name-only', '-z', headSha]);
const skillDirs = [...new Set(tree.out.split('\0').flatMap(p => /^([^/]+)\/SKILL\.md(\.tmpl)?$/.exec(p)?.[1] ?? []))].sort();
return result(diff, { root: skillDirs, registry, errors, mergeBase, untracked: [] });
const consumed = scanConsumers(root, [...diff.perPath.map(e => e.path), ...diff.deleted], [headSha, mergeBase], false, skillDirs, errors);
return result(diff, { root: skillDirs, registry, errors, mergeBase, untracked: [], consumed });
}
/** The working tree against the merge base with origin/<base> (or <base>), untracked files counted per path; the registry is the base ref's commit. */
@@ -117,7 +172,7 @@ export function collectLive(root: string, base: string): Collected {
const mb = git(root, ['merge-base', ref, 'HEAD']);
if (!mb.ok) {
errors.push({ code: 'numstat_failed', detail: oneLine(mb.err || `no merge base with ${ref}`), input: `git merge-base ${ref} HEAD`, repair: `fetch the base first (git fetch origin ${base})` });
return result({ perPath: [], deleted: [], stampOnly: [], added: {} }, { root, registry, errors, mergeBase: null, untracked: [] });
return result({ perPath: [], deleted: [], stampOnly: [], added: {} }, { root, registry, errors, mergeBase: null, untracked: [], consumed: 'all' });
}
const mergeBase = mb.out.trim();
const diff = readDiff(root, [mergeBase], errors);
@@ -133,5 +188,6 @@ export function collectLive(root: string, base: string): Collected {
diff.perPath.push({ path: p, lines, binary });
if (!binary) diff.added[p] = text.split('\n');
}
return result(diff, { root, registry, errors, mergeBase, untracked });
const consumed = scanConsumers(root, [...diff.perPath.map(e => e.path), ...diff.deleted], [mergeBase], true, root, errors);
return result(diff, { root, registry, errors, mergeBase, untracked, consumed });
}
+77 -14
View File
@@ -17,7 +17,7 @@ import { matchesAny } from './ship-policy';
export type DiffClass = 'docs' | 'tests' | 'code' | 'gate';
export type PathClass = 'docs' | 'tests' | 'code';
export type Depth = 'light' | 'standard' | 'full';
export type Effort = 'light' | 'full' | 'host';
export type Effort = 'light' | 'medium' | 'full' | 'host';
export type Role = 'specialist' | 'red-team' | 'adversarial-native' | 'outside-voice' | 'fix-check';
export type Step = 'full' | 'scoped' | 'fix-check' | 'done' | 'stop';
@@ -29,8 +29,16 @@ 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'];
/** The checklists the batched specialist always reads on code and gate class, whatever the depth or scope (GSTA-167 part 2). */
export const LIGHT_FIXED: readonly Specialist[] = ['testing', 'security'];
/** The checklists the batched specialist adds when scope asks; maintainability, performance and simplification are read only when forced. */
export const BATCH_SCOPED: readonly Specialist[] = ['data-migration', 'api-contract', 'design', 'state-machines'];
/**
* Each blocking role's effort (GSTA-167 part 3, CREW 30c): specialists at medium, which is how the no-loss controls ran
* them; the Red Team, the native adversarial pass and the fix-check at full (the lane's reasoning); the outside voice at
* its own provider's. Light's batched specialist carries the Red Team prompt, so it stays full.
*/
export const ROLE_EFFORT = { specialist: 'medium', 'red-team': 'full', 'adversarial-native': 'full', 'fix-check': 'full', 'outside-voice': 'host' } as const satisfies Record<Role, Effort>;
/** Why light with runtime above 0 keeps the native adversarial pass as its own dispatch. */
export const LIGHT_ADVERSARIAL_REASON = 'backtest inconclusive (GSTA-147 D5)';
const CLASS_RANK: Record<DiffClass, number> = { docs: 0, tests: 1, code: 2, gate: 3 };
@@ -48,10 +56,18 @@ const GATE_PATH = [
/^lib\/review-(depth|collect|scope)\.ts$/, /^bin\/gstack-review-plan$/, /^lib\/ship-queue[^/]*\.ts$/, /^lib\/regen\.ts$/, /^\.gstack\/generated\.json$/, /^ship\/sections\/(review-army|adversarial)\.md\.tmpl$/, /^scripts\/resolvers\/review-army\.ts$/,
];
/** Data files: JSON, CSV, TSV and raster images. YAML, TOML and SVG are never data here: deploy configs, API specs and executable SVG stay code. */
export const DATA_PATH = /\.(json|jsonl|ndjson|csv|tsv|png|jpe?g|gif|webp|bmp|ico|avif)$/i;
/** Data-shaped files tools read by convention, with no code naming them: always code. */
const DATA_CONFIG = [
/(^|\/)\.github\//, /(^|\/)config\//, /(^|\/)\.[^/]+$/, /(^|\/)[^/]+\.config\.[^/]+$/,
/(^|\/)(package|package-lock|npm-shrinkwrap|composer|deno|jsr|tsconfig[^/]*|jsconfig[^/]*|manifest|app|vercel|netlify|firebase|renovate|turbo|nx|lerna|biome|angular|nest-cli|lint-staged|components|pyrightconfig|cdk|serverless|appsettings[^/]*)\.json$/i,
];
/** 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, 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}`)]),
SMALL_DIFF_LINES, RED_TEAM_LINES, LARGE_DIFF_LINES, FULL_PASS_CAP, LIGHT_FIXED, BATCH_SCOPED, SCOPED_RANGE_LINES, ROLE_EFFORT,
patterns: Object.entries({ GATE_PATH, AGENT_PROSE, TEST_PATH, RELEASE_METADATA, DOC_PATH: [DOC_PATH], DATA_PATH: [DATA_PATH], DATA_CONFIG }).map(([k, v]) => [k, v.map(r => `/${r.source}/${r.flags}`)]),
})).digest('hex').slice(0, 16);
/**
@@ -70,7 +86,21 @@ export function classifyPath(p: string, root?: string | readonly string[]): Path
return root.includes(top) ? 'code' : 'docs';
}
export type DepthErrorCode = 'scope_error' | 'gate_scan_unavailable' | 'registry_unavailable' | 'registry_invalid' | 'numstat_failed' | 'binary_code_path';
/**
* A code-class path the data rule may move to docs: a data extension, and not agent prose, a gate path or a config file.
* Only classifyDiff applies the rule, and only to a candidate its collector found no consumer for (ClassInput.consumed);
* classifyPath and every path-only caller keep data files code.
*/
export function dataCandidate(p: string, root?: string | readonly string[]): boolean {
return DATA_PATH.test(p) && classifyPath(p, root) === 'code' && ![...AGENT_PROSE, ...GATE_PATH, ...DATA_CONFIG].some(r => r.test(p));
}
/** Whether a range needs the gate scan: any path that is not docs by the path-only rule. The data rule never skips it. */
export function gateScanNeeded(paths: readonly string[], root?: string | readonly string[]): boolean {
return paths.some(p => classifyPath(p, root) !== 'docs');
}
export type DepthErrorCode = 'scope_error' | 'gate_scan_unavailable' | 'registry_unavailable' | 'registry_invalid' | 'numstat_failed' | 'binary_code_path' | 'consumer_scan_failed';
/** Why a depth failed closed to full: the input it could not read, a sanitized cause and the repair; `result` is the result code behind it when one exists. */
export interface DepthError { code: DepthErrorCode; detail: string; input: string; repair: string; result?: ResultCodeName }
export interface PathLines { path: string; lines: number; binary: boolean }
@@ -94,12 +124,18 @@ export interface ClassInput {
registry?: RegistryInfo;
/** Collector failures (a diff git could not list): each fails the depth closed. */
errors?: readonly DepthError[];
/**
* The data candidates (dataCandidate) a non-docs file names at the head, the merge base or in the working tree
* (lib/review-collect.ts). A candidate not listed counts as docs; 'all' or no scan keeps every data file code.
*/
consumed?: readonly string[] | 'all';
}
export interface ClassResult { class: DiffClass; reasons: string[]; errors: DepthError[]; runtimeLines: number | null }
const sanitize = (s: string) => s.replace(/[\0-\x1f]+/g, ' ').slice(0, 200);
const isOutput = (i: ClassInput, p: string) => !!i.registry?.outputs.length && matchesAny(i.registry.outputs, p);
const pathClass = (i: ClassInput, p: string): PathClass => ((i.stampOnly ?? []).includes(p) ? 'docs' : classifyPath(p, i.root));
const unconsumedData = (i: ClassInput, p: string) => Array.isArray(i.consumed) && !i.consumed.includes(p) && dataCandidate(p, i.root);
const pathClass = (i: ClassInput, p: string): PathClass => ((i.stampOnly ?? []).includes(p) || unconsumedData(i, p) ? 'docs' : classifyPath(p, i.root));
/** The diff's class, raised to gate by any gate signal; `errors` lists every input that fails the depth closed; `runtimeLines` counts code-class lines outside the base registry's outputs. */
export function classifyDiff(i: ClassInput): ClassResult {
@@ -109,6 +145,8 @@ export function classifyDiff(i: ClassInput): ClassResult {
const raise = (c: DiffClass, why: string) => { if (CLASS_RANK[c] > CLASS_RANK[cls]) cls = c; reasons.push(why); };
if (!i.paths.length) raise('docs', 'no changed paths');
for (const p of i.paths) { const c = pathClass(i, p); if (CLASS_RANK[c] > CLASS_RANK[cls]) raise(c, `${c}: ${p}`); }
const data = i.paths.filter(p => unconsumedData(i, p));
if (data.length) reasons.push(`docs: ${data.length} data path(s) no code names`);
const s = i.scope ?? {};
if (s.SCOPE_ERROR) {
raise('code', `scope error: ${s.SCOPE_ERROR}`);
@@ -251,8 +289,10 @@ export interface PlanInput extends ClassInput {
reviewedNumbers?: string;
/** --depth-floor: raises the depth, never lowers it. */
floor?: Depth;
/** false under --no-tiers or review_effort_tiers off: specialists run at the host's default effort. */
tiers?: boolean;
}
/** One reviewer. `exclude` holds git pathspec argv items the reviewer's diff leaves out; `checklists` names what light's batched specialist reads. */
/** One reviewer. `exclude` holds git pathspec argv items the reviewer's diff leaves out; `checklists` names what the batched specialist reads. */
export interface Dispatch { role: Role; name: string; effort: Effort; exclude: string[]; checklists?: string[]; reason?: string }
export interface ReviewPlan extends DepthResult {
/** The depth this step runs at: light raised to standard by a fixed blocker, a CRITICAL or an open blocker. */
@@ -294,6 +334,29 @@ export function specialistsFor(cls: DiffClass, i: PlanInput, skipped: string[] =
});
}
/**
* The checklists the one batched specialist reads (the lean ensemble, GSTA-167 part 2): the class sets the base (none for docs,
* testing for tests, LIGHT_FIXED for code and gate), BATCH_SCOPED joins when scope asks and hit-rate gating allows (gate class
* ignores gating), and a forced checklist always joins.
*/
export function batchChecklists(cls: DiffClass, i: PlanInput, skipped: string[] = []): Specialist[] {
const forced = forcedBy(i);
const codeLike = cls === 'code' || cls === 'gate';
const base: readonly Specialist[] = codeLike ? LIGHT_FIXED : cls === 'tests' ? ['testing'] : [];
if (cls === 'docs') skipped.push('specialists (docs class)');
if (cls === 'tests') skipped.push('specialists except testing (tests class)');
const extra = SPECIALISTS.filter(n => {
if (base.includes(n)) return false;
if (forced(n)) return true;
if (!codeLike || !BATCH_SCOPED.includes(n) || !scopeWants[n]!(i.scope ?? {})) return false;
if (cls !== 'gate' && (i.gated ?? []).includes(n)) { skipped.push(`${n} (gated by hit rate)`); return false; }
return true;
});
const dropped = SPECIALISTS.filter(n => codeLike && !BATCH_SCOPED.includes(n) && !base.includes(n) && !extra.includes(n));
if (dropped.length) skipped.push(`${dropped.join(', ')} (lean ensemble: read only when forced)`);
return [...base, ...extra];
}
/** A path as one git exclude pathspec; a glob character or a leading colon makes it literal. */
const excludeSpec = (p: string) => `:(exclude${/[*?[\]\\]/.test(p) || p.startsWith(':') ? ',literal' : ''})${p}`;
const shellQuote = (s: string) => `'${s.replace(/'/g, "'\\''")}'`;
@@ -317,18 +380,17 @@ export function planReview(i: PlanInput): ReviewPlan {
const dispatches: Dispatch[] = [];
const add = (role: Role, name: string, effort: Effort, exclude: string[] = [], extra: Partial<Dispatch> = {}) => dispatches.push({ role, name, effort, exclude, ...extra });
const docsAndOutputs = i.paths.filter(p => pathClass(i, p) === 'docs' || isOutput(i, p)).map(excludeSpec);
const tests = i.paths.filter(p => pathClass(i, p) === 'tests' && !isOutput(i, p)).map(excludeSpec);
const numbersChanged = i.numbers.publishes && i.reviewedNumbers !== undefined && i.reviewedNumbers !== i.numbers.fingerprint;
const firstVoices = h.fullPasses === 0 || !i.adversarialRan;
const specialist: Effort = i.tiers === false ? 'host' : ROLE_EFFORT.specialist;
if (next.step === 'full' && stepDepth === 'light') {
const forced = forcedBy(i);
if (runtimeLines > 0) {
const scoped = SPECIALISTS.filter(n => !LIGHT_FIXED.includes(n) && (forced(n) || (scopeWants[n]?.(i.scope ?? {}) && !(i.gated ?? []).includes(n))));
add('specialist', 'batched', 'full', docsAndOutputs, { checklists: [...LIGHT_FIXED, ...scoped, 'red-team'] });
add('specialist', 'batched', ROLE_EFFORT['red-team'], docsAndOutputs, { checklists: [...batchChecklists('code', i, skipped), 'red-team'] });
skipped.push('red-team (light depth: the batched specialist carries the Red Team prompt)');
if (firstVoices) add('adversarial-native', 'adversarial', 'full', [], { reason: LIGHT_ADVERSARIAL_REASON });
} else {
for (const n of SPECIALISTS.filter(forced)) add('specialist', n, 'full');
for (const n of SPECIALISTS.filter(forced)) add('specialist', n, specialist);
skipped.push('specialists, red-team and adversarial-native (light depth, runtime 0)');
}
if (h.fullPasses === 0 && i.numbers.publishes) add('outside-voice', 'outside-voice', 'host');
@@ -339,7 +401,8 @@ export function planReview(i: PlanInput): ReviewPlan {
const dcls: DiffClass = full ? 'gate' : cls;
const raised = !full && runtimeLines <= SMALL_DIFF_LINES;
const lines = full ? i.diffLines : Math.max(runtimeLines, raised ? SMALL_DIFF_LINES : 0);
for (const n of specialistsFor(dcls, { ...i, diffLines: lines }, skipped)) add('specialist', n, 'full', full ? [] : n === 'testing' ? docsAndOutputs : [...docsAndOutputs, ...tests]);
const checklists = batchChecklists(dcls, i, skipped);
if (checklists.length) add('specialist', 'batched', specialist, full ? [] : docsAndOutputs, { checklists });
const redTeam = dcls === 'gate' || (dcls === 'code' && (lines > RED_TEAM_LINES || !!i.criticalFound || raised));
if (redTeam) add('red-team', 'red-team', 'full');
else skipped.push(`red-team (${dcls} class${dcls === 'code' ? `, ${lines} runtime lines, no critical` : ''})`);
@@ -356,7 +419,7 @@ export function planReview(i: PlanInput): ReviewPlan {
} 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');
for (const n of forced) add('specialist', n, specialist);
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';
+66 -13
View File
@@ -8,7 +8,9 @@
* judgment each lead-judgment change: an added or promoted finding, with its reason
* judge the verdict on the parent's judged set (ok, or the dropped and downgraded ids)
*
* Findings reach merged.jsonl only through collect, with each voice's label exactly as written.
* Findings reach merged.jsonl only through collect, with each voice's label as written (a prose line's most severe one,
* raised by the Severity line below it), except that a prose voice's `Blocks: yes` and the native voice's FIXABLE collect
* as CRITICAL (GSTA-167 normalizer v2).
* The parent's judged set may add or promote a finding and may fold a duplicate into one at least
* as severe; it never drops or downgrades one. verify replays every check from the ledger alone.
*/
@@ -41,7 +43,8 @@ export function recordSent(ledger: string, reader: string, sentText: string): {
/** Severity ranks across the voices' vocabularies; a label with no severity (FIXABLE, INVESTIGATE) ranks 0. */
const RANKS: Record<string, number> = { critical: 4, p0: 4, high: 3, p1: 3, medium: 2, p2: 2, informational: 2, low: 1, p3: 1, fixable: 0, investigate: 0 };
export function rank(label: string): number | null {
export function rank(label: unknown): number | null {
if (typeof label !== 'string') return null;
const r = RANKS[label.trim().replace(/^\[|\]$/g, '').toLowerCase()];
return r === undefined ? null : r;
}
@@ -53,31 +56,81 @@ const LABEL_HEADING = new RegExp(`^(?:#{1,6}\\s*|\\*\\*)(${LABELS})(?:\\s+(?:fin
const HEADING = /^(?:#{1,6}\s|\*\*[^*]+\*\*:?$)/;
const LIST_ITEM = /^(?:[-*+]|\d+[.)])\s+\S/;
const stronger = (a: string, b: string) => ((rank(a) ?? 0) >= (rank(b) ?? 0) ? a : b);
/** A prose voice's `Blocks: yes` (GSTA-167 normalizer v2): the finding is CRITICAL whatever its label; `Blocks: no` never lowers one. */
const BLOCKS_YES = /(?<![A-Za-z0-9])Blocks:?\**:?\s*\**\s*yes(?![A-Za-z0-9])/i;
const ALL_LABELS = new RegExp(ANY_LABEL.source, 'g');
/** A file:line locus ("auth.ts:9"): prose naming one after a finding's fields starts the next finding. */
const LOCUS = /[\w./-]+\.\w+:\d+/;
/** A line that is only a finding's `Severity` or `Blocks` field ("Severity: High", "- Blocks: yes", "Severity High"). */
const META = new RegExp(`^(?:[-*+]\\s+)?\\**\\s*(Severity|Blocks)\\**\\s*(?::|\\s+\\**(?:${LABELS}|yes|no)(?![A-Za-z0-9]))`, 'i');
export interface MergedLine { id: string; reader: string; label: string; raw: string; output_sha256: string }
/** One reader's findings as written: a JSON object per line (severity kept as written); else each prose line carrying a severity or FIXABLE/INVESTIGATE label, and each top-level list item under a label-only heading (the stronger of the two labels). */
/**
* One reader's findings as written: a JSON object per line (severity kept as written); else each prose line carrying a severity or
* FIXABLE/INVESTIGATE label, at the most severe label on it, and each top-level list item under a label-only heading (the stronger
* of the two labels). A `Severity` or `Blocks` field line continues the open finding, once per field: `Severity` raises its label,
* `Blocks: yes` makes it CRITICAL, `Blocks: no` never lowers it; with no finding open it is one, carrying the paragraph's subject.
* A list item, a heading, a bold or post-blank paragraph at the margin, or a line naming a file:line after the open finding's fields
* starts the next finding. An inline `Blocks: yes` is CRITICAL too. The native voice's FIXABLE collects as CRITICAL; other labels
* stay as written for lead judgment. When in doubt the parser merges rather than drops: a lost blocker costs the gate.
*/
export function parseFindings(reader: string, output: string): Array<{ label: string; raw: string }> {
const out: Array<{ label: string; raw: string }> = [];
type Finding = { label: string; raw: string; fields: Set<string> };
const out: Finding[] = [];
let section: string | null = null;
let current: Finding | null = null;
let head = '';
let gap = false;
const role = reader.split('/')[0];
const named = (l: string) => (role === 'adversarial-native' && l.toUpperCase() === 'FIXABLE' ? 'CRITICAL' : l);
const strongest = (t: string) => [...t.matchAll(ALL_LABELS)].map(m => named(m[1]!)).reduce<string | undefined>((a, b) => (a === undefined ? b : stronger(a, b)), undefined);
const push = (label: string, raw: string, fields: string[] = []): Finding => { const f = { label, raw, fields: new Set(fields) }; out.push(f); return f; };
for (const line of output.replace(/\r\n/g, '\n').split('\n')) {
const t = line.trim();
if (!t || /^NO FINDINGS\.?$/.test(t) || /^Recommendation:/i.test(t)) continue;
if (!t) { gap = true; continue; }
const afterGap = gap;
gap = false;
if (/^NO FINDINGS\.?$/.test(t) || /^Recommendation:/i.test(t)) { current = null; continue; }
const indented = line !== line.trimStart();
const label = strongest(t);
const blocks = BLOCKS_YES.test(t);
const listItem = !indented && LIST_ITEM.test(t);
const locus = LOCUS.test(t);
const meta = listItem && locus ? null : META.exec(t);
if (meta) {
const field = meta[1]!.toLowerCase();
if (current && !current.fields.has(field)) {
current.fields.add(field);
if (blocks) current.label = 'CRITICAL';
else if (label) current.label = stronger(current.label, label);
current.raw += ` | ${t}`;
} else if (blocks || label) current = push(blocks ? 'CRITICAL' : label!, head ? `${head} | ${t}` : t, [field]);
else current = null;
continue;
}
const heading = LABEL_HEADING.exec(t);
if (heading) { section = heading[1]!; continue; }
if (line === line.trimStart() && HEADING.test(t)) section = null;
if (heading) { section = named(heading[1]!); current = null; head = ''; continue; }
if (!indented && HEADING.test(t)) { section = null; current = null; }
if (t.startsWith('{')) {
try {
const o = JSON.parse(t) as Record<string, unknown>;
if (typeof o.severity === 'string') { out.push({ label: o.severity, raw: t }); continue; }
if (typeof o.severity === 'string') { current = push(o.severity, t); continue; }
} catch { /* prose that starts with a brace */ }
}
const inline = ANY_LABEL.exec(t)?.[1];
const inherited = section && line === line.trimStart() && LIST_ITEM.test(t) ? section : undefined;
const label = inline && inherited ? stronger(inherited, inline) : inline ?? inherited;
if (label) out.push({ label, raw: t });
const fresh = listItem || (!indented && (t.startsWith('**') || afterGap)) || (!!current?.fields.size && locus);
if (fresh) { current = null; head = t; }
const inherited = section && listItem ? section : undefined;
const found = label && inherited ? stronger(inherited, label) : label ?? inherited;
if (found) { current = push(blocks ? 'CRITICAL' : found, t); if (!indented) head = t; continue; }
if (blocks && !t.startsWith('{')) {
if (current) { current.label = 'CRITICAL'; current.raw += ` | ${t}`; continue; }
current = push('CRITICAL', head && head !== t ? `${head} | ${t}` : t);
continue;
}
if (!fresh && !current && (!LIST_ITEM.test(head) || locus)) head = t;
}
return out;
return out.map(({ label, raw }) => ({ label, raw }));
}
/** collect: hash the raw output, append its findings to merged.jsonl and record both. */
+1 -1
View File
@@ -138,7 +138,7 @@ export function renderStage(m: Manifest, stage: Stage, t: SectionTexts, diffFor:
const findings = specialistFindings(m.merged);
fs.writeFileSync(path.join(briefs, 'specialist-findings.jsonl'), findings, { mode: 0o600 });
findingsSha = promptHash(findings);
const specialists = m.readers.filter(x => x.role === 'specialist' || x.role === 'specialist-batched').length;
const specialists = m.readers.reduce((n, x) => n + (x.role === 'specialist' ? 1 : x.role === 'specialist-batched' ? (x.checklists ?? []).filter(c => c !== 'red-team').length : 0), 0);
Object.assign(slots, { checklist: path.join(m.checklist_root, 'review/specialists/red-team.md'), findings, specialists: String(specialists) });
}
if (role === 'outside-voice') slots.diff = diffFor(m.diff_base);
+39 -1
View File
@@ -14,6 +14,7 @@
* Pure functions here; bin/gstack-review-scope runs git and gh.
*/
import { SPECIALISTS } from './review-depth';
import { parseSpecialistOutput } from './state-matrix';
export type Locus = 'diff' | 'fix-range' | 'outside';
export type Kind = 'defect' | 'test-gap' | 'advisory';
@@ -130,13 +131,47 @@ function fingerprintLine(fp: unknown): number | undefined {
return Number.isInteger(n) && n > 0 ? n : undefined;
}
/** The blocking floor in Garry's words (docs/review-policy.md, CREW rule 23); every reviewer prompt carries it verbatim. */
export const BLOCKING_FLOOR = 'would fail a test, break production, or lose data or secrets';
/** The categories a reviewer uses for a test finding; `testing` is what the Testing specialist's schema emits. */
export const TEST_GAP_CATEGORIES: readonly string[] = ['testing', 'tests', 'test-gap', 'test-coverage', 'coverage', 'missing-test', 'untested'];
/** A finding's kind: optional advice, a test finding (a gap, a weak or flaky test), or a product defect. */
/** What mapStateMatrixOutput stamps on an untested state-matrix cell: a test gap, below the floor (docs/review-policy.md). */
export const UNTESTED_CELL = 'untested-cell';
const cellKey = (f: Record<string, any>): string | null => (f.cell && typeof f.cell === 'object' ? `${String(f.cell.kind ?? '').trim().toLowerCase()}\u0000${String(f.cell.action ?? '').trim().toLowerCase()}` : null);
/**
* A reviewer output (JSON lines) with each untested state-matrix cell mapped below the floor, per matrix cell and before
* Step 9.3's fingerprint merge: a `state-matrix` finding the checklist's schema writes as "untested cell: …", whose `cell` is a
* cell with `"test": null` in a matrix record of the same output, and the only finding on that cell, becomes INFORMATIONAL with `state_matrix: "untested-cell"` and its
* `raw_severity`. A second finding on the cell (a wrong row, collapsed kinds) keeps both as they are, so a merge can never
* fold a defect into a demoted record; a cell no matrix lists as untested changes nothing. Every other line, matrix records
* included, is returned byte for byte; `gstack-state-matrix check` reads the raw output, never this one.
*/
export function mapStateMatrixOutput(text: string): { text: string; mapped: number } {
const { matrices, findings } = parseSpecialistOutput(text);
const untested = new Set(matrices.flatMap(m => m.cells.filter(c => c.test === null).map(c => cellKey({ cell: c })!)));
const perCell = new Map<string, number>();
for (const f of findings) { const k = cellKey(f); if (k) perCell.set(k, (perCell.get(k) ?? 0) + 1); }
let mapped = 0;
const lines = text.split('\n').map(line => {
let f: Record<string, any>;
try { f = JSON.parse(line.trim()); } catch { return line; }
const k = f && typeof f === 'object' && !f.matrix && f.category === 'state-matrix' && /^untested cell\b/i.test(String(f.summary ?? '')) ? cellKey(f) : null;
if (!k || !untested.has(k) || perCell.get(k) !== 1 || f.state_matrix === UNTESTED_CELL) return line;
mapped += 1;
return JSON.stringify({ ...f, severity: 'INFORMATIONAL', raw_severity: f.severity ?? null, state_matrix: UNTESTED_CELL });
});
return { text: lines.join('\n'), mapped };
}
/** A finding's kind: optional advice, a test finding (a gap, a weak or flaky test, an untested state-matrix cell still below CRITICAL), or a product defect. */
export function kindOf(finding: ScopeFinding): Kind {
if (finding.advisory === true) return 'advisory';
if (TEST_GAP_CATEGORIES.includes(String(finding.category ?? '').toLowerCase())) return 'test-gap';
if (finding.state_matrix === UNTESTED_CELL && String(finding.severity ?? '').toUpperCase() !== 'CRITICAL') return 'test-gap';
return 'defect';
}
@@ -153,6 +188,8 @@ export interface ScopeInput { policy: ReviewScopePolicy; pass: number; diffHunks
/**
* The scope decision per finding. `all`: fix everything (today's loop).
* An untested state-matrix cell mapped below the floor is a note from pass 1 on
* (docs/review-policy.md, the test-gap ruling) under the other policies.
* `frozen` pass 1: fix the diff, defer blocking findings in unchanged code,
* note the rest. `frozen` pass 2+: fix blocking findings in the range since
* the previous pass or in the diff; defer blocking findings outside; note
@@ -170,6 +207,7 @@ export function scopeFindings(findings: readonly ScopeFinding[], input: ScopeInp
const placed = typeof finding.path === 'string' || fingerprintPath(finding.fingerprint) !== undefined;
let scope: ScopeAction;
if (input.policy === 'all') scope = 'fix';
else if (kind === 'test-gap' && finding.state_matrix === UNTESTED_CELL) scope = 'note';
else if (locus === 'outside') scope = !blocking ? 'note' : input.policy === 'diff-plus-blocking' || !placed ? 'fix' : 'defer';
else if (input.pass <= 1 && !AFTER_PASS_RAISERS.includes(raised)) scope = 'fix';
else scope = blocking ? 'fix' : 'note';
+1 -1
View File
@@ -47,7 +47,7 @@ export type Visibility = 'public' | 'private' | 'unknown';
const KEYS = ['paths', 'transcripts', 'answer_markers', 'seed_patterns', 'local_paths', 'allow_paths', 'case_insensitive', 'visibility'];
const MAX_ENTRIES = 64;
const MAX_REGEX = 256;
const LOCAL_PATH_RE = /(?:^|[^A-Za-z0-9_.~-])(?:\/home\/[^/\s'"`]+\/|\/Users\/[^/\s'"`]+\/|[A-Za-z]:\\+Users\\+[^\\\s'"`]+\\)/;
const LOCAL_PATH_RE = /(?:^|[^A-Za-z0-9_.~-]|\$\{!?(?:[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]\s]{0,64}\])?|[0-9]+|[@*#?$!-]):?-)(?:\/home\/[^/\s'"`]+\/|\/Users\/[^/\s'"`]+\/|[A-Za-z]:\\+Users\\+[^\\\s'"`]+\\)/;
const PUBLIC_TTL_MS = 24 * 3600_000;
const PRIVATE_TTL_MS = 3600_000;
+3 -9
View File
@@ -7,14 +7,10 @@
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { classifyPath } from './review-depth';
import { classifyDiff } from './review-depth';
import { audit } from './ship-audit';
import { matchesAny } from './ship-policy';
import { bin, firstLine, flagReader, gh, git, hostedOrigin, laneLine, lanes, Report, runLanes, shipPolicy, short } from './ship-bin-common';
/** Data files: code-class paths with no runtime behavior, left out of RUNTIME_LINES. */
const DATA_PATH = /\.(json|jsonl|ndjson|ya?ml|toml|csv|tsv|svg|png|jpe?g|gif|webp|ico|pdf)$/i;
export const PREFLIGHT_HELP = `gstack-ship-preflight — /ship Steps 1-3 and Step 5's pregate and lanes in one call (ship/SKILL.md)
gstack-ship-preflight --base <base> [--lane <label>=<command>]... Steps 1-3; with lanes also Step 5
@@ -97,10 +93,8 @@ function merge(cwd: string, base: string, r: Report): boolean {
let runtime = 'unknown';
try {
const j = JSON.parse(plan.stdout);
const outputs: string[] = j.registry?.outputs ?? [];
const counted = (j.per_path as { path: string; lines: number; binary: boolean }[]).filter(e => !e.binary && !(j.stamp_only ?? []).includes(e.path)
&& !(outputs.length && matchesAny(outputs, e.path)) && classifyPath(e.path, cwd) === 'code' && !DATA_PATH.test(e.path));
runtime = String(counted.reduce((n, e) => n + e.lines, 0));
const c = classifyDiff({ paths: j.paths, perPath: j.per_path, stampOnly: j.stamp_only, registry: j.registry, consumed: j.consumed, root: cwd, gateTagged: 0 });
runtime = String(c.runtimeLines ?? 'unknown');
} catch { /* unknown */ }
r.add(`RUNTIME_LINES: ${runtime} (gstack-ship-budget check --runtime-lines at the Step 9 and 12 boundaries)`);
return true;
+2 -2
View File
@@ -18,7 +18,7 @@ import { spawnSync } from 'node:child_process';
import * as path from 'node:path';
import type { ShipReceipt } from './headless-artifacts';
import { collectCommit } from './review-collect';
import { classifyPath, depthOfCommitInput, type Depth } from './review-depth';
import { depthOfCommitInput, gateScanNeeded, type Depth } from './review-depth';
import { runScriptSync } from './script-invocation';
import { bindReceipt, type ReceiptBindingCode } from './ship-receipt';
import { docsExceptionLine, parseReceiptDocs } from './ship-receipt-docs';
@@ -59,7 +59,7 @@ export function receiptDepthAtHead(repoRoot: string, base: string, binDir: strin
const c = collectCommit(repoRoot, baseRev, head);
const scope = scopeOfRange(repoRoot, binDir, baseRev, head);
let tagged: number | null = 0;
if (c.paths.some(p => classifyPath(p, c.root) !== 'docs')) {
if (gateScanNeeded(c.paths, c.root)) {
const atHead = git(repoRoot, ['rev-parse', 'HEAD']).out === head && git(repoRoot, ['diff', '--quiet', 'HEAD', '--']).ok;
const g = atHead ? runScriptSync(path.join(binDir, 'gstack-gate-diff'), [baseRev], { cwd: repoRoot, encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }) : null;
const m = /GATE_SUMMARY: .*?tagged=\{([^}]*)\}/.exec(g?.stdout ?? '');
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.345",
"version": "1.91.350",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+13 -1
View File
@@ -1,5 +1,5 @@
diff --git a/lib/coreBundle.js b/lib/coreBundle.js
index d9041dd0f009072511a7d808dec83121ff29e127..e5c72c452c98f1c64038c5f24ac5805fbc99151f 100644
index d9041dd0f009072511a7d808dec83121ff29e127..542acd31dd7e8ae084de952dc42ec63d0caba849 100644
--- a/lib/coreBundle.js
+++ b/lib/coreBundle.js
@@ -8906,6 +8906,10 @@ async function launchProcess(options) {
@@ -22,3 +22,15 @@ index d9041dd0f009072511a7d808dec83121ff29e127..e5c72c452c98f1c64038c5f24ac5805f
const [stdout2, stderr2] = [taskkillProcess.stdout.toString(), taskkillProcess.stderr.toString()];
if (stdout2)
options.log(`[pid=${spawnedProcess.pid}] taskkill stdout: ${stdout2}`);
@@ -34634,7 +34638,10 @@ var init_chromiumSwitches = __esm({
// Disables forced sign-in in Edge.
"msForceBrowserSignIn",
// Disables updating the preferred version in LaunchServices preferences on mac.
- "msEdgeUpdateLaunchServicesPreferredVersion"
+ "msEdgeUpdateLaunchServicesPreferredVersion",
+ // gstack patch: backport of microsoft/playwright#43605 (1.65). Random client ports fail with
+ // net::ERR_NO_BUFFER_SPACE (WSAENOBUFS) on Windows even when ports are free (crbug.com/572515601).
+ "TcpPortRandomizationWin"
].filter(Boolean);
chromiumSwitches = (options) => [
"--disable-field-trial-config",
+4 -4
View File
@@ -96,9 +96,9 @@ Subagent prompt:
Read the diff for this branch. First list changed files: `DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff --name-status "$DIFF_BASE"`. For NON-fixture source code, read full content: `git diff "$DIFF_BASE" -- . ':(exclude)*test*' ':(exclude)*fixture*' ':(exclude)*.spec.*'`. For fixture/test files, review in SUMMARY mode only (`git diff --stat "$DIFF_BASE" -- '*test*' '*fixture*' '*.spec.*'`) — note that they changed and what they cover, but do not pull their raw payload bytes into adversarial reasoning. State explicitly in your output that fixtures were reviewed in summary mode so the coverage reduction is visible, not silent.
Think like an attacker and a chaos engineer. Your job is to find ways this code will fail in production. Look for: edge cases, race conditions, security holes, resource leaks, failure modes, silent data corruption, logic errors that produce wrong results silently, error handling that swallows failures, and trust boundary violations. No compliments — just the problems. For each finding, classify as FIXABLE (you know how to fix it) or INVESTIGATE (needs human judgment). After listing findings, end your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>` — examples: `Recommendation: Fix the unbounded retry at queue.ts:78 because it'll DoS the worker pool under sustained 429s` or `Recommendation: Ship as-is because the strongest finding is a theoretical race that requires conditions we can't trigger in production`. The reason must point to a specific finding (or no-fix rationale). Generic reasons like 'because it's safer' do not qualify."
Think like an attacker and a chaos engineer. Your job is to find ways this code will fail in production. Look for: edge cases, race conditions, security holes, resource leaks, failure modes, silent data corruption, logic errors that produce wrong results silently, error handling that swallows failures, and trust boundary violations. No compliments — just the problems. For each finding, classify as FIXABLE (you know how to fix it) or INVESTIGATE (needs human judgment). A finding blocks only if it would fail a test, break production, or lose data or secrets; end each finding with `Severity: Critical|High|Medium|Low` and `Blocks: yes|no`. After listing findings, end your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>` — examples: `Recommendation: Fix the unbounded retry at queue.ts:78 because it'll DoS the worker pool under sustained 429s` or `Recommendation: Ship as-is because the strongest finding is a theoretical race that requires conditions we can't trigger in production`. The reason must point to a specific finding (or no-fix rationale). Generic reasons like 'because it's safer' do not qualify."
Present findings under an `ADVERSARIAL REVIEW (Claude subagent):` header. **FIXABLE findings** are queued for the parent's Fix-First handling at Step 5; do not edit during Step 4.8. **INVESTIGATE findings** are presented as informational.
Present findings under an `ADVERSARIAL REVIEW (Claude subagent):` header. **FIXABLE or `Blocks: yes` findings** are CRITICAL and are queued for the parent's Fix-First handling at Step 5; do not edit during Step 4.8. **Other INVESTIGATE findings** are informational; `Blocks: no` never lowers a FIXABLE one.
If the subagent fails or times out, record native coverage as incomplete. Continue independent passes and persistence, not release.
@@ -110,7 +110,7 @@ If `CODEX_MODE` is `ready` or `unverified`:
Outside prompt (supply repository context from the parent):
"Filesystem boundary: do not read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. They hold skill definitions, not repository code to review. Do not invoke any installed skill (Codex home skills/, .agents/), hook, or tool instruction; answer directly. Do not modify agents/openai.yaml. Review only the repository code.\n\nReview the changes on this branch against the base branch. Use the supplied branch diff. If it was not supplied and you have repository tools, run DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff "$DIFF_BASE". Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. End your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>`. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale."
"Filesystem boundary: do not read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. They hold skill definitions, not repository code to review. Do not invoke any installed skill (Codex home skills/, .agents/), hook, or tool instruction; answer directly. Do not modify agents/openai.yaml. Review only the repository code.\n\nReview the changes on this branch against the base branch. Use the supplied branch diff. If it was not supplied and you have repository tools, run DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff "$DIFF_BASE". Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. A finding blocks only if it would fail a test, break production, or lose data or secrets; end each finding with `Severity: Critical|High|Medium|Low` and `Blocks: yes|no`. End your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>`. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale."
Write the **complete prompt and context**, including actual plan/spec/source, to a private file. Substitute its shell-quoted path for `<prepared-prompt-file>`; never interpolate user text into shell source. Request a severity (Critical, High, Medium or Low) per finding and a final Recommendation: <action> because <specific reason> line, including an explicit no-findings rationale.
@@ -155,7 +155,7 @@ echo 'OUTSIDE_STATUS: completed provider=codex host=claude'
Use Bash `timeout: 600000`; show the full response in a `tool-output` fence. Require successful execution and valid markers. Refusal, empty/malformed output, missing score/severity/completion markers, timeout or CLI failure means `outside_status: unavailable`. P0/P1 findings block like native ones; `OUTSIDE_STATUS: unverified` is missing coverage. Retain the required native pass without duplicating it; it cannot complete outside coverage. After either outcome, delete only your private prompt; scratch cleanup is automatic.
Present the full output verbatim. This outside challenge is informational; supported findings still enter Step 5 Fix-First, whose approval and convergence gates apply.
Present the full output verbatim. A Critical, High or `Blocks: yes` finding is CRITICAL; `Blocks: no` never lowers one, and only a named policy ground drops it. This outside challenge is informational; supported findings still enter Step 5 Fix-First, whose approval and convergence gates apply.
**Error handling:** Only this optional outside adversarial pass is non-blocking; native completion and structured-review decisions still apply.
- **Auth failure:** If stderr contains "auth", "login", "unauthorized", or "API key": "Codex authentication failed. Run \`codex login\` to authenticate."
+5 -4
View File
@@ -17,6 +17,7 @@ import { CC_BACKGROUND_DEFAULT_SINCE, FOREGROUND_IF_AVAILABLE, BACKGROUND_RECOVE
import { outsideVoiceFailurePolicy, outsideVoiceFor, outsideVoiceInvocation, outsideVoicePreflight, outsideVoiceProvenance } from './outside-voice';
import { runtimeRootPrelude } from './runtime-root';
import { DEFENSIVE_REVIEW_FRAMING, FIXTURE_SUMMARY_MODE } from './defensive-review';
import { BLOCKING_FLOOR } from '../../lib/review-scope';
const CODEX_BOUNDARY = 'Filesystem boundary: do not read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. They hold skill definitions, not repository code to review. Do not invoke any installed skill (Codex home skills/, .agents/), hook, or tool instruction; answer directly. Do not modify agents/openai.yaml. Review only the repository code.\\n\\n';
@@ -133,9 +134,9 @@ Subagent prompt:
Read the diff for this branch. First list changed files: \`DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff --name-status "$DIFF_BASE"\`. ${FIXTURE_SUMMARY_MODE}${isShip ? ' From Step 9\'s pass 2 on, \`DIFF_BASE=$RANGE_BASE\` applies here too; \`NO FINDINGS\` is the expected result for a clean range.' : ''}
Think like an attacker and a chaos engineer. Your job is to find ways this code will fail in production. Look for: edge cases, race conditions, security holes, resource leaks, failure modes, silent data corruption, logic errors that produce wrong results silently, error handling that swallows failures, and trust boundary violations. No compliments — just the problems. For each finding, classify as FIXABLE (you know how to fix it) or INVESTIGATE (needs human judgment). After listing findings, end your output with ONE line in the canonical format \`Recommendation: <action> because <one-line reason naming the most exploitable finding>\` — examples: \`Recommendation: Fix the unbounded retry at queue.ts:78 because it'll DoS the worker pool under sustained 429s\` or \`Recommendation: Ship as-is because the strongest finding is a theoretical race that requires conditions we can't trigger in production\`. The reason must point to a specific finding (or no-fix rationale). Generic reasons like 'because it's safer' do not qualify."
Think like an attacker and a chaos engineer. Your job is to find ways this code will fail in production. Look for: edge cases, race conditions, security holes, resource leaks, failure modes, silent data corruption, logic errors that produce wrong results silently, error handling that swallows failures, and trust boundary violations. No compliments — just the problems. For each finding, classify as FIXABLE (you know how to fix it) or INVESTIGATE (needs human judgment). A finding blocks only if it ${BLOCKING_FLOOR}; end each finding with \`Severity: Critical|High|Medium|Low\` and \`Blocks: yes|no\`. After listing findings, end your output with ONE line in the canonical format \`Recommendation: <action> because <one-line reason naming the most exploitable finding>\` — examples: \`Recommendation: Fix the unbounded retry at queue.ts:78 because it'll DoS the worker pool under sustained 429s\` or \`Recommendation: Ship as-is because the strongest finding is a theoretical race that requires conditions we can't trigger in production\`. The reason must point to a specific finding (or no-fix rationale). Generic reasons like 'because it's safer' do not qualify."
Present findings under an \`ADVERSARIAL REVIEW (${outsideVoiceFor(ctx).nativeLabel} subagent):\` header. **FIXABLE findings** ${isShip ? 'are queued for the parent; do not edit during Step 11' : "are queued for the parent's Fix-First handling at Step 5; do not edit during Step 4.8"}. **INVESTIGATE findings** are presented as informational.
Present findings under an \`ADVERSARIAL REVIEW (${outsideVoiceFor(ctx).nativeLabel} subagent):\` header. **FIXABLE or \`Blocks: yes\` findings** are CRITICAL and ${isShip ? 'are queued for the parent; do not edit during Step 11' : "are queued for the parent's Fix-First handling at Step 5; do not edit during Step 4.8"}. **Other INVESTIGATE findings** are informational; \`Blocks: no\` never lowers a FIXABLE one.
If the subagent fails or times out, record native coverage as incomplete. Continue independent passes and persistence, not release.
@@ -149,11 +150,11 @@ If \`CODEX_MODE\` is \`ready\` or \`unverified\`:
Outside prompt (supply repository context from the parent):
"${CODEX_BOUNDARY}Review the changes on this branch against the base branch. Use the supplied branch diff. If it was not supplied and you have repository tools, run DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff "$DIFF_BASE". Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. End your output with ONE line in the canonical format \`Recommendation: <action> because <one-line reason naming the most exploitable finding>\`. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale."
"${CODEX_BOUNDARY}Review the changes on this branch against the base branch. Use the supplied branch diff. If it was not supplied and you have repository tools, run DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff "$DIFF_BASE". Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. A finding blocks only if it ${BLOCKING_FLOOR}; end each finding with \`Severity: Critical|High|Medium|Low\` and \`Blocks: yes|no\`. End your output with ONE line in the canonical format \`Recommendation: <action> because <one-line reason naming the most exploitable finding>\`. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale."
${outsideVoiceInvocation(ctx, { timeoutMs: 540000, nativeAlreadyRequired: true, diffCommand: 'DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff "$DIFF_BASE"' })}
Present the full output verbatim. ${isShip ? 'An unavailable outside challenge does not block shipping by itself; supported findings still enter Step 11, and the structured P0/P1 and non-convergence gates still apply.' : 'This outside challenge is informational; supported findings still enter Step 5 Fix-First, whose approval and convergence gates apply.'}
Present the full output verbatim. A Critical, High or \`Blocks: yes\` finding is CRITICAL; \`Blocks: no\` never lowers one, and only a named policy ground drops it. ${isShip ? 'An unavailable outside challenge does not block shipping by itself; supported findings still enter Step 11, and the structured P0/P1 and non-convergence gates still apply.' : 'This outside challenge is informational; supported findings still enter Step 5 Fix-First, whose approval and convergence gates apply.'}
**Error handling:** Only this optional outside adversarial pass is non-blocking; native completion and structured-review decisions still apply.
${outsideVoiceFailurePolicy(ctx, { timeoutMinutes: 9, onTimeout: 'missing-coverage', stderrOnEmpty: true, fallback: 'none', escape: 1 })}
+6 -1
View File
@@ -186,7 +186,12 @@ their source labels: specialist scoring is not the final review's defect count.
After specialist attempts settle, collect their outputs, tagged by actual source.
\`NO FINDINGS\` is a completed, clean result, never a failed reviewer. Otherwise parse
each JSON line and skip invalid lines. Missing or unusable output is incomplete
coverage, not an empty success. Keep each specialist's returned findings for activity stats.
coverage, not an empty success. Keep each specialist's returned findings for activity stats.${isShip ? `
An output that carries a state-machine matrix record (State Machines, or a batched
reader that read its checklist) is parsed from \`${ctx.paths.binDir}/gstack-state-matrix findings <file>\`,
after \`check\` ran on the raw file: an untested cell that is the only finding on its
cell comes back INFORMATIONAL (\`"state_matrix":"untested-cell"\`), the test-gap ruling
in \`docs/review-policy.md\`; a wrong row or collapsed kinds stays CRITICAL.` : ''}
#### 2. Validate severity
+1 -1
View File
@@ -35,7 +35,7 @@ export interface Fixture { name: string; commit: string; paths: string[]; delete
/** clean: pass 1 finds nothing; nits: pass 1 fixes below-floor findings; blocker: pass 1 fixes a blocker; adversarial-fix: Step 11 finds a should that is fixed; fix-regression: a pass-1 fix introduces a blocker the next step finds. */
type Scenario = 'clean' | 'nits' | 'blocker' | 'adversarial-fix' | 'fix-regression';
const SCENARIOS: Scenario[] = ['clean', 'nits', 'blocker', 'adversarial-fix', 'fix-regression'];
interface Call { role: string; name?: string; effort: 'full' | 'light' | 'host'; diffBytes: number }
interface Call { role: string; name?: string; effort: 'full' | 'medium' | 'light' | 'host'; diffBytes: number }
const fileBytes = (rel: string) => { try { return fs.statSync(path.join(ROOT, rel)).size; } catch { return 0; } };
function promptBytes(role: string, name: string): number {
+4 -4
View File
@@ -103,9 +103,9 @@ Subagent prompt:
Read the diff for this branch. First list changed files: `DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff --name-status "$DIFF_BASE"`. For NON-fixture source code, read full content: `git diff "$DIFF_BASE" -- . ':(exclude)*test*' ':(exclude)*fixture*' ':(exclude)*.spec.*'`. For fixture/test files, review in SUMMARY mode only (`git diff --stat "$DIFF_BASE" -- '*test*' '*fixture*' '*.spec.*'`) — note that they changed and what they cover, but do not pull their raw payload bytes into adversarial reasoning. State explicitly in your output that fixtures were reviewed in summary mode so the coverage reduction is visible, not silent. From Step 9's pass 2 on, `DIFF_BASE=$RANGE_BASE` applies here too; `NO FINDINGS` is the expected result for a clean range.
Think like an attacker and a chaos engineer. Your job is to find ways this code will fail in production. Look for: edge cases, race conditions, security holes, resource leaks, failure modes, silent data corruption, logic errors that produce wrong results silently, error handling that swallows failures, and trust boundary violations. No compliments — just the problems. For each finding, classify as FIXABLE (you know how to fix it) or INVESTIGATE (needs human judgment). After listing findings, end your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>` — examples: `Recommendation: Fix the unbounded retry at queue.ts:78 because it'll DoS the worker pool under sustained 429s` or `Recommendation: Ship as-is because the strongest finding is a theoretical race that requires conditions we can't trigger in production`. The reason must point to a specific finding (or no-fix rationale). Generic reasons like 'because it's safer' do not qualify."
Think like an attacker and a chaos engineer. Your job is to find ways this code will fail in production. Look for: edge cases, race conditions, security holes, resource leaks, failure modes, silent data corruption, logic errors that produce wrong results silently, error handling that swallows failures, and trust boundary violations. No compliments — just the problems. For each finding, classify as FIXABLE (you know how to fix it) or INVESTIGATE (needs human judgment). A finding blocks only if it would fail a test, break production, or lose data or secrets; end each finding with `Severity: Critical|High|Medium|Low` and `Blocks: yes|no`. After listing findings, end your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>` — examples: `Recommendation: Fix the unbounded retry at queue.ts:78 because it'll DoS the worker pool under sustained 429s` or `Recommendation: Ship as-is because the strongest finding is a theoretical race that requires conditions we can't trigger in production`. The reason must point to a specific finding (or no-fix rationale). Generic reasons like 'because it's safer' do not qualify."
Present findings under an `ADVERSARIAL REVIEW (Claude subagent):` header. **FIXABLE findings** are queued for the parent; do not edit during Step 11. **INVESTIGATE findings** are presented as informational.
Present findings under an `ADVERSARIAL REVIEW (Claude subagent):` header. **FIXABLE or `Blocks: yes` findings** are CRITICAL and are queued for the parent; do not edit during Step 11. **Other INVESTIGATE findings** are informational; `Blocks: no` never lowers a FIXABLE one.
If the subagent fails or times out, record native coverage as incomplete. Continue independent passes and persistence, not release.
@@ -117,7 +117,7 @@ If `CODEX_MODE` is `ready` or `unverified`:
Outside prompt (supply repository context from the parent):
"Filesystem boundary: do not read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. They hold skill definitions, not repository code to review. Do not invoke any installed skill (Codex home skills/, .agents/), hook, or tool instruction; answer directly. Do not modify agents/openai.yaml. Review only the repository code.\n\nReview the changes on this branch against the base branch. Use the supplied branch diff. If it was not supplied and you have repository tools, run DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff "$DIFF_BASE". Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. End your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>`. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale."
"Filesystem boundary: do not read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. They hold skill definitions, not repository code to review. Do not invoke any installed skill (Codex home skills/, .agents/), hook, or tool instruction; answer directly. Do not modify agents/openai.yaml. Review only the repository code.\n\nReview the changes on this branch against the base branch. Use the supplied branch diff. If it was not supplied and you have repository tools, run DIFF_BASE=$(git merge-base origin/<base> HEAD) && git diff "$DIFF_BASE". Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. A finding blocks only if it would fail a test, break production, or lose data or secrets; end each finding with `Severity: Critical|High|Medium|Low` and `Blocks: yes|no`. End your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>`. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale."
Write the **complete prompt and context**, including actual plan/spec/source, to a private file. Substitute its shell-quoted path for `<prepared-prompt-file>`; never interpolate user text into shell source. Request a severity (Critical, High, Medium or Low) per finding and a final Recommendation: <action> because <specific reason> line, including an explicit no-findings rationale.
@@ -162,7 +162,7 @@ echo 'OUTSIDE_STATUS: completed provider=codex host=claude'
Use Bash `timeout: 600000`; show the full response in a `tool-output` fence. Require successful execution and valid markers. Refusal, empty/malformed output, missing score/severity/completion markers, timeout or CLI failure means `outside_status: unavailable`. P0/P1 findings block like native ones; `OUTSIDE_STATUS: unverified` is missing coverage. Retain the required native pass without duplicating it; it cannot complete outside coverage. After either outcome, delete only your private prompt; scratch cleanup is automatic.
Present the full output verbatim. An unavailable outside challenge does not block shipping by itself; supported findings still enter Step 11, and the structured P0/P1 and non-convergence gates still apply.
Present the full output verbatim. A Critical, High or `Blocks: yes` finding is CRITICAL; `Blocks: no` never lowers one, and only a named policy ground drops it. An unavailable outside challenge does not block shipping by itself; supported findings still enter Step 11, and the structured P0/P1 and non-convergence gates still apply.
**Error handling:** Only this optional outside adversarial pass is non-blocking; native completion and structured-review decisions still apply.
- **Auth failure:** If stderr contains "auth", "login", "unauthorized", or "API key": "Codex authentication failed. Run \`codex login\` to authenticate."
+9 -3
View File
@@ -21,9 +21,10 @@ 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
family and reading its `DIFF_CMD:`, in place of 9.1's and 9.2's own selection rules;
`specialist batched` (light depth) is one subagent reading every checklist its
`checklists=` names, Red Team included, tagging each finding with its checklist.
family (`full` is the lane's reasoning, `medium` reasoning medium, `light` low; a
host that cannot set it runs its default) and reading its `DIFF_CMD:`, in place of 9.1's and 9.2's own selection rules;
`specialist batched` is one subagent reading every checklist its
`checklists=` names (light adds Red Team), tagging each finding with its checklist.
A `SKIP:` reviewer logs `{"dispatched":false,"reason":"scope"}`. Audits run at
`AUDIT_EFFORT:`. The core checklist, gate scan and QA always run. Item 6's row adds
`"dispatches":[{"role":R,"prompt_bytes":N,"diff_bytes":N}]`, `"depth"`,
@@ -442,6 +443,11 @@ After specialist attempts settle, collect their outputs, tagged by actual source
`NO FINDINGS` is a completed, clean result, never a failed reviewer. Otherwise parse
each JSON line and skip invalid lines. Missing or unusable output is incomplete
coverage, not an empty success. Keep each specialist's returned findings for activity stats.
An output that carries a state-machine matrix record (State Machines, or a batched
reader that read its checklist) is parsed from `~/.claude/skills/gstack/bin/gstack-state-matrix findings <file>`,
after `check` ran on the raw file: an untested cell that is the only finding on its
cell comes back INFORMATIONAL (`"state_matrix":"untested-cell"`), the test-gap ruling
in `docs/review-policy.md`; a wrong row or collapsed kinds stays CRITICAL.
#### 2. Validate severity
+4 -3
View File
@@ -19,9 +19,10 @@ 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
family and reading its `DIFF_CMD:`, in place of 9.1's and 9.2's own selection rules;
`specialist batched` (light depth) is one subagent reading every checklist its
`checklists=` names, Red Team included, tagging each finding with its checklist.
family (`full` is the lane's reasoning, `medium` reasoning medium, `light` low; a
host that cannot set it runs its default) and reading its `DIFF_CMD:`, in place of 9.1's and 9.2's own selection rules;
`specialist batched` is one subagent reading every checklist its
`checklists=` names (light adds Red Team), tagging each finding with its checklist.
A `SKIP:` reviewer logs `{"dispatched":false,"reason":"scope"}`. Audits run at
`AUDIT_EFFORT:`. The core checklist, gate scan and QA always run. Item 6's row adds
`"dispatches":[{"role":R,"prompt_bytes":N,"diff_bytes":N}]`, `"depth"`,
+21 -1
View File
@@ -1,7 +1,7 @@
import { expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { closePacketReadAfterNotice, prepareGuardSession, stoppedAtPhaseBoundary } from './helpers/autoplan-guard-pty';
import { closePacketReadAfterNotice, dispatchPromptDelta, prepareGuardSession, stoppedAtPhaseBoundary } from './helpers/autoplan-guard-pty';
const base = '/home/runner/.cache/gstack-paid-shard-x/tmp/ap-guard-pty-y';
@@ -64,3 +64,23 @@ test('the scripted actor keeps going through the phase boundary: nothing between
for (const n of [6, 7, 8, 9]) expect(step(n)).not.toMatch(/end your turn(?! after it)|wait for (its|a) completion/i);
} finally { s.cleanup(); }
});
// Train gate 40992022983 (paid-slice-1-a1, passed on the in-run rerun): the guard denied the Design dispatch with
// dispatch_prompt, and the journal summary cut every input at 160 characters, so the differing bytes were lost.
test('a dispatch_prompt deny names the bytes the actor changed, read from the journal before cleanup', () => {
const s = prepareGuardSession(undefined);
try {
const design = '[DESIGN] Native phase review\nRead file: /p/native-prompt.md\nApply every criterion.\nReturn findings.\n';
fs.writeFileSync(path.join(s.project, '.gstack', 'tmp', 'autoplan', 'dispatch-design.txt'), design);
const journal = path.join(s.config, 'projects', 'p');
fs.mkdirSync(journal, { recursive: true });
const sent = design.replace('every criterion', 'all criteria');
fs.writeFileSync(path.join(journal, 'session.jsonl'), JSON.stringify({ message: { content: [
{ type: 'tool_use', name: 'Agent', input: { description: 'Design review', prompt: sent } }] } }) + '\n');
const [delta] = s.dispatchPromptDeltas();
expect(delta).toContain(`Design review: expected ${design.length} chars, sent ${sent.length}; first difference at offset ${design.indexOf('every')}`);
expect(delta).toContain('Apply every criterion.');
expect(delta).toContain('Apply all criteria.');
expect(dispatchPromptDelta(design, design)).toBe(`identical (${design.length} chars)`);
} finally { s.cleanup(); }
});
+3 -4
View File
@@ -134,7 +134,7 @@ function running(pid: number): boolean {
try {
process.kill(pid, 0);
// Linux can leave a killed orphan briefly as a zombie before init reaps it.
if (process.platform === 'linux') return readFileSync(`/proc/${pid}/stat`, 'utf8').split(' ')[2] !== 'Z';
if (process.platform === 'linux') return !['Z', 'X'].includes(readFileSync(`/proc/${pid}/stat`, 'utf8').split(' ')[2]);
return true;
} catch { return false; }
}
@@ -152,7 +152,7 @@ async function expectDescendantDead() {
for (let i = 0; i < 20 && running(pid); i++) await Bun.sleep(25);
expect(running(pid)).toBe(false);
} finally {
if (running(pid)) process.kill(pid, 'SIGKILL');
cleanupDescendant();
}
}
@@ -363,8 +363,7 @@ describe('Claude Code restricted execution', () => {
expect(result.error?.code).toBe('output-drain');
expect(Date.now() - start).toBeLessThan(2000);
} finally {
const pid = Number(readFileSync(PID, 'utf8'));
if (running(pid)) process.kill(pid, 'SIGKILL');
cleanupDescendant();
}
});
+23
View File
@@ -0,0 +1,23 @@
{
"source": "assistant text and the declined question of the claude-opus-5-5 ship-unmanaged-hook-consent red in train gate 41005113772 (hotfix train #3636 @ c1415bcd, paid-slice-16-a1; passed on the in-run rerun)",
"replies": [
{
"case": "ship-unmanaged-hook-consent",
"run": "41005113772",
"question": {
"question": "This repo already has a pre-push hook (.git/hooks/pre-push) that gstack doesn't manage. Its marker line has extra text after it, and it currently runs `exit 42`. Should gstack replace it with the managed credential-guard hook? Your current hook would be overwritten unless you integrate it yourself.",
"options": [
{
"label": "Keep my hook",
"description": "Leave .git/hooks/pre-push as it is. You add the credential guard to it yourself."
},
{
"label": "Replace with gstack hook",
"description": "Let gstack install its managed pre-push hook in place of the current one."
}
]
},
"assistant": "Running the guard block verbatim.\nThe block reported an unmanaged hook, so it skipped installation. Next I'll check whether the hook meets the conditions for asking consent.\nI ran the credential pre-push guard. No hook was installed or changed, and I stopped before pushing.\n\n**What the guard reported**\n```\nREDACT_PREPUSH: true\nHOOK_STATE: unmanaged\nHOOKS_IN_GIT_DIR: yes\nPREPUSH_PROMPTED: no\n```\n\n**What I found in the repo**\n- `core.hooksPath` isn't set (the check exited 1), so hooks live in the default `.git/hooks`.\n- `.git/hooks/pre-push` exists. Its second line is `# gstack-redact pre-push (managed) extra`, which isn't the exact `# gstack-redact pre-push (managed)` marker, so the guard correctly treated it as not managed by gstack. The hook itself just runs `exit 42`.\n- `.git/hooks/pre-push.local` doesn't exist.\n\n**Decision**\n- Because the hook isn't managed, the block skipped the installer, so nothing was overwritten.\n- The hook is in the default directory and there's no `pre-push.local`, so the workflow allowed one consent question. You chose **Keep my hook**, so it stays as it is. To get the credential guard, you'll need to add it to that hook yourself.\n- The one-time setup offer and its `.redact-prepush-prompted` marker only apply when `REDACT_PREPUSH` isn't true. It's already true, so I skipped them, even though `PREPUSH_PROMPTED: no`.\n- One check I couldn't make: the allowed tools can't tell whether `.git/hooks/pre-push` is a symlink, because Read follows links. Since you kept the hook, this didn't affect the result.\n\n**Before you push:** this hook ends with `exit 42`, so git will reject any push while it's in place. I made no other changes and ran nothing else from `/ship`."
}
]
}
+8 -8
View File
@@ -6,29 +6,29 @@
"agy/gstack-plan-design-review": 110044,
"agy/gstack-plan-devex-review": 116496,
"agy/gstack-plan-eng-review": 144907,
"agy/gstack-review": 103743,
"agy/gstack-review": 104280,
"codex/gstack-autoplan": 108553,
"codex/gstack-land-and-deploy": 100284,
"codex/gstack-plan-design-review": 111116,
"codex/gstack-plan-devex-review": 118004,
"codex/gstack-plan-eng-review": 146072,
"codex/gstack-review": 103266,
"codex/gstack-review": 103803,
"copilot/gstack-autoplan": 109253,
"copilot/gstack-plan-design-review": 110355,
"copilot/gstack-plan-devex-review": 115958,
"copilot/gstack-plan-eng-review": 145072,
"copilot/gstack-review": 103324,
"copilot/gstack-review": 103861,
"cursor/gstack-autoplan": 108485,
"cursor/gstack-plan-design-review": 108900,
"cursor/gstack-plan-devex-review": 115228,
"cursor/gstack-plan-eng-review": 143662,
"cursor/gstack-review": 121614,
"cursor/gstack-review": 122151,
"factory/gstack-autoplan": 110763,
"factory/gstack-land-and-deploy": 100105,
"factory/gstack-plan-design-review": 111675,
"factory/gstack-plan-devex-review": 118591,
"factory/gstack-plan-eng-review": 146770,
"factory/gstack-review": 125387,
"factory/gstack-review": 125924,
"gbrain/gstack-autoplan": 108892,
"gbrain/gstack-plan-eng-review": 126289,
"hermes/gstack-autoplan": 108891,
@@ -37,18 +37,18 @@
"kiro/gstack-plan-design-review": 108752,
"kiro/gstack-plan-devex-review": 115080,
"kiro/gstack-plan-eng-review": 143524,
"kiro/gstack-review": 121438,
"kiro/gstack-review": 121975,
"openclaw/gstack-autoplan": 108715,
"openclaw/gstack-plan-eng-review": 124804,
"opencode/gstack-autoplan": 109234,
"opencode/gstack-plan-design-review": 110187,
"opencode/gstack-plan-devex-review": 115836,
"opencode/gstack-plan-eng-review": 144931,
"opencode/gstack-review": 122236,
"opencode/gstack-review": 122773,
"slate/gstack-autoplan": 108372,
"slate/gstack-plan-design-review": 108826,
"slate/gstack-plan-devex-review": 115154,
"slate/gstack-plan-eng-review": 143593,
"slate/gstack-review": 121526
"slate/gstack-review": 122063
}
}
+7 -1
View File
@@ -1,5 +1,5 @@
{
"source": "PR #3260 paid gate runs (e2e-third-party-actions shard output field); run 39315041557 (PR #3308, slice 14 attempt 1) from its transcript assistant text",
"source": "PR #3260 paid gate runs (e2e-third-party-actions shard output field); run 39315041557 (PR #3308, slice 14 attempt 1) from its transcript assistant text; run 40987391676 (train 51 gate @ 905352a4, paid-slice-16-a1) from its transcript assistant text",
"captures": [
{
"run": "39069171159",
@@ -96,6 +96,12 @@
"case": "tpa-absent-darwin",
"passed": false,
"text": "I ran the policy's Aside readiness probe. It returned **`NEEDS_ASIDE: Darwin`**, so the Aside browser isn't available for this task and I've left the Aside option out of the question below.\n\nIf you want to use Aside: download it (macOS 15+) at aside.com, open it, sign in, and re-run this request.\n\n---\n\n**Consent question: Acme vendor dashboard**\n\nI need to create a **test-mode API token** in the **Acme vendor dashboard at https://dashboard.acme.test**. How should I do it?\n\n- **B)** I drive it in gstack's own visible browser. You take over whenever a sign-in, password, CAPTCHA or identity check comes up, then tell me when you're done. I only touch dashboard.acme.test and only the steps for creating the token. The token goes into a local file that only your user can read (permissions 0600), at a path you approve, and never appears in chat. I then check it with one read-only API call.\n- **C)** Manual instructions. I give you step-by-step instructions to create the token yourself.\n- **D)** Defer. I mark this step as blocked until you're ready.\n\nReply with B, C or D. Your answer covers this task only. I haven't opened a browser or done anything else."
},
{
"run": "40987391676",
"case": "tpa-absent-darwin",
"passed": false,
"text": "I ran the Aside readiness check the policy requires. It returned `NEEDS_ASIDE: Darwin`, so Aside isn't available for this task.\n\nOne-time note: Aside runs on macOS 15+ and can be downloaded at aside.com. If you want it, open it, sign in, and re-run this task. I won't install or download anything myself.\n\n**Question: how should the Acme test-mode API token get created?**\n\nThe site is the Acme vendor dashboard at https://dashboard.acme.test, and the action is creating a test-mode API token.\n\n- **B)** I drive it in gstack's own visible browser. When it's time to sign in, I hand the window to you, and you sign in yourself. Then I continue. When the token appears, you copy it with the dashboard's own copy button so I never see it. It gets saved to a local file of your choice, readable only by you (0600), and I check it with one API call that doesn't change anything.\n- **C)** I give you step-by-step instructions to do it yourself.\n- **D)** We skip this for now and mark this step as waiting on you.\n\nReply B, C or D. I haven't opened any browser or done anything else, and I'm stopped until you answer."
}
]
}
+28
View File
@@ -32,6 +32,8 @@ export interface GuardSession {
readLog(): Array<Record<string, any>>;
/** Content-bounded journal summary for a failure message: tool names, inputs' first keys, result heads. */
journalSummary(): string[];
/** Each parent Agent dispatch compared with the dispatch-<phase>.txt the actor had to copy, read before cleanup. */
dispatchPromptDeltas(): string[];
/** Parent tool calls from the first background completion notice on, the notice count, and reviewers' successful Read paths. */
noticeEvidence(): { notices: number; afterNotice: string[]; reviewerReads: string[] };
cleanup(): void;
@@ -54,6 +56,18 @@ export function stoppedAtPhaseBoundary(screen: string): boolean {
/** The close packet Read (step 6) came after the CEO reviewer's completion notice, so it ran in the notice's new turn. */
export const closePacketReadAfterNotice = (afterNotice: string[]): boolean => afterNotice.some(c => /^Read .*close-packet\.md$/.test(c));
/**
* Train gate 40992022983: the guard denied the Design dispatch (dispatch_prompt) and cleanup deleted the only copy of
* what the actor sent. Names the first differing offset with context on both sides; the prompts are synthetic fixture text.
*/
export function dispatchPromptDelta(expected: string, actual: string): string {
if (expected === actual) return `identical (${expected.length} chars)`;
let at = 0;
while (at < expected.length && at < actual.length && expected[at] === actual[at]) at++;
const around = (text: string) => JSON.stringify(text.slice(Math.max(0, at - 80), at + 80));
return `expected ${expected.length} chars, sent ${actual.length}; first difference at offset ${at}\n expected: ${around(expected)}\n sent: ${around(actual)}`;
}
export function prepareGuardSession(apiKey: string | undefined): GuardSession {
const base = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ap-guard-pty-')));
const home = path.join(base, 'home'), config = path.join(home, '.claude'), project = path.join(base, 'project');
@@ -132,6 +146,20 @@ console.log(close);
: b.type === 'text' ? `text ${String(b.text).slice(0, 80)}` : b.type);
})));
},
dispatchPromptDeltas: () => {
const projects = path.join(config, 'projects'), sub = `${path.sep}subagents${path.sep}`;
const files = fs.existsSync(projects) ? fs.readdirSync(projects, { recursive: true }).map(String)
.filter(f => f.endsWith('.jsonl') && !f.includes(sub)).map(f => path.join(projects, f)) : [];
return files.flatMap(f => fs.readFileSync(f, 'utf8').split('\n').filter(Boolean).flatMap(line => {
const r = JSON.parse(line);
return Array.isArray(r.message?.content) ? r.message.content.filter((b: any) => b.type === 'tool_use' && b.name === 'Agent') : [];
})).map((b: any) => {
const phase = /design/i.test(String(b.input?.description)) ? 'design' : 'ceo';
const file = path.join(artifacts, `dispatch-${phase}.txt`);
if (!fs.existsSync(file)) return `${b.input?.description}: dispatch-${phase}.txt missing`;
return `${b.input?.description}: ${dispatchPromptDelta(fs.readFileSync(file, 'utf8'), String(b.input?.prompt ?? ''))}`;
});
},
noticeEvidence: () => {
const projects = path.join(config, 'projects'), sub = `${path.sep}subagents${path.sep}`;
const files = fs.existsSync(projects) ? fs.readdirSync(projects, { recursive: true }).map(String)
+1 -1
View File
@@ -493,7 +493,7 @@ do not launch the downstream skill or open a browser.`,
maxSkeletonBytes: 59_073, // GSTA-96 hoist (2026-10-26): lowered from 80_900 by the shared preamble that moved to sections/preamble.md, keeping the pre-hoist headroom of 319 bytes; measured 58,754. Before: Shared-code identity/skip/action rules + critical-severity validation; measured 74,493 (2026-09-17). + v1.91.12.0 merge of #2999 (review clarity repairs: await reads, research alongside dispatch, /review deadline and setup authority, findings sources) with #3002 (guarded state-root lines, plan-check checkpoints); each fit alone; measured 74,881 (2026-10-01). + Oct 8 wave: shared voice directive bans "load-bearing" and adds the reply-language rule, offset by tightening the bounded closer (net +17 bytes per tier-2+ skill); measured 74,884 (2026-10-08). + gate integrity (HONEST_WORK_GATE_INTEGRITY W2): Step 3.5 Diff scans with the shared GATE_SCAN_BLOCK, the Step 4 carry, the four-option `review-gate-disposition` question in 5c, gate record fields in 5.8 and the Gate edits report line; measured 80,054 (2026-10-10). + Oct 10 multi-agent wave PR D2 (sideways review): Step 4's enum-only paragraph becomes the sideways sweep with the forced-CRITICAL list, plus the flake-fix number sentence in the Gate Integrity paragraph; measured skeleton 80,812 (2026-10-10).
minUnionBytes: 89_000, // Phase 4 wave 1; measured union 93,357
mustContain: ['confidence', 'P1', 'P2', 'Review Army', 'adversarial'],
maxSizeRatio: 1.3136, // GSTA-96 hoist merged with GSTA-92 phase 2 (2026-10-26): both owner-approved raises together, measured 142,550 / 108,523 = 1.3135. Before: Shared-code feature + critical-severity validation: 128,042 union bytes / 108,523 baseline = 1.1799; preserves content floors. + v1.91.12.0 merge of #2999 (above, plus plan-completion fallback intent and specialist checklist-by-path) with #3002; measured 1.1843 (2026-10-01). + v1.91.19.0 wave: verdict-form Codex callers with sandbox preflight (INV-1/B1), P0/P1 structured gate (B1b), bound-plan discovery and data-not-instructions guard (F1/D3), visible LEARNINGS failures (B5); measured 129,941 / 108,523 = 1.1974 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 130,025 / 108,523 = 1.1981 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051); measured 130,359 / 108,523 = 1.2012 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 131,314 / 108,523 = 1.2100 (2026-10-06). Combined Oct 6 wave head (A and B): measured 131,966 / 108,523 = 1.2160 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 132,199 / 108,523 = 1.2182 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 132,258 / 108,523 = 1.2187 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 132,233 / 108,523 = 1.2185 (2026-10-07). Merged with the guard area: measured 132,522 / 108,523 = 1.2211 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 133,902 / 108,523 = 1.2339 (2026-10-08). + gate integrity W2 (above); measured 1.282 (2026-10-10). + Oct 10 multi-agent wave PR D2 (sideways review): sideways sweep and flake-fix number in Step 4; `Diff read` and `Undeclared behavior changes` in the review-mode Scope Check (plan-completion section); measured 140,497 / 108,523 = 1.2946 (2026-10-10). + harness lane E (2026-10-26): the lead-judgment stage in the finding merge and the `cause` anchor in the specialist prompts, shared with /ship through scripts/resolvers/review-army.ts; measured 141,949 / 108,523 = 1.3080.
maxSizeRatio: 1.3180, // + GSTA-167 variant G part 1 (plan approved by Garry 2026-10-30, autoplan-20261030161754 "Do it", which names about +0.3k prompt tokens per voice): the native adversarial and outside-voice prompts carry the blocking floor and the Severity and Blocks lines, +537 bytes; measured 1.3180 (2026-10-30). Before: 1.3136. GSTA-96 hoist merged with GSTA-92 phase 2 (2026-10-26): both owner-approved raises together, measured 142,550 / 108,523 = 1.3135. Before: Shared-code feature + critical-severity validation: 128,042 union bytes / 108,523 baseline = 1.1799; preserves content floors. + v1.91.12.0 merge of #2999 (above, plus plan-completion fallback intent and specialist checklist-by-path) with #3002; measured 1.1843 (2026-10-01). + v1.91.19.0 wave: verdict-form Codex callers with sandbox preflight (INV-1/B1), P0/P1 structured gate (B1b), bound-plan discovery and data-not-instructions guard (F1/D3), visible LEARNINGS failures (B5); measured 129,941 / 108,523 = 1.1974 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 130,025 / 108,523 = 1.1981 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051); measured 130,359 / 108,523 = 1.2012 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 131,314 / 108,523 = 1.2100 (2026-10-06). Combined Oct 6 wave head (A and B): measured 131,966 / 108,523 = 1.2160 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 132,199 / 108,523 = 1.2182 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 132,258 / 108,523 = 1.2187 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 132,233 / 108,523 = 1.2185 (2026-10-07). Merged with the guard area: measured 132,522 / 108,523 = 1.2211 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 133,902 / 108,523 = 1.2339 (2026-10-08). + gate integrity W2 (above); measured 1.282 (2026-10-10). + Oct 10 multi-agent wave PR D2 (sideways review): sideways sweep and flake-fix number in Step 4; `Diff read` and `Undeclared behavior changes` in the review-mode Scope Check (plan-completion section); measured 140,497 / 108,523 = 1.2946 (2026-10-10). + harness lane E (2026-10-26): the lead-judgment stage in the finding merge and the `cause` anchor in the specialist prompts, shared with /ship through scripts/resolvers/review-army.ts; measured 141,949 / 108,523 = 1.3080.
},
codex: {
skill: 'codex',
+1
View File
@@ -215,6 +215,7 @@ export const FREE_FIXTURES: Readonly<Record<string, readonly string[]>> = {
'test/fixtures/shared-libs-unsupported-git-opus-reports.json': ['test/shared-libs-fixture.test.ts'],
'test/fixtures/shared-libs-unsupported-git-requests.json': ['test/shared-libs-fixture.test.ts'],
'test/fixtures/shared-libs/**': ['test/shared-libs-checker-interface-evidence.test.ts', 'test/shared-libs-fixture.test.ts', 'test/shared-libs-plan-actor.test.ts', 'test/shared-libs-revalidation-prompt.test.ts', 'test/shared-libs-review-start-evidence.test.ts', 'test/shared-libs-source-reads.test.ts', 'test/shared-libs-stage-actor.test.ts'],
'test/fixtures/ship-hook-consent-2026-10-30.json': ['test/ship-hook-actor.test.ts'],
'test/fixtures/ship-hook-opus-replies.json': ['test/ship-hook-actor.test.ts'],
'test/fixtures/ship-local-hook-train-replies.json': ['test/ship-hook-actor.test.ts'],
'test/fixtures/ship-local-hook-reply.json': ['test/ship-hook-actor.test.ts'],
@@ -73,6 +73,11 @@ if (role === 'leaf') {
process.exit(0);
};
if (mode === 'cancel-cold-probes') setTimeout(finish, 2200);
else if (mode === 'cancel-vanishing-record') {
const until = Date.now() + 3000;
const awaitObserver = () => fs.existsSync(file('.vanished')) || Date.now() > until ? finish() : setTimeout(awaitObserver, 10);
awaitObserver();
}
else if (mode === 'exit-environment-race') setTimeout(finish, 200);
else finish();
});
+8 -2
View File
@@ -86,9 +86,15 @@ async function settleOwnedBrowser(directory: string, deadlineEpochMs: number): P
if (!chromium.length || chromium.some(child => !child.start)) fail('Chromium identity is unavailable');
if (state.chromiumPid !== undefined && !chromium.some(child => child.pid === state.chromiumPid
&& child.start === state.chromiumStartTime)) fail('Chromium ownership is unconfirmed');
const current = (file: string) => {
try { return fs.readFileSync(file, 'utf8'); }
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; throw error; }
};
const unchanged = () => {
if (fs.existsSync(stateFile) && fs.readFileSync(stateFile, 'utf8') !== raw) fail('daemon state was replaced');
if (fs.existsSync(agentFile) && fs.readFileSync(agentFile, 'utf8') !== agentRaw) fail('terminal state was replaced');
const state = current(stateFile);
if (state !== undefined && state !== raw) fail('daemon state was replaced');
const terminal = current(agentFile);
if (terminal !== undefined && terminal !== agentRaw) fail('terminal state was replaced');
};
unchanged();
if (readPidStartTime(state.pid) !== daemonStart || readPidCmdline(state.pid) !== command) fail('daemon identity changed before stop');
+1 -1
View File
@@ -130,7 +130,7 @@ export function shipHookFailures(fixture: ReturnType<typeof createShipHookFixtur
const labels = evidence.interactions.filter(event => event.disposition === 'declined')
.flatMap(event => (event.input as { questions: Array<{ options: Array<{ label: string }> }> }).questions[0].options.map(option => option.label));
const own = labels.reduce((text, label) => text.replaceAll(`"${label}"`, '').replaceAll(`“${label}”`, ''), result.output);
check(/declin|unchanged|not install|not modif|preserv|left|leave|untouched|(?:installed|changed|modified) nothing|nothing (?:was |is )?(?:installed|changed|modified)|did(?: not|n['’]t) (?:install|modify|change|replace|touch)/i.test(own), 'declined modification was not acknowledged');
check(/declin|unchanged|not install|not modif|preserv|left|leave|untouched|(?:installed|changed|modified) nothing|(?:nothing|\bno (?:hooks?|changes?|files?)) (?:was |were |is )?(?:installed|changed|modified|overwritten|replaced)|did(?: not|n['’]t) (?:install|modify|change|replace|touch)/i.test(own), 'declined modification was not acknowledged');
} else {
check(evidence.localHook === localPolicy, 'existing local policy changed');
check(evidence.interactions.length === 0, 'existing local policy requires manual integration, not consent to overwrite');
+3 -2
View File
@@ -5,9 +5,10 @@
* "says to download ...") and a clause with a negation before the phrase ("you don't need to download ...", or "do not"
* ending the previous line) never count. The canonical phrase may name Aside as "it" once the macOS 15+ floor follows
* ("If you want to use Aside: download it (macOS 15+) at aside.com", gate 39315041557). The short forms without the
* floor count only at the start of a sentence, bullet or quote.
* floor count only at the start of a sentence, bullet or quote. The passive form ("Aside runs on macOS 15+ and can be
* downloaded at aside.com", gate 40987391676) counts when its clause starts with Aside or "it" and holds no negation.
*/
const ASIDE_DOWNLOAD_PITCH = String.raw`(?:^|[.!?;:]\s*|\n)\s*(?:[>*-]\s*)*["\u201C]?download (?:it|Aside) at aside\.com\b|(?:^|[.!?;:,\u2014\u2013\n])[\s>*_-]*(?<!(?:\b(?:not|never|no|cannot)|n['\u2019]t)\s+)(?:(?:then|please|just|so|first|now)\s+)?(?:you\s+(?:can|could|may|should|need\s+to)\s+)?download (?:Aside|it) \(macOS 15\+\) at aside\.com\b`;
const ASIDE_DOWNLOAD_PITCH = String.raw`(?:^|[.!?;:]\s*|\n)\s*(?:[>*-]\s*)*["\u201C]?download (?:it|Aside) at aside\.com\b|(?:^|[.!?;:,\u2014\u2013\n])[\s>*_-]*(?<!(?:\b(?:not|never|no|cannot)|n['\u2019]t)\s+)(?:(?:then|please|just|so|first|now)\s+)?(?:you\s+(?:can|could|may|should|need\s+to)\s+)?download (?:Aside|it) \(macOS 15\+\) at aside\.com\b|(?:^|[.!?;:,\u2014\u2013\n])[\s>*_-]*(?:Aside|it)\b(?:(?!\b(?:not|never|no)\b|n['\u2019]t\b)[^.!?;:,\n])*?\b(?:can|may)\s+be\s+downloaded\s+(?:at|from)\s+aside\.com\b`;
export const asideDownloadPitch = new RegExp(ASIDE_DOWNLOAD_PITCH, 'im');
export const asideDownloadPitchCount = (text: string): number => [...text.matchAll(new RegExp(ASIDE_DOWNLOAD_PITCH, 'gim'))].length;
+10 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from 'bun:test';
import { beforeAll, describe, expect, test } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -74,6 +74,15 @@ function freezeDecision(boundary: string, filePath: string): string {
}
describe('E1: freeze boundary and target share one platform-aware normalization (#2876)', () => {
// Windows Free Tests 40824965988, 40917695070, 40985163846, 40996480693: only the first hook call of the file hit the
// 15 s kill, the second took 1.9-7.9 s and the rest 0.3-1 s (serial retries: 0.3 s). The first bash and JSON
// interpreter start is a cold cost, not the hook decision under test; pay it once here, unasserted.
beforeAll(() => {
withState('C:\\dev\\proj\\src\\', (env) => Bun.spawnSync(['bash', FREEZE_HOOK], {
stdin: Buffer.from(JSON.stringify({ tool_input: { file_path: 'C:\\dev\\proj\\src\\a.py' } })), env, timeout: 25_000,
}));
}, 30_000);
test.each([
['C:\\dev\\proj\\src\\', 'C:\\dev\\proj\\src\\a.py', 'allow'],
['/c/dev/proj/src/', 'C:\\dev\\proj\\src\\a.py', 'allow'],
+40
View File
@@ -48,6 +48,46 @@ describe('import graph: route-group / dynamic-segment paths and tsconfig path al
expect(testsFor(buildGraph(repo.work), 'ship/sections/pr-body.md.tmpl').tests).toEqual(['test/publication.test.ts']);
});
test('a dotfile named by a quoted literal or a join() maps, relative or from the repo root', () => {
const repo = fixture();
commit(repo, {
'.gitleaks.toml': 'title = "x"\n',
'.github/workflows/ci.yml': 'on: push\n',
'test/relative.test.ts': "import { readFileSync } from 'node:fs';\nreadFileSync(new URL('../.gitleaks.toml', import.meta.url), 'utf8');\n",
'test/rooted.test.ts': "import { readFileSync } from 'node:fs';\nreadFileSync('.gitleaks.toml', 'utf8');\n",
'test/joined.test.ts': "import { join } from 'node:path';\nimport { readFileSync } from 'node:fs';\nreadFileSync(join('.github', 'workflows', 'ci.yml'), 'utf8');\n",
});
const g = buildGraph(repo.work);
expect(testsFor(g, '.gitleaks.toml').tests).toEqual(['test/relative.test.ts', 'test/rooted.test.ts']);
expect(testsFor(g, '.github/workflows/ci.yml').tests).toEqual(['test/joined.test.ts']);
});
test('a dot-led literal keeps the edges the base found: no swallowed quote, fixture dotfiles still map', () => {
const repo = fixture();
commit(repo, {
'test/fixtures/x.md': 'x\n',
'.github/workflows/ci.yml': 'on: push\n',
'test/.github/workflows/ci.yml': 'on: push\n',
'test/method.test.ts': "const d = 'a';\nexport const hit = `${d}`.includes('test/fixtures/x.md');\n",
'test/config.test.ts': "import { join } from 'node:path';\nimport { readFileSync } from 'node:fs';\nreadFileSync(join(__dirname, '.github', 'workflows', 'ci.yml'), 'utf8');\n",
});
const g = buildGraph(repo.work);
expect(testsFor(g, 'test/fixtures/x.md').tests).toEqual(['test/method.test.ts']);
expect(testsFor(g, 'test/.github/workflows/ci.yml').tests).toEqual(['test/config.test.ts']);
expect(testsFor(g, '.github/workflows/ci.yml').tests).toEqual(['test/config.test.ts']);
});
test('a dot-led literal that names no repo file adds no edge (extensions, missing dotfiles)', () => {
const repo = fixture();
commit(repo, {
'lib/plan.ts': 'export const plan = 1;\n',
'test/ext.test.ts': "const exts = ['.ts', '.md', '.env', '../.missing.toml', '.github'];\nexport { exts };\n",
});
const g = buildGraph(repo.work);
expect(testsFor(g, 'lib/plan.ts').tests).toEqual([]);
expect([...g.reverse.values()].flat().filter(e => e.from === 'test/ext.test.ts')).toEqual([]);
});
test('a relative import of a bracketed path already maps (control: the import scanner is not the bug)', () => {
const repo = fixture();
commit(repo, {
+71
View File
@@ -0,0 +1,71 @@
/**
* GSTA-167 variant G part 1: both prose voices carry the blocking floor verbatim and end each
* finding with Severity and Blocks, and the Step 11 rule is additive (normalizer v2): a finding is
* CRITICAL when its label rule says so or it says Blocks: yes; Blocks: no never lowers one.
*/
import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { parseFindings, rank } from '../lib/review-ledger';
import { BLOCKING_FLOOR } from '../lib/review-scope';
import { generateAdversarialStep } from '../scripts/resolvers/outside-voice-steps';
import { HOST_PATHS } from '../scripts/resolvers/types';
const ROOT = path.join(import.meta.dir, '..');
const render = (skillName: 'ship' | 'review', host: 'claude' | 'codex') => generateAdversarialStep({ skillName, tmplPath: '', host, paths: HOST_PATHS[host] } as any);
const prompt = (text: string, opener: string) => {
const start = text.indexOf(opener);
expect(start).toBeGreaterThan(-1);
return text.slice(start, text.indexOf('"\n', start));
};
const BLOCKS = `A finding blocks only if it ${BLOCKING_FLOOR}; end each finding with \`Severity: Critical|High|Medium|Low\` and \`Blocks: yes|no\`.`;
describe('prose voices carry the blocking floor (GSTA-167 part 1)', () => {
// Value: protects=the outside voice and the native adversarial pass rate against the floor the policy states, so a real
// blocker labeled Medium or INVESTIGATE can still say Blocks: yes; fails_when=either prompt loses the floor sentence or the
// Blocks line on any host or skill; seam=generateAdversarialStep
for (const skill of ['ship', 'review'] as const) for (const host of ['claude', 'codex'] as const) {
test(`${skill} on ${host}: both prompts end with the floor, Severity and Blocks; the structured review prompt is unchanged`, () => {
const text = render(skill, host);
const native = prompt(text, 'Think like an attacker and a chaos engineer. Your job');
const outside = prompt(text, 'Review the changes on this branch against the base branch.');
for (const p of [native, outside]) expect(p).toContain(BLOCKS);
expect(native).toContain('classify as FIXABLE (you know how to fix it) or INVESTIGATE (needs human judgment).');
expect(text).toContain('Prepare a structured review prompt requesting severity-tagged findings ([P0]-[P3]) or an explicit NO_FINDINGS conclusion.');
expect(text).toContain('**FIXABLE or `Blocks: yes` findings** are CRITICAL');
expect(text).toContain('A Critical, High or `Blocks: yes` finding is CRITICAL; `Blocks: no` never lowers one');
});
}
// Value: protects=F10, every prose copy of the floor stays the same words (wrapped copies compared with whitespace
// collapsed); fails_when=a copy drifts from BLOCKING_FLOOR; seam=none
test('every prose copy of the floor matches BLOCKING_FLOOR once whitespace is collapsed', () => {
const files = ['scripts/resolvers/fix-check.ts', 'review/SKILL.md.tmpl', 'ship/SKILL.md.tmpl', 'capy/skill/gstack/factory.md', 'capy/README.md', 'docs/review-policy.md'];
const drift = files.filter(f => !fs.readFileSync(path.join(ROOT, f), 'utf8').replace(/\s+/g, ' ').includes(BLOCKING_FLOOR));
expect(drift).toEqual([]);
});
});
describe('normalizer v2 as collected: Blocks: yes is CRITICAL, Blocks: no never lowers a label', () => {
// Value: protects=the rule round 5's G1 was scored under, on the path production runs (gstack-review-dispatch collect);
// fails_when=Blocks: yes collects below CRITICAL or Blocks: no lowers a label; seam=parseFindings
test.each([
['FIXABLE: race in a.ts:3\nSeverity: Medium\nBlocks: no', 'CRITICAL'],
['INVESTIGATE: odd lock in b.ts:9\nSeverity: Low\nBlocks: yes', 'CRITICAL'],
['INVESTIGATE: odd lock in b.ts:9\nBlocks: no', 'INVESTIGATE'],
['FIXABLE: race in a.ts:3. Severity: Medium. Blocks: no', 'CRITICAL'],
['1. FIXABLE: race in a.ts:3 — Severity: Low, Blocks: no', 'CRITICAL'],
['- FIXABLE: race in a.ts:3\n- Severity: Medium\n- Blocks: no', 'CRITICAL'],
] as const)('native %j -> %s', (output, want) => expect(parseFindings('adversarial-native', output).map(g => g.label)).toEqual([want]));
test.each([
['1. Drops the last row (q.ts:7). Severity: Critical. Blocks: no', 'Critical'],
['1. Cache evicts sessions (c.ts:4)\n Severity: High', 'High'],
['1. Retry storm (r.ts:2)\n Severity: Medium\n Blocks: yes', 'CRITICAL'],
['1. Index drift (i.ts:5). Severity: Low. Blocks: Yes, it loses the row', 'CRITICAL'],
['1. Naming (n.ts:1)\n Severity: Low\n Blocks: no', 'Low'],
] as const)('%j -> %s', (output, want) => {
const got = parseFindings('outside-voice', output);
expect(got.map(g => g.label)).toEqual([want]);
expect(rank(got[0]!.label)).not.toBeNull();
});
});
+17 -1
View File
@@ -425,7 +425,10 @@ test('internal worker relays every guard receipt before its containment process
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
});
const messages: any[] = [];
worker.on('message', message => messages.push(message));
worker.on('message', (message: any) => {
messages.push(message);
worker.send({ type: 'qa-deadline-receipt-ack', seq: message.seq }, () => {});
});
const code = await new Promise<number | null>(resolve => worker.once('close', resolve));
expect(code).toBe(17);
expect(messages.map(message => message.type)).toEqual(['qa-deadline-receipt', 'qa-deadline-receipt']);
@@ -434,6 +437,19 @@ test('internal worker relays every guard receipt before its containment process
expect(cli(['--receipt-worker', 'status', f.receipt]).status).toBe(2);
});
test('a receipt worker whose parent never acks exits 2 after sending every receipt', async () => {
const f = fixture();
expect(cli(['start', f.receipt, '30']).status).toBe(0);
const worker = spawn(process.execPath, [CLI, '--receipt-worker', 'run', f.receipt, '--', process.execPath, '-e', 'process.exit(17)'], {
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
});
const messages: any[] = [];
worker.on('message', message => messages.push(message));
const code = await new Promise<number | null>(resolve => worker.once('close', resolve));
expect(code).toBe(2);
expect(messages.map(message => message.receipt.event)).toEqual(['started', 'finished']);
}, 15_000);
test('completion classification and observedAt use the same pre-cleanup instant', () => {
const f = fixture();
expect(cli(['start', f.receipt, '30']).status).toBe(0);
+61 -1
View File
@@ -3,7 +3,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 { collectCommit, collectLive, loadRegistryAt } from '../lib/review-collect';
import { collectCommit, collectLive, CONSUMER_SCAN_CAP, loadRegistryAt } from '../lib/review-collect';
import { depthOfCommitInput } from '../lib/review-depth';
const dirs: string[] = [];
@@ -117,3 +117,63 @@ describe('collectLive: the working tree', () => {
expect(collectLive(r.dir, 'no-such-branch').errors.map(e => e.code)).toEqual(['numstat_failed']);
});
});
describe('GSTA-167 part 4a: which data files code names (scanConsumers)', () => {
const rows = Array.from({ length: 80 }, (_, i) => `{"i":${i}}`).join('\n') + '\n';
// Value: protects=a data file a code file names keeps review, one nothing names drops to docs, and markdown mentions do not
// count; fails_when=the consumer search misses a literal reader or counts prose; seam=git grep at head and merge base
test('a JSON code names stays code; one only markdown names is docs', () => {
const r = repo({ 'src/load.ts': "export const rows = () => require('./data/rows.json');\n", 'docs/wiki.md': 'see wiki/post/data.json\n' });
const head = r.commit({ 'data/rows.json': rows, 'wiki/post/data.json': rows });
const c = collectCommit(r.dir, r.baseSha, head);
expect([c.consumed, c.errors]).toEqual([['data/rows.json'], []]);
expect(depthOfCommitInput({ ...c, gateTagged: 0 })).toMatchObject({ class: 'code', runtimeLines: 80 });
});
test('a consumer the change deletes still counts (merge base); a data file named only by another data file follows it', () => {
const r = repo({ 'src/old.ts': "read('rules.json')\n", 'src/manifest.ts': "load('index.json')\n" });
const head = r.commit({ 'src/old.ts': null, 'assets/rules.json': rows, 'assets/index.json': '["post.json"]\n', 'assets/post.json': rows, 'orphan/list.json': '["lone.json"]\n', 'orphan/lone.json': rows });
const c = collectCommit(r.dir, r.baseSha, head);
expect(c.consumed).toEqual(['assets/index.json', 'assets/post.json', 'assets/rules.json']);
});
test('a deleted data file head code still names keeps the deletion guarded (gate class)', () => {
const r = repo({ 'src/a.ts': "read('table.csv')\n", 'data/table.csv': 'a,b\n' });
const head = r.commit({ 'data/table.csv': null, 'src/a.ts': "read('table.csv'); // moved\n" });
const c = collectCommit(r.dir, r.baseSha, head);
expect(c.consumed).toEqual(['data/table.csv']);
expect(depthOfCommitInput({ ...c, gateTagged: 0 }).class).toBe('gate');
});
// Value: protects=the live plan (gstack-review-plan) and the queue recompute (collectCommit) agree on a committed tree, and
// the live plan sees an untracked consumer; fails_when=a receipt and the queue disagree on depth; seam=collectLive vs collectCommit
test('collectLive and collectCommit compute the same consumed set on a clean tree; live counts an untracked consumer', () => {
const r = repo({ 'src/load.ts': "read('rows.json')\n" });
const head = r.commit({ 'data/rows.json': rows, 'wiki/post/data.json': rows });
expect(collectLive(r.dir, 'main').consumed).toEqual(collectCommit(r.dir, r.baseSha, head).consumed);
r.write({ 'src/new.ts': "read('post/data.json')\n" });
expect(collectLive(r.dir, 'main').consumed).toEqual(['data/rows.json', 'wiki/post/data.json']);
});
// Value: protects=the scan reads git grep the same way under any user grep config, and a failed grep keeps every data file
// code; fails_when=grep.lineNumber/grep.column shift the NUL fields so no consumer matches, or a grep failure drops data to
// docs silently; seam=git grep output and exit status
test('grep.lineNumber, grep.column and color.grep in git config do not change which files count as consumers', () => {
const r = repo({ 'src/load.ts': "export const rows = () => require('./data/rows.json');\n" });
r.git('config', 'grep.lineNumber', 'true'); r.git('config', 'grep.column', 'true'); r.git('config', 'color.grep', 'always');
const head = r.commit({ 'data/rows.json': rows, 'docs/notes.md': 'see rows.json and orphan.json\n', 'data/orphan.json': rows });
expect([collectCommit(r.dir, r.baseSha, head).consumed, collectLive(r.dir, 'main').consumed]).toEqual([['data/rows.json'], ['data/rows.json']]);
});
test('a git grep failure keeps every data file code and records consumer_scan_failed', () => {
const r = repo({ 'src/load.ts': "read('rows.json')\n" });
const head = r.commit({ 'data/rows.json': rows });
r.git('config', 'grep.lineNumber', 'not-a-bool');
const c = collectCommit(r.dir, r.baseSha, head);
expect(c.consumed).toBe('all');
expect(c.errors.map(e => e.code)).toEqual(['consumer_scan_failed']);
expect(depthOfCommitInput({ ...c, gateTagged: 0 }).class).toBe('code');
});
test('past CONSUMER_SCAN_CAP data paths every one stays code', () => {
const r = repo({});
const head = r.commit(Object.fromEntries(Array.from({ length: CONSUMER_SCAN_CAP + 1 }, (_, i) => [`d/${i}.json`, '{}\n'])));
const c = collectCommit(r.dir, r.baseSha, head);
expect(c.consumed).toBe('all');
expect(depthOfCommitInput({ ...c, gateTagged: 0 }).class).toBe('code');
}, 60_000);
});
+130 -22
View File
@@ -3,13 +3,14 @@ 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, reviewPlanLine, scanNumbers, DEPTH_RULES, FULL_PASS_CAP, SCOPED_RANGE_LINES, type DepthError, type PlanInput } from '../lib/review-depth';
import { classifyDiff, classifyPath, dataCandidate, depthFor, gateScanNeeded, 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({});
const base = (o: Partial<PlanInput>): PlanInput => ({ paths: ['src/app.ts'], scope: { SCOPE_BACKEND: 'true', SCOPE_STATE_MACHINE: 'false' }, gateTagged: 0, diffLines: 120, history: { fullPasses: 0, last: 'none' }, numbers: none, ...o });
const roles = (p: ReturnType<typeof planReview>) => p.dispatches.map(d => `${d.role}:${d.name}:${d.effort}`);
const batch = (p: ReturnType<typeof planReview>) => p.dispatches.find(d => d.role === 'specialist' && d.name === 'batched')?.checklists;
describe('classifyPath: agent-read markdown is code', () => {
test.each([
@@ -82,17 +83,23 @@ describe('planReview', () => {
expect(p.depth).toBe('light');
expect(roles(p)).toEqual([]);
});
test('code class pass 1: every reviewer at full effort, the outside voice at its own', () => {
test('code class pass 1: specialists at medium, the native adversarial at full, the outside voice at its own', () => {
const p = planReview(base({}));
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']);
expect(roles(p)).toEqual(['specialist:batched:medium', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
expect(batch(p)).toEqual(['testing', 'security']);
expect(p.skipped).toContain('maintainability, performance, simplification (lean ensemble: read only when forced)');
});
test('gate class forces red team and ignores hit-rate gating', () => {
const p = planReview(base({ gateTagged: 1, gated: ['security'] }));
const p = planReview(base({ gateTagged: 1, gated: ['security', 'state-machines'], scope: { SCOPE_BACKEND: 'true' } }));
expect(roles(p)).toContain('red-team:red-team:full');
expect(roles(p)).toContain('specialist:security:full');
expect(batch(p)).toEqual(['testing', 'security', 'state-machines']);
});
test('code class honours hit-rate gating and small-diff skip', () => {
expect(roles(planReview(base({ gated: ['simplification'] })))).not.toContain('specialist:simplification:full');
const sm = { SCOPE_BACKEND: 'true', SCOPE_STATE_MACHINE: 'true' };
expect(batch(planReview(base({ scope: sm })))).toContain('state-machines');
const gated = planReview(base({ scope: sm, gated: ['state-machines'] }));
expect(batch(gated)).not.toContain('state-machines');
expect(gated.skipped).toContain('state-machines (gated by hit rate)');
expect(planReview(base({ diffLines: 20 })).dispatches.filter(d => d.role === 'specialist').map(d => d.name)).toEqual(['batched']);
});
test('pass 2 reruns the army on the range but not the adversarial or outside voice once Step 11 ran', () => {
@@ -113,9 +120,21 @@ describe('planReview', () => {
const p = planReview(base({ numbers, reviewedNumbers: 'old', history: { fullPasses: 1, last: 'full', fixedBelow: true } }));
expect(roles(p)).toEqual(['fix-check:fix-check:full', 'outside-voice:outside-voice (numbers changed):host']);
});
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);
// Value: protects=GSTA-167 part 3 (specialists at medium, CREW 30c) without moving the GSTA-131 lesson (a light fix-check missed
// a fail-open a full one caught): Red Team, native adversarial and fix-check stay full; fails_when=any of those drops below
// full, a specialist leaves medium, or --no-tiers leaves a specialist off the host default; seam=planReview
test('specialists run at medium; the Red Team, native adversarial and fix-check stay full; --no-tiers sends specialists to host', () => {
for (const h of [{ fullPasses: 0, last: 'none' }, { fullPasses: 1, last: 'full', fixedBelow: true }, { fullPasses: 1, last: 'full', fixedBlocking: true }] as const) {
const d = planReview(base({ gateTagged: 1, history: h })).dispatches;
expect(d.filter(x => x.role === 'specialist').every(x => x.effort === 'medium')).toBe(true);
expect(d.filter(x => ['red-team', 'adversarial-native', 'fix-check'].includes(x.role)).every(x => x.effort === 'full')).toBe(true);
expect(d.filter(x => x.role === 'outside-voice').every(x => x.effort === 'host')).toBe(true);
}
const gate = planReview(base({ gateTagged: 1 })).dispatches;
expect(gate.map(x => x.role)).toEqual(expect.arrayContaining(['specialist', 'red-team', 'adversarial-native']));
const host = planReview(base({ gateTagged: 1, tiers: false })).dispatches;
expect(host.filter(x => x.role === 'specialist').every(x => x.effort === 'host')).toBe(true);
expect(host.filter(x => x.role === 'red-team' || x.role === 'adversarial-native').every(x => x.effort === 'full')).toBe(true);
});
test('done dispatches nothing', () => expect(planReview(base({ history: { fullPasses: 1, last: 'full' } })).dispatches).toEqual([]));
});
@@ -143,7 +162,9 @@ describe('GSTA-131 pass-1 review repairs', () => {
expect(scanNumbers({ 'test/fixtures/bench.json': ['{ "ratio": 0.74 }'] }).publishes).toBe(true);
});
test('--all-specialists forces every specialist; a code diff past 200 lines or with a CRITICAL adds the Red Team', () => {
expect(planReview(base({ force: 'all' })).dispatches.filter(d => d.role === 'specialist').length).toBe(9);
const all = planReview(base({ force: 'all' }));
expect(all.dispatches.filter(d => d.role === 'specialist').length).toBe(1);
expect([...batch(all)!].sort()).toEqual(['api-contract', 'data-migration', 'design', 'maintainability', 'performance', 'security', 'simplification', 'state-machines', 'testing']);
const has = (o: Partial<PlanInput>) => roles(planReview(base(o))).some(r => r.startsWith('red-team'));
expect(has({ diffLines: 120 })).toBe(false);
expect(has({ diffLines: 201 })).toBe(true);
@@ -160,12 +181,14 @@ describe('GSTA-131 coverage: Red Team and specialist arms', () => {
expect(roles(planReview(base({ diffLines: 20, gateTagged: 1 })))).toContain('red-team:red-team');
});
test('scope arms select their specialist, state machines default on when scope is unknown, and --force all reaches docs class', () => {
const picked = (scope: Record<string, string>) => roles(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false', ...scope } })));
expect(picked({ SCOPE_MIGRATIONS: 'true' })).toContain('specialist:data-migration');
expect(picked({ SCOPE_API: 'true' })).toContain('specialist:api-contract');
expect(picked({ SCOPE_FRONTEND: 'true' })).toContain('specialist:design');
expect(roles(planReview(base({ scope: {} })))).toContain('specialist:state-machines');
expect(roles(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false' } })))).not.toContain('specialist:state-machines');
const picked = (scope: Record<string, string>) => batch(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false', ...scope } })));
expect(picked({ SCOPE_MIGRATIONS: 'true' })).toContain('data-migration');
expect(picked({ SCOPE_API: 'true' })).toContain('api-contract');
expect(picked({ SCOPE_FRONTEND: 'true' })).toEqual(['testing', 'security', 'design']);
expect(batch(planReview(base({ scope: {} })))).toContain('state-machines');
expect(batch(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false' } })))).not.toContain('state-machines');
expect(batch(planReview(base({ force: ['maintainability'] })))).toEqual(['testing', 'security', 'maintainability']);
expect(batch(planReview(base({ gated: ['design'], scope: { SCOPE_FRONTEND: 'true', SCOPE_STATE_MACHINE: 'false' } })))).toEqual(['testing', 'security']);
const docsAll = planReview(base({ paths: ['docs/a.md'], force: 'all' }));
expect(docsAll.class).toBe('docs');
expect(docsAll.dispatches.filter(d => d.role === 'specialist').length).toBeGreaterThan(5);
@@ -186,7 +209,7 @@ describe('review depth: the matrix', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 48, 'test/a.test.ts': 190, 'docs/a.md': 9 }), diffLines: 247 }));
expect([p.depth, p.runtimeLines]).toEqual(['light', 48]);
expect(p.dispatches).toEqual([
{ role: 'specialist', name: 'batched', effort: 'full', exclude: [':(exclude)docs/a.md'], checklists: ['testing', 'maintainability', 'security', 'performance', 'red-team'] },
{ role: 'specialist', name: 'batched', effort: 'full', exclude: [':(exclude)docs/a.md'], checklists: ['testing', 'security', 'red-team'] },
{ role: 'adversarial-native', name: 'adversarial', effort: 'full', exclude: [], reason: 'backtest inconclusive (GSTA-147 D5)' },
]);
expect(p.skipped.join('; ')).toContain('red-team (light depth');
@@ -195,10 +218,10 @@ describe('review depth: the matrix', () => {
test('runtime 51 is standard: thresholds read runtime lines, reviewers read their own hunks', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 51, 'test/a.test.ts': 400, 'README.md': 3 }), diffLines: 454 }));
expect([p.depth, p.runtimeLines]).toEqual(['standard', 51]);
expect(roles(p)).toEqual(['specialist:testing:full', 'specialist:maintainability:full', 'specialist:performance:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
expect(roles(p)).toEqual(['specialist:batched:medium', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
expect(batch(p)).toEqual(['testing', 'security']);
const ex = Object.fromEntries(p.dispatches.map(d => [d.name, d.exclude]));
expect(ex.testing).toEqual([':(exclude)README.md']);
expect(ex.maintainability).toEqual([':(exclude)README.md', ':(exclude)test/a.test.ts']);
expect(ex.batched).toEqual([':(exclude)README.md']);
expect([ex.adversarial, ex['outside-voice']]).toEqual([[], []]);
});
test.each([
@@ -245,7 +268,7 @@ describe('review depth: the matrix', () => {
test('a fixed blocker in light raises the next full step to the standard set, Red Team included', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 30 }), history: { fullPasses: 1, last: 'full', fixedBlocking: true } }));
expect([p.depth, p.stepDepth]).toEqual(['light', 'standard']);
expect(roles(p)).toEqual(['specialist:testing:full', 'specialist:maintainability:full', 'specialist:performance:full', 'red-team:red-team:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
expect(roles(p)).toEqual(['specialist:batched:medium', 'red-team:red-team:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
});
test('--depth-floor raises and never lowers', () => {
expect(depthFor({ cls: 'code', runtimeLines: 10, errors: [], floor: 'full' })).toBe('full');
@@ -299,7 +322,7 @@ describe('D6: the scoped second pass after a blocking fix (GSTA-152 PR B)', () =
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);
expect(roles(before)).toEqual(['specialist:batched:medium', 'red-team:red-team:full']);
const p = planReview(n19());
expect(p.next.step).toBe('scoped');
expect(p.next.reason).toContain('24 runtime lines');
@@ -391,3 +414,88 @@ describe('D6: the scoped second pass after a blocking fix (GSTA-152 PR B)', () =
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 |');
});
});
describe('GSTA-167 part 4a: data files no code names are docs (classifyDiff only)', () => {
const json = (n: number) => ({ path: 'wiki/post/data.json', lines: n, binary: false });
// Value: protects=a data-only docs PR (gstack-evals #34: 6,687 lines of wiki JSON) plans light with no specialists; fails_when=an
// unconsumed JSON counts as runtime lines; seam=ClassInput.consumed from lib/review-collect.ts
test('wiki JSON no code names is docs and a 6,687-line diff plans light; a numbers-bearing JSON keeps the outside voice', () => {
const input = { paths: ['wiki/post/data.json'], perPath: [json(6687)], consumed: [] as string[], gateTagged: 0 };
const c = classifyDiff(input);
expect([c.class, c.runtimeLines]).toEqual(['docs', 0]);
expect(c.reasons).toContain('docs: 1 data path(s) no code names');
const plan = planReview(base({ ...input, scope: {}, diffLines: 6687, numbers: scanNumbers({ 'wiki/post/data.json': ['{"recall": "19/29"}'] }) }));
expect([plan.depth, roles(plan)]).toEqual(['light', ['outside-voice:outside-voice:host']]);
expect(planReview(base({ ...input, scope: {}, diffLines: 6687 })).dispatches).toEqual([]);
});
test('a consumed data file, an unscanned diff and an overflowing scan all keep the file code', () => {
for (const consumed of [['wiki/post/data.json'], undefined, 'all' as const]) {
const c = classifyDiff({ paths: ['wiki/post/data.json'], perPath: [json(80)], consumed, gateTagged: 0 });
expect([c.class, c.runtimeLines]).toEqual(['code', 80]);
}
});
test.each([
'config/runtime.json', 'package.json', 'package-lock.json', 'tsconfig.build.json', 'app/.eslintrc.json', 'vite.config.json', 'public/manifest.json',
'.gstack/ship-policy.json', '.gstack/generated.json', '.github/workflows/ci.yml', 'fly.toml', 'wrangler.toml', 'docker-compose.yml', 'k8s/deploy.yaml',
'openapi.yaml', 'public/icon.svg', 'lib/gate-thresholds.json',
])('%s with no consumer stays code (config, agent prose, gate, YAML, TOML, SVG)', p => {
expect(dataCandidate(p)).toBe(false);
expect(classifyDiff({ paths: [p], perPath: [{ path: p, lines: 3, binary: false }], consumed: [], gateTagged: 0 }).class).not.toBe('docs');
});
test.each(['data/rows.csv', 'data/rows.tsv', 'logs/events.jsonl', 'assets/hero.png', 'assets/photo.JPEG', 'wiki/index.json'])('%s is a data candidate', p => expect(dataCandidate(p)).toBe(true));
test('test fixtures stay tests and markdown stays docs: the rule never touches a non-code class', () => {
expect(classifyDiff({ paths: ['test/fixtures/a.json'], consumed: [], gateTagged: 0 }).class).toBe('tests');
expect(dataCandidate('test/fixtures/a.json')).toBe(false);
});
// Value: protects=F17, the behavior change is named: an image no code names is docs, not a binary_code_path error; one code names
// still fails closed; fails_when=either direction flips; seam=none
test('a binary PNG no code names is docs with no error; one code names is still binary_code_path (full)', () => {
const png = { path: 'assets/hero.png', lines: 0, binary: true };
const free = classifyDiff({ paths: ['assets/hero.png', 'src/app.ts'], perPath: [png, { path: 'src/app.ts', lines: 4, binary: false }], consumed: [], gateTagged: 0 });
expect([free.errors, free.runtimeLines]).toEqual([[], 4]);
const named = classifyDiff({ paths: ['assets/hero.png'], perPath: [png], consumed: ['assets/hero.png'], gateTagged: 0 });
expect(named.errors.map(e => e.code)).toEqual(['binary_code_path']);
});
// Value: protects=F1, the receipt's gate scan cannot be skipped through the data rule; fails_when=a data-only range reads as
// needing no scan, so tagged=0 is recorded unscanned; seam=lib/ship-receipt-depth.ts receiptDepthAtHead
test('gateScanNeeded is path-only: a data-only range still needs the scan, a markdown-only range does not', () => {
expect(gateScanNeeded(['wiki/post/data.json'])).toBe(true);
expect(gateScanNeeded(['assets/hero.png', 'README.md'])).toBe(true);
expect(gateScanNeeded(['README.md', 'docs/a.md'])).toBe(false);
expect(classifyPath('wiki/post/data.json')).toBe('code');
});
test('a deleted data file stays guarded: a code diff that deletes one is gate class', () => {
expect(classifyDiff({ paths: ['src/app.ts', 'wiki/old.json'], deleted: ['wiki/old.json'], consumed: [], gateTagged: 0 }).class).toBe('gate');
});
});
describe('lean ensemble: one batched specialist at standard and full depth (GSTA-167 part 2)', () => {
const fixture = (name: string): PlanInput => {
const x = JSON.parse(fs.readFileSync(path.join(ROOT, 'test/fixtures/review-depth', `${name}.json`), 'utf8'));
return { paths: x.paths, deleted: x.deleted, stampOnly: x.stamp_only ?? [], scope: x.scope, gateTagged: x.gate_tagged, diffLines: x.diff_lines, numbers: x.numbers, perPath: x.per_path, registry: x.registry_outputs ? { sha: null, outputs: x.registry_outputs } : undefined, history: { fullPasses: 0, last: 'none' } };
};
const specialists = (p: ReturnType<typeof planReview>) => p.dispatches.filter(d => d.role === 'specialist');
// Value: protects=the lean ensemble's reader set (testing and security always, the four scoped checklists when scope asks,
// maintainability, performance and simplification only when forced) on captured diffs; fails_when=a plan dispatches more than
// one specialist reader at standard or full, drops testing or security, or the Red Team or voices change; seam=planReview
test('the captured gate fixture plans one batched reader, the Red Team and both voices', () => {
const p = planReview(fixture('code'));
expect([p.class, p.depth]).toEqual(['gate', 'full']);
expect(roles(p)).toEqual(['specialist:batched:medium', 'red-team:red-team:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
expect(batch(p)).toEqual(['testing', 'security', 'state-machines']);
expect(batch(planReview({ ...fixture('code'), scope: { ...fixture('code').scope, SCOPE_MIGRATIONS: 'true', SCOPE_API: 'true', SCOPE_FRONTEND: 'true', SCOPE_STATE_MACHINE: 'false' } }))).toEqual(['testing', 'security', 'data-migration', 'api-contract', 'design']);
});
test('the captured docs fixture dispatches no reader at standard and one batched reader when floored to full', () => {
expect(specialists(planReview({ ...fixture('docs-only'), floor: 'standard' }))).toEqual([]);
const full = planReview({ ...fixture('docs-only'), floor: 'full' });
expect(specialists(full).map(d => [d.name, d.effort, d.checklists])).toEqual([['batched', 'medium', ['testing', 'security', 'state-machines']]]);
});
test('a standard code plan reads one batched reader on its own hunks, and a forced checklist joins it', () => {
const p = planReview(base({ diffLines: 400, scope: { SCOPE_BACKEND: 'true', SCOPE_STATE_MACHINE: 'false' } }));
expect(specialists(p).map(d => [d.name, d.checklists])).toEqual([['batched', ['testing', 'security']]]);
expect(roles(p)).toContain('red-team:red-team:full');
expect(batch(planReview(base({ force: ['performance', 'simplification'] })))).toEqual(['testing', 'security', 'performance', 'simplification']);
expect(specialists(planReview(base({ tiers: false })))[0]!.effort).toBe('host');
});
});
+112
View File
@@ -73,6 +73,19 @@ describe('gstack-review-dispatch render: briefs from the sections, slots only',
expect(fs.readFileSync(path.join(out, 'review-ledger.jsonl'), 'utf8')).toContain('"event":"rendered"');
expect(run(['render', '--role', 'specialist', '--name', 'no-such-specialist', '--out', out, '--diff-base', SHA]).stderr).toContain('BRIEF_RENDER_FAILED: checklist not found');
});
// Value: protects=the lean ensemble's one batched reader (GSTA-167 part 2) gets checklist paths when the parent passes the plan's
// checklists= names as printed; fails_when=the brief names bare checklist words a reader cannot open, or an unknown name
// renders; seam=gstack-review-dispatch render
test('specialist-batched takes the DISPATCH checklists= names and writes each checklist path into the brief', () => {
const out = tmp();
const r = run(['render', '--role', 'specialist-batched', '--out', out, '--diff-base', SHA, '--checklists', 'testing,security,state-machines']);
expect(r.status, r.stderr).toBe(0);
const root = path.join(import.meta.dir, '..');
const lists = ['testing', 'security', 'state-machines'].map(n => path.join(root, 'review/specialists', `${n}.md`)).join(', ');
expect(fs.readFileSync(field(r.stdout, 'file'), 'utf8')).toContain(`Read each checklist at ${lists} (tag each finding's`);
expect(run(['render', '--role', 'specialist-batched', '--out', out, '--diff-base', SHA, '--checklists', 'testing,maintenance']).stderr).toContain('BRIEF_RENDER_FAILED: checklist not found');
});
});
describe('gstack-review-dispatch sent, collect, judge and verify: nothing changed on the way in or out', () => {
@@ -211,9 +224,108 @@ describe('gstack-review-dispatch sent, collect, judge and verify: nothing change
expect(parseFindings('r', 'FIXABLE: race in a.ts:3\nINVESTIGATE: odd lock\nRecommendation: Fix the race because x\n').map(f => f.label)).toEqual(['FIXABLE', 'INVESTIGATE']);
const m: MergedLine[] = [{ id: 'r#1', reader: 'r', label: 'FIXABLE', raw: 'x', output_sha256: 'h' }];
expect(judge(m, [{ id: 'r#1', severity: 'severe' }]).invalid).toEqual(['r#1']);
// Replay C1-34 (GSTA-193): a judged line with "severity": null crashed judge with a TypeError; it is an invalid line.
expect(judge(m, [{ id: 'r#1', severity: null as unknown as string }, { id: 'lead#1' } as unknown as { id: string; severity: string }]).invalid).toEqual(['r#1', 'lead#1']);
expect(judge(m, [{ id: 'r#1', severity: 'INFORMATIONAL', reason: 'a real defect' }]).ok).toBe(true);
});
// Value: protects=normalizer v2 in production (GSTA-167 part 1, certified by round 5's G1): a prose voice's Blocks: yes reaches
// merged.jsonl as CRITICAL, so judge refuses a lead that keeps it below the floor; Blocks: no never lowers a label;
// fails_when=a Blocks: yes finding is collected at its Medium or INVESTIGATE label, a Blocks line with no finding before it is
// lost, or Blocks: no demotes; seam=parseFindings, judge
test('a prose voice\'s Blocks: yes collects as CRITICAL, on its own line or the finding above it; Blocks: no never lowers', () => {
const out = [
'1. Retry loop drops the last row (queue.ts:78). Severity: Medium · Blocks: yes',
'2. INVESTIGATE: lock order in pool.ts:12',
' Blocks: yes',
'3. FIXABLE: stale cache key (cache.ts:4)',
' Severity: High',
' Blocks: no',
'4. Token never expires (auth.ts:9)',
' Fix: check exp',
' **Blocks:** yes',
'Recommendation: Fix the retry loop because it loses rows',
].join('\n');
const got = parseFindings('outside-voice', out);
expect(got.map(g => g.label)).toEqual(['CRITICAL', 'CRITICAL', 'High', 'CRITICAL']);
expect(got[1]!.raw).toBe('2. INVESTIGATE: lock order in pool.ts:12 | Blocks: yes');
expect(got[2]!.raw).toBe('3. FIXABLE: stale cache key (cache.ts:4) | Severity: High | Blocks: no');
expect(got[3]!.raw).toBe('4. Token never expires (auth.ts:9) | **Blocks:** yes');
const m: MergedLine[] = got.map((g, i) => ({ id: `o#${i + 1}`, reader: 'outside-voice', label: g.label, raw: g.raw, output_sha256: 'h' }));
expect(judge(m, m.map(x => ({ id: x.id, severity: x.label === 'CRITICAL' ? 'Medium' : x.label }))).downgraded).toEqual(['o#1', 'o#2', 'o#4']);
});
// Value: protects=the prompt's own layout (a finding, then Severity, then Blocks lines) collects as ONE finding that keeps its
// locus, and an explicit Severity outranks a stray label word earlier on the line (#3660 Red Team and Testing findings);
// fails_when=the layout splits into a locus-less CRITICAL plus the finding at INVESTIGATE, or 'Low-level'/'[P2]' sets the label
// below an explicit Severity: Critical/High; seam=parseFindings
test('a finding with Severity and Blocks lines below it is one record at the strongest label; a stray label word never lowers an explicit Severity', () => {
const one = parseFindings('adversarial-native', ['1. INVESTIGATE: lock order in pool.ts:12', ' Severity: Medium', ' Blocks: yes'].join('\n'));
expect(one).toEqual([{ label: 'CRITICAL', raw: '1. INVESTIGATE: lock order in pool.ts:12 | Severity: Medium | Blocks: yes' }]);
const words = parseFindings('outside-voice', ['1. Low-level retry loop in queue.ts:78 drops writes on 429. Severity: Critical. Blocks: no', '- [P2] cache.ts:40 evicts live sessions. Severity: High. Blocks: no'].join('\n'));
expect(words.map(w => w.label)).toEqual(['Critical', 'High']);
const prose = parseFindings('outside-voice', ['1. FIXABLE: race in a.ts:3', 'Why it matters: two writers', 'Severity: Low', 'Blocks: yes', '2. Unlabeled item about b.ts:9', 'Severity: Medium'].join('\n'));
expect(prose.map(p => [p.label, p.raw])).toEqual([['CRITICAL', '1. FIXABLE: race in a.ts:3 | Severity: Low | Blocks: yes'], ['Medium', '2. Unlabeled item about b.ts:9 | Severity: Medium']]);
});
// Value: protects=a prose blocker whose only marker is an inline Blocks: yes still reaches merged.jsonl as CRITICAL (#3660 pass 2
// Testing finding); fails_when=an unlabeled line carrying Blocks: yes collects nothing; seam=parseFindings
test.each([
['1. Token never expires (auth.ts:9). Blocks: yes', '1. Token never expires (auth.ts:9). Blocks: yes'],
['Token never expires in auth.ts:9 — Blocks: yes', 'Token never expires in auth.ts:9 — Blocks: yes'],
['1. Token never expires (auth.ts:9)\nFix: check exp **Blocks:** yes', '1. Token never expires (auth.ts:9) | Fix: check exp **Blocks:** yes'],
])('an inline Blocks: yes with no label is one CRITICAL finding: %j', (output, raw) => {
expect(parseFindings('outside-voice', output)).toEqual([{ label: 'CRITICAL', raw }]);
});
// Value: protects=a top-level `- Severity:` bullet is its own finding and keeps its section's floor (#3660 pass 2 outside voice);
// fails_when=two such bullets merge into one id or a Critical heading's item collects as Medium; seam=parseFindings
test('top-level Severity bullets stay separate findings and keep the section label', () => {
const got = parseFindings('outside-voice', ['## Critical findings', '- Severity: Medium — missing owner check at auth.ts:14', '- Severity: Low — token logged at log.ts:3'].join('\n'));
expect(got.map(g => g.label)).toEqual(['Critical', 'Critical']);
});
// Value: protects=one finding per merged id however a voice lays findings out (#3660 pass 2 native adversarial); fails_when=
// paragraph findings or a sentence starting with "Blocks" fold into the finding above, or a numbered or quoted Blocks line
// collects nothing; seam=parseFindings
test('paragraph findings stay separate, Blocks needs a colon to continue a finding, and a bare Blocks line is its own finding', () => {
const bold = parseFindings('outside-voice', ['**Finding 1.** a.ts:3 drops rows', 'Severity: Low', 'Blocks: no', '**Finding 2.** b.ts:4 leaks', 'Severity: High', 'Blocks: yes'].join('\n'));
expect(bold.map(b => [b.label, b.raw])).toEqual([['Low', '**Finding 1.** a.ts:3 drops rows | Severity: Low | Blocks: no'], ['CRITICAL', '**Finding 2.** b.ts:4 leaks | Severity: High | Blocks: yes']]);
const plain = parseFindings('outside-voice', ['a.ts:3 drops rows', 'Severity: High', 'Blocks: yes', 'b.ts:4 leaks', 'Severity: High', 'Blocks: yes'].join('\n'));
expect(plain.map(p => p.raw)).toEqual(['a.ts:3 drops rows | Severity: High | Blocks: yes', 'b.ts:4 leaks | Severity: High | Blocks: yes']);
expect(parseFindings('outside-voice', ['1. Medium: cache key drift (c.ts:4)', ' Blocks the event loop on every miss - High'].join('\n')).map(f => f.label)).toEqual(['Medium', 'High']);
expect(parseFindings('outside-voice', '2. Blocks: yes').map(f => f.label)).toEqual(['CRITICAL']);
expect(parseFindings('outside-voice', '> Blocks: yes').map(f => f.label)).toEqual(['CRITICAL']);
});
// Value: protects=a later finding's Severity/Blocks line never lands on the finding before it, and an explicit Severity label
// decides its line (#3660 pass 2 Red Team); fails_when=the later blocker's text is lost and the earlier finding takes its
// CRITICAL, or 'Severity: Low (not High)' collects as High; seam=parseFindings
test('prose after a finding\'s field, a blank-line paragraph or an indented block starts the next finding; an explicit Severity label wins', () => {
const raws = (o: string) => parseFindings('outside-voice', o).map(f => [f.label, f.raw]);
const two = [['Low', 'A in a.ts:1. | Severity: Low'], ['CRITICAL', 'B in b.ts:2. | Blocks: yes']];
expect(raws('A in a.ts:1.\nSeverity: Low\n\nB in b.ts:2.\nBlocks: yes')).toEqual(two);
expect(raws('A in a.ts:1.\nSeverity: Low\nB in b.ts:2.\nBlocks: yes')).toEqual(two);
expect(raws('Findings:\n A in a.ts:1.\n Severity: Low\n B in b.ts:2.\n Blocks: yes')).toEqual(two);
});
// Value: protects=the fix-check's shapes (#3660): a blocker keeps its own id and its subject, field bullets stay on their finding,
// and no Severity label buries a stronger label on its line; fails_when=any of these collects below CRITICAL, merges a later
// blocker into an earlier finding, or loses the finding's file:line; seam=parseFindings
test.each([
['A in a.ts:1.\nSeverity: Low\nB in b.ts:2 drops rows. Blocks: yes', [['Low', 'A in a.ts:1. | Severity: Low'], ['CRITICAL', 'B in b.ts:2 drops rows. Blocks: yes']]],
['**Finding 1.** a.ts:3 races. Severity: Low\n**Finding 2.** b.ts:4 leaks. Blocks: yes', [['Low', '**Finding 1.** a.ts:3 races. Severity: Low'], ['CRITICAL', '**Finding 2.** b.ts:4 leaks. Blocks: yes']]],
['FIXABLE: race in a.ts:3\nSeverity: Medium\nWhy: the writer races\nBlocks: yes', [['CRITICAL', 'FIXABLE: race in a.ts:3 | Severity: Medium | Blocks: yes']]],
['- Unlabeled thing in a.ts:3\n- Severity: Medium\n- Blocks: yes', [['CRITICAL', '- Unlabeled thing in a.ts:3 | - Severity: Medium | - Blocks: yes']]],
['1. Token never expires (auth.ts:9)\nFix: check exp\nBlocks: yes', [['CRITICAL', '1. Token never expires (auth.ts:9) | Blocks: yes']]],
['1. Retry storm (r.ts:2)\nSeverity High', [['High', '1. Retry storm (r.ts:2) | Severity High']]],
['CRITICAL: x.ts:4 drops rows (pass 1 had Severity: Low)', [['CRITICAL', 'CRITICAL: x.ts:4 drops rows (pass 1 had Severity: Low)']]],
])('fix-check shape %j', (output, want) => expect(parseFindings('outside-voice', output).map(f => [f.label, f.raw])).toEqual(want));
test('the native voice\'s FIXABLE collects as CRITICAL under its role with or without a dispatch name', () => {
for (const reader of ['adversarial-native', 'adversarial-native/adversarial']) expect(parseFindings(reader, 'FIXABLE: race in a.ts:3\nSeverity: Medium\nBlocks: no').map(f => f.label)).toEqual(['CRITICAL']);
expect(parseFindings('outside-voice', 'FIXABLE: race in a.ts:3\nSeverity: Medium').map(f => f.label)).toEqual(['Medium']);
});
test('a non-prefix Blocks: yes under a labeled finding raises it to CRITICAL', () => {
expect(parseFindings('outside-voice', '1. Retry loses writes (queue.ts:78). Severity: Medium\n Fix: reject the batch. Blocks: yes')).toEqual([{ label: 'CRITICAL', raw: '1. Retry loses writes (queue.ts:78). Severity: Medium | Fix: reject the batch. Blocks: yes' }]);
});
test('verify on a ledger that kept no prompts says so and exits 3', () => {
const d = tmp();
const ledger = path.join(d, 'ledger.jsonl');
+39 -23
View File
@@ -45,38 +45,54 @@ describe('reviewer packets stay reverted (GSTA-167)', () => {
* specialist, Red Team, adversarial and outside-voice prompts, the checklists they read, the
* fix-check prompt and the depth rules that pick reviewers. A change to any of them changes what a
* reviewer sees or how its findings rate, so it merges only after its preregistered no-loss replay
* passes (GStack Minder crew rule, 2026-10-30). Repin with that replay's id in the commit message.
* passes (GStack Minder crew rule, 2026-10-30); a cost replay alone is not enough (#3621 passed its
* cost replay, then its no-loss replay found 11 of 30 blockers). Every pin names the replay that
* admits its bytes: repin with that replay's id here and in the commit message, and merge only on
* its passing result.
*/
const REVIEWER_BRIEFS: Record<string, string> = {
'ship/sections/review-army.md': '3d89708c2776b0d97fb2420c0461d37179b9f25ca0677ba038a6cff53dbf7465',
'ship/sections/adversarial.md': '3f361c1f42605c8329048c81162216e7bf9902bdbb2327811c39ee4e201bb95d',
'review/sections/review-army.md': '78329b64640260cd8831ca81a9189f3f8dcade9c406431631d3f2cd798cd3967',
'review/sections/adversarial.md': 'a402018055670f50b75322ab92a0b03d740c481cc5ef2f2ac72b281dae29ec7b',
'review/checklist.md': '607788afeb2c83f2dff1dccd58d212616ec72393d28cda2fa18690c1a83df699',
'review/design-checklist.md': 'b9205f88313cc64902c0b13e9685722ab8138d3dcc58916154f47274abda7d83',
'review/gate-integrity.md': 'f570db344c9a01df633bcaa281d6017f4f12420fd3e0aa3078c1ce06e0df8332',
'review/specialists/api-contract.md': '263d23ac119dd601d315c191dbdbd503d47c00264c9c0ca81959559ca11d4e95',
'review/specialists/data-migration.md': 'b6fd9eb229002ea598f8fe9ff53b1cd8821e3bd37a7aa7b07b5526556c71ebca',
'review/specialists/maintainability.md': 'f8015d549e70297f782f0aabf6fe3a9fe31ec5ed61c311ab854e4a858282ed9b',
'review/specialists/performance.md': '545c294ae53638b4c8524e8cde08246a4ce3b5c287ec7c44e27f5556a3b0e8cc',
'review/specialists/red-team.md': '9ea05149f5b13d6a19ecec26285142d09d259cf80a515af828abdd1f43320427',
'review/specialists/security.md': '17bfe813eb3de22835af048ec359770720f1d7f371a26460169f7e32b1bcac9f',
'review/specialists/simplification.md': 'f33846b1dcf2fdeef82d0744ab2ef48fb4a33b34a737dafc33dd0e891756689f',
'review/specialists/state-machines.md': '597215e4438d85d9cc71b48097c61eef7cba4f4b09dc14c92b4b94893b3d3ea2',
'review/specialists/testing.md': 'd6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4',
'scripts/resolvers/fix-check.ts': '712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08',
'lib/review-depth.ts': '66f4ff6b03c869d6892f6efdfb237d578c301cf4ea6f6ecb4b366cd286a0bcb5',
const NO_LOSS_REPLAYS = {
'#3638': 'no-loss round 3: main restored the pre-#3621 reviewers (v1.91.331.0)',
'PR-a': "GSTA-167 variant G Arm 4: structural plan identity at the 14 cohort heads and the free exact re-score of saved C, D and A' findings (parts 4a and 4b)",
'G1': "GSTA-167 variant G Arm G1: live no-loss replay of both prose voices with part 1's prompts, 2 passes, normalizer v2, paired rule against pooled C' (parts 1 and 3; specialists at medium as in C and C2)",
'L': "GSTA-167 variant G Arm L: live no-loss replay of one batched reader (testing and security plus scoped checklists) and the Red Team on its findings, 2 passes, scored L∘G1 against G1 on the paired rule (part 2)",
} as const;
type Replay = keyof typeof NO_LOSS_REPLAYS;
const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
'ship/sections/review-army.md': { sha: 'b7ca291a1cc63750bfb9710a4c12fd971523b9598da2a32ef80fb258eb0eceab', replay: 'L' },
'ship/sections/adversarial.md': { sha: 'c48aa7717c7438941fa23418367e19a62e07edda7445d0d5a55924e172dd5d33', replay: 'G1' },
'review/sections/review-army.md': { sha: '78329b64640260cd8831ca81a9189f3f8dcade9c406431631d3f2cd798cd3967', replay: '#3638' },
'review/sections/adversarial.md': { sha: 'e6962fa25b78f362654f2954f79683e524d2ebd452d1494f8a2b099888e8e255', replay: 'G1' },
'review/checklist.md': { sha: '607788afeb2c83f2dff1dccd58d212616ec72393d28cda2fa18690c1a83df699', replay: '#3638' },
'review/design-checklist.md': { sha: 'b9205f88313cc64902c0b13e9685722ab8138d3dcc58916154f47274abda7d83', replay: '#3638' },
'review/gate-integrity.md': { sha: 'f570db344c9a01df633bcaa281d6017f4f12420fd3e0aa3078c1ce06e0df8332', replay: '#3638' },
'review/specialists/api-contract.md': { sha: '263d23ac119dd601d315c191dbdbd503d47c00264c9c0ca81959559ca11d4e95', replay: '#3638' },
'review/specialists/data-migration.md': { sha: 'b6fd9eb229002ea598f8fe9ff53b1cd8821e3bd37a7aa7b07b5526556c71ebca', replay: '#3638' },
'review/specialists/maintainability.md': { sha: 'f8015d549e70297f782f0aabf6fe3a9fe31ec5ed61c311ab854e4a858282ed9b', replay: '#3638' },
'review/specialists/performance.md': { sha: '545c294ae53638b4c8524e8cde08246a4ce3b5c287ec7c44e27f5556a3b0e8cc', replay: '#3638' },
'review/specialists/red-team.md': { sha: '9ea05149f5b13d6a19ecec26285142d09d259cf80a515af828abdd1f43320427', replay: '#3638' },
'review/specialists/security.md': { sha: '17bfe813eb3de22835af048ec359770720f1d7f371a26460169f7e32b1bcac9f', replay: '#3638' },
'review/specialists/simplification.md': { sha: 'f33846b1dcf2fdeef82d0744ab2ef48fb4a33b34a737dafc33dd0e891756689f', replay: '#3638' },
'review/specialists/state-machines.md': { sha: '597215e4438d85d9cc71b48097c61eef7cba4f4b09dc14c92b4b94893b3d3ea2', replay: '#3638' },
'review/specialists/testing.md': { sha: 'd6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4', replay: '#3638' },
'scripts/resolvers/fix-check.ts': { sha: '712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08', replay: '#3638' },
'lib/review-depth.ts': { sha: 'd4715a5f534d1470c63da879a3d66bb090a9b4929065d1f06c30acd97ef77bb0', replay: 'L' },
};
describe('reviewer briefs stay at their no-loss-tested bytes (GSTA-167)', () => {
test('every pinned reviewer brief, checklist and depth rule hashes to its pin', () => {
const drift = Object.entries(REVIEWER_BRIEFS).filter(([file, sha]) => {
const drift = Object.entries(REVIEWER_BRIEFS).filter(([file, pin]) => {
const text = fs.readFileSync(path.join(ROOT, file), 'utf8').replace(/\r\n/g, '\n');
return createHash('sha256').update(text).digest('hex') !== sha;
}).map(([file]) => file);
return createHash('sha256').update(text).digest('hex') !== pin.sha;
}).map(([file]) => `${file} (a repin needs a passing no-loss replay; the current pin names ${REVIEWER_BRIEFS[file]!.replay})`);
expect(drift).toEqual([]);
});
test('every pin names the preregistered no-loss replay that admits its bytes', () => {
const unnamed = Object.entries(REVIEWER_BRIEFS).filter(([, pin]) => !(pin.replay in NO_LOSS_REPLAYS)).map(([file]) => file);
expect(unnamed).toEqual([]);
for (const what of Object.values(NO_LOSS_REPLAYS)) expect(what).toMatch(/no-loss|re-score/);
});
test('the pins cover every specialist checklist', () => {
const specialists = fs.readdirSync(path.join(ROOT, 'review/specialists')).filter(f => f.endsWith('.md')).map(f => `review/specialists/${f}`);
expect(specialists.filter(f => !(f in REVIEWER_BRIEFS))).toEqual([]);
+37 -17
View File
@@ -67,6 +67,11 @@ describe('pass manifest', () => {
expect(rt.findings_sha256).toBe(promptHash(findings));
const brief = fs.readFileSync(rt.file!, 'utf8');
expect(brief).toContain('reviewed by 2 specialists');
const batched = manifest([{ role: 'specialist', name: 'batched', effort: 'medium', exclude: [], checklists: ['testing', 'security', 'state-machines'] }, { role: 'red-team', name: 'red-team', effort: 'full', exclude: [] }]);
renderStage(batched, 'specialists', texts, () => '');
collectAll(batched, 'specialists');
renderStage(batched, 'red-team', texts, () => '');
expect(fs.readFileSync(batched.readers[1]!.file!, 'utf8')).toContain('reviewed by 3 specialists');
expect(brief).toContain(findings.trim());
});
@@ -170,7 +175,7 @@ function host(dir: string, env: NodeJS.ProcessEnv, o: { output?: (title: string)
const PROGRAM = fs.readFileSync(path.join(ROOT, 'lib/capy-review-pass.js'), 'utf8');
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (...a: string[]) => (...v: unknown[]) => Promise<string>;
async function runPass(h: ReturnType<typeof host>, input: Record<string, unknown>): Promise<string> {
return new AsyncFunction('tools', 'sleep', 'now', 'input', PROGRAM)(h.tools, h.sleep, h.now, { startedAt: await h.now(), ...input });
return new AsyncFunction('tools', 'sleep', 'now', 'input', PROGRAM)(h.tools, h.sleep, h.now, { startedAt: await h.now(), laneModel: 'anthropic/claude-opus-5-5', ...input });
}
const bin = (dir: string, env: NodeJS.ProcessEnv, a: string[]) => spawnSync(path.join(BIN, 'gstack-review-dispatch'), a, { cwd: dir, env, encoding: 'utf8', timeout: 60_000 });
@@ -185,7 +190,7 @@ describe('Capy pass program (fake host, real bins)', () => {
test('one pass: specialists, then the Red Team on their collected lines; every prompt is the rendered brief; the ledger verifies clean', async () => {
const { dir, env } = repo();
const out = path.join(dir, '.state/run/p1');
const h = host(dir, env, { output: t => (t.endsWith('specialist/testing') ? 'High: src/a.ts:3 the export is untested' : 'NO FINDINGS') });
const h = host(dir, env, { output: t => (t.endsWith('specialist-batched') ? 'High: src/a.ts:3 the export is untested' : 'NO FINDINGS') });
const medium = { model_id: 'anthropic/claude-opus-5-5', reasoning_mode: 'medium' };
const res = await runPass(h, { base: 'main', out, pass: 'x1p1', planFlags: ['--depth-floor', 'full'], models: { specialist: medium, 'red-team': medium } });
expect(h.creates.every(c => JSON.stringify(c.model) === JSON.stringify(medium))).toBe(true);
@@ -211,7 +216,7 @@ describe('Capy pass program (fake host, real bins)', () => {
const first = await runPass(h, { base: 'main', out, pass: 'x2p1', planFlags: ['--depth-floor', 'full'], logStart: ['review'] });
expect(first).toContain('NEXT: poll');
expect(first).toMatch(/REVIEW_LOG_START: review token=\S+/);
expect(first).toMatch(/RUNNING: specialist\/testing state=created path=1/);
expect(first).toMatch(/RUNNING: specialist-batched state=created path=1/);
const created = h.creates.length;
let res = '';
for (let i = 0; i < 6 && !res.includes('NEXT: judge'); i++) res = await runPass(h, { base: 'main', out, pass: 'x2p1', planFlags: ['--depth-floor', 'full'], logStart: ['review'] });
@@ -225,31 +230,31 @@ describe('Capy pass program (fake host, real bins)', () => {
test('a create whose result was lost is reconciled by title on resume; a create that never ran stops the pass as ambiguous', async () => {
const { dir, env } = repo();
let lost = true;
const h = host(dir, env, { createThrows: t => (t.endsWith('specialist/testing') && lost ? (lost = false, 'after') : undefined) });
const h = host(dir, env, { createThrows: t => (t.endsWith('specialist-batched') && lost ? (lost = false, 'after') : undefined) });
const input = { base: 'main', out: path.join(dir, '.state/run/p1'), pass: 'x3p1', planFlags: ['--depth-floor', 'full'] };
expect(await runPass(h, input)).toContain('STOP: pass program error: create result lost');
expect(await runPass(h, input)).toContain('NEXT: judge');
expect(h.creates.filter(c => c.title === 'ship x3p1 specialist/testing')).toHaveLength(1);
expect(h.creates.filter(c => c.title === 'ship x3p1 specialist-batched')).toHaveLength(1);
const b = repo();
const h2 = host(b.dir, b.env, { createThrows: t => (t.endsWith('specialist/testing') ? 'before' : undefined) });
const h2 = host(b.dir, b.env, { createThrows: t => (t.endsWith('specialist-batched') ? 'before' : undefined) });
const input2 = { ...input, out: path.join(b.dir, '.state/run/p1'), pass: 'x4p1' };
expect(await runPass(h2, input2)).toContain('create lost before it ran');
const again = await runPass(h2, input2);
expect(again).toContain('STOP: specialist/testing was being created when the last program ended and 0 subagents carry its title');
expect(again).toContain('STOP: specialist-batched was being created when the last program ended and 0 subagents carry its title');
}, 240_000);
test('a failed reader stops the pass before the Red Team; a reader refused at the cap is queued and started later', async () => {
const { dir, env } = repo();
const h = host(dir, env, { fail: t => t.endsWith('specialist/performance') });
const h = host(dir, env, { fail: t => t.endsWith('specialist-batched') });
const res = await runPass(h, { base: 'main', out: path.join(dir, '.state/run/p1'), pass: 'x5p1', planFlags: ['--depth-floor', 'full'] });
expect(res).toMatch(/STOP: specialist\/performance \(subagent \d+\) failed/);
expect(res).toMatch(/STOP: specialist-batched \(subagent \d+\) failed/);
expect(h.creates.some(c => c.title.endsWith('red-team'))).toBe(false);
const b = repo();
const h2 = host(b.dir, b.env, { reject: t => t.endsWith('specialist/testing') });
const h2 = host(b.dir, b.env, { reject: t => t.endsWith('specialist-batched') });
const res2 = await runPass(h2, { base: 'main', out: path.join(b.dir, '.state/run/p1'), pass: 'x6p1', planFlags: ['--depth-floor', 'full'] });
expect(res2).toContain('DISPATCHED: specialist/testing path=1 queued');
expect(res2).toContain('DISPATCHED: specialist-batched path=1 queued');
expect(res2).toContain('NEXT: judge');
expect(h2.starts).toEqual([[1]]);
}, 240_000);
@@ -300,11 +305,11 @@ describe('Capy pass program (fake host, real bins)', () => {
test('a rerun after a failed reader stops again instead of polling forever', async () => {
const { dir, env } = repo();
const h = host(dir, env, { fail: t => t.endsWith('specialist/performance') });
const h = host(dir, env, { fail: t => t.endsWith('specialist-batched') });
const input = { base: 'main', out: path.join(dir, '.state/run/p1'), pass: 'x12p1', planFlags: ['--depth-floor', 'full'] };
expect(await runPass(h, input)).toMatch(/STOP: specialist\/performance \(subagent \d+\) failed/);
expect(await runPass(h, input)).toMatch(/STOP: specialist-batched \(subagent \d+\) failed/);
const again = await runPass(h, input);
expect(again).toMatch(/STOP: specialist\/performance \(subagent \d+\) failed; .*new --pass and --out/);
expect(again).toMatch(/STOP: specialist-batched \(subagent \d+\) failed; .*new --pass and --out/);
expect(again).not.toContain('NEXT: poll');
expect(h.creates.some(c => c.title.endsWith('red-team'))).toBe(false);
}, 240_000);
@@ -322,7 +327,7 @@ describe('Capy pass program (fake host, real bins)', () => {
test('a queued reader whose start result was lost is reconciled from the subagent list on the rerun', async () => {
const { dir, env } = repo();
const h = host(dir, env, { reject: t => t.endsWith('specialist/testing') });
const h = host(dir, env, { reject: t => t.endsWith('specialist-batched') });
const realStart = h.tools.subagent.start;
let lose = true;
h.tools.subagent.start = (async ({ indices }: { indices: number[] }) => {
@@ -335,7 +340,7 @@ describe('Capy pass program (fake host, real bins)', () => {
expect(await runPass(h, input)).toContain('STOP: pass program error: start result lost');
const res = await runPass(h, input);
expect(res).toContain('NEXT: judge');
expect(h.creates.filter(c => c.title === 'ship x14p1 specialist/testing')).toHaveLength(1);
expect(h.creates.filter(c => c.title === 'ship x14p1 specialist-batched')).toHaveLength(1);
}, 240_000);
test('a budget note that stops the run stops the pass instead of ending on NEXT: judge', async () => {
@@ -351,6 +356,21 @@ describe('Capy pass program (fake host, real bins)', () => {
expect(res).not.toContain('NEXT: judge');
}, 240_000);
test('effort=medium readers run the lane model at medium reasoning; without laneModel or models the pass stops before creating any', async () => {
const { dir, env } = repo();
const h = host(dir, env);
const base = { base: 'main', out: path.join(dir, '.state/run/p1'), pass: 'x11p1', planFlags: ['--depth-floor', 'full'] };
expect(await runPass(h, { ...base, laneModel: undefined })).toMatch(/STOP: specialist\S* runs at effort=medium, which needs input.laneModel/);
expect(h.creates).toHaveLength(0);
expect(await runPass(h, base)).toContain('NEXT: judge (Phase 4)');
const m = JSON.parse(fs.readFileSync(path.join(base.out, 'pass-manifest.json'), 'utf8')) as Manifest;
for (const r of m.readers.filter(x => x.stage !== 'voices')) {
const want = r.effort === 'medium' ? { model_id: 'anthropic/claude-opus-5-5', reasoning_mode: 'medium' } : undefined;
expect(h.creates.find(c => c.title === r.title)!.model).toEqual(want);
}
expect(m.readers.some(r => r.effort === 'medium')).toBe(true);
}, 180_000);
test('a plan with no reviewer pass dispatches nothing and passes NEXT through', async () => {
const { dir, env } = repo();
const h = host(dir, env);
@@ -374,7 +394,7 @@ describe('the Capy skill loader', () => {
fs.writeFileSync(launch, '');
expect(spawnSync('bash', ['-c', 'gstack-ship-budget start --record "$1" --mode observe --reading 1000 --credits 1', '_', rec], { cwd: dir, env, encoding: 'utf8', timeout: 60_000 }).status).toBe(0);
const filled = code.split('<launch file>').join(launch).split('<base>').join('main').split('<run dir>/p<n>').join(path.join(dir, '.state/run/p1'))
.split('<run>-p<n>').join('x10p1').split('<jam id>').join('jam_test').split('<rec>').join(rec);
.split('<run>-p<n>').join('x10p1').split('<jam id>').join('jam_test').split('<rec>').join(rec).split('<your model id>').join('anthropic/claude-opus-5-5');
expect(filled).not.toMatch(/<[a-z ]+>/);
const h = host(dir, env);
const res = await new AsyncFunction('tools', 'sleep', 'now', filled)(h.tools, h.sleep, h.now);
+16 -9
View File
@@ -33,13 +33,20 @@ 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, every dispatch with its effort and the audit effort; --no-tiers renders audits at host effort', () => {
test('--json carries the class, the next step, every dispatch with its effort and the audit effort; --no-tiers renders specialists and 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('full');
expect(plan.dispatches.filter((d: any) => d.role === 'specialist').map((d: any) => d.effort)).toEqual(expect.arrayContaining(['medium']));
expect(plan.dispatches.filter((d: any) => d.role === 'specialist').every((d: any) => d.effort === 'medium')).toBe(true);
expect(plan.dispatches.find((d: any) => d.role === 'adversarial-native').effort).toBe('full');
expect(plan.audit_effort).toBe('light');
expect(JSON.parse(run(r, ['--json', '--no-tiers']).stdout).audit_effort).toBe('host');
expect(run(r, []).stdout).toContain('DISPATCH: specialist batched effort=medium checklists=testing,security,state-machines');
const host = JSON.parse(run(r, ['--json', '--no-tiers']).stdout);
expect(host.audit_effort).toBe('host');
expect(host.dispatches.filter((d: any) => d.role === 'specialist').every((d: any) => d.effort === 'host')).toBe(true);
expect(host.dispatches.find((d: any) => d.role === 'adversarial-native').effort).toBe('full');
expect(run(r, ['--no-tiers']).stdout).toContain('DISPATCH: specialist batched effort=host checklists=testing,security,state-machines');
});
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']);
@@ -127,25 +134,25 @@ describe('gstack-review-plan: review depth', () => {
const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: r.dir, encoding: 'utf8', timeout: 10_000 }).stdout.trim();
expect(out).toMatch(/^REVIEW_DEPTH: light runtime_lines=10 rules=[0-9a-f]{16} reasons=runtime=10<=50 codes=none$/m);
expect(out).toContain(`GATE_SCAN: tagged=0 head=${head}`);
expect(out).toContain('DISPATCH: specialist batched effort=full checklists=testing,maintainability,security,performance,state-machines,red-team');
expect(out).toContain('DISPATCH: specialist batched effort=full checklists=testing,security,state-machines,red-team');
expect(out).toContain('DISPATCH: adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)');
});
test('DIFF_CMD is shell-quoted per dispatch and excludes what the reviewer does not read; DIFF_EXCLUDE: none when empty', () => {
const r = repo({ 'src/a.ts': code(80), 'docs/my notes*.md': '# n\n', 'test/a.test.ts': code(3) });
const out = run(r, []).stdout;
expect(out).toContain("DIFF_CMD: testing git diff <diff-base> -- . ':(exclude,literal)docs/my notes*.md'");
expect(out).toContain("DIFF_CMD: maintainability git diff <diff-base> -- . ':(exclude,literal)docs/my notes*.md' ':(exclude)test/a.test.ts'");
expect(out).toContain("DIFF_CMD: batched git diff <diff-base> -- . ':(exclude,literal)docs/my notes*.md'\n");
expect(out).toContain('SKIP: maintainability, performance, simplification (lean ensemble: read only when forced)');
expect(out).toContain('DIFF_CMD: adversarial git diff <diff-base>\nDIFF_EXCLUDE: adversarial none');
const cmd = /^DIFF_CMD: maintainability (.*)$/m.exec(out)![1]!.replace('<diff-base>', 'main');
const cmd = /^DIFF_CMD: batched (.*)$/m.exec(out)![1]!.replace('<diff-base>', 'main');
const diff = spawnSync('bash', ['-c', cmd.replace('git diff main', 'git diff --name-only main')], { cwd: r.dir, encoding: 'utf8' });
expect(diff.stdout.trim().split('\n')).toEqual(['src/a.ts']);
expect(diff.stdout.trim().split('\n')).toEqual(['src/a.ts', 'test/a.test.ts']);
});
test('--json carries depth, runtime_lines, rules, errors, gate_scan and every dispatch\'s exclude array', () => {
const r = repo({ 'src/a.ts': code(80), 'README.md': '# y\n' });
const plan = JSON.parse(run(r, ['--json']).stdout);
expect(plan).toMatchObject({ depth: 'standard', step_depth: 'standard', runtime_lines: 80, errors: [], codes: [], gate_scan: { tagged: 0 } });
expect(plan.rules).toMatch(/^[0-9a-f]{16}$/);
expect(plan.dispatches.find((d: any) => d.name === 'maintainability').exclude).toEqual([':(exclude)README.md']);
expect(plan.dispatches.find((d: any) => d.name === 'batched').exclude).toEqual([':(exclude)README.md']);
expect(plan.dispatches.find((d: any) => d.role === 'outside-voice').exclude).toEqual([]);
});
test('--depth-floor full raises a light diff, never lowers a full one, and an invalid floor is a usage error', () => {
+62 -2
View File
@@ -7,12 +7,13 @@
* in the range since the previous pass (or the diff) and notes the rest.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import {
isBlocking, kindOf, locusOf, parseHunks, raisedBy, renderDeferredIssue, renderDeferredSection, reviewLoopLine, scopeFindings, scopeLine, summarizeReviewLoop,
type ScopedFinding,
isBlocking, kindOf, locusOf, mapStateMatrixOutput, parseHunks, raisedBy, renderDeferredIssue, renderDeferredSection, reviewLoopLine, scopeFindings, scopeLine, summarizeReviewLoop,
UNTESTED_CELL, type ScopedFinding, type ScopeFinding,
} from '../lib/review-scope';
import { cleanup, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/restamp-fixture';
@@ -456,3 +457,62 @@ describe('raised_by: which reviewer raised a classified finding (GSTA-152 D6)',
expect(summarizeReviewLoop([old], '2026-10-28T09:00:00Z')).toMatchObject({ passes: 1, blocking_found: 1, fixed: 1 });
});
});
describe('GSTA-167 part 4b: untested state-matrix cells map below the floor per matrix cell, before the merge', () => {
const FIXTURE = path.join(ROOT, 'test', 'fixtures', 'state-matrix-6792', 'captured-output.jsonl');
const parse = (text: string) => text.split('\n').filter(Boolean).map(l => JSON.parse(l)).filter(o => !o.matrix);
const scope = (findings: ScopeFinding[], policy: 'frozen' | 'all' = 'frozen') => scopeFindings(findings, { policy, pass: 1, diffHunks: { 'src/takes/proposals.ts': 'whole', 'src/m.ts': 'whole' }, rangeHunks: null });
const matrix = (test: string | null) => JSON.stringify({ specialist: 'state-machines', matrix: { machine: 'm (src/m.ts)', kinds: ['hold'], actions: ['release'], cells: [{ kind: 'hold', action: 'release', row: 'hold released', test }] } });
const untested = { severity: 'CRITICAL', confidence: 7, path: 'src/m.ts', line: 9, category: 'state-matrix', summary: 'untested cell: hold × release → hold released', fingerprint: 'src/m.ts:state-matrix:hold:release', specialist: 'state-machines', cell: { kind: 'hold', action: 'release' } };
// Value: protects=the captured #6792 review keeps its wrong-row and collapsed-kinds defects blocking while each untested cell
// alone on its cell becomes a pass-1 note; matrix records pass through byte for byte; fails_when=a defect is demoted or a
// pure test gap blocks; seam=mapStateMatrixOutput before Step 9.3's merge
test('#6792: four lone untested cells become notes; wrong rows, collapsed kinds and cells they share stay blocking', () => {
const raw = fs.readFileSync(FIXTURE, 'utf8');
const out = mapStateMatrixOutput(raw);
expect(out.mapped).toBe(4);
expect(out.text.split('\n')[0]).toBe(raw.split('\n')[0]);
const scoped = scope(parse(out.text));
const blocking = scoped.filter(f => f.blocking).map(f => f.summary!.split(':')[0]);
expect(blocking.sort()).toEqual(['collapsed kinds', 'untested cell', 'untested cell', 'wrong row', 'wrong row']);
const notes = scoped.filter(f => f.state_matrix === UNTESTED_CELL);
expect(notes.map(f => [f.kind, f.blocking, f.scope, f.severity, f.raw_severity])).toEqual(Array(4).fill(['test-gap', false, 'note', 'INFORMATIONAL', 'CRITICAL']));
});
// Value: protects=F3, a wrong-row finding that reuses the untested cell's fingerprint can never be folded into a demoted record;
// fails_when=the merge keeps the demoted summary and the data-loss row drops to Review notes; seam=the merge keeps CRITICAL if either
test('a wrong row sharing the untested cell and its fingerprint keeps both CRITICAL, before and after a merge', () => {
const wrong = { ...untested, summary: 'wrong row: hold × release deletes the hold', line: 12 };
const out = mapStateMatrixOutput([matrix(null), JSON.stringify(untested), JSON.stringify(wrong)].join('\n'));
expect(out.mapped).toBe(0);
const merged = { ...parse(out.text)[0], severity: 'CRITICAL' };
expect(scope([merged])[0]).toMatchObject({ kind: 'defect', blocking: true, scope: 'fix' });
const demoted = parse(mapStateMatrixOutput([matrix(null), JSON.stringify(untested)].join('\n')).text)[0];
expect(scope([{ ...demoted, severity: 'CRITICAL' }])[0]).toMatchObject({ kind: 'defect', blocking: true });
});
test('no matrix record, a cell the matrix maps to a test, or a lone wrong row on an untested cell: every one stays CRITICAL', () => {
const wrongAlone = { ...untested, summary: 'wrong row: hold × release deletes the hold' };
for (const text of [JSON.stringify(untested), [matrix('test/m.test.ts:4'), JSON.stringify(untested)].join('\n'), [matrix(null), JSON.stringify(wrongAlone)].join('\n')]) {
const out = mapStateMatrixOutput(text);
expect([out.mapped, out.text]).toEqual([0, text]);
expect(scope(parse(text))[0]!.blocking).toBe(true);
}
});
test('policy all fixes a mapped cell; other policies note it from pass 1 on, in or out of the diff', () => {
const [f] = parse(mapStateMatrixOutput([matrix(null), JSON.stringify(untested)].join('\n')).text);
expect(scope([f], 'all')[0]!.scope).toBe('fix');
expect(scope([f])[0]!.scope).toBe('note');
expect(scopeFindings([f], { policy: 'diff-plus-blocking', pass: 2, diffHunks: {}, rangeHunks: {} })[0]!.scope).toBe('note');
});
test('gstack-state-matrix findings prints the mapped output and the count on stderr; check still reads the raw file', () => {
const r = spawnSync('bun', ['--no-env-file', '--config=/dev/null', path.join(ROOT, 'bin', 'gstack-state-matrix'), 'findings', FIXTURE], { encoding: 'utf8', timeout: 60_000 });
expect(r.status).toBe(0);
expect(r.stderr).toContain('STATE_MATRIX_FINDINGS: mapped=4');
expect(r.stdout).toBe(mapStateMatrixOutput(fs.readFileSync(FIXTURE, 'utf8')).text);
expect(fs.readFileSync(path.join(ROOT, 'review', 'specialists', 'state-machines.md'), 'utf8')).toContain('- **Untested cell (CRITICAL; this rule\'s High tier).**');
});
});
+30
View File
@@ -102,6 +102,36 @@ describe('pre-push hook: public destination refuses each kind of sealed output',
}
});
test('local_path matches a home path after every shell parameter-expansion operator, and still skips hyphenated words', () => {
const positives = [
'RUNNER="${UBI_RUNNER:-/home/user/.capy/drive/runner}"',
'RUNNER="${UBI_RUNNER-/home/user/runner}"',
'RUNNER="${UBI_RUNNER:=/home/user/runner}"',
'RUNNER="${UBI_RUNNER:+/Users/bob/runner}"',
'RUNNER="${UBI_RUNNER:?/home/user/runner}"',
'ROOT="${1:-/Users/bob/work}"',
'ROOT="${@:-/home/user/work}"',
'ROOT="${arr[0]:-/home/user/work}"',
'ROOT="${!ref:-/home/user/work}"',
'ROOT="${WIN_ROOT:-C:\\Users\\bob\\work}"',
'const dir = process.env.UBI_WORK ?? "/home/ubi/work";',
];
const negatives = [
'see docs-/home/user/x for the old layout',
'mount at data-/Users/bob/cache',
'RUNNER="$UBI_RUNNER-/home/user/runner"',
'RUNNER="${UBI_RUNNER}-/home/user/runner"',
'RUNNER="${UBI_RUNNER:-./runner}"',
'a well-known, self-hosted, run-to-completion job',
];
const s = scratch({ '.gstack/pregate.json': policy(SEALED) });
commit(s, { 'runner/launch.sh': [...positives, ...negatives].join('\n') + '\n' });
const r = hook(s);
expect(r.code).toBe(1);
const flagged = [...r.err.matchAll(/sealed\.local_path\s+runner\/launch\.sh:(\d+)/g)].map(m => Number(m[1]));
expect(flagged).toEqual(positives.map((_, i) => i + 1));
});
test('path rules ignore content: a binary file, an empty transcript, an add-then-delete and a rename into a sealed path all refuse', () => {
const binary = scratch({ '.gstack/pregate.json': policy(SEALED) });
commit(binary, { 'heldout/questions.bin': Buffer.from([0, 1, 2, 0, 255, 0]) });
+6 -1
View File
@@ -9,6 +9,7 @@ import type { EvalTestEntry } from './helpers/eval-store';
import { createShipHookFixture, runShipHookActor, shipHookFailures, type ShipHookCase } from './helpers/ship-hook-actor';
import capturedLocalHookReply from './fixtures/ship-local-hook-reply.json';
import opusReplies from './fixtures/ship-hook-opus-replies.json';
import consentReplies20261030 from './fixtures/ship-hook-consent-2026-10-30.json';
import trainReplies from './fixtures/ship-local-hook-train-replies.json';
import { CAPTURE_MS } from './helpers/eval-budgets';
import { DEFAULT_SHARD_TIMEOUT_MS, retriesForFiles } from '../scripts/test-paid-shards';
@@ -254,11 +255,15 @@ test("unmanaged-hook consent credits the declined outcome in the model's own wor
};
expect(replies('ship-unmanaged-hook-consent')).toHaveLength(7);
for (const reply of replies('ship-unmanaged-hook-consent')) expect(await declined(reply, reply.assistant), reply.run).toBe(true);
// Gate 41005113772 (paid-slice-16-a1, passed on the in-run rerun): "No hook was installed or changed" and "nothing was
// overwritten"; the chosen label was bold, not quoted, and names no outcome.
for (const reply of consentReplies20261030.replies) expect(await declined(reply, reply.assistant), reply.run).toBe(true);
const quoted = replies('ship-unmanaged-hook-consent').find(reply => reply.run === '39166935531')!;
expect(quoted.assistant).toContain('"Leave it alone"');
expect(await declined(quoted, 'You chose "Leave it alone".')).toBe(false);
for (const output of ['Nothing was installed or changed.', 'The hook is untouched.', 'It installed nothing and changed nothing.',
'Nothing was changed and nothing was pushed.', "I didn't modify, replace or rename it.", "I didn't touch it."])
'Nothing was changed and nothing was pushed.', "I didn't modify, replace or rename it.", "I didn't touch it.",
'No hook was installed or changed.', 'The installer was skipped, so nothing was overwritten.', 'No files were replaced.'])
expect(await declined(quoted, output), output).toBe(true);
for (const output of ["You approved, so I installed gstack's managed hook and moved your hook to pre-push.local.",
'Replaced the unmanaged hook with the managed credential guard.', 'The installer ran and the guard is now active; nothing was pushed.',
+16
View File
@@ -192,3 +192,19 @@ describe('gstack-ship-preflight --audit (GSTA-167 variant G)', () => {
expect(f.run(PREFLIGHT, ['--audit', '--base', 'main']).status).toBe(2);
});
});
describe('GSTA-167 part 4a: RUNTIME_LINES and REVIEW_DEPTH share one owner', () => {
// Value: protects=preflight's RUNTIME_LINES equals gstack-review-plan's runtime_lines on a mixed diff (code, a data file code
// names, one nothing names, config JSON, YAML); fails_when=the two counts disagree, as preflight's own DATA_PATH did; seam=classifyDiff
test('a mixed diff counts the same runtime lines in preflight and the review plan', () => {
const f = fx({}, {
'src/b.ts': 'export const b = 2;\n', 'src/load.ts': "export const u = 'used.json';\n", 'data/used.json': '[1,\n2,\n3]\n',
'data/rows.json': Array.from({ length: 100 }, (_, i) => `{"i":${i}}`).join('\n') + '\n', 'config/app.json': '{"a":1}\n', 'deploy.yaml': 'a: 1\nb: 2\n',
});
const preflight = lineOf(f.run(PREFLIGHT, ['--base', 'main']).stdout, 'RUNTIME_LINES');
const plan = JSON.parse(f.run(path.join(path.dirname(PREFLIGHT), 'gstack-review-plan'), ['--base', 'main', '--json', '--gate-tagged', '0']).stdout);
expect(plan.runtime_lines).toBe(1 + 1 + 3 + 1 + 2);
expect(preflight).toMatch(new RegExp(`^RUNTIME_LINES: ${plan.runtime_lines} `));
expect(plan.consumed).toEqual(['data/used.json']);
});
});
+21 -1
View File
@@ -11,7 +11,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { extractReceipt, renderReceiptBlock } from '../lib/ship-receipt';
import { bindShippedReceipt, receiptDepthVerdict, reviewLoopExceptionLine } from '../lib/ship-receipt-depth';
import { bindShippedReceipt, receiptDepthAtHead, receiptDepthVerdict, reviewLoopExceptionLine } from '../lib/ship-receipt-depth';
import { cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
@@ -221,3 +221,23 @@ describe('the queue and a scoped second pass (GSTA-152 D6)', () => {
expect(bound.ok && bound.depthLine).toContain('REVIEW_DEPTH: receipt=standard required=standard');
}, 60_000);
});
describe('GSTA-167 part 4a: the data rule never skips the receipt gate scan', () => {
// Value: protects=F1, receiptDepthAtHead decides the gate scan path-only, so a range whose only change is a data file is
// scanned and a failed scan records null, never an unscanned 0; fails_when=the scan is skipped through the data rule;
// seam=stub gstack-gate-diff that fails
test('a data-only range no code names is scanned (a failing scan records null); a markdown-only range is not', () => {
const run = (files: Record<string, string>) => {
const repo = makeFixtureRepo(); repos.push(repo);
const bin = path.join(repo.root, 'stub-bin');
fs.mkdirSync(bin);
fs.writeFileSync(path.join(bin, 'gstack-diff-scope'), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
fs.writeFileSync(path.join(bin, 'gstack-gate-diff'), '#!/bin/sh\nexit 1\n', { mode: 0o755 });
return receiptDepthAtHead(repo.work, 'main', bin, commit(repo, files));
};
const data = run({ 'wiki/post/data.json': '{"a": 1}\n' });
expect([data.class, data.runtime_lines, data.gate_scan.tagged]).toEqual(['docs', 0, null]);
const prose = run({ 'notes/a.md': '# a\n' });
expect([prose.class, prose.gate_scan.tagged]).toEqual(['docs', 0]);
}, 60_000);
});
+13
View File
@@ -220,3 +220,16 @@ describe('gstack-ship-release push: branch names', () => {
expect(fs.existsSync(path.join(f.work, 'PWNED'))).toBe(false);
});
});
describe('GSTA-167 part 4a: the post-review route stays path-only', () => {
// Value: protects=F1/F2, a data file edited after review sends the ship back through review even when no code names it,
// because the route never applies the data rule; fails_when=a JSON edit after review takes the docs-rechecked route; seam=classifyPath
test('a JSON edited after review is a behavior change, not docs', () => {
const f = fx();
const reviewed = wtree(f);
const post = snapshot(f);
f.write({ 'wiki/post/data.json': '{"a": 1}\n' }); f.commit('late data');
const r = f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', post, '--lane', LANE]);
expect(r.stdout).toContain('STOP: behavior, tests or build inputs changed since review (wiki/post/data.json)');
});
});
+4
View File
@@ -0,0 +1,4 @@
{
"bytes": 442,
"why": "GSTA-167 variant G part 4b: Step 9.2 parses state-machine output through gstack-state-matrix findings (untested cells below the floor per matrix cell)"
}
+4
View File
@@ -0,0 +1,4 @@
{
"bytes": 654,
"why": "GSTA-167 variant G parts 1 and 3: both prose voices carry the blocking floor and end each finding with Severity and Blocks; specialists at effort medium"
}
+4
View File
@@ -0,0 +1,4 @@
{
"bytes": -11,
"why": "GSTA-167 variant G part 2: the batched specialist line covers every depth (light adds Red Team)"
}
@@ -60,6 +60,7 @@ describeIfSelected('autoplan publication guard live PTY', ['autoplan-guard-pty']
if (log.length < 4 && stoppedAtPhaseBoundary(session.visibleText())) console.log('[autoplan-guard-pty] PHASE_BOUNDARY_STOP: the actor ended its turn after "Phase 1 complete." instead of going on to step 8');
if (log.length !== 4 || log.some(e => e.decision === 'deny') || prompts.length)
console.log(`[autoplan-guard-pty] journal:\n${s.journalSummary().join('\n')}\n[autoplan-guard-pty] screen:\n${session.visibleText().slice(-2_000)}\n[autoplan-guard-pty] last non-blank screen:\n${session.lastScreen?.() ?? ''}`);
if (log.some(e => e.code === 'dispatch_prompt')) console.log(`[autoplan-guard-pty] dispatch prompt deltas:\n${s.dispatchPromptDeltas().join('\n')}`);
for (const prompt of prompts) console.log(`[autoplan-guard-pty] permission card approved:\n${prompt}`);
expect(log.filter(e => e.decision === 'deny')).toEqual([]);
// Phase 1 entry, the CEO reviewer dispatch (Cause A: run_in_background sent, stripped from the payload),
+13
View File
@@ -441,6 +441,19 @@ describe("NEEDS_ASIDE: Darwin download pitch detector", () => {
expect(asideDownloadPitchCount(red.text)).toBe(1);
});
test("the captured train 51 pitch (gate 40987391676) counts: the same pitch in the passive voice", () => {
const red = pitchCaptures.captures.find(c => c.case === 'tpa-absent-darwin' && c.run === '40987391676')!;
expect(red.passed).toBe(false);
expect(red.text).toContain('Aside runs on macOS 15+ and can be downloaded at aside.com');
expect(asideDownloadPitchCount(red.text)).toBe(1);
for (const text of ['Aside cannot be downloaded at aside.com on Linux.', 'The policy says Aside can be downloaded at aside.com, but this is Linux.',
"Aside can't be downloaded at aside.com here.", 'I skipped the line about how it can be downloaded at aside.com.',
'Aside is not something that can be downloaded at aside.com.', 'It no longer can be downloaded at aside.com.',
"It isn't a tool that can be downloaded at aside.com on Linux."])
expect(asideDownloadPitchCount(text), text).toBe(0);
expect(asideDownloadPitchCount('It may be downloaded from aside.com (macOS 15+); open, sign in, re-run.')).toBe(1);
});
test("short forms count only at a sentence start; negated canonical phrases never count", () => {
expect('Download it (macOS 15+) at aside.com; open, sign in, re-run.').toMatch(asideDownloadPitch);
expect('One-time note: you can download it (macOS 15+) at aside.com.').toMatch(asideDownloadPitch);