train S1: merge #3920 at 8bdc6327ab v1.94.30.0

This commit is contained in:
garrytan committed 2000-01-01 00:00:00 +00:00
commit 7e29b959d7
40 files changed
+973 -114

No files matched your search

+18
View File
@@ -1,5 +1,23 @@
# Changelog
## [1.94.30.0] - 2026-11-03
**/ship sends more diffs to full review: spend-binding code, eval scorers, a repo's own risk paths and docs that print secrets now count as risk, and a lane can declare a class the matchers miss (GSTA-258 build C).** Today's reviewer panel stays the default. The one-reader shape for non-risk code ships as an opt-in (`review.panel risk`) because its no-loss replay didn't pass: with only the diff and no repository, the reader found 2 of 68 non-risk blockers, so the replay is being redone with readers working in a checkout. A repo with platform review on can skip /ship's readers on plain docs.
### Added
- **A `review` key in `.gstack/ship-policy.json`**, read from the base branch only (needs `min_gstack_version` at the release that adds it): `panel` (`all` by default, today's panel; `risk` or `off` opt in to one reader), `reader_model` (`lane` by default), `platform` (`off` by default) and `risk_paths` (globs of code paths that count as risk class `policy`).
- **`gstack-review-plan` prints `REVIEW_MODE:`** and, under `review.platform on`, `PLATFORM_REVIEW:`. `DISPATCH:` lines name the reader's `model=` and `classes=`. New flags: `--fix-checks <n>` and `--pr <n>`.
- **The platform depth.** Under `review.platform on`, a docs or tests diff with no numeric, results or prereg claims, whose PR carries capy-ai[bot]'s review at the exact head, plans `NEXT: platform` with a planned `SKIP:` for every reader. Step 11.5 accepts it, and `gstack-ship-receipt write --platform-review <id>` records `review_depth: platform` with that review's id and head. It never applies to runtime code, and never where platform review is off.
- **New risk classes**: `eval` (scorer, judge and grader paths) and `policy` (`review.risk_paths`). A docs line that can print a credential (an `Authorization:` header, a bearer value, a key-shaped literal, `mcp get`) is security risk (gbrain#8417).
- **The receipt records `review_depth.mode`** (lean, panel or platform).
- **Declared risk classes** (GST-1129). `gstack-review-plan --declare-risk <classes>` and `gstack-ship-receipt write --declare-risk <classes>` add classes the matchers missed. A declaration only raises the depth, the plan prints `RISK_DECLARED:`, and the receipt keeps `review_depth.declared` beside `review_depth.detected`.
- **Code that reserves, binds, leases or settles spend is money** (GST-1129): `bindCheck(ledger, …)`, `reserveSpend` or `bindCap` in a code file now plans full depth. gstack-evals#170 got its full review only because an unrelated `UBICLOUD_API_KEY` line was in the diff.
### Changed
- **Opt-in one-reader review** (`review.panel risk`): non-risk code gets one native adversarial reader at the lane's reasoning, carrying the code checklists and the Red Team checklist, and risk class keeps the full panel. One fix-check follows a fixed blocker. It isn't the default until its no-loss replay passes.
- **Fewer false risk triggers** (harness-evals#231): committed `*.sqlite` and other binary data files count as data, never code; JSON, YAML and other config files are judged by their path, so a `lease_usd` value no longer means money; analysis scripts under `docs/`, `results/` or `benchmarks/` are judged by their path.
- The outside voice reads its diff from a file the brief names with its sha256, instead of a diff pasted into the prompt. Past 2,000,000 bytes it reads a path-scoped slice whose brief lists the files it left out, instead of stopping the pass.
## [1.94.29.0] - 2026-11-03
**Every part of gstack now has to earn its place.** Garry, 2026-11-02: "Also work this principle of only include it if it is beneficial and good into gstack". A new skill, step, instruction, check or default needs evidence that it helps on frontier models, or a reason, an owner and a dated plan to measure it. The first inventory reads untested almost everywhere: one part, `/investigate`, has a fair no-effect result (a CUT-CANDIDATE, measured on Opus 5.5 only), 3 have a fair test under way, 14 more have one proposed, and 5 safety guards stay on their stated reason.
+1 -1
View File
@@ -1 +1 @@
1.94.29.0
1.94.30.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.94.29.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.94.30.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,
+24 -12
View File
@@ -16,10 +16,11 @@
* lib/review-pass.ts (the staged manifest) and lib/capy-review-pass.js (the Capy program that runs it).
*/
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { BRIEF_ROLES, BriefError, briefFile, checkReviewedTree, checkoutRoot, checkoutTree, promptHash, renderBrief, sectionTexts, withWorkspace, type BriefRole, type Slots, type TreeCheck } from '../lib/review-brief';
import { BRIEF_ROLES, BriefError, briefFile, checkReviewedTree, checkoutRoot, checkoutTree, promptHash, renderBrief, sectionTexts, sliceDiff, sliceNote, withWorkspace, type BriefRole, type Slots, type TreeCheck } from '../lib/review-brief';
import { cleanMarkers, collect, fixableRaises, mayBeUnavailable, parseFindings, readJsonl, readLedger, readerKey, recordJudgment, recordSent, appendEvent, verify, type JudgedLine, type MergedLine, type VerifyRow } from '../lib/review-ledger';
import { buildManifest, chunkCount, chunkOf, manifestPath, markReader, readManifest, renderStage, writeManifest, STAGES, type Extra, type Manifest, type PassReader, type ReaderState, type Stage } from '../lib/review-pass';
import type { Dispatch } from '../lib/review-depth';
@@ -108,8 +109,9 @@ function render(): number {
const diffBase = flag('--diff-base');
const checklist = flag('--checklist') ?? (role === 'red-team' ? path.join(ROOT, 'review/specialists/red-team.md') : role === 'targeted' ? path.join(ROOT, 'review/specialists/targeted.md') : role === 'specialist' && name ? path.join(ROOT, 'review/specialists', `${name}.md`) : undefined);
if (checklist && (role === 'specialist' || role === 'red-team' || role === 'targeted') && !fs.existsSync(checklist)) throw new BriefError(`checklist not found: ${checklist}`);
const brief = role === 'targeted' ? readFile(checklist, 'targeted brief') : undefined;
if (role === 'targeted') {
const merged = role === 'adversarial-native' && !!flag('--classes');
const brief = role === 'targeted' || merged ? readFile(merged ? path.join(ROOT, 'review/specialists/targeted.md') : checklist, 'targeted brief') : undefined;
if (role === 'targeted' || merged) {
const base = resolveBaseBranch(process.cwd(), flag('--base'));
const mb = mergeBaseWith(base);
const full = diffBase ? spawnSync('git', ['rev-parse', '--verify', '--quiet', `${diffBase}^{commit}`], { encoding: 'utf8', timeout: 30_000 }).stdout?.trim() : undefined;
@@ -127,11 +129,19 @@ function render(): number {
if (over) { process.stderr.write(over.line + '\n'); return over.exit; }
if (role === 'outside-voice') diff = branchDiff(diffBase, { exclude: flag('--exclude'), keep: keepItems });
}
let diffFile: Slots = {};
if (diff !== undefined) {
fs.mkdirSync(out, { recursive: true, mode: 0o700 });
const f = path.join(path.resolve(out), 'outside-voice.diff');
const s = sliceDiff(diff, REVIEW_DIFF_MAX_BYTES);
fs.writeFileSync(f, s.text, { mode: 0o600 });
diffFile = { diff_file: f, diff_bytes: String(s.bytes), diff_sha256: createHash('sha256').update(s.text).digest('hex'), ...(s.sliced ? { diff_slice: sliceNote(s, REVIEW_DIFF_MAX_BYTES) } : {}) };
}
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: checklists?.join(','),
findings: readFile(flag('--findings-file'), 'findings'), specialists: flag('--specialists'), diff,
classes: flag('--classes'), brief,
findings: readFile(flag('--findings-file'), 'findings'), specialists: flag('--specialists'), ...diffFile,
classes: flag('--classes'), brief, ...(merged ? { checklist: path.join(ROOT, 'review/specialists/targeted.md') } : {}),
...(flag('--disclosure') ? { disclosure: flag('--disclosure'), policy_exclude: policyItems.map(quote).join(' ') || undefined, keep: keepItems.map(quote).join(' ') || undefined } : {}),
fix_list: readFile(flag('--fix-list'), 'fix list'), hunks: readFile(flag('--hunks'), 'hunks'), tests: readFile(flag('--tests'), 'tests'), callers: readFile(flag('--callers'), 'callers'),
};
@@ -144,8 +154,8 @@ function render(): number {
fs.writeFileSync(file, text, { mode: 0o600 });
const ledger = flag('--ledger') ?? path.join(out, 'review-ledger.jsonl');
const sha = promptHash(text);
const targeted = brief === undefined ? {} : { classes: parseClassList(flag('--classes')!), rules: TARGETED_RULES, brief_sha: targetedBriefSha(checklist), diff_base: diffBase ?? null, ...(diffBase ? targetedRead(process.cwd(), diffBase) : { patch_id: 'none', head: null }) };
appendEvent(ledger, { event: 'rendered', reader: readerKey(role, name), role, name: name ?? null, file, sha256: sha, bytes: Buffer.byteLength(text), workdir, tree, ...targeted, ...(disclosureDigest(flag('--disclosure')) ? { exclude: disclosureDigest(flag('--disclosure')) } : {}) });
const targeted = brief === undefined ? {} : { classes: parseClassList(flag('--classes')!), rules: TARGETED_RULES, brief_sha: targetedBriefSha(merged ? path.join(ROOT, 'review/specialists/targeted.md') : checklist), diff_base: diffBase ?? null, ...(diffBase ? targetedRead(process.cwd(), diffBase) : { patch_id: 'none', head: null }) };
appendEvent(ledger, { event: 'rendered', reader: readerKey(role, name), role, name: name ?? null, file, sha256: sha, bytes: Buffer.byteLength(text), workdir, tree, ...targeted, ...(diffFile.diff_file ? { payload: { file: diffFile.diff_file, sha256: diffFile.diff_sha256 } } : {}), ...(disclosureDigest(flag('--disclosure')) ? { exclude: disclosureDigest(flag('--disclosure')) } : {}) });
process.stdout.write(`BRIEF: reader=${readerKey(role, name)} sha256=${sha} bytes=${Buffer.byteLength(text)} file=${file}\n`);
return EXIT.ok;
}
@@ -178,7 +188,7 @@ function ceiling(context: string, base: string, outDir: string, readers: Readonl
const at = r.diff_base ?? diffBase;
const d = selectionOf(at, r.exclude, r.keep);
if (!d.ok) return { exit: EXIT.fail, line: unreadableLine(context, r.reader, d, at, base, outDir) };
if (d.bytes > REVIEW_DIFF_MAX_BYTES) return { exit: EXIT.refused, line: tooLargeLine(context, r.reader, d, ceilingContext(process.cwd(), base, known)).line };
if (d.bytes > REVIEW_DIFF_MAX_BYTES && r.reader !== 'outside-voice') return { exit: EXIT.refused, line: tooLargeLine(context, r.reader, d, ceilingContext(process.cwd(), base, known)).line };
}
return null;
}
@@ -329,19 +339,21 @@ function program(): number {
* collected, and it was rendered on a committed diff (uncommitted never proves what was read). `head` names the commit it read:
* a later commit does not undo the read, and gstack-ship-receipt write binds the record only while that commit's product patch is
* the receipt's (GST-986: Step 11 fixes after the read printed status=failed findings=0 for a completed reader).
* R65's merged reader (an adversarial-native brief rendered with --classes) is a targeted dispatch too (design C7).
*/
function targetedRecord(ledger: string, rows: VerifyRow[]): { classes: string[]; rules: string; brief_sha: string; status: 'done' | 'failed'; findings: number; raised_by: string; head: string | null } | null {
const events = readLedger(ledger);
const render = events.filter(e => e.event === 'rendered' && e.reader === 'targeted').at(-1);
const render = events.filter(e => e.event === 'rendered' && (e.reader === 'targeted' || (e.reader === 'adversarial-native' && typeof e.brief_sha === 'string'))).at(-1);
if (!render) return null;
const label = rows.filter(r => r.kind === 'BRIEF' && r.reader.split('@')[0] === 'targeted').at(-1)?.reader;
const key = String(render.reader);
const label = rows.filter(r => r.kind === 'BRIEF' && r.reader.split('@')[0] === key).at(-1)?.reader;
const brief = rows.filter(r => r.kind === 'BRIEF' && r.reader === label).at(-1);
const output = rows.filter(r => r.kind === 'OUTPUT' && r.reader === label).at(-1);
const head = typeof render.head === 'string' && /^[0-9a-f]{40,64}$/.test(render.head) ? render.head : null;
const committed = typeof render.patch_id === 'string' && !['uncommitted', 'none'].includes(render.patch_id) && head !== null;
const done = brief?.status === 'MATCH' && output?.status === 'MATCH' && committed;
const ids = events.filter(e => e.event === 'output' && e.reader === 'targeted').at(-1)?.ids;
return { classes: (render.classes as string[]) ?? [], rules: String(render.rules), brief_sha: String(render.brief_sha), status: done ? 'done' : 'failed', findings: Array.isArray(ids) ? ids.length : 0, raised_by: 'targeted', head };
const ids = events.filter(e => e.event === 'output' && e.reader === key).at(-1)?.ids;
return { classes: (render.classes as string[]) ?? [], rules: String(render.rules), brief_sha: String(render.brief_sha), status: done ? 'done' : 'failed', findings: Array.isArray(ids) ? ids.length : 0, raised_by: key, head };
}
/**
+30 -7
View File
@@ -5,7 +5,7 @@
* gstack-review-plan --base <ref> [--pass <full passes run>] [--last none|full|fix-check] [--fixed-blocking] [--fixed-below]
* [--blocking-open] [--touched-gate] [--critical-found] [--adversarial-ran] [--gate-tagged <n>] [--gated a,b] [--force a,b|all]
* [--reviewed-numbers <fingerprint>] [--depth-floor standard|full] [--range-base <sha>] [--pass1-depth light|standard|full] [--no-tiers]
* [--targeted-ran <targeted_patch_id>] [--grant <approval id>] [--json]
* [--targeted-ran <targeted_patch_id>] [--grant <approval id>] [--fix-checks <n>] [--pr <n>] [--json]
* gstack-review-plan --fix-check-prompt --fix-list <file> --hunks <file> --tests <file> [--callers <file>]
*
* Reads the diff against the merge base with <ref> (committed, tracked uncommitted
@@ -32,9 +32,12 @@ import { baseNote } from '../lib/gate-diff/git';
import { collectCommit, collectLive } from '../lib/review-collect';
import { excludeLines, reviewExclude, type Exclusion } from '../lib/review-exclude';
import { isUnavailable } from '../lib/ship-policy';
import { reviewPolicy } from '../lib/ship-policy-review';
import { platformReviewAt } from '../lib/review-platform';
import { loadPolicyAt } from '../lib/ship-policy-at';
import { classifyDiff, diffCommand, keepCommand, planReview, reviewDepthLine, reviewPlanLine, scanNumbers, SCOPED_RANGE_LINES, type Depth, type History } from '../lib/review-depth';
import { classList, targetedBriefSha, targetedClasses, targetedPatchId } from '../lib/review-targeted';
import { RISK_CLASSES, type RiskClass } from '../lib/review-risk';
import { renderFixCheckPrompt } from '../scripts/resolvers/fix-check';
const HELP = `gstack-review-plan — the review depth of the next /ship step (docs/review-policy.md#review-depth)
@@ -67,9 +70,19 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do
its ask or scope naming review_loop_grant and the stop head's sha, at or after --range-base and at or
before HEAD; needs --range-base): the stop after a round-2 full pass with a fixed blocking finding or a
gate-class edit plans that fix-check instead; a fix-check never earns another; any other row is refused (exit 3)
--fix-checks <n> fix-checks already run in this invocation (R65's lean loop: the owner's grant adds one only after the first)
--pr <n> the PR whose platform review counts under review.platform on (default: this branch's PR)
--declare-risk <classes> risk classes the lane declares for the diff (comma list of ${RISK_CLASSES.join(', ')}): they join
the detected classes and only raise the depth; RISK_DECLARED: prints both (GST-1129)
--fix-check-prompt print the fix-check reviewer's prompt from --fix-list, --hunks, --tests and --callers files
Depth (R51, risk not size): full on gate class, a risk class (money, auth, secret, security, data) or any DEPTH_ERROR;
Mode (R65, the base policy's review key): review.panel all (default) keeps today's panel on every class. review.panel risk
(opt-in; its one-reader shape has not passed a no-loss replay) runs lean off risk class, and review.panel off everywhere: lean runs one reader: risk class gets the merged reader on review.risk_model
(the native adversarial brief plus the checklists, the Red Team and the targeted brief), non-risk code the single reader at the
lane's reasoning (cross-family when it publishes numbers), docs and tests the core read; one fix-check after fixes, never more
than 2 rounds. review.platform on (any panel): a light diff with no numeric claims whose PR
carries capy-ai[bot]'s review at HEAD plans NEXT: platform and a planned SKIP for every reader.
Depth (R51, risk not size): full on gate class, a risk class (money, auth, secret, security, data, review.risk_paths) or any DEPTH_ERROR;
standard only by --depth-floor or a raise; otherwise light. TARGETED: the money,
gate, auth and secret classes the diff touches; any class dispatches the targeted reader at every depth. NEXT: full, scoped
(a full pass on the range: fix-check, Red Team, native adversarial), fix-check, voices (gate class: Step 11 before any stop,
@@ -102,6 +115,8 @@ function main(): number {
if (!['none', 'full', 'fix-check'].includes(last)) { process.stderr.write('gstack-review-plan: --last must be none, full or fix-check\n'); return EXIT.usage; }
const pass1Depth = (flag('--pass1-depth') ?? 'full') as Depth;
if (!['light', 'standard', 'full'].includes(pass1Depth)) { process.stderr.write('gstack-review-plan: --pass1-depth must be light, standard or full\n'); return EXIT.usage; }
const fixChecks = Number(flag('--fix-checks') ?? (last === 'fix-check' ? '1' : '0'));
if (!Number.isInteger(fixChecks) || fixChecks < 0) { process.stderr.write('gstack-review-plan: --fix-checks needs an integer >= 0\n'); return EXIT.usage; }
const pass = Number(flag('--pass') ?? '0');
if (!Number.isInteger(pass) || pass < 0) { process.stderr.write('gstack-review-plan: --pass needs an integer >= 0\n'); return EXIT.usage; }
const floor = flag('--depth-floor') as Depth | undefined;
@@ -111,6 +126,8 @@ function main(): number {
const rangeBase = flag('--range-base');
if (has('--range-base') && (!rangeBase || rangeBase.startsWith('-'))) { process.stderr.write('gstack-review-plan: --range-base needs the fix range\'s base commit (this pass\'s PASS_HEAD)\n'); return EXIT.usage; }
if (grant && !rangeBase) { process.stderr.write('gstack-review-plan: --grant needs --range-base (round 2\'s PASS_HEAD), which binds the grant to the stop it answers\n'); return EXIT.usage; }
const declared = list('--declare-risk');
if (has('--declare-risk') && (!declared.length || declared.some(c => !(RISK_CLASSES as readonly string[]).includes(c)))) { process.stderr.write(`gstack-review-plan: --declare-risk ${flag('--declare-risk') ?? ''}: each class is one of ${RISK_CLASSES.join(', ')} (a declaration only raises the depth)\n`); return EXIT.usage; }
const root = git(['rev-parse', '--show-toplevel'])?.trim();
if (!root) { process.stderr.write('gstack-review-plan: run it inside a git checkout\n'); return EXIT.fail; }
process.chdir(root);
@@ -152,6 +169,9 @@ function main(): number {
const baseSha = collected.registry.sha ?? git(['rev-parse', '--verify', '-q', `${ref}^{commit}`])?.trim() ?? '';
const policy = baseSha ? loadPolicyAt(root, baseSha, ref) : null;
const declares = !!policy && !isUnavailable(policy) && policy.policy.review_exclude_paths.length > 0;
const review = reviewPolicy(policy && !isUnavailable(policy) ? policy.policy : null);
const riskPaths = review.risk_paths.length ? { riskPaths: review.risk_paths } : {};
const platform = review.platform === 'on' ? platformReviewAt(root, head, { pr: flag('--pr') }) : null;
let tagged: Set<string> | null = null;
let gateTagged: number | null;
if (declares) {
@@ -179,7 +199,7 @@ function main(): number {
else if (git(['diff', '--quiet', 'HEAD', '--']) === null || collected.untracked.length) fixRange = unread('uncommitted or untracked edits: commit the fixes first');
else {
const commit = collectCommit(root, resolved, 'HEAD');
const range = classifyDiff({ ...commit, registry, scope: scopeOf([resolved, '--head', 'HEAD']), gateTagged: gateTagsOf([resolved]), ...(active ? { excluded: active.excluded } : {}) });
const range = classifyDiff({ ...commit, registry, scope: scopeOf([resolved, '--head', 'HEAD']), gateTagged: gateTagsOf([resolved]), ...riskPaths, ...(active ? { excluded: active.excluded } : {}) });
fixRange = range.errors.length ? unread(range.errors.map(e => `${e.code} ${e.input}`).join('; '))
: !commit.paths.length ? unread('the fix range is empty: commit the fix after the range base')
: { base: rangeBase, runtime_lines: range.runtimeLines, ...(range.class === 'gate' ? { touched_gate: true as const } : {}) };
@@ -189,26 +209,29 @@ function main(): number {
const patch = collected.mergeBase ? targetedPatchId(process.cwd(), collected.mergeBase) : 'none';
const brief = targetedBriefSha();
const plan = planReview({
...collected, ...exclusionInput, scope, gateTagged, targeted: { classes: targeted.classes, ran: !['uncommitted', 'none'].includes(patch) && flag('--targeted-ran') === patch }, numbers: scanNumbers(kept(added)), force, gated: list('--gated'), floor, tiers: auditEffort !== 'host',
...collected, ...exclusionInput, ...riskPaths, declared: declared as RiskClass[], shape: { panel: review.panel, readerModel: review.reader_model, platform: review.platform, head, platformReview: platform?.ok ? platform.review : null }, scope, gateTagged, targeted: { classes: targeted.classes, ran: !['uncommitted', 'none'].includes(patch) && flag('--targeted-ran') === patch }, numbers: scanNumbers(kept(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, granted: grant },
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, granted: grant, fixChecks: has('--fix-checks') ? fixChecks : undefined },
});
const gateScan = { tagged: gateTagged, head };
if (has('--json')) {
const { runtimeLines, stepDepth, depthReasons, ...rest } = plan;
process.stdout.write(JSON.stringify({ ...rest, targeted: { ...targeted, brief, targeted_patch_id: patch }, 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, base: collected.base, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly, consumed: collected.consumed, drawings: collected.drawings,
process.stdout.write(JSON.stringify({ ...rest, review_policy: review, platform_review: platform ? (platform.ok ? platform.review : { none: platform.reason }) : null, targeted: { ...targeted, brief, targeted_patch_id: patch }, 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, base: collected.base, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly, consumed: collected.consumed, drawings: collected.drawings,
...(exclusion.state === 'head-only' ? { review_exclude: exclusion } : active ? { review_exclude: { state: 'active', ...active.record, specs: active.specs, keep: active.keep, kept_files: active.kept, content: active.content, references: active.references } } : {}) }) + '\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}`, `REVIEW_BASE: ${ref} ${baseSha.slice(0, 12)}`,
`TARGETED: classes=${classList(targeted.classes)} rules=${targeted.rules} brief=${brief} targeted_patch_id=${patch}${targeted.reasons.length ? ` reasons=${targeted.reasons.join('; ')}` : ''}`];
out.push(...excludeLines(exclusion, base));
out.push(`REVIEW_MODE: ${plan.mode} panel=${review.panel} reader_model=${review.reader_model} platform=${review.platform}`);
if (declared.length) out.push(`RISK_DECLARED: classes=${plan.declared.join(',')} detected=${plan.detected.join(',') || 'none'}`);
if (platform) out.push(platform.ok ? `PLATFORM_REVIEW: ${platform.review.id} reviewer=${platform.review.reviewer} head=${platform.review.head}` : `PLATFORM_REVIEW: none (${platform.reason})`);
if (fixRange) out.push(`FIX_RANGE: ${fixRange.runtime_lines === null ? `unread (${fixRange.error})` : `runtime_lines=${fixRange.runtime_lines}`} base=${fixRange.base}`);
out.push(reviewPlanLine(plan, pass + (plan.next.step === 'full' || plan.next.step === 'scoped' ? 1 : 0)), `NEXT: ${plan.next.step} (${plan.next.reason})`);
for (const e of plan.errors) out.push(`DEPTH_ERROR: ${e.code} input=${e.input} detail=${e.detail}; fix: ${e.repair}${e.result ? ` (${e.result})` : ''}`);
for (const d of plan.dispatches) {
const key = d.name.split(' ')[0];
out.push(`DISPATCH: ${d.role} ${d.name} effort=${d.effort}${d.checklists ? ` checklists=${d.checklists.join(',')}` : ''}${d.reason ? ` reason=${d.reason}` : ''}`, `DIFF_CMD: ${key} ${diffCommand(d)}`);
out.push(`DISPATCH: ${d.role} ${d.name} effort=${d.effort}${d.model ? ` model=${d.model}` : ''}${d.checklists ? ` checklists=${d.checklists.join(',')}` : ''}${d.classes ? ` classes=${d.classes.join(',')}` : ''}${d.reason ? ` reason=${d.reason}` : ''}`, `DIFF_CMD: ${key} ${diffCommand(d)}`);
if (!d.exclude.length) out.push(`DIFF_EXCLUDE: ${key} none`);
const keepCmd = keepCommand(d);
if (keepCmd) out.push(`DIFF_KEEP: ${key} ${keepCmd}`);
+17 -3
View File
@@ -50,7 +50,8 @@ import {
} from '../lib/ship-receipt';
import { checkDocsCaps, docsCapLine, docsExceptionLine, parseReceiptDocs } from '../lib/ship-receipt-docs';
import { comparePath, contextPacket, PACKET_SECTION_CAP, readScopedReview, touchedPaths } from '../lib/ship-patch';
import { deeper, depthBelow, exceptionProblems, parseReviewLoopException, parseReviewTargeted, receiptExclude, receiptReviewAtHead, receiptTargetedAtHead, reviewLoopExceptionLine, targetedClassesSince } from '../lib/ship-receipt-depth';
import { deeper, depthBelow, exceptionProblems, parseReviewLoopException, parseReviewTargeted, platformReceiptDepth, receiptExclude, receiptReviewAtHead, receiptTargetedAtHead, reviewLoopExceptionLine, targetedClassesSince } from '../lib/ship-receipt-depth';
import { RISK_CLASSES, type RiskClass } from '../lib/review-risk';
import { checkReceiptProse, proseLine } from '../lib/ship-receipt-prose';
import { resolveStateRoot } from '../lib/state-root';
import { readPause, receiptBaseRed } from '../lib/base-red';
@@ -102,6 +103,11 @@ write flags: --pr --head --base --version --gated-tree --predecessor --gate <jso
RECEIPT_INVALID field=review_exclude and nothing is written; recorded as review_exclude. Carry and the queue compare that digest.
The check binds the digest to the base policy, not to a review: write does not read the review ledger, so the expected=
digest a refusal prints passes too. After a refusal, rerun the review and pass the digest its verify prints.
--declare-risk <classes> (the same list gstack-review-plan --declare-risk took): joins review_depth's risk and only raises
its depth; review_depth.declared and .detected record both lists (GST-1129). An unknown class is RECEIPT_INVALID.
--platform-review <id> (NEXT: platform's PLATFORM_REVIEW: id): records review_depth platform with capy-ai[bot]'s review <id> at HEAD;
refused (RECEIPT_INVALID) unless the base policy has review.platform on, HEAD needs light review of a docs or tests diff with
no numeric, results or prereg claims, and that review is on the PR at HEAD. review_depth.mode records lean, panel or platform.
--review-targeted <json|@file> ({classes, rules, brief_sha, status, findings, raised_by, head}: gstack-review-dispatch verify
--targeted-out); recorded as review_targeted bound to the receipt's product_patch_id, and refused (RECEIPT_INVALID) when the
commit the reader read (head) is not an ancestor or a change since it carries a targeted class (gate scan included). write and carry
@@ -232,8 +238,16 @@ function cmdWrite(rest: string[], json: boolean): number {
const line = reviewLoopExceptionLine(loopException.exception);
if (bodyFile && !bodyText.split(/\r?\n/).some(l => l.trim() === line)) return refuse(`${bodyFile} does not carry the exception line verbatim: ${JSON.stringify(line)}`);
}
const review = receiptReviewAtHead(repoRoot, base, path.dirname(Bun.fileURLToPath(import.meta.url)), headSha ?? head);
const reviewDepth = review.depth;
const declared = (flag(rest, '--declare-risk') ?? '').split(',').map(s => s.trim()).filter(Boolean);
if (rest.includes('--declare-risk') && (!declared.length || declared.some(c => !(RISK_CLASSES as readonly string[]).includes(c)))) { process.stderr.write(resultLine('gstack-ship-receipt write', 'RECEIPT_INVALID', `--declare-risk ${flag(rest, '--declare-risk') ?? ''}: each class is one of ${RISK_CLASSES.join(', ')}; nothing written`) + '\n'); return EXIT.fail; }
const review = receiptReviewAtHead(repoRoot, base, path.dirname(Bun.fileURLToPath(import.meta.url)), headSha ?? head, declared as RiskClass[]);
let reviewDepth = review.depth;
const platformId = flag(rest, '--platform-review');
if (platformId !== undefined) {
const p = platformReceiptDepth(repoRoot, base, reviewDepth, headSha ?? head, platformId, flag(rest, '--pr'));
if (!p.ok) { process.stderr.write(resultLine('gstack-ship-receipt write', 'RECEIPT_INVALID', `${p.message}; nothing written`) + '\n'); return EXIT.fail; }
reviewDepth = p.depth;
}
if (review.exclude.state === 'refused') { for (const l of review.exclude.lines) process.stderr.write(`${l}; nothing written\n`); return EXIT.refused; }
const excludeNow = review.exclude.state === 'active' ? review.exclude.record : null;
const excludeGiven = flag(rest, '--review-exclude') ?? 'none';
+48 -3
View File
@@ -182,6 +182,46 @@ reads everything), one `SKIP:` per reviewer left out, and `--json` with the same
base as `base`).
A plan that fails closed to full exits 0; a diff git cannot list exits 1.
### Who reads (R65, the base policy's `review` key)
The depth says how much review a diff needs; the base branch's `.gstack/ship-policy.json`
`review` key says who does it (`review.panel`, default `all`: today's panel on every class). `gstack-review-plan`
prints `REVIEW_MODE: <lean|panel|platform> panel=<p> reader_model=<m> platform=<on|off>`
and, under `review.platform on`, `PLATFORM_REVIEW: <id> reviewer=capy-ai[bot] head=<sha>`
or `PLATFORM_REVIEW: none (<reason>)`.
Under the opt-in `review.panel risk` (and `review.platform on` under any panel):
| Diff | Pass 1 | After fixes (never more than 2 rounds) |
|---|---|---|
| Risk class: gate class, an R51 class (money, auth, secret, security, data), `eval` (scorer, judge and grader paths) or `policy` (`review.risk_paths`) | The full-depth plan as printed: the batched specialist, Red Team, native adversarial, outside voice and the targeted reader | A fixed blocker re-runs the printed plan on the fixed head as round 2; below-floor fixes get one fix-check |
| Non-risk code | One native adversarial reader (`DISPATCH: adversarial-native single`) at the lane's reasoning, on `review.reader_model` (`lane` by default), carrying the code checklists and the Red Team checklist; it also carries the targeted brief when the diff has a targeted class | A fixed blocker or a gate-class edit gets one fix-check on the touched hunks; below-floor fixes ship as notes |
| Docs and tests | The core read; the outside voice only when the diff publishes numbers | One fix-check after a fixed blocker |
| Docs or tests with no numeric, results or prereg claims, under `review.platform on`, with capy-ai[bot]'s review on the PR at HEAD | No reader: `NEXT: platform` and a planned `SKIP:` for every reader, the core read included | none |
`review.panel all`, the default, keeps today's panel and loop on every class. `risk` is
opt-in because its one-reader shape has not passed its no-loss replay: build C's replay
scored it FIX-METHOD (its readers saw the diff without the repository). `review.panel off` gives
risk class one merged reader too (`DISPATCH: adversarial-native merged`: the native
adversarial brief, the gate-class checklists, the Red Team checklist and the targeted
brief, whose ledger record is the receipt's `review_targeted`). The risk classes come
from paths and changed lines, never from a recorded `review_depth` (FOUN-64). Two
content rules: data and config files (JSON, YAML, TOML, CSV and the like) and analysis
scripts under `docs/`, `results/`, `benchmarks/`, `analysis/` or `notebooks/` are
judged by their path, so a `lease_usd` value or an `os.path.join(sys.argv[1], …)` in a
stats script names no class (HARN-1, harness-evals#231), and binary data such as
`*.sqlite` is data wherever it sits; a docs line that can print a credential (an
`Authorization:` header, a bearer or key-shaped value, an environment key set to a value,
a get or show command for auth config such as `mcp get`) is security (gbrain#8417); code
that reserves, binds, leases or settles spend against a ledger, budget or cap (`bindCheck(ledger, …)`,
`reserveSpend`, `bindCap`) is money (GST-1129, gstack-evals#170). When the matchers miss a class the
lane knows the diff has, `gstack-review-plan --declare-risk <classes>` adds it: a declaration joins the
detected classes, only raises the depth, and prints `RISK_DECLARED: classes=<declared> detected=<found>`.
The outside voice reads its diff from a file the brief names with its sha256; past
2,000,000 bytes it reads a path-scoped slice whose brief lists the files left out.
`--fix-checks <n>` tells the lean loop how many fix-checks ran, so the owner's grant
adds one fix-check after the first and never a second.
**Classes** are unchanged. A mixed diff takes its most restrictive class: **docs**
(markdown no agent or gate reads), **tests** (test files and fixtures), **code**
(everything else, including every `SKILL.md` and template, markdown in a skill
@@ -483,9 +523,14 @@ specialist stats they record). The summary line appends `fix_checks=`,
loop prints the old line.
Beside the counters, `write` records `review_depth` (the depth, runtime lines,
rules hash, base and registry commits, the gate scan bound to the head and the risk classes) and
the queue refuses a receipt below the depth its head needs
([ship-policy.md#ship-receipt](ship-policy.md#ship-receipt)). A stop the owner
rules hash, base and registry commits, the gate scan bound to the head, the risk classes and
`mode`: lean, panel or platform; with `--declare-risk`, `declared` and `detected` beside `risk`, so a
class the lane added shows apart from what the matchers found) and the queue refuses a receipt below the depth its head needs
([ship-policy.md#ship-receipt](ship-policy.md#ship-receipt)). After `NEXT: platform`,
`write --platform-review <id>` records `review_depth: platform` with `platform_review`
(capy-ai[bot]'s review id, reviewer and head); write and the queue accept it only under
`review.platform on`, on a docs or tests diff with no numeric, results or prereg claims,
with the review at that exact head, so a new head needs `/ship` again. A stop the owner
accepted after a fix-check edit goes in `review_loop_exception`, never in prose
under `REVIEW_LOOP:`. Step 16 takes the same record:
`gstack-ship-release finish --review-loop-exception <json|@file>` routes the
+1 -1
View File
@@ -101,7 +101,7 @@ const CERTIFIED_OUTSIDE = { model_id: 'openai/gpt-6.1-sol', reasoning_mode: 'med
const voiceFile = input.out.replace(/\/+$/, '') + '/voice-models.json';
let voices = { laneModel: null, readers: {} };
let laneFamily = family(input.laneModel);
const modelOf = (r) => r.model || (r.reader in voices.readers ? voices.readers[r.reader] || undefined
const modelOf = (r) => r.model || (r.cross_family ? (input.models || {})['outside-voice'] || (laneFamily === 'anthropic' ? CERTIFIED_OUTSIDE : undefined) : undefined) || (r.reader in voices.readers ? voices.readers[r.reader] || undefined
: (input.models || {})[r.role === 'specialist-batched' ? 'specialist' : r.role]
|| (r.role === 'outside-voice' && laneFamily === 'anthropic' ? CERTIFIED_OUTSIDE : undefined)
|| (REASONING[r.effort] && input.laneModel ? { model_id: input.laneModel, reasoning_mode: REASONING[r.effort] } : undefined));
+2 -2
View File
@@ -120,7 +120,7 @@ export interface ShipReceipt {
head_tree?: string; product_patch_id?: string;
/** The gstack that wrote the receipt, and the review depth the reviewed head needs, recomputed at write (lib/ship-receipt-depth.ts receiptDepthAtHead). */
gstack_version?: string;
review_depth?: { depth: 'light' | 'standard' | 'full'; class: 'docs' | 'tests' | 'code' | 'gate'; runtime_lines: number; rules: string; base: string; registry: string | null; gate_scan: { tagged: number | null; head: string }; reasons: string[]; risk?: string[] };
review_depth?: { depth: 'platform' | 'light' | 'standard' | 'full'; class: 'docs' | 'tests' | 'code' | 'gate'; runtime_lines: number; rules: string; base: string; registry: string | null; gate_scan: { tagged: number | null; head: string }; reasons: string[]; risk?: string[]; mode?: 'lean' | 'panel' | 'platform'; platform_review?: { id: string; head: string; reviewer: string }; declared?: string[]; detected?: string[] };
/** The owner's acceptance of a stop after a fix-check edit under frozen scope (#3559); the PR body repeats it word for word. */
review_loop_exception?: { accepted_by: string; date: string; via: string; reason: string; fix_check: { head: string; findings: number; files?: string[] } };
/** The targeted pass on money, gate, auth and secret code (lib/review-targeted.ts), bound to the receipt's product_patch_id; `done` only with its output collected. */
@@ -258,7 +258,7 @@ export const ARTIFACT_SCHEMAS = {
head_tree: str, product_patch_id: str, gstack_version: str,
base_red: { type: 'object', required: ['cases', 'files', 'base', 'lane', 'expires_at'], properties: { cases: strList, files: strList, base: str, lane: str, expires_at: str } },
flaky_exception: { type: 'array', items: { type: 'object', required: ['lane', 'runs', 'cases', 'grant', 'by'], properties: { lane: str, runs: strList, cases: strList, grant: str, by: str } } },
review_depth: { type: 'object', required: ['depth', 'class', 'runtime_lines', 'rules', 'base', 'gate_scan'], properties: { depth: { enum: ['light', 'standard', 'full'] }, class: { enum: ['docs', 'tests', 'code', 'gate'] }, runtime_lines: { type: 'integer', minimum: 0 }, rules: str, base: str, registry: nullableStr, gate_scan: { type: 'object', required: ['tagged', 'head'], properties: { tagged: { type: ['integer', 'null'], minimum: 0 }, head: str } }, reasons: strList, risk: { type: 'array', items: { enum: ['money', 'auth', 'secret', 'security', 'data'] } } } },
review_depth: { type: 'object', required: ['depth', 'class', 'runtime_lines', 'rules', 'base', 'gate_scan'], properties: { depth: { enum: ['platform', 'light', 'standard', 'full'] }, mode: { enum: ['lean', 'panel', 'platform'] }, platform_review: { type: 'object', required: ['id', 'head', 'reviewer'], properties: { id: str, head: str, reviewer: str } }, class: { enum: ['docs', 'tests', 'code', 'gate'] }, runtime_lines: { type: 'integer', minimum: 0 }, rules: str, base: str, registry: nullableStr, gate_scan: { type: 'object', required: ['tagged', 'head'], properties: { tagged: { type: ['integer', 'null'], minimum: 0 }, head: str } }, reasons: strList, risk: { type: 'array', items: { enum: ['money', 'auth', 'secret', 'security', 'data', 'eval', 'policy'] } }, declared: { type: 'array', items: { enum: ['money', 'auth', 'secret', 'security', 'data', 'eval', 'policy'] } }, detected: { type: 'array', items: { enum: ['money', 'auth', 'secret', 'security', 'data', 'eval', 'policy'] } } } },
review_targeted: { type: 'object', required: ['classes', 'rules', 'brief_sha', 'product_patch_id', 'status', 'findings', 'raised_by'], properties: { classes: { type: 'array', items: { enum: ['money', 'gate', 'auth', 'secret'] } }, rules: str, brief_sha: str, product_patch_id: str, status: { enum: ['done', 'failed', 'not-run'] }, findings: { type: 'integer', minimum: 0 }, raised_by: str } },
review_exclude: { type: 'object', required: ['source', 'patterns', 'applied', 'excluded', 'excluded_bytes', 'kept', 'gate_tagged', 'patterns_sha256', 'list_sha256', 'digest'], properties: { source: str, patterns: strList, applied: strList, excluded: { type: 'integer', minimum: 0 }, excluded_bytes: { type: 'integer', minimum: 0 }, kept: { type: 'integer', minimum: 0 }, gate_tagged: strList, patterns_sha256: str, list_sha256: str, digest: str } },
review_loop_exception: { type: 'object', required: ['accepted_by', 'date', 'via', 'reason', 'fix_check'], properties: { accepted_by: str, date: str, via: str, reason: str, fix_check: { type: 'object', required: ['head', 'findings'], properties: { head: str, findings: { type: 'integer', minimum: 0 }, files: strList } } } },
+32 -3
View File
@@ -37,6 +37,26 @@ export function promptHash(text: string): string {
export class BriefError extends Error {}
/**
* The outside voice's diff within a byte cap (FOUN-32 §3): whole per-file sections in diff order while they fit, so a reader past
* the cap still starts on a path-scoped slice and the brief names what it left out, instead of failing to start on a 3-4 MB brief.
*/
export function sliceDiff(text: string, cap: number): { text: string; sliced: boolean; files: number; kept: number; bytes: number; total: number; left: string[] } {
const total = Buffer.byteLength(text);
const starts = [...text.matchAll(/^diff --git a\/(\S+)/gm)];
if (total <= cap || !starts.length) return { text, sliced: false, files: starts.length, kept: starts.length, bytes: total, total, left: [] };
let out = '', bytes = 0;
const left: string[] = [];
starts.forEach((m, k) => {
const section = text.slice(m.index!, k + 1 < starts.length ? starts[k + 1]!.index! : text.length);
const n = Buffer.byteLength(section);
if (bytes + n <= cap) { out += section; bytes += n; } else left.push(m[1]!);
});
return { text: out, sliced: true, files: starts.length, kept: starts.length - left.length, bytes, total, left };
}
/** The brief's line for a sliced diff: what was read and what was left out (at most 50 paths named). */
export const sliceNote = (s: ReturnType<typeof sliceDiff>, cap: number) => `This is a path-scoped slice: ${s.kept} of ${s.files} files, ${s.bytes} of ${s.total} bytes (the cap is ${cap} bytes). Not in the slice, so not reviewed here: ${s.left.slice(0, 50).join(', ')}${s.left.length > 50 ? ` and ${s.left.length - 50} more` : ''}.`;
/** The text strictly between start and end, end searched after start; both anchors are required. */
function between(text: string, start: string, end: string, what: string): string {
const i = text.indexOf(start);
@@ -134,12 +154,21 @@ function renderRole(role: BriefRole, slots: Slots, t: SectionTexts): string {
['{red-team checklist path}', need(slots, 'checklist')], ['<diff-base>', diffBase]], role);
return fixtures(`${t.framing}\n\n${t.fixtures}\n\n${body}`);
}
case 'adversarial-native':
return fixtures(fill(t.native, [['DIFF_BASE=$(git merge-base origin/<base> HEAD)', `DIFF_BASE=${diffBase}`]], role));
case 'adversarial-native': {
const native = fixtures(fill(t.native, [['DIFF_BASE=$(git merge-base origin/<base> HEAD)', `DIFF_BASE=${diffBase}`]], role));
const lists = (slots.checklists ?? '').split(',').map(s => s.trim()).filter(Boolean);
const merged = lists.length ? `${native}\n\nIn the same read, apply each checklist at ${lists.join(', ')} to the diff, and tag each finding's "specialist" with the name of the checklist it came from (adversarial for your own).` : native;
if (!slots.classes) return merged;
const classes = parseClassList(slots.classes);
if (!classes?.length) throw new BriefError(`slot classes must list money, gate, auth or secret, got ${JSON.stringify(slots.classes)}`);
try { return `${targetedBrief(need(slots, 'brief'), classes)}\n\n${merged}\n\nApply the targeted brief above (${need(slots, 'checklist')}: ${classes.join(', ')}) in the same read; your reply is this diff's targeted pass.`; } catch (e) { throw e instanceof BriefError ? e : new BriefError((e as Error).message); }
}
case 'outside-voice': {
const filled = fill(t.outside, [['DIFF_BASE=$(git merge-base origin/<base> HEAD)', `DIFF_BASE=${diffBase}`]], role);
const body = slots.policy_exclude ? filled.split('git diff "$DIFF_BASE".').join(`git diff "$DIFF_BASE" -- . ${slots.policy_exclude}${slots.keep ? ` and git diff "$DIFF_BASE" -- ${slots.keep}` : ''}.`) : filled;
return [body, '', ...(slots.disclosure ? [slots.disclosure, ''] : []), `## Branch diff (git diff ${diffBase})`, FENCE + 'diff', (slots.diff ?? '').trim(), FENCE].join('\n');
const head = [body, '', ...(slots.disclosure ? [slots.disclosure, ''] : []), `## Branch diff (git diff ${diffBase})`];
if (slots.diff_file) return [...head, `The supplied branch diff is the file ${slots.diff_file} (${need(slots, 'diff_bytes')} bytes, sha256 ${need(slots, 'diff_sha256')}). Read it from disk; it is ${slots.diff_slice ? 'the part of the diff under review' : 'the whole diff under review'}.`, ...(slots.diff_slice ? [slots.diff_slice] : [])].join('\n');
return [...head, FENCE + 'diff', (slots.diff ?? '').trim(), FENCE].join('\n');
}
case 'targeted': {
const classes = parseClassList(need(slots, 'classes'));
+119 -18
View File
@@ -13,14 +13,14 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import type { ResultCodeName } from './result-codes';
import { matchesAny } from './ship-policy';
import { decodeCharRefs, matchRisk, RISK_RULES, SVG_PATH, svgActive, type RiskClass } from './review-risk';
import { decodeCharRefs, DOC_SECRET_LINE, matchRisk, RISK_CLASSES, RISK_RULES, SVG_PATH, svgActive, type RiskClass } from './review-risk';
export type DiffClass = 'docs' | 'tests' | 'code' | 'gate';
export type PathClass = 'docs' | 'tests' | 'code';
export type Depth = 'light' | 'standard' | 'full';
export type Effort = 'light' | 'medium' | 'full' | 'host';
export type Role = 'specialist' | 'red-team' | 'adversarial-native' | 'outside-voice' | 'fix-check' | 'targeted';
export type Step = 'full' | 'scoped' | 'fix-check' | 'voices' | 'done' | 'stop';
export type Step = 'full' | 'scoped' | 'fix-check' | 'voices' | 'platform' | 'done' | 'stop';
export const SPECIALISTS = ['testing', 'maintainability', 'security', 'performance', 'data-migration', 'api-contract', 'design', 'simplification', 'state-machines'] as const;
export type Specialist = (typeof SPECIALISTS)[number];
@@ -63,6 +63,8 @@ export const isGatePath = (p: string) => GATE_PATH.some(r => r.test(p));
export const isAgentProse = (p: string) => AGENT_PROSE.some(r => r.test(p));
export const isDataConfig = (p: string) => DATA_CONFIG.some(r => r.test(p));
/** Binary data, media and run logs (HARN-1: committed sqlite ledgers, trial logs): data by path wherever they sit, never code, consumed or not. */
export const BINARY_DATA_PATH = /\.(sqlite3?|db|duckdb|parquet|arrow|feather|avro|orc|npy|npz|h5|hdf5|pdf|mp3|mp4|mov|wav|webm|log|out)$|(^|\/)logs\/[^/.]+$/i;
/** Data files: JSON, CSV, TSV and raster images. YAML and TOML are never data here: deploy configs and API specs stay code. An SVG is data only as a drawing (svgDrawing); executable SVG stays 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. */
@@ -164,7 +166,7 @@ export function svgDrawing(text: string): boolean {
/** 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({
RISK_RULES, SMALL_DIFF_LINES, RED_TEAM_LINES, LARGE_DIFF_LINES, FULL_PASS_CAP, LIGHT_FIXED, BATCH_SCOPED, SCOPED_RANGE_LINES, ROLE_EFFORT, SVG_ELEMENTS: [...SVG_ELEMENTS],
patterns: Object.entries({ GATE_PATH, AGENT_PROSE, TEST_PATH, RELEASE_METADATA, DOC_PATH: [DOC_PATH], DATA_PATH: [DATA_PATH], DATA_CONFIG, SVG_MARKUP: [SVG_MARKUP], SVG_REFUSED, SVG_HREF: [SVG_HREF], SVG_STYLE_OPEN: [SVG_STYLE_OPEN], SVG_STYLE_CLOSE: [SVG_STYLE_CLOSE], SVG_STYLE_TAG: [SVG_STYLE_TAG], SVG_TAG_NAME: [SVG_TAG_NAME], SVG_ATTR: [SVG_ATTR], SVG_CSS_REF: [SVG_CSS_REF], SVG_CSS_REFUSED: [SVG_CSS_REFUSED] }).map(([k, v]) => [k, v.map(r => `/${r.source}/${r.flags}`)]),
patterns: Object.entries({ BINARY_DATA_PATH: [BINARY_DATA_PATH], GATE_PATH, AGENT_PROSE, TEST_PATH, RELEASE_METADATA, DOC_PATH: [DOC_PATH], DATA_PATH: [DATA_PATH], DATA_CONFIG, SVG_MARKUP: [SVG_MARKUP], SVG_REFUSED, SVG_HREF: [SVG_HREF], SVG_STYLE_OPEN: [SVG_STYLE_OPEN], SVG_STYLE_CLOSE: [SVG_STYLE_CLOSE], SVG_STYLE_TAG: [SVG_STYLE_TAG], SVG_TAG_NAME: [SVG_TAG_NAME], SVG_ATTR: [SVG_ATTR], SVG_CSS_REF: [SVG_CSS_REF], SVG_CSS_REFUSED: [SVG_CSS_REFUSED] }).map(([k, v]) => [k, v.map(r => `/${r.source}/${r.flags}`)]),
})).digest('hex').slice(0, 16);
/**
@@ -240,13 +242,19 @@ export interface ClassInput {
removed?: Record<string, readonly string[]>;
/** Paths the base policy's review_exclude_paths leave out of every reader's diff (lib/review-exclude.ts): classified as docs. */
excluded?: ReadonlySet<string>;
/** The base policy's review.risk_paths: a code path they match is risk class `policy`; data and binary files under them never are. */
riskPaths?: readonly string[];
/** Risk classes the lane declares for the diff (GST-1129, --declare-risk): they join the detected ones and can only raise the depth. */
declared?: readonly RiskClass[];
}
export interface ClassResult { class: DiffClass; reasons: string[]; errors: DepthError[]; runtimeLines: number | null; risk: RiskClass[] }
/** risk is detected plus declared; `detected` is what the matchers found, `declared` what the lane added. */
export interface ClassResult { class: DiffClass; reasons: string[]; errors: DepthError[]; runtimeLines: number | null; risk: RiskClass[]; detected: RiskClass[]; declared: RiskClass[] }
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 unconsumedData = (i: ClassInput, p: string) => Array.isArray(i.consumed) && !i.consumed.includes(p) && (dataCandidate(p, i.root) || (!!i.drawings?.includes(p) && svgCandidate(p, i.root)));
const pathClass = (i: ClassInput, p: string): PathClass => (i.excluded?.has(p) || (i.stampOnly ?? []).includes(p) || unconsumedData(i, p) ? 'docs' : classifyPath(p, i.root));
const binaryData = (i: ClassInput, p: string) => BINARY_DATA_PATH.test(p) && classifyPath(p, i.root) === 'code' && ![...AGENT_PROSE, ...GATE_PATH, ...DATA_CONFIG].some(r => r.test(p));
const pathClass = (i: ClassInput, p: string): PathClass => (i.excluded?.has(p) || (i.stampOnly ?? []).includes(p) || unconsumedData(i, p) || binaryData(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 {
@@ -277,17 +285,23 @@ export function classifyDiff(i: ClassInput): ClassResult {
for (const p of i.paths) if (GATE_PATH.some(r => r.test(p))) { raise('gate', `gate path: ${p}`); break; }
if (i.registry?.error) errors.push(i.registry.error);
}
const riskHits = CLASS_RANK[cls] >= CLASS_RANK.code ? matchRisk({ codePaths: i.paths.filter(p => pathClass(i, p) === 'code'), added: i.added, removed: i.removed, scopeAuth: s.SCOPE_AUTH === 'true' }) : new Map<RiskClass, string>();
const risk = [...riskHits.keys()];
const riskHits = CLASS_RANK[cls] >= CLASS_RANK.code ? matchRisk({ codePaths: i.paths.filter(p => pathClass(i, p) === 'code'), added: i.added, removed: i.removed, scopeAuth: s.SCOPE_AUTH === 'true', ...(i.riskPaths?.length ? { isRiskPath: (p: string) => matchesAny(i.riskPaths!, p) && !DATA_PATH.test(p) } : {}) }) : new Map<RiskClass, string>();
const docSecret = i.paths.find(p => pathClass(i, p) === 'docs' && DOC_PATH.test(p) && (i.added?.[p] ?? []).some(l => DOC_SECRET_LINE.some(r => r.test(l))));
if (docSecret && !riskHits.has('security')) riskHits.set('security', `docs that can print a credential: ${docSecret}`);
const byRank = (a: RiskClass, b: RiskClass) => RISK_CLASSES.indexOf(a) - RISK_CLASSES.indexOf(b);
const detected = [...riskHits.keys()].sort(byRank);
const declared = [...new Set(i.declared ?? [])].sort(byRank);
for (const d of declared) if (!riskHits.has(d)) riskHits.set(d, `declared: ${d}`);
const risk = [...riskHits.keys()].sort(byRank);
reasons.push(...[...riskHits.values()].map(r => `risk: ${r}`));
if (!i.perPath) return { class: cls, reasons, errors, runtimeLines: null, risk };
if (!i.perPath) return { class: cls, reasons, errors, runtimeLines: null, risk, detected, declared };
let runtimeLines = 0;
for (const e of i.perPath) {
if (pathClass(i, e.path) !== 'code' || isOutput(i, e.path)) continue;
if (e.binary) errors.push({ code: 'binary_code_path', detail: 'git reports a binary change, so its lines cannot be counted', input: e.path, repair: "register it as an output of an entry with a command in the base branch's .gstack/generated.json, or accept the full review" });
else runtimeLines += e.lines;
}
return { class: cls, reasons, errors, runtimeLines, risk };
return { class: cls, reasons, errors, runtimeLines, risk, detected, declared };
}
/** Risk, not size (R51): any classifier error, gate class or risk class is full; everything else is light. A floor only raises; a fixed blocker or a CRITICAL raises light to standard for the next step (planReview). */
@@ -296,14 +310,14 @@ export function depthFor(o: { cls: DiffClass; runtimeLines: number; errors: read
return o.floor && DEPTH_RANK[o.floor] > DEPTH_RANK[d] ? o.floor : d;
}
export interface DepthResult { depth: Depth; class: DiffClass; runtimeLines: number; reasons: string[]; errors: DepthError[]; codes: ResultCodeName[]; rules: string; risk: RiskClass[] }
export interface DepthResult { depth: Depth; class: DiffClass; runtimeLines: number; reasons: string[]; errors: DepthError[]; codes: ResultCodeName[]; rules: string; risk: RiskClass[]; detected: RiskClass[]; declared: RiskClass[] }
function decideDepth(c: ClassResult, runtimeLines: number, floor?: Depth): DepthResult {
const unfloored = depthFor({ cls: c.class, runtimeLines, errors: c.errors, risk: c.risk });
const reasons = [...c.errors.map(e => `error:${e.code}`), ...(c.class === 'gate' ? ['gate-class'] : []), ...c.risk.map(r => `risk:${r}`), `runtime=${runtimeLines}`];
if (floor && DEPTH_RANK[floor] > DEPTH_RANK[unfloored]) reasons.push(`floor:${floor}`);
const codes = [...new Set(c.errors.flatMap(e => (e.result ? [e.result] : [])))];
return { depth: depthFor({ cls: c.class, runtimeLines, errors: c.errors, risk: c.risk, floor }), class: c.class, runtimeLines, reasons, errors: c.errors, codes, rules: DEPTH_RULES, risk: c.risk };
return { depth: depthFor({ cls: c.class, runtimeLines, errors: c.errors, risk: c.risk, floor }), class: c.class, runtimeLines, reasons, errors: c.errors, codes, rules: DEPTH_RULES, risk: c.risk, detected: c.detected, declared: c.declared };
}
/** The depth of a collected range (lib/review-collect.ts) plus a recorded gate-scan result: what the queue recomputes at a head. */
@@ -362,6 +376,10 @@ export interface History {
adversarialRan?: boolean;
/** The approval id of the owner's recorded grant of one scoped fix-check after round 2 (review_loop_exception). */
granted?: string;
/** R65's lean table (planReview sets it from the base policy's review.panel): one reviewer, then at most one fix-check. */
lean?: boolean;
/** Fix-checks already run in this invocation (lean: the grant adds one only after the first). */
fixChecks?: number;
}
export interface NextStep { step: Step; reason: string }
@@ -378,13 +396,31 @@ export interface NextStep { step: Step; reason: string }
* one fix-check; a fix-check never earns another, so the grant cannot repeat.
*/
export function nextStep(h: History, depth?: Depth): NextStep {
const n = loopStep(h, depth);
const n = h.lean ? leanStep(h, depth) : loopStep(h, depth);
if (n.step !== 'stop') return n;
if (h.gate && !h.adversarialRan) return { step: 'voices', reason: `gate class: Step 11's voices (adversarial, outside, targeted) run before the owner's exit, then ask again with --adversarial-ran (${n.reason})` };
if (h.granted && !h.blockingOpen && h.last === 'full' && h.fullPasses === FULL_PASS_CAP) return { step: 'fix-check', reason: `the owner's grant ${h.granted} adds one scoped fix-check after round ${FULL_PASS_CAP} (review_loop_exception), never a fourth round` };
if (h.gate && !h.adversarialRan) return { step: 'voices', reason: `gate class: Step 11's ${h.lean ? 'one reader' : 'voices (adversarial, outside, targeted)'} run${h.lean ? 's' : ''} before the owner's exit, then ask again with --adversarial-ran (${n.reason})` };
if (!h.lean && h.granted && !h.blockingOpen && h.last === 'full' && h.fullPasses === FULL_PASS_CAP) return { step: 'fix-check', reason: `the owner's grant ${h.granted} adds one scoped fix-check after round ${FULL_PASS_CAP} (review_loop_exception), never a fourth round` };
return n;
}
/**
* R65's lean table (GSTA-258): pass 1 is the one reviewer; any fix on a risk-class diff, and a fixed blocker or a gate-class
* edit on any diff, earns one scoped fix-check (round 2, the cap); below-floor fixes on a light diff ship as notes. After the
* fix-check a blocker or gate-class edit stops for the owner, whose recorded grant adds one more fix-check, never two.
*/
function leanStep(h: History, depth?: Depth): NextStep {
if (h.last === 'none') return { step: 'full', reason: 'pass 1: one reviewer reads the whole diff (R65)' };
if (h.last === 'fix-check') {
if (!h.blockingOpen && !h.fixedBlocking && !h.touchedGate) return { step: 'done', reason: h.fixedBelow ? 'round cap 2 reached: the below-floor fixes ship as notes in the PR body and receipt' : 'the fix-check found nothing blocking' };
if (h.granted && !h.blockingOpen && h.fixChecks === 1) return { step: 'fix-check', reason: `the owner's grant ${h.granted} adds one scoped fix-check after round ${FULL_PASS_CAP} (review_loop_exception), never a second` };
return { step: 'stop', reason: `the fix-check ${h.blockingOpen || h.fixedBlocking ? 'found a blocking finding' : 'edited a gate-class path'}: name it for the owner, whose grant may add one scoped fix-check, never a third round` };
}
if (h.blockingOpen) return { step: 'stop', reason: 'a blocking finding is still open in fix scope: name it for the owner' };
if (!h.fixedBlocking && !h.fixedBelow && !h.touchedGate) return { step: 'done', reason: 'the step applied no edits' };
if (depth === 'light' && !h.fixedBlocking && !h.touchedGate) return { step: 'done', reason: 'light depth: the below-floor fixes ship as notes in the PR body and receipt without a fix-check (R65)' };
return { step: 'fix-check', reason: `${h.fixedBlocking ? 'a blocking finding was fixed' : h.touchedGate ? 'a fix touched a gate-class path' : 'below-floor fixes on a risk-class diff'}: one scoped fix-check reads the touched hunks (round 2, the cap)` };
}
function loopStep(h: History, depth?: Depth): NextStep {
if (h.last === 'none') return { step: 'full', reason: 'pass 1 reviews the whole diff' };
const fixed = !!(h.fixedBlocking || h.fixedBelow);
@@ -432,14 +468,20 @@ export interface PlanInput extends ClassInput {
targeted?: { classes: readonly string[]; ran?: boolean };
/** The base policy's exclusion: one `:(top,exclude,glob)` pathspec per applied pattern, added to every reader, and the kept files under them as `:(top,literal)` pathspecs. */
exclusion?: { specs: readonly string[]; keep: readonly string[] };
/** The base policy's review key and the platform evidence at HEAD; absent plans today's panel (review.panel all). */
shape?: ReviewShape;
}
/** How much review a repo runs (lib/ship-policy-review.ts) plus HEAD and the platform review found at it (lib/review-platform.ts). */
export interface ReviewShape { panel: 'off' | 'risk' | 'all'; readerModel: string; platform: 'off' | 'on'; head: string; platformReview: { id: string; head: string; reviewer: string } | null }
/** 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; keep?: string[] }
export interface Dispatch { role: Role; name: string; effort: Effort; exclude: string[]; checklists?: string[]; reason?: string; keep?: string[]; model?: string; classes?: string[] }
/** lean: R65's one reviewer; panel: today's panel (review.panel); platform: the platform review at HEAD stands in for every reader. */
export type ReviewMode = 'lean' | 'panel' | 'platform';
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. */
stepDepth: Depth;
depthReasons: string[];
next: NextStep; numbers: NumbersResult; dispatches: Dispatch[]; skipped: string[];
next: NextStep; numbers: NumbersResult; dispatches: Dispatch[]; skipped: string[]; mode: ReviewMode;
}
const scopeWants: Partial<Record<Specialist, (s: Record<string, string>) => boolean>> = {
@@ -517,7 +559,11 @@ export function planReview(i: PlanInput): ReviewPlan {
const c = classifyDiff(i);
const depth = decideDepth(c, c.runtimeLines ?? (CLASS_RANK[c.class] >= CLASS_RANK.code ? i.diffLines : 0), i.floor);
const { class: cls, runtimeLines } = depth;
const next = nextStep({ ...i.history, gate: cls === 'gate', adversarialRan: !!i.adversarialRan }, depth.depth);
const sh = i.shape;
const platformNow = !!sh && sh.platform === 'on' && sh.platformReview?.head === sh.head && i.history.fullPasses === 0 && !riskClass(c, depth) && platformFits(c.class, i.paths, depth.runtimeLines, i.numbers);
const panel = !platformNow && (!sh || sh.panel === 'all' || (sh.panel === 'risk' && riskClass(c, depth)));
const next = nextStep({ ...i.history, gate: cls === 'gate', adversarialRan: !!i.adversarialRan, lean: !panel }, depth.depth);
if (!panel) return leanPlan(i, c, depth, next);
const h = i.history;
const raise = depth.depth === 'light' && (h.fixedBlocking || h.blockingOpen || i.criticalFound);
const stepDepth: Depth = raise ? 'standard' : depth.depth;
@@ -587,7 +633,62 @@ export function planReview(i: PlanInput): ReviewPlan {
if (i.targeted.ran) skipped.push('targeted (already read this product patch)');
else add('targeted', 'targeted', ROLE_EFFORT.targeted);
}
return { ...depth, stepDepth, depthReasons, reasons: c.reasons, next, numbers: i.numbers, dispatches, skipped };
return { ...depth, stepDepth, depthReasons, reasons: c.reasons, next, numbers: i.numbers, dispatches, skipped, mode: 'panel' };
}
/**
* R65's lean dispatch (docs/review-policy.md#review-depth): pass 1 runs one native adversarial reader at the lane's reasoning, on
* review.reader_model (the lane's model by default: FOUN-32, ggstack#52). A risk-class diff under review.panel off gets the merged
* reader: the gate-class checklists, the Red Team checklist and the targeted brief for its classes; a non-risk code diff gets the
* code-class checklists and the Red Team. Docs and tests keep the core read. A docs or tests diff with no numeric, results or prereg
* claims, under review.platform on, with the platform review at HEAD, runs no reader at all (mode platform).
*/
/** Risk class read from the paths and lines (gate class, an R51 or review.risk_paths class, a classifier error), or a floor above light (FOUN-64 item 7: never the recorded review_depth). */
const riskClass = (c: ClassResult, depth: DepthResult) => c.class === 'gate' || c.risk.length > 0 || c.errors.length > 0 || depth.depth !== 'light';
/** Whether a light diff may take the platform depth (FOUN-64 item 2): docs or tests only, no numeric claims, no results, prereg, eval or checker path. The receipt and the queue ask the same. */
export const platformFits = (cls: DiffClass, paths: readonly string[], runtimeLines: number, numbers: NumbersResult) => (cls === 'docs' || cls === 'tests') && runtimeLines === 0 && !numbers.publishes && !paths.some(p => CLAIM_PATH.test(p));
/** Paths that carry results or preregistration claims: never the platform depth (FOUN-64 item 2). */
const CLAIM_PATH = /(^|\/|[-_.])(results?|prereg|preregistration|benchmarks?|evals?|scorers?|checkers?)([-_./]|$)/i;
function leanPlan(i: PlanInput, c: ClassResult, depth: DepthResult, next: NextStep): ReviewPlan {
const sh = i.shape!;
const risky = riskClass(c, depth);
const skipped: string[] = [];
const dispatches: Dispatch[] = [];
const x = i.exclusion;
const add = (role: Role, name: string, effort: Effort, exclude: string[] = [], extra: Partial<Dispatch> = {}) =>
dispatches.push({ role, name, effort, exclude: x ? [...exclude, ...x.specs] : exclude, ...extra, ...(x?.keep.length ? { keep: [...x.keep] } : {}) });
const plan = (n: NextStep, mode: ReviewMode): ReviewPlan => ({ ...depth, stepDepth: depth.depth, depthReasons: depth.reasons, reasons: c.reasons, next: n, numbers: i.numbers, dispatches, skipped, mode });
const pr = sh.platformReview;
const platformFit = !risky && platformFits(c.class, i.paths, depth.runtimeLines, i.numbers);
if (next.step === 'full' && platformFit && sh.platform === 'on' && pr && pr.head === sh.head) {
const why = `planned skip: platform review ${pr.id} by ${pr.reviewer} at ${pr.head.slice(0, 12)}`;
for (const r of ['core checklist', 'specialists', 'red-team', 'adversarial-native', 'outside-voice', 'targeted']) skipped.push(`${r} (${why})`);
return plan({ step: 'platform', reason: `review.platform on: the platform review ${pr.id} read HEAD ${pr.head.slice(0, 12)}, so /ship runs no reader (Step 11.5 binds the planned skip)` }, 'platform');
}
if (sh.platform === 'on' && next.step === 'full' && !risky) skipped.push(`platform depth (${depth.runtimeLines > 0 ? 'runtime code' : i.numbers.publishes ? 'the diff publishes numbers' : !platformFit ? 'a results, prereg, eval or checker path' : pr ? `the platform review read ${pr.head.slice(0, 12)}, not HEAD` : 'no platform review at HEAD'}: the light read runs)`);
const classes = i.targeted?.classes.length && !i.targeted.ran ? { classes: [...i.targeted.classes] } : {};
const docsAndOutputs = i.paths.filter(p => !i.excluded?.has(p) && ((pathClass(i, p) === 'docs' && !(i.numbers.publishes && DOC_PATH.test(p))) || isOutput(i, p))).map(excludeSpec);
const reader = () => risky
? add('adversarial-native', 'merged', 'full', [], { checklists: [...batchChecklists('gate', i, skipped), 'red-team'], model: sh.readerModel, ...classes })
: add('adversarial-native', 'single', 'full', docsAndOutputs, { checklists: [...batchChecklists('code', i, skipped), 'red-team'], model: sh.readerModel, ...classes });
if (next.step === 'full' && (risky || depth.runtimeLines > 0)) {
reader();
skipped.push(`specialists, red-team and outside-voice as their own readers (R65: the one ${risky ? 'risk-class' : 'light'} reader carries them)`);
} else if (next.step === 'full') {
for (const n of SPECIALISTS.filter(forcedBy(i))) add('specialist', n, i.tiers === false ? 'host' : ROLE_EFFORT.specialist);
if (i.numbers.publishes) add('outside-voice', 'outside-voice', 'host');
else skipped.push(`outside-voice (${depth.class} class, no numeric claims)`);
} else if (next.step === 'voices') reader();
else if (next.step === 'fix-check') {
add('fix-check', 'fix-check', 'full');
if (i.numbers.publishes && i.reviewedNumbers !== undefined && i.reviewedNumbers !== i.numbers.fingerprint) add('outside-voice', 'outside-voice (numbers changed)', 'host');
}
if (next.step === 'fix-check' && i.targeted?.classes.length) {
if (i.targeted.ran) skipped.push('targeted (already read this product patch)');
else add('targeted', 'targeted', ROLE_EFFORT.targeted);
}
return plan(next, 'lean');
}
export function reviewPlanLine(p: ReviewPlan, pass: number): string {
+5 -1
View File
@@ -14,6 +14,7 @@
* 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.
*/
import { createHash } from 'node:crypto';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { checkReviewedTree, isReviewedTreeLine, promptHash, type TreeCheck } from './review-brief';
@@ -439,7 +440,10 @@ export function verify(ledger: string): { rows: VerifyRow[]; clean: boolean; bri
const collected = outputs.some(({ e }) => reviewed(e, r));
const unavailable = !collected && outputs.some(({ e }) => unavailableOutput(e));
const status = !sent.length ? 'NOT_SENT' : mismatched ? 'MISMATCH' : collected ? 'MATCH' : unavailable ? 'UNAVAILABLE' : 'NOT_COLLECTED';
const tampered = fileNow && fileNow !== r.sha256 ? ' file_changed_since_render' : '';
const payload = r.payload as { file?: unknown; sha256?: unknown } | undefined;
let payloadNow: string | null = null;
if (!overwritten && payload && typeof payload.file === 'string') { try { payloadNow = createHash('sha256').update(fs.readFileSync(payload.file)).digest('hex'); } catch { payloadNow = ''; } }
const tampered = (fileNow && fileNow !== r.sha256 ? ' file_changed_since_render' : '') + (payloadNow !== null && payloadNow !== payload!.sha256 ? ' payload_changed_since_render' : '');
rows.push({ kind: 'BRIEF', reader: p.label, status: tampered ? 'MISMATCH' : status, detail: `rendered=${short(r.sha256)} sent=${sent.map(({ e }) => short(e.sha256)).join(',') || '-'}${epVoided ? ` voided=${epVoided}` : ''}${outputs.length && !collected && !unavailable ? ' not_reviewed' : ''}${tampered}` });
}
for (const [i, o] of events.entries()) {
+17 -5
View File
@@ -11,10 +11,11 @@
* persisted before its subagent is created and its path right after, so a resumed program
* reconciles by deterministic title and never doubles a reader.
*/
import { createHash } from 'node:crypto';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { BriefError, briefFile, checkoutRoot, checkoutTree, promptHash, renderBrief, withWorkspace, type BriefRole, type SectionTexts, type Slots } from './review-brief';
import { disclosureDigest } from './review-exclude';
import { BriefError, briefFile, checkoutRoot, checkoutTree, promptHash, renderBrief, sliceDiff, sliceNote, withWorkspace, type BriefRole, type SectionTexts, type Slots } from './review-brief';
import { REVIEW_DIFF_MAX_BYTES, disclosureDigest } from './review-exclude';
import { appendEvent, readerKey } from './review-ledger';
import { diffCommand, type Dispatch } from './review-depth';
import { TARGETED_RULES, parseClassList, targetedBriefSha, targetedRead } from './review-targeted';
@@ -31,6 +32,8 @@ export interface PassReader {
classes?: string[]; diff_base?: string;
/** An audit reader: its prompt is a file the parent wrote from its section; its output is returned, never collected. */
prompt_file?: string; model?: PassModel;
/** The plan's model=cross-family (R65's risk reader or a numbers diff's one reader): the program runs it on the outside voice's model. */
cross_family?: boolean;
file?: string; sha256?: string; bytes?: number; chunks?: number; findings_sha256?: string;
/** With the base policy's review_exclude_paths active: its `:(top,exclude,glob)` items, shell-quoted (the fixture command's slot), and the kept files' `:(top,literal)` items. */
policy_exclude?: string; keep?: string[];
@@ -103,6 +106,8 @@ export function buildManifest(o: {
readers.push({ reader, stage, title: `ship ${o.pass} ${reader}`, state: 'planned', role, name, effort: d.effort, exclude, checklists: d.checklists,
...(policy.length ? { policy_exclude: policy.map(shellQuote).join(' ') } : {}), ...(d.keep?.length ? { keep: d.keep } : {}),
...(role === 'targeted' ? { classes: [...(o.targetedClasses ?? [])], diff_base: o.mergeBase ?? o.diffBase } : {}),
...(role === 'adversarial-native' && d.classes?.length ? { classes: [...d.classes], diff_base: o.mergeBase ?? o.diffBase } : {}),
...(d.model === 'cross-family' ? { cross_family: true } : d.model && d.model !== 'lane' ? { model: { model_id: d.model } } : {}),
...(role === 'red-team' && o.rangeBase ? { diff_base: o.rangeBase } : {}) });
}
for (const e of o.extras ?? []) {
@@ -176,14 +181,21 @@ export function renderStage(m: Manifest, stage: Stage, t: SectionTexts, diffFor:
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, r);
let slice: Record<string, unknown> = {};
if (role === 'outside-voice') {
const s = sliceDiff(diffFor(m.diff_base, r), REVIEW_DIFF_MAX_BYTES);
const diffFile = path.join(briefs, 'outside-voice.diff');
fs.writeFileSync(diffFile, s.text, { mode: 0o600 });
Object.assign(slots, { diff_file: diffFile, diff_bytes: String(s.bytes), diff_sha256: createHash('sha256').update(s.text).digest('hex'), ...(s.sliced ? { diff_slice: sliceNote(s, REVIEW_DIFF_MAX_BYTES) } : {}) });
slice = { payload: { file: diffFile, sha256: slots.diff_sha256 }, ...(s.sliced ? { diff_slice: { kept: s.kept, files: s.files, bytes: s.bytes, total: s.total } } : {}) };
}
if (role === 'fix-check') {
const read = (f: string | undefined, what: string) => { if (!f) return undefined; try { return fs.readFileSync(f, 'utf8'); } catch { throw new BriefError(`cannot read the fix-check ${what} ${f}`); } };
const fc = m.fix_check!;
Object.assign(slots, { fix_list: read(fc.fix_list, 'fix list'), hunks: read(fc.hunks, 'hunks'), tests: read(fc.tests, 'tests'), callers: read(fc.callers, 'callers') });
}
let targeted = {};
if (role === 'targeted') {
if (role === 'targeted' || (role === 'adversarial-native' && r.classes?.length)) {
const checklist = path.join(m.checklist_root, 'review/specialists/targeted.md');
let brief: string;
try { brief = fs.readFileSync(checklist, 'utf8'); } catch { throw new BriefError(`cannot read the targeted brief ${checklist}`); }
@@ -195,7 +207,7 @@ export function renderStage(m: Manifest, stage: Stage, t: SectionTexts, diffFor:
fs.writeFileSync(file, text, { mode: 0o600 });
const sha = promptHash(text);
const bytes = Buffer.byteLength(text);
appendEvent(m.ledger, { event: 'rendered', reader: r.reader, role, name: r.name ?? null, file, sha256: sha, bytes, pass: m.pass, stage, workdir, tree, ...(findingsSha ? { findings_sha256: findingsSha } : {}), ...targeted, ...(disclosureDigest(m.disclosure) ? { exclude: disclosureDigest(m.disclosure) } : {}) });
appendEvent(m.ledger, { event: 'rendered', reader: r.reader, role, name: r.name ?? null, file, sha256: sha, bytes, pass: m.pass, stage, workdir, tree, ...(findingsSha ? { findings_sha256: findingsSha } : {}), ...targeted, ...slice, ...(disclosureDigest(m.disclosure) ? { exclude: disclosureDigest(m.disclosure) } : {}) });
Object.assign(r, { file, sha256: sha, bytes, chunks: chunkCount(text), state: 'rendered', tree, ...(findingsSha ? { findings_sha256: findingsSha } : {}) });
done.push(r);
}
+32
View File
@@ -0,0 +1,32 @@
/**
* review-platform — the exact-head evidence that a platform review read a PR (design C6, FOUN-32 on founderstar#496).
* Capy's automatic review posts a pull-request review by capy-ai[bot] whose commit_id is the head it read; that review's
* id is the run id the plan's planned skip and the receipt's review_depth platform record. Anything else (no PR, no such
* review, a review at another commit, one that requested changes or was dismissed, gh unavailable) is no evidence, and the
* single light reader runs.
*/
import { spawnSync } from 'node:child_process';
export const PLATFORM_REVIEWER = 'capy-ai[bot]';
export interface PlatformReview { id: string; head: string; reviewer: string }
function gh(root: string, args: string[]): { ok: boolean; out: string } {
const r = spawnSync('gh', args, { cwd: root, encoding: 'utf8', timeout: 60_000, maxBuffer: 16 * 1024 * 1024 });
return { ok: r.status === 0, out: (r.stdout ?? '').trim() };
}
/** The newest submitted platform review of the PR (`pr`, or the current branch's) at `head`; `id`, when given, must be that review. */
export function platformReviewAt(root: string, head: string, o: { pr?: string; id?: string } = {}): { ok: true; review: PlatformReview } | { ok: false; reason: string } {
const view = gh(root, ['pr', 'view', ...(o.pr ? [o.pr] : []), '--json', 'number']);
const pr = view.ok ? (JSON.parse(view.out || '{}') as { number?: number }).number : undefined;
if (!pr) return { ok: false, reason: o.pr ? `gh cannot read PR ${o.pr}` : 'no PR for this branch yet' };
const list = gh(root, ['api', '--paginate', '--slurp', `repos/{owner}/{repo}/pulls/${pr}/reviews`]);
if (!list.ok) return { ok: false, reason: `gh cannot list the reviews of PR #${pr}` };
let reviews: Array<{ id?: number; user?: { login?: string }; commit_id?: string; state?: string }>;
try { reviews = (JSON.parse(list.out) as unknown[]).flat() as typeof reviews; } catch { return { ok: false, reason: `PR #${pr}'s reviews are not JSON` }; }
const at = reviews.filter(r => r.user?.login === PLATFORM_REVIEWER && r.state !== 'PENDING' && r.commit_id?.toLowerCase() === head.toLowerCase() && r.id !== undefined);
const newest = at.at(-1);
const hit = newest && (!o.id || String(newest.id) === o.id) && (newest.state === 'APPROVED' || newest.state === 'COMMENTED') ? newest : undefined;
if (!hit) return { ok: false, reason: o.id ? `review ${o.id} on PR #${pr} is not a ${PLATFORM_REVIEWER} review at ${head.slice(0, 12)}` : `no ${PLATFORM_REVIEWER} review of PR #${pr} at ${head.slice(0, 12)}` };
return { ok: true, review: { id: String(hit.id), head: head.toLowerCase(), reviewer: PLATFORM_REVIEWER } };
}
+53 -7
View File
@@ -12,7 +12,7 @@
*/
import { createHash } from 'node:crypto';
export const RISK_CLASSES = ['money', 'auth', 'secret', 'security', 'data'] as const;
export const RISK_CLASSES = ['money', 'auth', 'secret', 'security', 'data', 'eval', 'policy'] as const;
export type RiskClass = (typeof RISK_CLASSES)[number];
/** Metering and charge words (GST-850: gbrain-evals #289 changed a metering proxy and billing clients with none of the older tokens); they also count on a line. */
@@ -22,12 +22,23 @@ export const MONEY_PATH_TOKENS = ['cost', 'costs', 'spend', 'spending', 'budget'
export const MONEY_PAIRS: readonly (readonly [string, readonly string[]])[] = [
['usage', ['token', 'tokens', 'cost', 'costs', 'spend', 'billing', 'price', 'pricing', 'budget']], ['proxy', ['billing', 'bill']], ['client', ['cost', 'costs', 'charge', 'charges', 'billing', 'price', 'pricing', 'spend']],
];
/**
* Spend a run reserves, binds, leases or settles against a ledger, a budget or a cap (GST-1129: gstack-evals#170 bound paid spend
* with `bindCheck(ledger, …)`): a verb beside a spend word on one changed code line. Prose lines (PROSE_PATH) never count: skill
* text says "binds" and "ledger" in passing (#3698 and #3798 would have read as money).
*/
const SPEND_BIND = ['reserve', 'reserves', 'reserved', 'reservation', 'reservations', 'bind', 'binds', 'lease', 'leases', 'leased', 'settle', 'settles', 'settled'];
const SPEND_OF = ['ledger', 'ledgers', 'spend', 'budget', 'budgets', 'usd', 'cost', 'costs', 'credits', 'billing', 'cap', 'caps'];
export const SPEND_PAIRS: readonly (readonly [string, readonly string[]])[] = [...SPEND_BIND.map(w => [w, SPEND_OF] as const), ['cap', ['spend', 'usd', 'budget', 'ledger']]];
export const PROSE_PATH = /\.(md|mdx|markdown|txt|rst|adoc|tmpl)$/i;
/** Identifiers ending in _usd, Usd or cap_usd, any spend_cap spelling, and paid-provider call sites (the providers lib/pricing.ts prices). */
export const MONEY_LINE = [
/\b[A-Za-z0-9_]*_usd\b/i, /\b[a-z][A-Za-z0-9]*Usd\b/, /\bspend[_-]?cap\b/i,
/\bapi\.(?:anthropic|openai|voyageai)\.com\b/, /\bgenerativelanguage\.googleapis\.com\b/, /\bopenrouter\.ai\b/,
/\bnew\s+(?:Anthropic|OpenAI|AnthropicBedrock|AnthropicVertex)\s*\(/, /\.(?:messages|chat\.completions|responses|embeddings)\.create\s*\(/,
];
/** The MONEY_LINE entries a config file or results script still answers to: a spend cap and a paid-provider call site, never a bare _usd value. */
export const PAID_MONEY_LINE = MONEY_LINE.slice(2);
export const AUTH_PATH_TOKENS = ['auth', 'authn', 'authz', 'oauth', 'oauth2', 'jwt', 'session', 'sessions', 'permission', 'permissions', 'role', 'roles'];
export const AUTH_PATH_PREFIXES = ['authenticat', 'authoriz'];
export const SECRET_PATH = /(^|\/)(secrets?|credentials?|keychain|token[s]?)([._/-]|$)/i;
@@ -70,20 +81,45 @@ export const decodeCharRefs = (s: string) => s.replace(/&#(?:[xX]0*([0-9a-fA-F]+
return c <= 0x10ffff ? String.fromCodePoint(c) : '';
});
export const svgActive = (text: string) => { const d = decodeCharRefs(text); const squeezed = d.replace(/[\s\0-\x1f]+/g, ''); return SVG_ACTIVE.some(r => r.test(d) || r.test(squeezed)); };
/** Eval scorers, judges and graders (FOUN-32 §4): code that decides a published result is risk class eval, by path. */
const EVAL_PATH_TOKENS = ['scorer', 'scorers', 'scoring', 'judge', 'judges', 'grader', 'graders', 'grade', 'grading', 'rubric', 'rubrics'];
const DATA_PATH_TOKENS = ['pii', 'gdpr', 'ccpa', 'privacy', 'personal', 'account', 'accounts', 'customer', 'customers', 'cookie', 'cookies', 'persistence', 'storage', 'database'];
export const DATA_LINE = [
/\bDELETE\s+FROM\b/, /\b(?:drop\s+(?:table|column|database|schema)|truncate\s+table)\b/i, /\bdelete\s+from\s+[\w."`]+\s*(?:where\b|;|$|["'`)])/i,
/\.(?:deleteMany|destroyAll|bulkDelete)\s*\(/, /\b(?:destroy_all|delete_all)\b/,
];
/**
* Data and config files (HARN-1, harness-evals#231): judged by their path only, so a lease_usd value in a VM record never names money.
* A key in a config file still names a secret.
*/
export const CONFIG_DATA_PATH = /\.(json|jsonl|ndjson|csv|tsv|ya?ml|toml|ini|cfg|conf|env|properties|xml|plist)$/i;
/**
* Analysis scripts that live with their results (docs/benchmarks/.../results/cost-intervals.py): their path tokens and generic
* lines name no risk class, but a secret, a spend cap or a paid-provider call in one still does (PAID_MONEY_LINE).
*/
export const STATS_SCRIPT = /(^|\/)results\/(?:[^/]+\/)*[^/]+\.(py|r|jl|ipynb)$/i;
/**
* Docs that can print a credential (gbrain#8417: `mcp get gbrain` shows a stored bearer token): an Authorization header, a
* bearer value, a key-shaped literal, an environment key set to a value, or a get/show command for auth config. Read on the
* added lines of docs paths, which matchRisk never sees, and counted as security (FOUN-64 item 4).
*/
export const DOC_SECRET_LINE = [
/\bAuthorization\s*:\s*\S/i, /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/, /\b(?:sk|pk|rk)-[A-Za-z0-9_-]{16,}|\bgh[pousr]_[A-Za-z0-9]{20,}|\bxox[abposr]-[A-Za-z0-9-]{10,}|\bAKIA[0-9A-Z]{16}\b/,
new RegExp(String.raw`\b${KEY_NAME}\s*[=:]\s*['"]?[^\s'"$<{]{8,}`),
/\b(?:mcp|auth|config|credentials?|tokens?|secrets?|keys?|login)\s+(?:get|show|print|view|cat|token)\b/i, /\b(?:get|show|print|cat)\s+(?:\S+[ ./-])?(?:tokens?|auth|credentials?|secrets?|api[_-]?keys?|bearer)\b/i,
];
/** Whether a path or line's tokens name money: a metering or charge word (a cost word too, for a path), or a pair. */
const namesMoney = (t: readonly string[], words: readonly string[]) => t.some(x => words.includes(x)) || MONEY_PAIRS.some(([w, beside]) => t.includes(w) && t.some(x => beside.includes(x)));
const pairs = (t: readonly string[], list: typeof MONEY_PAIRS) => list.some(([w, beside]) => t.includes(w) && t.some(x => beside.includes(x)));
const namesMoney = (t: readonly string[], words: readonly string[]) => t.some(x => words.includes(x)) || pairs(t, MONEY_PAIRS);
/** A hash of the patterns that decide the risk classes, not of this file's bytes; lib/review-depth.ts folds it into DEPTH_RULES. */
export const RISK_RULES = createHash('sha256').update(JSON.stringify({
RISK_CLASSES, MONEY_PATH_TOKENS, AUTH_PATH_TOKENS, AUTH_PATH_PREFIXES, KEY_NAME, SECURITY_PATH_TOKENS, DATA_PATH_TOKENS,
MONEY_LINE_TOKENS, MONEY_PAIRS,
patterns: Object.entries({ MONEY_LINE, SECRET_LINE, SECRET_PATH: [SECRET_PATH], SECURITY_LINE, SECURITY_JOINED, DATA_LINE, SVG_PATH: [SVG_PATH], SVG_ACTIVE }).map(([k, v]) => [k, v.map(r => '/' + r.source + '/' + r.flags)]),
RISK_CLASSES, MONEY_PATH_TOKENS, AUTH_PATH_TOKENS, AUTH_PATH_PREFIXES, KEY_NAME, SECURITY_PATH_TOKENS, DATA_PATH_TOKENS, EVAL_PATH_TOKENS,
MONEY_LINE_TOKENS, MONEY_PAIRS, SPEND_PAIRS,
patterns: Object.entries({ PROSE_PATH: [PROSE_PATH], DOC_SECRET_LINE, CONFIG_DATA_PATH: [CONFIG_DATA_PATH], STATS_SCRIPT: [STATS_SCRIPT], MONEY_LINE, PAID_MONEY_LINE, SECRET_LINE, SECRET_PATH: [SECRET_PATH], SECURITY_LINE, SECURITY_JOINED, DATA_LINE, SVG_PATH: [SVG_PATH], SVG_ACTIVE }).map(([k, v]) => [k, v.map(r => '/' + r.source + '/' + r.flags)]),
})).digest('hex').slice(0, 16);
export const tokens = (p: string) => p.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
@@ -95,6 +131,8 @@ export interface RiskInput {
removed?: Record<string, readonly string[]>;
/** gstack-diff-scope's SCOPE_AUTH: the auth class needs it and an auth token in the path. */
scopeAuth: boolean;
/** The base policy's review.risk_paths matcher: a code path it matches is risk class `policy`. */
isRiskPath?: (p: string) => boolean;
}
/** The risk classes the code paths touch, the first reason per class, in RISK_CLASSES order. Nothing fails closed here: input errors are the caller's. */
@@ -102,16 +140,24 @@ export function matchRisk(i: RiskInput): Map<RiskClass, string> {
const why = new Map<RiskClass, string>();
const hit = (c: RiskClass, reason: string) => { if (!why.has(c)) why.set(c, reason); };
for (const p of i.codePaths) {
const t = tokens(p);
if (i.isRiskPath?.(p)) hit('policy', 'risk_paths: ' + p);
const stats = STATS_SCRIPT.test(p);
const t = stats ? [] : tokens(p);
const config = CONFIG_DATA_PATH.test(p);
const pathOnly = config || stats;
const prose = PROSE_PATH.test(p);
if (i.scopeAuth && t.some(x => AUTH_PATH_TOKENS.includes(x) || AUTH_PATH_PREFIXES.some(a => x.startsWith(a)))) hit('auth', 'auth path: ' + p);
if (SECRET_PATH.test(p)) hit('secret', 'secret path: ' + p);
if (namesMoney(t, MONEY_PATH_TOKENS)) hit('money', 'money path: ' + p);
if (t.some(x => SECURITY_PATH_TOKENS.includes(x))) hit('security', 'security path: ' + p);
if (t.some(x => DATA_PATH_TOKENS.includes(x))) hit('data', 'data path: ' + p);
if (!config && t.some(x => EVAL_PATH_TOKENS.includes(x))) hit('eval', 'eval scorer path: ' + p);
if (SVG_PATH.test(p) && [i.added?.[p], i.removed?.[p]].some(l => l && svgActive(l.join('\n')))) hit('security', 'active SVG lines: ' + p);
for (const line of [...(i.added?.[p] ?? []), ...(i.removed?.[p] ?? [])]) {
if (!why.has('money') && (MONEY_LINE.some(r => r.test(line)) || namesMoney(tokens(line), MONEY_LINE_TOKENS))) hit('money', 'money line: ' + p);
if (!why.has('secret') && SECRET_LINE.some(r => r.test(line))) hit('secret', 'secret line: ' + p);
if (!why.has('money') && pathOnly && PAID_MONEY_LINE.some(r => r.test(line))) hit('money', 'spend cap or paid call: ' + p);
if (pathOnly) continue;
if (!why.has('money') && (MONEY_LINE.some(r => r.test(line)) || namesMoney(tokens(line), MONEY_LINE_TOKENS) || (!prose && pairs(tokens(line), SPEND_PAIRS)))) hit('money', 'money line: ' + p);
if (!why.has('security') && SECURITY_LINE.some(r => r.test(line))) hit('security', 'security line: ' + p);
if (!why.has('data') && DATA_LINE.some(r => r.test(line))) hit('data', 'data line: ' + p);
}
+3 -3
View File
@@ -21,7 +21,7 @@ import { createHash } from 'node:crypto';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { classifyPath } from './review-depth';
import { AUTH_PATH_PREFIXES, AUTH_PATH_TOKENS, KEY_NAME, MONEY_LINE, MONEY_LINE_TOKENS, MONEY_PAIRS, MONEY_PATH_TOKENS, SECRET_LINE, SECRET_PATH, matchRisk } from './review-risk';
import { AUTH_PATH_PREFIXES, AUTH_PATH_TOKENS, KEY_NAME, MONEY_LINE, MONEY_LINE_TOKENS, MONEY_PAIRS, MONEY_PATH_TOKENS, PROSE_PATH, SECRET_LINE, SECRET_PATH, SPEND_PAIRS, matchRisk } from './review-risk';
export const TARGETED_CLASSES = ['money', 'gate', 'auth', 'secret'] as const;
export type TargetedClass = (typeof TARGETED_CLASSES)[number];
@@ -31,8 +31,8 @@ export const secretLine = (line: string) => SECRET_LINE.some(r => r.test(line));
/** A hash of the patterns that decide the classes, not of this file's bytes: a comment edit keeps it, a pattern edit changes it. */
export const TARGETED_RULES = createHash('sha256').update(JSON.stringify({
TARGETED_CLASSES, MONEY_PATH_TOKENS, MONEY_LINE_TOKENS, MONEY_PAIRS, AUTH_PATH_TOKENS, AUTH_PATH_PREFIXES, KEY_NAME,
patterns: Object.entries({ MONEY_LINE, SECRET_LINE, SECRET_PATH: [SECRET_PATH] }).map(([k, v]) => [k, v.map(r => `/${r.source}/${r.flags}`)]),
TARGETED_CLASSES, MONEY_PATH_TOKENS, MONEY_LINE_TOKENS, MONEY_PAIRS, SPEND_PAIRS, AUTH_PATH_TOKENS, AUTH_PATH_PREFIXES, KEY_NAME,
patterns: Object.entries({ MONEY_LINE, SECRET_LINE, SECRET_PATH: [SECRET_PATH], PROSE_PATH: [PROSE_PATH] }).map(([k, v]) => [k, v.map(r => `/${r.source}/${r.flags}`)]),
})).digest('hex').slice(0, 16);
export interface TargetedInput {
+26
View File
@@ -0,0 +1,26 @@
/**
* ship-policy-review — the base-read `review` key of .gstack/ship-policy.json (docs/review-policy.md#review-depth),
* kept beside lib/ship-policy.ts so that module stays under the size ratchet. `panel` keeps today's reviewer panel
* for a class (risk by default: the printed full-depth plan and its full pass 2 on risk class, one reader elsewhere; R65 as amended by FOUN-64), `reader_model` is the model of that one reader,
* `platform` lets a non-risk diff rely on the platform review at its exact head, and `risk_paths` adds code
* paths to R51's risk classes.
*/
export interface ReviewPolicy { panel: 'off' | 'risk' | 'all'; reader_model: string; platform: 'off' | 'on'; risk_paths: string[] }
export const REVIEW_POLICY_DEFAULT: ReviewPolicy = { panel: 'all', reader_model: 'lane', platform: 'off', risk_paths: [] };
const MODEL = /^[A-Za-z0-9][\w./:-]{0,127}$/;
/** The key with every omitted field at its default (a policy file may set any subset). */
export function reviewPolicy(p: { review?: Partial<ReviewPolicy> | null } | null | undefined): ReviewPolicy {
return { ...REVIEW_POLICY_DEFAULT, ...(p?.review ?? {}) };
}
/** `review`: each field's error at its path; `globs` checks risk_paths as ship-policy checks every glob list. */
export function reviewErrors(value: unknown, out: { code: 'POLICY_INVALID'; path: string; message: string }[] | any[], globs: (key: string, v: unknown) => void): void {
if (!value || typeof value !== 'object' || Array.isArray(value)) { out.push({ code: 'POLICY_INVALID', path: 'review', message: 'must be { panel?, reader_model?, platform?, risk_paths? }' }); return; }
const v = value as Record<string, unknown>;
for (const k of Object.keys(v)) if (!(k in REVIEW_POLICY_DEFAULT)) out.push({ code: 'POLICY_INVALID', path: `review.${k}`, message: 'unknown key' });
if ('panel' in v && !['off', 'risk', 'all'].includes(v.panel as string)) out.push({ code: 'POLICY_INVALID', path: 'review.panel', message: `must be off | risk | all, got ${JSON.stringify(v.panel)}` });
if ('platform' in v && !['off', 'on'].includes(v.platform as string)) out.push({ code: 'POLICY_INVALID', path: 'review.platform', message: `must be off | on, got ${JSON.stringify(v.platform)}` });
if ('reader_model' in v && (typeof v.reader_model !== 'string' || !MODEL.test(v.reader_model))) out.push({ code: 'POLICY_INVALID', path: 'review.reader_model', message: 'must be lane, cross-family or a model id (letters, digits and . _ / : -)' });
if ('risk_paths' in v) globs('review.risk_paths', v.risk_paths);
}
+9 -8
View File
@@ -17,6 +17,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { defaultBaseBranch, noDefaultBaseLine } from './default-base';
import { RESULT_CODES, type ResultCodeName } from './result-codes';
import { reviewErrors, type ReviewPolicy } from './ship-policy-review';
import { cmpVersion, parseVersion } from './version-source';
import { placeholderErrors, templateErrors } from './ship-policy-stamps';
export { VERSION_SLOT } from './ship-policy-stamps';
@@ -58,6 +59,7 @@ export interface ShipPolicy {
review_scope: ReviewScope;
/** Git glob pathspecs (root-anchored) whose data, docs, binary and log files /ship's reviewers leave out; read from the base only (docs/ship-policy.md#review-exclude-paths). */
review_exclude_paths: string[];
/** How much review /ship runs (docs/review-policy.md#review-depth): the panel fallback, the risk reader's model, platform review and extra risk paths; read from the base only. */ review: ReviewPolicy;
/** Consumed by the plan reviews (D1). */
pr_count: { default_pr_count: number; split_requires_owner: boolean };
/** Maintainer logins for contributor-PR mode (E3); empty means "use push permission". */
@@ -161,6 +163,7 @@ export const DEFAULT_POLICY: ShipPolicy = {
stamp_sensitive_gates: [],
review_scope: 'frozen',
review_exclude_paths: [],
review: { panel: 'all', reader_model: 'lane', platform: 'off', risk_paths: [] },
pr_count: { default_pr_count: 1, split_requires_owner: true },
maintainers: [],
stamp_files: [],
@@ -194,6 +197,7 @@ export const POLICY_EXPLANATIONS: Record<PolicyKey, string> = {
stamp_sensitive_gates: 'Evidence labels (gstack-evidence --label) that read VERSION or generated bytes; the tree receipt never marks them reusable.',
review_scope: 'frozen | diff-plus-blocking | all. frozen (default): /ship Step 9 fixes findings in the diff on pass 1, files blocking findings in code the PR did not change as issues, and later passes read only the range since the previous pass and fix only blocking findings; diff-plus-blocking: the same, but a blocking finding in code the PR did not change is fixed in this PR instead of filed; all: every finding is fixed now (the pre-1.91.249 loop).',
review_exclude_paths: 'Git glob pathspecs, root-anchored and case-sensitive (`receipts/**`, `**/*.jsonl`; at most 64, 256 characters each, no leading `:` or `!`): /ship\'s reviewers leave out the data, docs, binary and .log files they match. Read from the base branch only, so a branch cannot exclude its own files. Code, gate paths, agent prose, config, executables, .gstack/ and .github/ files, gate-tagged or secret-bearing files and files a kept code or test file names by full path stay reviewed (docs/ship-policy.md#review-exclude-paths).',
review: '{ panel?, reader_model?, platform?, risk_paths? }, read from the base branch only. panel: all (default: today\'s panel and loop everywhere) | risk (opt-in: today\'s panel at full depth on risk class, one reader on the rest) | off (one merged reader on risk class too); risk and off run one fix round. Their one-reader shape has not passed a no-loss replay yet (GSTA-258 C: FIX-METHOD). reader_model: the model of the one reader a lean depth runs, `lane` (default: the lane\'s own model, the native adversarial reader), `cross-family` or a model id. platform: off (default) | on: a docs or tests diff with no numeric, results or prereg claims whose PR carries the platform review (capy-ai[bot]) at the exact head skips /ship\'s readers (review_depth platform); never runtime code. risk_paths: globs of code paths that are risk class on top of R51\'s; data, docs and binary files under them stay out (docs/review-policy.md#review-depth).',
pr_count: '{ default_pr_count, split_requires_owner }: the PR-count row in plan reviews.',
maintainers: 'GitHub logins treated as maintainers by /review contributor mode; empty uses repository push permission.',
stamp_files: 'The repo’s stamp rule: globs of the only files that may differ by a stamp (version file, manifests, plugin trees, the PR’s own since: rows). Empty keeps gstack’s per-file rules; set, it applies before every rule.',
@@ -244,6 +248,7 @@ export const BASE_RED_KEY_VERSION = '1.91.374.0';
export const TRAIN_GATES_KEY_VERSION = '1.92.0.0';
export const STAMP_PLACEHOLDERS_KEY_VERSION = '1.94.16.0';
export const REVIEW_EXCLUDE_KEY_VERSION = '1.93.33.0';
export const REVIEW_POLICY_KEY_VERSION = '1.94.30.0';
export const POLICY_KEY_INTRODUCED: Record<PolicyKey, string> = {
bump: '1.91.73.0', queue_mode: '1.91.73.0', release_tool: '1.91.73.0', release_outputs: '1.91.73.0', stamp_paths: '1.91.73.0',
changelog: '1.91.73.0', mirror: '1.91.73.0', history: '1.91.73.0', preregistration_shas: '1.91.73.0', stamp_sensitive_gates: '1.91.73.0',
@@ -251,7 +256,7 @@ export const POLICY_KEY_INTRODUCED: Record<PolicyKey, string> = {
migrations: '1.91.98.0', gate_command: SHIP_QUEUE_KEYS_VERSION, full_e2e: SHIP_QUEUE_KEYS_VERSION, min_gstack_version: SHIP_QUEUE_KEYS_VERSION,
train: TRAIN_KEYS_VERSION, review_scope: REVIEW_SCOPE_KEY_VERSION, docs: DOCS_CAPS_KEY_VERSION,
changelog_fragments: CHANGELOG_FRAGMENTS_KEY_VERSION,
base_red: BASE_RED_KEY_VERSION, review_exclude_paths: REVIEW_EXCLUDE_KEY_VERSION,
base_red: BASE_RED_KEY_VERSION, review_exclude_paths: REVIEW_EXCLUDE_KEY_VERSION, review: REVIEW_POLICY_KEY_VERSION,
stamp_placeholders: STAMP_PLACEHOLDERS_KEY_VERSION,
};
@@ -277,15 +282,10 @@ export function minimumReader(raw: Record<string, unknown>): string {
export interface PolicyError { code: ResultCodeName; path: string; message: string }
const MAX_LIST = 64;
const MAX_PATTERN = 256;
const MAX_REGEX = 512;
const MAX_COMMAND = 512;
const MAX_LIST = 64, MAX_PATTERN = 256, MAX_REGEX = 512, MAX_COMMAND = 512;
const SHA_RE = /^[0-9a-f]{7,64}$/;
function hasDotDot(p: string): boolean {
return p.split(/[\\/]+/).some(seg => seg === '..');
}
const hasDotDot = (p: string): boolean => p.split(/[\\/]+/).some(seg => seg === '..');
/** A repo-relative path or glob: no absolute paths, drive letters, `..` segments, NULs or control bytes. */
export function containedRelative(p: string): string | null {
@@ -482,6 +482,7 @@ export function validatePolicy(raw: unknown, opts: { gstackVersion?: string } =
oneOf('history', ['merge-only', 'squash-ok']);
oneOf('review_scope', ['frozen', 'diff-plus-blocking', 'all']);
if ('review_exclude_paths' in obj) excludeErrors(obj.review_exclude_paths, errors);
if ('review' in obj) reviewErrors(obj.review, errors, (k, v) => listErrors(k, v, errors, globError));
if ('release_tool' in obj && obj.release_tool !== null) {
const t = obj.release_tool;
if (typeof t !== 'string' || !t.trim()) errors.push({ code: 'POLICY_INVALID', path: 'release_tool', message: 'must be a non-empty string or null' });
+50 -12
View File
@@ -25,7 +25,10 @@ import { collectCommit } from './review-collect';
import { reviewExclude, type Exclusion } from './review-exclude';
import { isUnavailable } from './ship-policy';
import { loadPolicyAt } from './ship-policy-at';
import { depthOfCommitInput, gateScanNeeded, type Depth } from './review-depth';
import { depthOfCommitInput, gateScanNeeded, platformFits, scanNumbers, type Depth } from './review-depth';
import type { RiskClass } from './review-risk';
import { platformReviewAt } from './review-platform';
import { reviewPolicy, type ReviewPolicy } from './ship-policy-review';
import { TARGETED_CLASSES, classList, targetedClasses, type TargetedClass } from './review-targeted';
import { runScriptSync } from './script-invocation';
import { bindReceipt, type ReceiptBindingCode } from './ship-receipt';
@@ -33,7 +36,7 @@ import { docsExceptionLine, parseReceiptDocs } from './ship-receipt-docs';
export type ReceiptDepth = NonNullable<ShipReceipt['review_depth']>;
export type ReviewLoopException = NonNullable<ShipReceipt['review_loop_exception']>;
const RANK: Record<Depth, number> = { light: 0, standard: 1, full: 2 };
const RANK: Record<Depth | 'platform', number> = { platform: -1, light: 0, standard: 1, full: 2 };
const text = (v: unknown): v is string => typeof v === 'string' && v.trim().length > 0 && !/[\x00-\x1f\x7f]/.test(v);
const sha = (v: unknown): v is string => typeof v === 'string' && /^[0-9a-f]{40}$/.test(v);
const BIN_DIR = path.join(import.meta.dir, '..', 'bin');
@@ -70,8 +73,15 @@ function exclusionAt(root: string, ref: string, baseSha: string | null, head: st
return reviewExclude({ root, baseSha, ref, mergeBase: c.mergeBase, head, perPath: c.perPath, added: c.added, classRoot: c.root ?? [], gateTagged, context });
}
/** Whether the base policy declares patterns, so the gate scan's per-path tags are worth reading. */
const declaresExclude = (root: string, ref: string, sha: string | null) => { if (!sha) return false; const l = loadPolicyAt(root, sha, ref); return !isUnavailable(l) && l.policy.review_exclude_paths.length > 0; };
/** The base policy's review_exclude_paths and review key (lib/ship-policy-review.ts), defaults when it cannot be read. */
function basePolicy(root: string, ref: string, sha: string | null): { declares: boolean; review: ReviewPolicy } {
const l = sha ? loadPolicyAt(root, sha, ref) : null;
const p = l && !isUnavailable(l) ? l.policy : null;
return { declares: !!p && p.review_exclude_paths.length > 0, review: reviewPolicy(p) };
}
const riskPathsOf = (r: ReviewPolicy) => (r.risk_paths.length ? { riskPaths: r.risk_paths } : {});
/** The mode the plan ran for this depth under the base policy's review.panel (lib/review-depth.ts planReview). */
const modeFor = (depth: Depth, r: ReviewPolicy): 'lean' | 'panel' => (r.panel === 'all' || (r.panel === 'risk' && depth !== 'light') ? 'panel' : 'lean');
/**
* The depth record for `head`: the range from git objects (collectCommit) and
@@ -84,8 +94,11 @@ export function receiptDepthAtHead(repoRoot: string, base: string, binDir: strin
return receiptReviewAtHead(repoRoot, base, binDir, head).depth;
}
/** receiptDepthAtHead plus the exclusion the head's review read under: the depth classifies excluded paths as docs (GST-508). */
export function receiptReviewAtHead(repoRoot: string, base: string, binDir: string, head: string): { depth: ReceiptDepth; exclude: Exclusion } {
/**
* receiptDepthAtHead plus the exclusion the head's review read under: the depth classifies excluded paths as docs (GST-508).
* `declared` (write --declare-risk, GST-1129) joins the detected risk classes and only raises; the record then keeps both lists.
*/
export function receiptReviewAtHead(repoRoot: string, base: string, binDir: string, head: string, declared: readonly RiskClass[] = []): { depth: ReceiptDepth; exclude: Exclusion } {
const branch = base.replace(/^origin\//, '');
const baseSha = baseCommit(repoRoot, `origin/${branch}`);
const baseRev = baseSha.ok ? baseSha.out : branch;
@@ -93,7 +106,7 @@ export function receiptReviewAtHead(repoRoot: string, base: string, binDir: stri
const sha = baseSha.ok ? baseSha.out : (git(repoRoot, ['rev-parse', '--verify', '--quiet', `${branch}^{commit}`]).out || null);
const c = collectCommit(repoRoot, baseRev, head);
const scope = scopeOfRange(repoRoot, binDir, baseRev, head);
const declares = declaresExclude(repoRoot, ref, sha);
const { declares, review } = basePolicy(repoRoot, ref, sha);
let tagged: number | null = 0;
let paths: Set<string> | null = declares ? new Set() : null;
if (declares || gateScanNeeded(c.paths, c.root)) {
@@ -104,8 +117,25 @@ export function receiptReviewAtHead(repoRoot: string, base: string, binDir: stri
paths = declares && tagged !== null ? taggedPaths(g?.stdout ?? '') : null;
}
const exclude = exclusionAt(repoRoot, ref, sha, head, c, paths, 'gstack-ship-receipt');
const d = depthOfCommitInput({ ...c, scope, gateTagged: tagged, ...(exclude.state === 'active' ? { excluded: exclude.excluded } : {}) });
return { depth: { depth: d.depth, class: d.class, runtime_lines: d.runtimeLines, rules: d.rules, base: baseRev, registry: c.registry.sha, gate_scan: { tagged, head }, reasons: d.reasons.slice(0, 12), risk: d.risk }, exclude };
const d = depthOfCommitInput({ ...c, scope, gateTagged: tagged, ...riskPathsOf(review), declared, ...(exclude.state === 'active' ? { excluded: exclude.excluded } : {}) });
return { depth: { depth: d.depth, class: d.class, runtime_lines: d.runtimeLines, rules: d.rules, base: baseRev, registry: c.registry.sha, gate_scan: { tagged, head }, reasons: d.reasons.slice(0, 12), risk: d.risk, mode: modeFor(d.depth, review), ...(d.declared.length ? { declared: d.declared, detected: d.detected } : {}) }, exclude };
}
/**
* `write --platform-review <id>` (design C6, FOUN-63): the receipt says honestly that no /ship reader ran because the platform
* review read this head. Allowed only under the base policy's review.platform on, on a light docs or tests diff with no numeric,
* results or prereg claims, with capy-ai[bot]'s review <id> on the PR at this exact head.
*/
export function platformReceiptDepth(repoRoot: string, base: string, depth: ReceiptDepth, head: string, id: string, pr?: string): { ok: true; depth: ReceiptDepth } | { ok: false; message: string } {
const branch = base.replace(/^origin\//, '');
const baseSha = baseCommit(repoRoot, `origin/${branch}`);
const { review } = basePolicy(repoRoot, baseSha.ok ? `origin/${branch}` : branch, baseSha.ok ? baseSha.out : null);
if (review.platform !== 'on') return { ok: false, message: `--platform-review needs review.platform on in ${branch}'s .gstack/ship-policy.json; with platform review off at least one /ship reader runs` };
const c = collectCommit(repoRoot, depth.base, head);
if (depth.depth !== 'light' || !platformFits(depth.class, c.paths, depth.runtime_lines, scanNumbers(c.added))) return { ok: false, message: `head ${head.slice(0, 12)} needs ${depth.depth} review (${depth.class} class, ${depth.runtime_lines} runtime lines): the platform depth covers only docs or tests diffs with no numeric, results or prereg claims` };
const found = platformReviewAt(repoRoot, head, { pr, id });
if (!found.ok) return { ok: false, message: `--platform-review ${id}: ${found.reason}` };
return { ok: true, depth: { ...depth, depth: 'platform', mode: 'platform', platform_review: found.review } };
}
/** The paths of the hunks a `gstack-gate-diff --format jsonl` run tagged. */
@@ -113,8 +143,8 @@ export function taggedPaths(jsonl: string): Set<string> {
return new Set(jsonl.split('\n').slice(1).flatMap(l => { try { const h = JSON.parse(l) as { path?: string; tags?: unknown[] }; return h.path && h.tags?.length ? [h.path] : []; } catch { return []; } }));
}
export const depthBelow = (have: Depth, need: Depth) => RANK[have] < RANK[need];
export const deeper = (a: Depth, b: Depth): Depth => (RANK[a] >= RANK[b] ? a : b);
export const depthBelow = (have: Depth | 'platform', need: Depth | 'platform') => RANK[have] < RANK[need];
export const deeper = <D extends Depth | 'platform'>(a: D, b: D): D => (RANK[a] >= RANK[b] ? a : b);
/** `write --review-loop-exception` input and the queue's check: who accepted the stop, when, where, why, and the fix-check it followed. */
export function parseReviewLoopException(value: unknown): { ok: true; exception: ReviewLoopException } | { ok: false; message: string } {
@@ -254,10 +284,18 @@ export function receiptDepthVerdict(root: string, baseRef: string, r: ShipReceip
const { baseSha, c, scope, x } = at ?? headInputs(root, baseRef, head, r);
const tagged = boundScan(r, head);
const scanBound = tagged !== null;
const need = depthOfCommitInput({ ...c, scope, gateTagged: tagged, ...(x.state === 'active' ? { excluded: x.excluded } : {}) });
const { review } = basePolicy(root, baseRef, baseSha.ok ? baseSha.out : null);
const need = depthOfCommitInput({ ...c, scope, gateTagged: tagged, ...riskPathsOf(review), ...(x.state === 'active' ? { excluded: x.excluded } : {}) });
const have = rd?.depth;
const inputs = `base@${short(baseSha.ok ? baseSha.out : null)},registry@${short(c.registry.sha)},gate_scan@${scanBound ? short(rd!.gate_scan.head) : 'missing'}`;
const line = `REVIEW_DEPTH: receipt=${have ?? 'none'} required=${need.depth} runtime_lines=${need.runtimeLines} class=${need.class} reasons=${need.reasons.join(',')} inputs=${inputs} rounds=${r.review_loop?.rounds ?? (r.review_loop ? r.review_loop.passes + (r.review_loop.fix_checks ?? 0) : 'none')}`;
if (have === 'platform') {
const pr = rd!.platform_review;
const fits = need.depth === 'light' && platformFits(need.class, c.paths, need.runtimeLines, scanNumbers(c.added));
const ok = review.platform === 'on' && fits && !!pr && pr.head === head.toLowerCase();
if (ok) return { ok: true, line: `${line} platform_review=${pr!.id}@${pr!.head.slice(0, 12)}` };
return { ok: false, line, message: `the receipt records the platform depth, but ${review.platform !== 'on' ? `the base policy has review.platform ${review.platform}` : !fits ? `head ${head.slice(0, 12)} needs ${need.depth} review of a diff the platform depth does not cover` : `its platform review is not at head ${head.slice(0, 12)}`}; the receipt is unchanged; run /ship on ${head.slice(0, 12)}` };
}
if (have && !depthBelow(have, need.depth)) return { ok: true, line };
const why = have ? `was reviewed at ${have} depth` : 'records no review_depth though its writer records one';
const errs = need.errors.map(e => `${e.code} (${e.input})`).join('; ');
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.94.29",
"version": "1.94.30",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+1 -1
View File
@@ -81,7 +81,7 @@ function newShip(f: Fixture, sc: Scenario): { calls: Call[]; ending: string } {
let adversarialRan = false;
for (let i = 0; i < 6; i++) {
const plan = planReview({ ...base(f), history: h, criticalFound: sc === 'blocker' && i === 0, adversarialRan });
if (plan.next.step === 'done' || plan.next.step === 'stop') { ending = plan.next.step; break; }
if (plan.next.step === 'done' || plan.next.step === 'stop' || plan.next.step === 'platform') { ending = plan.next.step; break; }
const diffBytes = plan.next.step === 'fix-check' ? f.diff_bytes * FIX_CHECK_FRACTION : h.fullPasses === 0 ? f.diff_bytes : f.diff_bytes * RANGE_FRACTION;
for (const d of plan.dispatches) calls.push({ role: d.role, name: d.name, effort: d.effort, diffBytes: d.role === 'outside-voice' ? f.diff_bytes : diffBytes });
adversarialRan ||= plan.dispatches.some(d => d.role === 'adversarial-native');
+2 -1
View File
@@ -4,7 +4,8 @@
`DISPATCH: adversarial-native` and the outside voice only on
`DISPATCH: outside-voice`. Without the native line (light depth at runtime 0;
above 0 the plan lists it beside the batched specialist), print `ADVERSARIAL: skipped (review plan)`;
with neither line, take decision 3 of Finish the adversarial phase. On `DISPATCH: targeted`, also run the targeted reader
with neither line, take decision 3 of Finish the adversarial phase. The native pass takes its line's
`checklists=` and `classes=` as render flags and runs on its `model=` (`lane`: yours). On `DISPATCH: targeted`, also run the targeted reader
(`gstack-review-dispatch render --role targeted --classes <TARGETED classes> --base <base> --diff-base <merge base>`, the merge base on every pass, never RANGE_BASE) like the voices; `verify --targeted-out .gstack/tmp/targeted.json` feeds the receipt's `--review-targeted`. Under a token cap,
reserve the planned voices as one batch (`gstack-ship-budget check --next dispatch:<roles>`)
and add `--reservation <id>` to the `--start adversarial-review` call below.
+2 -1
View File
@@ -2,7 +2,8 @@
`DISPATCH: adversarial-native` and the outside voice only on
`DISPATCH: outside-voice`. Without the native line (light depth at runtime 0;
above 0 the plan lists it beside the batched specialist), print `ADVERSARIAL: skipped (review plan)`;
with neither line, take decision 3 of Finish the adversarial phase. On `DISPATCH: targeted`, also run the targeted reader
with neither line, take decision 3 of Finish the adversarial phase. The native pass takes its line's
`checklists=` and `classes=` as render flags and runs on its `model=` (`lane`: yours). On `DISPATCH: targeted`, also run the targeted reader
(`gstack-review-dispatch render --role targeted --classes <TARGETED classes> --base <base> --diff-base <merge base>`, the merge base on every pass, never RANGE_BASE) like the voices; `verify --targeted-out .gstack/tmp/targeted.json` feeds the receipt's `--review-targeted`. Under a token cap,
reserve the planned voices as one batch (`gstack-ship-budget check --next dispatch:<roles>`)
and add `--reservation <id>` to the `--start adversarial-review` call below.
+8 -3
View File
@@ -17,7 +17,9 @@ Finish the complete review and QA before applying any fix in Step 9.4.
**Review plan.** Pass 1 runs `~/.claude/skills/gstack/bin/gstack-review-plan
--base <base>`, plus `--depth-floor <d>` on every plan run when the /ship prompt
gave `--depth-floor <d>` (it only raises); a repeat uses the plan run that picked it.
gave `--depth-floor <d>` (it only raises); add `--declare-risk <classes>` when the diff touches a risk
class the plan's `REVIEW_DEPTH:` misses (spend it reserves or binds: `money`), and give Step 16's
receipt write the same flag; a repeat uses the plan run that picked it.
A `scoped` repeat is a full pass on the range: its `fix-check` is decision 3.5's
reviewer and prompt with `git diff $RANGE_BASE` as the hunks, inside this pass.
Save its `REVIEW_PLAN:` line and fingerprint, `REVIEW_DEPTH:` (depth,
@@ -27,6 +29,9 @@ family (`full` is the lane's reasoning, `medium` reasoning medium, `light` low;
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.
`NEXT: platform` runs no reader: log item 6's row with `"depth":"platform"` and the
`PLATFORM_REVIEW:` id (Step 16's receipt write takes `--platform-review <id>`; Step 11.5 selects no Step 11
record), then go to Step 10.
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"`,
@@ -822,9 +827,9 @@ Decision 1 always applies first. Then, under `frozen`, run `gstack-review-plan -
with each flag that holds (`--fixed-blocking`, `--fixed-below`, `--blocking-open`,
`--touched-gate` for a fix to an item 2.5 path, `--critical-found` when this pass
logged a CRITICAL, `--adversarial-ran` once Step 11's native pass ran, `--reviewed-numbers <fp>`,
`--depth-floor <d>` when given, `--grant <approval id>` once the owner's grant of one scoped fix-check is
`--depth-floor <d>` when given, `--fix-checks <n>` after a fix-check, `--grant <approval id>` once the owner's grant of one scoped fix-check is
in `gstack-approvals` naming `review_loop_grant` and the stop head), `--range-base $PASS_HEAD` and `--pass1-depth <pass 1's REVIEW_DEPTH>`;
its `NEXT:` alone picks the next decision: `stop` 2, `full` 3, `fix-check` 3.5, `done` 4. `voices` 4: Step 11 runs before the stop.
its `NEXT:` alone picks the next decision: `stop` 2, `full` 3, `fix-check` 3.5, `done` 4, `platform` 4. `voices` 4: Step 11 runs before the stop.
`scoped` is 3 too. A full pass 1 always gets a full pass 2.
Other scopes apply decisions 2–4 in order.
+8 -3
View File
@@ -15,7 +15,9 @@ Finish the complete review and QA before applying any fix in Step 9.4.
**Review plan.** Pass 1 runs `~/.claude/skills/gstack/bin/gstack-review-plan
--base <base>`, plus `--depth-floor <d>` on every plan run when the /ship prompt
gave `--depth-floor <d>` (it only raises); a repeat uses the plan run that picked it.
gave `--depth-floor <d>` (it only raises); add `--declare-risk <classes>` when the diff touches a risk
class the plan's `REVIEW_DEPTH:` misses (spend it reserves or binds: `money`), and give Step 16's
receipt write the same flag; a repeat uses the plan run that picked it.
A `scoped` repeat is a full pass on the range: its `fix-check` is decision 3.5's
reviewer and prompt with `git diff $RANGE_BASE` as the hunks, inside this pass.
Save its `REVIEW_PLAN:` line and fingerprint, `REVIEW_DEPTH:` (depth,
@@ -25,6 +27,9 @@ family (`full` is the lane's reasoning, `medium` reasoning medium, `light` low;
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.
`NEXT: platform` runs no reader: log item 6's row with `"depth":"platform"` and the
`PLATFORM_REVIEW:` id (Step 16's receipt write takes `--platform-review <id>`; Step 11.5 selects no Step 11
record), then go to Step 10.
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"`,
@@ -231,9 +236,9 @@ Decision 1 always applies first. Then, under `frozen`, run `gstack-review-plan -
with each flag that holds (`--fixed-blocking`, `--fixed-below`, `--blocking-open`,
`--touched-gate` for a fix to an item 2.5 path, `--critical-found` when this pass
logged a CRITICAL, `--adversarial-ran` once Step 11's native pass ran, `--reviewed-numbers <fp>`,
`--depth-floor <d>` when given, `--grant <approval id>` once the owner's grant of one scoped fix-check is
`--depth-floor <d>` when given, `--fix-checks <n>` after a fix-check, `--grant <approval id>` once the owner's grant of one scoped fix-check is
in `gstack-approvals` naming `review_loop_grant` and the stop head), `--range-base $PASS_HEAD` and `--pass1-depth <pass 1's REVIEW_DEPTH>`;
its `NEXT:` alone picks the next decision: `stop` 2, `full` 3, `fix-check` 3.5, `done` 4. `voices` 4: Step 11 runs before the stop.
its `NEXT:` alone picks the next decision: `stop` 2, `full` 3, `fix-check` 3.5, `done` 4, `platform` 4. `voices` 4: Step 11 runs before the stop.
`scoped` is 3 too. A full pass 1 always gets a full pass 2.
Other scopes apply decisions 2–4 in order.
+1 -1
View File
@@ -1 +1 @@
{"sha":"25050a7a61f81df649d1df0d0237dbf961671356395317c6a527741f5bb56877","replay":"GST-940"}
{"sha":"fb21c0b80e98685ca8c5192aeaa217cfffe9fbdaa8890455c97e01be1b044a53","replay":"GSTA-258-C+GST-940"}
+1 -1
View File
@@ -1 +1 @@
{"sha":"08464b78a723ee463ca3a28d0abbc4eb8f4f512192061c703743707b66b5a311","replay":"GST-942"}
{"sha":"e2a4d2aabe02e28140b3b8f3d5288bf0eac1872b5cb375eb011e4f0cde2ad67a","replay":"GSTA-258-C+GST-942"}
@@ -1 +1 @@
{"sha":"0142d6bc3f2a5bceb9f8b2adef1d3a206e5836c99a0667fe995568da2e80a5a6","replay":"GST-508+GST-464+GST-967"}
{"sha":"c623d65c4c53054108ac0166773c89233d76a25b0d35eb72d1c546f7bec4bdc9","replay":"GSTA-258-C"}
@@ -1 +1 @@
{"sha":"de3e0d35f2af56492c42fd287b6868fbe19bce96b86321e74a4947ba909b74eb","replay":"GST-508+GST-464+GST-967"}
{"sha":"25c09be7ea5c0e438ee059af2064f5bf038f6b69e3b5378f6c47be669f7ecd17","replay":"GSTA-258-C"}
@@ -0,0 +1 @@
{"replay":"GSTA-258-C+GST-940","justification":"merge of main bf2bcf14b into #3920 under a rule 19 grant: lib/review-depth.ts is main's GST-940 bytes plus exactly the GSTA-258-C lines (the merged file minus main equals the branch minus its merge base f6d6215f4, line for line, except that the patterns list on the one conflicted line keeps main's SVG_STYLE_TAG, SVG_TAG_NAME and SVG_ATTR beside C's BINARY_DATA_PATH), so GST-940's verdict no-loss check and GSTA-258-C's planner no-loss check both hold; the planner over the last 200 first-parent main commits to bf2bcf14b gives the same depth, class, risk classes, next step and dispatches on 195, and the other 5 keep full depth with main's reviewers or raise to main's full panel: the four the GSTA-258-C entry names, plus 8d276f9e31 gaining money at full gate (GStack Minder drive handoffs/GSTA-258/C-planner-noloss/)"}
@@ -0,0 +1 @@
{"replay":"GSTA-258-C+GST-942","justification":"pre-READY merge of main 5f705f367 into #3920: lib/review-risk.ts is main's GST-942 bytes plus exactly the GSTA-258-C lines (the merged file minus main equals the branch minus its merge base 6f1dfd5b7c, line for line, except that RISK_RULES keeps main's SECURITY_JOINED beside C's patterns), so GST-942's verdict-identity no-loss check and GSTA-258-C's planner no-loss check both hold; the joined rm rule still runs on every code path after the line loop, and the planner over the last 200 first-parent main commits to 5f705f367 plans every commit as main's code does or only raises (GStack Minder drive handoffs/GSTA-258/C-planner-noloss/)"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GSTA-258-C","justification":"GSTA-258 build C planner no-loss check: review.panel defaults to all, so the default plan is today's panel; over the last 200 first-parent main commits to 6f1dfd5b7c, planReview and collectCommit from origin/main and from this branch give the same depth, class, risk classes, next step and dispatches on 196, and the other 4 only raise: 4eff47c5cf goes light to full (eval class, main's own full panel adds the red team), e25997515c gains money at full gate, and 83ae96e086 and 7f815411f3 lose a money class from JSON lines (HARN-1) but keep full depth and the same reviewers (GStack Minder drive handoffs/GSTA-258/C-planner-noloss/); the one-reader shape ships opt-in (review.panel risk) because its own no-loss replay was FIX-METHOD (handoffs/GSTA-258/C-noloss-results.md); every brief body lib/review-brief.ts cuts from the ship sections still hashes to its pin, so only lead instructions outside the cut spans changed"}
+100
View File
@@ -0,0 +1,100 @@
import { describe, test, expect } from 'bun:test';
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, depthOfCommitInput } from '../lib/review-depth';
import { matchRisk } from '../lib/review-risk';
import { receiptReviewAtHead } from '../lib/ship-receipt-depth';
import { ARTIFACT_SCHEMAS, checkSchema, type ValidationError } from '../lib/headless-artifacts';
const BIN = path.join(import.meta.dir, '..', 'bin');
const money = (p: string, lines: string[]) => matchRisk({ codePaths: [p], added: { [p]: lines }, scopeAuth: false }).get('money');
// GST-1129: gstack-evals#170 (GEVA-221) reserves and binds paid spend in quick/laneb/r3/screen.ts. These are its run() hunk's added
// lines without the UBICLOUD_API_KEY line, which was the only thing that took the review to full depth.
const SCREEN = 'quick/laneb/r3/screen.ts';
const GEVA221 = [
' const bound = ledger + ".bound";',
' if (existsSync(bound)) throw new Error(boundTo());',
' const runner = process.env.UBI_RUNNER ?? "";',
' const ttl = process.env.UBI_TTL_HOURS || "3";',
' bindCheck(ledger, m, configs);',
' throw new Error(boundTo());',
];
describe('GST-1129 item 1: code that reserves or binds spend is money (GEVA-221)', () => {
test("GEVA-221's run() hunk, without the UBICLOUD_API_KEY line, plans full depth with money in the risk list", () => {
expect(money(SCREEN, GEVA221)).toBe('money line: ' + SCREEN);
const d = depthOfCommitInput({ paths: [SCREEN], gateTagged: 0, added: { [SCREEN]: GEVA221 }, perPath: [{ path: SCREEN, lines: GEVA221.length, binary: false }] });
expect([d.depth, d.risk]).toEqual(['full', ['money']]);
});
test.each([['reserveSession(ledger, item)'], ['const ok = bindCap(item, cap);'], ['lease.settle(budget)'], ['await reserveSpend(id)'], ['if (spendCap === null) return;'], ['const leased = leaseCredits(n);']])('money: %s', line => {
expect(money('src/run.ts', [line])).toBeDefined();
});
test.each([['appendEvent(m.ledger, { event: "rendered" });'], ['const bound = Math.min(a, b);'], ['reserve(slots);'], ['handler.bind(this);'], ['const cap = Math.max(0, n);']])('not money: %s', line => {
expect(money('src/run.ts', [line])).toBeUndefined();
});
test('prose never names money by these pairs: skill text says binds and ledger in passing (#3698, #3798)', () => {
expect(money('capy/skill/gstack/SKILL.md', ['Each pass binds its reviewers to the review ledger before Step 11.'])).toBeUndefined();
expect(money('src/run.ts', ['Each pass binds its reviewers to the review ledger before Step 11.'])).toBeDefined();
});
test('code paths only (HARN-1): a data file and a stats script with the same words never name money', () => {
expect(money('runs/item.json', [' "bindCheck": "ledger", "reserve": "budget"'])).toBeUndefined();
expect(money('docs/results/bind-check.py', ['bindCheck(ledger, m)'])).toBeUndefined();
});
});
describe('GST-1129 item 2: a declared risk class only raises the depth', () => {
const light = { paths: ['src/app.ts'], gateTagged: 0, perPath: [{ path: 'src/app.ts', lines: 10, binary: false }] };
test('a class the matcher missed raises light to full, and the result keeps detected and declared apart', () => {
expect(depthOfCommitInput(light).depth).toBe('light');
const d = depthOfCommitInput({ ...light, declared: ['money'] });
expect([d.depth, d.risk, d.detected, d.declared]).toEqual(['full', ['money'], [], ['money']]);
});
test('declaring nothing or another class never drops a detected one; a docs diff with a declared class plans full', () => {
const c = classifyDiff({ paths: ['lib/billing.ts'], gateTagged: 0, declared: [] });
expect([c.risk, c.detected, c.declared]).toEqual([['money'], ['money'], []]);
const both = classifyDiff({ paths: ['lib/billing.ts'], gateTagged: 0, declared: ['auth'] });
expect([both.risk, both.detected, both.declared]).toEqual([['money', 'auth'], ['money'], ['auth']]);
expect(depthOfCommitInput({ paths: ['docs/a.md'], gateTagged: 0, declared: ['security'] }).depth).toBe('full');
});
function repo(): { dir: string; head: string; state: string } {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'declared-'));
const state = fs.mkdtempSync(path.join(os.tmpdir(), 'declared-state-'));
const git = (...a: string[]) => { const r = spawnSync('git', a, { cwd: dir, encoding: 'utf8', timeout: 30_000 }); if (r.status !== 0) throw new Error(r.stderr); return r.stdout.trim(); };
git('init', '-q', '-b', 'main'); git('config', 'user.email', 't@example.com'); git('config', 'user.name', 't'); git('config', 'commit.gpgsign', 'false');
fs.writeFileSync(path.join(dir, 'README.md'), '# x\n'); git('add', '.'); git('commit', '-qm', 'base');
git('update-ref', 'refs/remotes/origin/main', 'HEAD');
git('checkout', '-qb', 'feat');
fs.mkdirSync(path.join(dir, 'src')); fs.writeFileSync(path.join(dir, 'src/app.ts'), 'export const a = 1;\n');
git('add', '.'); git('commit', '-qm', 'change');
return { dir, head: git('rev-parse', 'HEAD'), state };
}
test('gstack-review-plan --declare-risk prints the declared and detected classes and plans full; an unknown class is a usage error', () => {
const r = repo();
const plan = (...a: string[]) => spawnSync(path.join(BIN, 'gstack-review-plan'), ['--base', 'main', '--gate-tagged', '0', ...a], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env: { ...process.env, GSTACK_STATE_ROOT: r.state } });
expect(plan().stdout).toMatch(/^REVIEW_DEPTH: light /m);
const out = plan('--declare-risk', 'money').stdout;
expect(out).toMatch(/^REVIEW_DEPTH: full .*reasons=risk:money,/m);
expect(out).toContain('RISK_DECLARED: classes=money detected=none');
const bad = plan('--declare-risk', 'cash');
expect([bad.status, bad.stdout]).toEqual([2, '']);
expect(bad.stderr).toContain('--declare-risk cash');
}, 60_000);
test('the receipt records declared and detected classes beside the raised depth, and the schema accepts them', () => {
const r = repo();
const prior = process.env.GSTACK_STATE_ROOT;
process.env.GSTACK_STATE_ROOT = r.state;
let d: ReturnType<typeof receiptReviewAtHead>['depth'];
try {
d = receiptReviewAtHead(r.dir, 'main', BIN, r.head, ['money']).depth;
expect(receiptReviewAtHead(r.dir, 'main', BIN, r.head).depth).not.toHaveProperty('declared');
} finally { if (prior === undefined) delete process.env.GSTACK_STATE_ROOT; else process.env.GSTACK_STATE_ROOT = prior; }
expect([d.depth, d.risk, d.declared, d.detected]).toEqual(['full', ['money'], ['money'], []]);
const errors: ValidationError[] = [];
checkSchema({ schema_version: 1, head: r.head, base: 'main', review_depth: d }, ARTIFACT_SCHEMAS['ship-receipt'], '', errors);
expect(errors.filter(e => JSON.stringify(e).includes('review_depth'))).toEqual([]);
}, 60_000);
});
+246
View File
@@ -0,0 +1,246 @@
import { describe, test, expect } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { classifyDiff, nextStep, planReview, platformFits, scanNumbers, type PlanInput, type ReviewShape } from '../lib/review-depth';
import { appendEvent, verify } from '../lib/review-ledger';
import * as os from 'node:os';
import { createHash } from 'node:crypto';
import { matchRisk } from '../lib/review-risk';
import { renderBrief, sectionTexts, sliceDiff, sliceNote } from '../lib/review-brief';
import { validatePolicy, DEFAULT_POLICY, REVIEW_POLICY_KEY_VERSION } from '../lib/ship-policy';
const ROOT = path.join(import.meta.dir, '..');
const none = scanNumbers({});
const HEAD = 'a'.repeat(40);
const lean = (o: Partial<ReviewShape> = {}): ReviewShape => ({ panel: 'risk', readerModel: 'lane', platform: 'off', head: HEAD, platformReview: null, ...o });
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, shape: lean(), ...o });
const roles = (p: ReturnType<typeof planReview>) => p.dispatches.map(d => `${d.role}:${d.name}:${d.effort}:${d.model ?? '-'}`);
describe('HARN-1 false triggers (harness-evals#231 on 1.93.7.0): data, config and stats scripts never raise the depth', () => {
const sqlite = 'docs/benchmarks/2026-11-01-installer-cc-codex/results/cc-none.smoke.ledger.sqlite';
test('committed sqlite ledgers git cannot count are data, not code, and never name money', () => {
const c = classifyDiff({ paths: [sqlite, 'README.md'], gateTagged: 0, perPath: [{ path: sqlite, lines: 0, binary: true }, { path: 'README.md', lines: 3, binary: false }], consumed: [] });
expect([c.class, c.errors.map(e => e.code), c.risk]).toEqual(['docs', [], []]);
});
test('run logs are data too: *.log, *.out and extensionless files under logs/ never name money or security (subagent replay at f6a7eba)', () => {
const logs = ['docs/b/results/cc-installer.plan.log', 'docs/b/results/trials/t1/r1/agent.out', 'docs/b/results/trials/t1/r1/logs/ready'];
const c = classifyDiff({ paths: logs, gateTagged: 0, added: Object.fromEntries(logs.map(p => [p, ['spend_cap_usd=5 fetch(url) rm -rf /tmp/x']])) });
expect([c.class, c.risk]).toEqual(['docs', []]);
expect(classifyDiff({ paths: ['src/logs/rotate.ts'], gateTagged: 0 }).class).toBe('code');
});
test('a sqlite file is data even when a script names it', () => {
const c = classifyDiff({ paths: [sqlite, 'scripts/load.py'], gateTagged: 0, perPath: [{ path: sqlite, lines: 0, binary: true }, { path: 'scripts/load.py', lines: 4, binary: false }], consumed: [sqlite] });
expect([c.class, c.errors.map(e => e.code), c.runtimeLines]).toEqual(['code', [], 4]);
});
test('money on lease_usd lines of a JSON config a script reads is not a risk class; the config is judged by its path', () => {
const p = 'docs/benchmarks/x/results/cc-none.shard-vm.json';
expect(matchRisk({ codePaths: [p], added: { [p]: [' "lease_usd": 32.9,', ' "list_price_usd": 1.2485,'] }, scopeAuth: false }).size).toBe(0);
expect(matchRisk({ codePaths: ['config/billing.json'], added: { 'config/billing.json': ['{}'] }, scopeAuth: false }).has('money')).toBe(true);
expect(matchRisk({ codePaths: ['lib/lease.ts'], added: { 'lib/lease.ts': ['const lease_usd = 3;'] }, scopeAuth: false }).has('money')).toBe(true);
});
test('a stats script under a results directory is judged by its path: no security or money from its name or lines', () => {
const p = 'docs/benchmarks/2026-11-01-installer-cc-codex/results/cost-intervals.py';
expect(matchRisk({ codePaths: [p], added: { [p]: ["rows = [json.loads(l) for l in open(os.path.join(sys.argv[1], 'trials.jsonl'))]"] }, scopeAuth: false }).size).toBe(0);
expect(matchRisk({ codePaths: ['tools/fetch.py'], added: { 'tools/fetch.py': ['p = os.path.join(sys.argv[1], name)'] }, scopeAuth: false }).has('security')).toBe(true);
});
test('a paid-provider call, a spend cap or a key in a results script or config file still counts', () => {
const run = 'benchmarks/run_models.py', sweep = 'analysis/sweep.py', cfg = 'evals/config.yaml';
expect(matchRisk({ codePaths: [run], added: { [run]: ['resp = client.messages.create(model=m)'] }, scopeAuth: false }).has('money')).toBe(true);
expect(matchRisk({ codePaths: [sweep], added: { [sweep]: ['MAX = spend_cap(5)'] }, scopeAuth: false }).has('money')).toBe(true);
expect(matchRisk({ codePaths: [run], added: { [run]: ['key = os.environ["OPENAI_API_KEY"]'] }, scopeAuth: false }).has('secret')).toBe(true);
expect(matchRisk({ codePaths: [cfg], added: { [cfg]: ['spend_cap: 9999'] }, scopeAuth: false }).has('money')).toBe(true);
expect(matchRisk({ codePaths: [cfg], added: { [cfg]: ['lease_usd: 3'] }, scopeAuth: false }).has('money')).toBe(false);
expect(matchRisk({ codePaths: ['lib/analysis/billing.py'], added: { 'lib/analysis/billing.py': ['charge(customer, amount_usd)'] }, scopeAuth: false }).has('money')).toBe(true);
expect(matchRisk({ codePaths: ['docs/conf.py'], added: { 'docs/conf.py': ['p = os.path.join(sys.argv[1], name)'] }, scopeAuth: false }).has('security')).toBe(true);
});
test('a secret in a config file still counts', () => {
expect(matchRisk({ codePaths: ['deploy/app.yaml'], added: { 'deploy/app.yaml': [' key: ${OPENAI_API_KEY}'] }, scopeAuth: false }).has('secret')).toBe(true);
});
});
describe('FOUN-64 item 4: docs that can print a credential are security risk (gbrain#8417)', () => {
const docsWith = (line: string) => classifyDiff({ paths: ['docs/mcp.md'], gateTagged: 0, added: { 'docs/mcp.md': [line] } });
test.each([['$ claude mcp get gbrain'], ['Authorization: Bearer abcdefgh12345678'], ['export OPENAI_API_KEY=sk-live-0123456789abcdefghij'], ['gh auth token']])('%s', l => expect(docsWith(l).risk).toEqual(['security']));
test('ordinary docs stay risk-free', () => expect(docsWith('Run gstack-review-plan --base main to see the plan.').risk).toEqual([]));
});
describe('FOUN-32 §4: eval scorers, judges and graders are risk class eval by path', () => {
test.each([['lib/eval-scorer.ts'], ['evals/judges/rubric.py'], ['scripts/grade-trials.ts']])('%s', p => expect(classifyDiff({ paths: [p], gateTagged: 0 }).risk).toContain('eval'));
test('a test helper named judge stays tests class, and an ordinary path is not eval', () => {
expect(classifyDiff({ paths: ['test/helpers/llm-judge.ts'], gateTagged: 0 }).risk).toEqual([]);
expect(classifyDiff({ paths: ['lib/review-pass.ts'], gateTagged: 0 }).risk).not.toContain('eval');
});
});
describe('risk_paths: a repo extends the risk classes from its base policy (FOUN-64)', () => {
const rp = ['src/balance/**', 'src/outcome*.ts'];
test('a matching code path is risk class policy; data and docs under the glob are not', () => {
expect(classifyDiff({ paths: ['src/balance/view.tsx'], gateTagged: 0, riskPaths: rp }).risk).toEqual(['policy']);
expect(classifyDiff({ paths: ['src/outcome-score.ts'], gateTagged: 0, riskPaths: rp }).risk).toEqual(['policy']);
expect(classifyDiff({ paths: ['src/balance/snap.sqlite', 'src/balance/notes.md'], gateTagged: 0, riskPaths: rp, consumed: [] }).risk).toEqual([]);
expect(classifyDiff({ paths: ['src/app.ts'], gateTagged: 0, riskPaths: rp }).risk).toEqual([]);
});
test('a policy risk path plans the merged reader at full depth', () => {
const p = planReview(base({ paths: ['src/balance/x.ts'], riskPaths: rp, shape: lean({ panel: 'off' }) }));
expect([p.depth, roles(p)]).toEqual(['full', ['adversarial-native:merged:full:lane']]);
expect(planReview(base({ paths: ['src/balance/x.ts'], riskPaths: rp })).mode).toBe('panel');
});
});
describe('R65 lean dispatch (GSTA-258 build C): one reviewer, once', () => {
test('non-risk code: one native reader at the lane\'s reasoning carrying the adversarial brief, the checklists and the Red Team', () => {
const p = planReview(base({}));
expect([p.mode, roles(p)]).toEqual(['lean', ['adversarial-native:single:full:lane']]);
expect(p.dispatches[0]!.checklists).toEqual(['testing', 'security', 'red-team']);
expect(p.skipped.join('\n')).toMatch(/outside-voice/);
});
test('non-risk code that publishes numbers keeps the native reader by default (FOUN-32 §3, ggstack#52); cross-family is a setting', () => {
const p = planReview(base({ paths: ['src/app.ts', 'docs/results.md'], numbers: scanNumbers({ 'docs/results.md': ['recall 87% (26/30)'] }) }));
expect(roles(p)).toEqual(['adversarial-native:single:full:lane']);
expect(roles(planReview(base({ shape: lean({ readerModel: 'cross-family' }) })))).toEqual(['adversarial-native:single:full:cross-family']);
});
test('panel risk (opt-in, FOUN-64 item 1): a risk-class diff gets the printed full-depth panel; a fixed blocker re-runs the printed plan as round 2 (FOUN-32 §5)', () => {
const today = roles(planReview(base({ paths: ['lib/billing.ts'], targeted: { classes: ['money'] }, shape: undefined })));
const p = planReview(base({ paths: ['lib/billing.ts'], targeted: { classes: ['money'] } }));
expect([p.depth, p.mode, roles(p)]).toEqual(['full', 'panel', today]);
expect(today.length).toBeGreaterThan(1);
expect(planReview(base({ paths: ['lib/billing.ts'], history: { fullPasses: 1, last: 'full', fixedBlocking: true } })).next.step).toBe('full');
expect(planReview(base({ paths: ['lib/billing.ts'], history: { fullPasses: 2, last: 'full', fixedBlocking: true } })).next.step).toBe('stop');
expect(planReview(base({ history: { fullPasses: 1, last: 'full', fixedBlocking: true } })).next.step).toBe('fix-check');
});
test('panel off: risk class gets one reader on the configured model with the merged panel brief; it carries the targeted classes', () => {
const p = planReview(base({ paths: ['lib/billing.ts'], targeted: { classes: ['money'] }, shape: lean({ panel: 'off' }) }));
expect([p.depth, roles(p)]).toEqual(['full', ['adversarial-native:merged:full:lane']]);
expect(p.dispatches[0]!.classes).toEqual(['money']);
expect(p.dispatches[0]!.checklists).toContain('red-team');
const opus = planReview(base({ paths: ['.github/workflows/ci.yml'], shape: lean({ panel: 'off', readerModel: 'anthropic/claude-opus-5-5' }) }));
expect(roles(opus)).toEqual(['adversarial-native:merged:full:anthropic/claude-opus-5-5']);
});
test('docs and tests stay core only; docs with numbers keep the outside voice', () => {
expect(planReview(base({ paths: ['README.md'], gateTagged: null })).dispatches).toEqual([]);
expect(roles(planReview(base({ paths: ['docs/r.md'], numbers: scanNumbers({ 'docs/r.md': ['cost $4.10'] }) })))).toEqual(['outside-voice:outside-voice:host:-']);
});
test('panel all is today\'s panel; panel risk keeps it on risk class only', () => {
const today = (o: Partial<PlanInput>) => roles(planReview(base({ ...o, shape: undefined })));
expect(roles(planReview(base({ paths: ['lib/billing.ts'], shape: lean({ panel: 'all' }) })))).toEqual(today({ paths: ['lib/billing.ts'] }));
expect(roles(planReview(base({ paths: ['lib/billing.ts'], shape: lean({ panel: 'risk' }) })))).toEqual(today({ paths: ['lib/billing.ts'] }));
expect(planReview(base({ shape: lean({ panel: 'risk' }) })).mode).toBe('lean');
expect(planReview(base({ shape: lean({ panel: 'all' }) })).mode).toBe('panel');
});
});
describe('C6 platform depth (FOUN-32, founderstar#496): a planned skip needs the platform review at the exact head', () => {
test('runtime code never fits the platform depth, even with no runtime lines (a registered generated output, a mode-only change)', () => {
const none = scanNumbers({});
expect(platformFits('code', ['dist/app.js'], 0, none)).toBe(false);
expect(platformFits('gate', ['scripts/start.sh'], 0, none)).toBe(false);
expect(platformFits('docs', ['docs/a.md'], 0, none)).toBe(true);
expect(platformFits('tests', ['test/a.test.ts'], 0, none)).toBe(true);
});
test('verify marks an outside-voice diff file changed after render as a mismatch', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'payload-'));
const ledger = path.join(dir, 'ledger.jsonl'), brief = path.join(dir, 'brief.md'), diff = path.join(dir, 'outside-voice.diff');
fs.writeFileSync(brief, 'brief'); fs.writeFileSync(diff, '+code\n');
const sha = (s: string) => createHash('sha256').update(s).digest('hex');
appendEvent(ledger, { event: 'rendered', reader: 'outside-voice', role: 'outside-voice', file: brief, sha256: sha('brief'), payload: { file: diff, sha256: sha('+code\n') } } as any);
expect(verify(ledger).rows.find(r => r.kind === 'BRIEF')!.detail).not.toContain('payload_changed_since_render');
fs.writeFileSync(diff, '+docs only\n');
const row = verify(ledger).rows.find(r => r.kind === 'BRIEF')!;
expect([row.status, row.detail.includes('payload_changed_since_render')]).toEqual(['MISMATCH', true]);
});
const on = (review: ReviewShape['platformReview']) => lean({ platform: 'on', platformReview: review });
const docs = { paths: ['docs/howto-install.md', 'test/a.test.ts'] };
test('platform on with the review at HEAD on a docs or tests diff: no reader, a planned SKIP for every reader, the core read included', () => {
const p = planReview(base({ ...docs, shape: on({ id: '5744388738', head: HEAD, reviewer: 'capy-ai[bot]' }) }));
expect([p.mode, p.next.step, p.dispatches]).toEqual(['platform', 'platform', []]);
expect(p.skipped.filter(s => /planned skip/.test(s)).length).toBeGreaterThanOrEqual(2);
expect(p.skipped.join('\n')).toMatch(/core.*planned skip.*5744388738/);
});
test('without the evidence, or with it at another head, the core read stands', () => {
expect(planReview(base({ ...docs, shape: on(null) })).mode).toBe('lean');
expect(planReview(base({ ...docs, shape: on({ id: '1', head: 'b'.repeat(40), reviewer: 'capy-ai[bot]' }) })).next.step).toBe('full');
});
test('FOUN-64 item 2: runtime code, results or prereg paths never take the platform depth', () => {
const ev = { id: '1', head: HEAD, reviewer: 'capy-ai[bot]' };
expect(roles(planReview(base({ shape: on(ev) })))).toEqual(['adversarial-native:single:full:lane']);
expect(planReview(base({ paths: ['docs/benchmarks/run/results/summary.md'], shape: on(ev) })).mode).toBe('lean');
expect(planReview(base({ paths: ['evals/prereg.md'], shape: on(ev) })).mode).toBe('lean');
});
test('risk class and published numbers never take the platform depth', () => {
const ev = { id: '1', head: HEAD, reviewer: 'capy-ai[bot]' };
expect(planReview(base({ paths: ['lib/billing.ts'], shape: on(ev) })).mode).toBe('panel');
expect(planReview(base({ paths: ['src/app.ts', 'docs/r.md'], numbers: scanNumbers({ 'docs/r.md': ['p50 4.7 h'] }), shape: on(ev) })).mode).toBe('lean');
});
test('under the default panel all, platform on still takes the platform depth at HEAD; everything else keeps today\'s panel', () => {
const ev = { id: '1', head: HEAD, reviewer: 'capy-ai[bot]' };
const all = (review: ReviewShape['platformReview']) => lean({ panel: 'all', platform: 'on', platformReview: review });
expect(planReview(base({ ...docs, shape: all(ev) })).mode).toBe('platform');
expect(planReview(base({ ...docs, shape: all(null) })).mode).toBe('panel');
expect(planReview(base({ shape: all(ev) })).mode).toBe('panel');
});
test('the platform skip is pass 1 only: a later step plans as lean', () => {
const p = planReview(base({ shape: on({ id: '1', head: HEAD, reviewer: 'capy-ai[bot]' }), history: { fullPasses: 1, last: 'full', fixedBlocking: true } }));
expect(p.next.step).toBe('fix-check');
});
});
describe('R65 lean loop: one fix round, never more than 2 rounds', () => {
const h = (o: object) => ({ fullPasses: 1, last: 'full' as const, lean: true, ...o });
test('non-risk: a fixed blocker earns one fix-check; below-floor fixes ship as notes', () => {
expect(nextStep(h({ fixedBlocking: true }), 'light').step).toBe('fix-check');
expect(nextStep(h({ fixedBelow: true }), 'light')).toMatchObject({ step: 'done', reason: expect.stringContaining('notes') });
});
test('risk: any fix earns one scoped fix-check, a gate-class edit included, never a second full pass', () => {
for (const o of [{ fixedBlocking: true }, { fixedBelow: true }, { fixedBelow: true, touchedGate: true }]) expect(nextStep(h(o), 'full').step).toBe('fix-check');
});
test('a fix-check blocker stops for the owner; a recorded grant adds one fix-check, never two', () => {
expect(nextStep(h({ last: 'fix-check', fixedBlocking: true, fixChecks: 1 }), 'full').step).toBe('stop');
expect(nextStep(h({ last: 'fix-check', fixedBlocking: true, fixChecks: 1, granted: 'apr_1' }), 'full').step).toBe('fix-check');
expect(nextStep(h({ last: 'fix-check', fixedBlocking: true, fixChecks: 2, granted: 'apr_1' }), 'full').step).toBe('stop');
expect(nextStep(h({ last: 'fix-check', fixedBlocking: true, granted: 'apr_1' }), 'full').step).toBe('stop');
});
test('planReview marks the loop lean and a fix-check dispatches one full-effort reader', () => {
expect(roles(planReview(base({ paths: ['lib/billing.ts'], history: { fullPasses: 1, last: 'full', fixedBlocking: true }, shape: lean({ panel: 'off' }) })))).toEqual(['fix-check:fix-check:full:-']);
});
});
describe('C7 merged reader brief and the outside voice reading its diff from disk', () => {
const t = sectionTexts(ROOT);
const sha = 'c'.repeat(40);
test('the merged reader renders the targeted brief and every checklist in one native adversarial context', () => {
const brief = fs.readFileSync(path.join(ROOT, 'review/specialists/targeted.md'), 'utf8');
const text = renderBrief('adversarial-native', { diff_base: sha, checklists: '/x/testing.md,/x/red-team.md', classes: 'money', brief, checklist: '/x/targeted.md' }, t);
expect(text).toContain(t.native.slice(0, 60));
expect(text).toContain('/x/testing.md, /x/red-team.md');
expect(text).toMatch(/money/);
});
test('past the cap the outside voice reads a path-scoped slice that names what it left out (FOUN-32 §3)', () => {
const sec = (p: string, n: number) => `diff --git a/${p} b/${p}\n+${'x'.repeat(n)}\n`;
const s = sliceDiff(sec('a.ts', 10) + sec('big.json', 500) + sec('b.ts', 10), 100);
expect([s.sliced, s.kept, s.files, s.left]).toEqual([true, 2, 3, ['big.json']]);
expect(s.text).toContain('b/b.ts');
expect(sliceNote(s, 100)).toMatch(/2 of 3 files.*big\.json/);
expect(sliceDiff(sec('a.ts', 10), 100).sliced).toBe(false);
});
test('the outside voice gets a path and a hash, not the pasted diff', () => {
const text = renderBrief('outside-voice', { diff_base: sha, diff_file: '/tmp/o/outside-voice.diff', diff_sha256: 'd'.repeat(64), diff_bytes: '1234' }, t);
expect(text).toContain('/tmp/o/outside-voice.diff');
expect(text).toContain('d'.repeat(64));
expect(text).not.toContain('```diff');
});
});
describe('ship policy: the base-read review key', () => {
const codes = (raw: object) => validatePolicy(raw).map(e => `${e.code}:${e.path}`);
test('defaults: panel all (today\'s panel; risk is opt-in until its no-loss replay passes), the lane\'s native reader, platform off, no extra risk paths', () => expect(DEFAULT_POLICY.review).toEqual({ panel: 'all', reader_model: 'lane', platform: 'off', risk_paths: [] }));
test('valid values pass with min_gstack_version; bad ones name the field', () => {
const v = REVIEW_POLICY_KEY_VERSION;
expect(codes({ review: { platform: 'on', reader_model: 'anthropic/claude-opus-5-5', panel: 'risk', risk_paths: ['src/balance/**'] }, min_gstack_version: v })).toEqual([]);
expect(codes({ review: { platform: 'yes' }, min_gstack_version: v })).toEqual(['POLICY_INVALID:review.platform']);
expect(codes({ review: { panel: 'some' }, min_gstack_version: v })).toEqual(['POLICY_INVALID:review.panel']);
expect(codes({ review: { reader_model: 'a b' }, min_gstack_version: v })).toEqual(['POLICY_INVALID:review.reader_model']);
expect(codes({ review: { risk_paths: ['../x'] }, min_gstack_version: v })).toEqual(['POLICY_CONTAINMENT:review.risk_paths[0]']);
expect(codes({ review: { other: 1 }, min_gstack_version: v })).toEqual(['POLICY_INVALID:review.other']);
expect(codes({ review: { platform: 'on' } })).toContain('POLICY_INVALID:review');
});
});
+7 -3
View File
@@ -118,8 +118,11 @@ describe('what review_exclude_paths excludes and keeps', () => {
for (const f of KEPT) expect(read.has(f)).toBe(true);
const outside = briefs.find(b => b.includes('## Branch diff'))!;
expect(outside).toContain(`git diff "$DIFF_BASE" -- . ${m.readers[0].policy_exclude} and git diff "$DIFF_BASE" -- '${reader.keep[0]}'`);
for (const f of EXCLUDED) expect(outside).not.toContain(`diff --git a/${f}`);
expect(outside).toContain('diff --git a/receipts/b/result.json');
const diffFile = path.join(out, 'briefs', 'outside-voice.diff');
expect(outside).toContain(`The supplied branch diff is the file ${diffFile} `);
const supplied = fs.readFileSync(diffFile, 'utf8');
for (const f of EXCLUDED) expect(supplied).not.toContain(`diff --git a/${f}`);
expect(supplied).toContain('diff --git a/receipts/b/result.json');
for (const b of briefs) expect(b).toContain(`(selection ${m.review_exclude.digest})`);
}, 180_000);
@@ -150,7 +153,8 @@ describe('what review_exclude_paths excludes and keeps', () => {
const c = JSON.parse(plan(control, ['--json']).out);
const w = JSON.parse(plan(with_, ['--json']).out);
expect(c.numbers.publishes).toBe(true);
expect(c.targeted.classes).toEqual(['money', 'secret']);
// HARN-1 (harness-evals#231): a JSON data file is judged by its path, so cost_usd names no money even unexcluded; its secret line still counts
expect(c.targeted.classes).toEqual(['secret']);
expect(w.review_exclude.kept_files).toEqual([{ path: 'receipts/s.json', reason: 'secret-line' }]);
expect(w.numbers.publishes).toBe(false);
expect(w.targeted.classes).toEqual(['secret']);
+12 -7
View File
@@ -94,20 +94,23 @@ describe('pass manifest', () => {
expect(() => renderStage(d, 'specialists', texts, () => '')).toThrow(/cannot read audit\/plan's prompt file/);
});
test('a brief over 1 MiB with invalid UTF-8 in its diff travels in bounded chunks that rejoin to the rendered sha', () => {
const m = manifest([{ role: 'outside-voice', name: 'outside-voice', effort: 'host', exclude: [] }]);
const diff = Buffer.concat([Buffer.from('+line \u00e9\n'.repeat(90_000)), Buffer.from([0xff, 0xfe, 0x0a]), Buffer.from('+"quoted" \\ back\n'.repeat(20_000))]).toString('utf8');
renderStage(m, 'voices', texts, () => diff);
// The outside voice now reads its diff from a file (test/review-depth-lean.test.ts), so a large brief here is an audit
// reader's prompt file carrying the same bytes.
test('a brief over 1 MiB with invalid UTF-8 in it travels in bounded chunks that rejoin to the rendered sha', () => {
const big = path.join(tmp('rp-big-'), 'plan.md');
fs.writeFileSync(big, Buffer.concat([Buffer.from('+line \u00e9\n'.repeat(90_000)), Buffer.from([0xff, 0xfe, 0x0a]), Buffer.from('+"quoted" \\ back\n'.repeat(20_000))]));
const m = manifest([], { extras: [{ name: 'plan', prompt_file: big }] });
renderStage(m, 'specialists', texts, () => '');
const r = m.readers[0]!;
const text = fs.readFileSync(r.file!, 'utf8');
expect(Buffer.byteLength(text)).toBeGreaterThan(1024 * 1024);
expect(r.chunks).toBe(chunkCount(text));
let joined = '';
for (let i = 0; i < r.chunks!; i++) {
const c = spawnSync(path.join(BIN, 'gstack-review-dispatch'), ['chunk', '--manifest', writeManifestFile(m), '--reader', 'outside-voice', '--index', String(i)], { encoding: 'utf8', timeout: 30_000, maxBuffer: 8 * 1024 * 1024 });
const c = spawnSync(path.join(BIN, 'gstack-review-dispatch'), ['chunk', '--manifest', writeManifestFile(m), '--reader', 'audit/plan', '--index', String(i)], { encoding: 'utf8', timeout: 30_000, maxBuffer: 8 * 1024 * 1024 });
expect(c.status, c.stderr).toBe(0);
const [head, body] = c.stdout.split('\n');
expect(head).toBe('BRIEF_CHUNK: reader=outside-voice index=' + i + ' of=' + r.chunks + ' sha256=' + r.sha256);
expect(head).toBe('BRIEF_CHUNK: reader=audit/plan index=' + i + ' of=' + r.chunks + ' sha256=' + r.sha256);
expect(Buffer.byteLength(body!)).toBeLessThan(1024 * 1024);
joined += JSON.parse(body!);
}
@@ -1024,7 +1027,9 @@ describe('a scoped pass after a blocker fix (GST-619)', () => {
expect(redTeam).not.toContain(s.mergeBase);
expect(bin(s.dir, s.env, ['stage', '--manifest', s.mf, '--stage', 'voices']).status).toBe(0);
const voices = JSON.parse(fs.readFileSync(s.mf, 'utf8')) as Manifest;
expect(fs.readFileSync(voices.readers.find(r => r.reader === 'outside-voice')!.file!, 'utf8')).toContain('+export const v1 = 1;');
const outsideBrief = voices.readers.find(r => r.reader === 'outside-voice')!.file!;
expect(fs.readFileSync(outsideBrief, 'utf8')).toContain(`The supplied branch diff is the file ${path.join(path.dirname(outsideBrief), 'outside-voice.diff')} `);
expect(fs.readFileSync(path.join(path.dirname(outsideBrief), 'outside-voice.diff'), 'utf8')).toContain('+export const v1 = 1;');
const again = bin(s.dir, s.env, s.args);
expect(again.status, again.stdout + again.stderr).toBe(0);
expect(again.stdout).toMatch(/^FIX_CHECK: planned effort=full /m);
+2 -2
View File
@@ -121,7 +121,7 @@ describe('gstack-review-plan', () => {
}
expect(plan([...capStop.slice(0, -2), '--grant', id]).status).toBe(2);
expect(plan([...capStop, '--grant', '--json']).status).toBe(2);
});
}, 30_000);
test('--fix-check-prompt renders the fix-check reviewer prompt from the parent\'s files and refuses a missing one', () => {
const r = repo({ 'docs/howto.md': '# x\n' });
const w = (n: string, t: string) => { const f = path.join(r.state, n); fs.writeFileSync(f, t); return f; };
@@ -271,7 +271,7 @@ describe('gstack-review-plan: the scoped second pass (GSTA-152 D6)', () => {
expect(dirty).toMatch(/^FIX_RANGE: unread \(uncommitted or untracked edits.*\)/m);
expect(dirty).toMatch(/^NEXT: full /m);
expect(run(r, [...pass2, '--range-base']).status).toBe(2);
});
}, 30_000);
test('a fix range that touches a gate path keeps the full pass 2 without --touched-gate, and an empty range is unread', () => {
const r = repo({ 'src/a.ts': code(80), '.github/workflows/ci.yml': 'on: push\n' });
const git = (...a: string[]) => spawnSync('git', a, { cwd: r.dir, encoding: 'utf8', timeout: 10_000 });
+1 -1
View File
@@ -43,7 +43,7 @@ describe('ship source controller', () => {
test('the review plan drives the repeat, the fix-check binds under its own skill, and Step 11.5 accepts a fix-check or a planned adversarial skip', () => {
const decide = review.slice(review.indexOf('### Decide whether to repeat Step 9'), review.indexOf('**Required-probe parent gate:**'));
for (const flag of ['--fixed-blocking', '--fixed-below', '--blocking-open', '--touched-gate', '--critical-found', '--adversarial-ran', '--reviewed-numbers <fp>']) expect(decide).toContain(flag);
expect(decide).toContain('its `NEXT:` alone picks the next decision: `stop` 2, `full` 3, `fix-check` 3.5, `done` 4.');
expect(decide).toContain('its `NEXT:` alone picks the next decision: `stop` 2, `full` 3, `fix-check` 3.5, `done` 4, `platform` 4.');
expect(decide).toContain('`gstack-review-log --start review-fix-check` token');
expect(decide).toContain('`"skill":"review-fix-check"` with `--finish <token>`');
expect(compact(read('ship/sections/adversarial.md.tmpl'))).toContain("Step 9's last full-pass plan governs this step");
+84
View File
@@ -0,0 +1,84 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
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 { platformReceiptDepth, receiptDepthVerdict, receiptReviewAtHead } from '../lib/ship-receipt-depth';
import { REVIEW_POLICY_KEY_VERSION } from '../lib/ship-policy';
import { platformReviewAt } from '../lib/review-platform';
import type { ShipReceipt } from '../lib/headless-artifacts';
const BIN = path.join(import.meta.dir, '..', 'bin');
const git = (cwd: string, ...a: string[]) => { const r = spawnSync('git', a, { cwd, encoding: 'utf8', timeout: 30_000 }); if (r.status !== 0) throw new Error(r.stderr); return r.stdout.trim(); };
function repo(policy: object, files: Record<string, string>): { dir: string; head: string } {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'plat-'));
git(dir, 'init', '-q', '-b', 'main');
git(dir, 'config', 'user.email', 't@example.com'); git(dir, 'config', 'user.name', 't'); git(dir, 'config', 'commit.gpgsign', 'false');
fs.mkdirSync(path.join(dir, '.gstack'));
fs.writeFileSync(path.join(dir, '.gstack/ship-policy.json'), JSON.stringify({ min_gstack_version: REVIEW_POLICY_KEY_VERSION, ...policy }));
fs.writeFileSync(path.join(dir, 'README.md'), '# x\n');
fs.mkdirSync(path.join(dir, 'src')); fs.writeFileSync(path.join(dir, 'src/a.ts'), 'export const a = 1;\n');
git(dir, 'add', '.'); git(dir, 'commit', '-qm', 'base');
git(dir, 'update-ref', 'refs/remotes/origin/main', 'HEAD');
git(dir, 'checkout', '-qb', 'feat');
for (const [f, c] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(dir, f)), { recursive: true }); fs.writeFileSync(path.join(dir, f), c); }
git(dir, 'add', '.'); git(dir, 'commit', '-qm', 'change');
return { dir, head: git(dir, 'rev-parse', 'HEAD') };
}
let fakeBin = '';
const withGh = <T,>(head: string, fn: () => T, state = 'COMMENTED'): T => {
const prev = process.env.PATH, prevHead = process.env.FAKE_REVIEW_HEAD, prevState = process.env.FAKE_REVIEW_STATE;
process.env.PATH = fakeBin + path.delimiter + prev; process.env.FAKE_REVIEW_HEAD = head; process.env.FAKE_REVIEW_STATE = state;
try { return fn(); } finally {
process.env.PATH = prev;
if (prevHead === undefined) delete process.env.FAKE_REVIEW_HEAD; else process.env.FAKE_REVIEW_HEAD = prevHead;
if (prevState === undefined) delete process.env.FAKE_REVIEW_STATE; else process.env.FAKE_REVIEW_STATE = prevState;
}
};
let priorState: string | undefined;
beforeAll(() => {
fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), 'fakegh-'));
priorState = process.env.GSTACK_STATE_ROOT;
process.env.GSTACK_STATE_ROOT = path.join(fakeBin, 'state');
fs.writeFileSync(path.join(fakeBin, 'gh'), '#!/bin/sh\ncase "$1" in\n pr) echo \'{"number":7}\' ;;\n api) echo "[[{\\"id\\":98,\\"user\\":{\\"login\\":\\"capy-ai[bot]\\"},\\"commit_id\\":\\"$FAKE_REVIEW_HEAD\\",\\"state\\":\\"COMMENTED\\"},{\\"id\\":99,\\"user\\":{\\"login\\":\\"capy-ai[bot]\\"},\\"commit_id\\":\\"$FAKE_REVIEW_HEAD\\",\\"state\\":\\"$FAKE_REVIEW_STATE\\"}]]" ;;\nesac\n', { mode: 0o755 });
});
afterAll(() => { if (priorState === undefined) delete process.env.GSTACK_STATE_ROOT; else process.env.GSTACK_STATE_ROOT = priorState; });
describe('C6 receipt: review_depth platform, honest and bound to the exact head (FOUN-63)', () => {
test('a docs diff under review.platform on with capy-ai[bot]\'s review at HEAD records platform, and the queue accepts it at that head only', () => {
const { dir, head } = repo({ review: { platform: 'on' } }, { 'docs/howto.md': 'Run the installer.\n' });
const d = receiptReviewAtHead(dir, 'main', BIN, head).depth;
expect([d.depth, d.mode]).toEqual(['light', 'panel']);
const p = withGh(head, () => platformReceiptDepth(dir, 'main', d, head, '99'));
expect(p.ok && [p.depth.depth, p.depth.mode, p.depth.platform_review]).toEqual(['platform', 'platform', { id: '99', head, reviewer: 'capy-ai[bot]' }]);
const r = { gstack_version: REVIEW_POLICY_KEY_VERSION, review_depth: (p as any).depth } as ShipReceipt;
expect(receiptDepthVerdict(dir, 'origin/main', r, head)).toMatchObject({ ok: true, line: expect.stringContaining('platform_review=99@') });
const moved = { ...r, review_depth: { ...r.review_depth!, platform_review: { id: '99', head: 'b'.repeat(40), reviewer: 'capy-ai[bot]' } } } as ShipReceipt;
expect(receiptDepthVerdict(dir, 'origin/main', moved, head).ok).toBe(false);
});
test('refused: platform review off, runtime code, a results path, a review at another head', () => {
const off = repo({}, { 'docs/howto.md': 'x\n' });
const dOff = receiptReviewAtHead(off.dir, 'main', BIN, off.head).depth;
expect(withGh(off.head, () => platformReceiptDepth(off.dir, 'main', dOff, off.head, '99'))).toMatchObject({ ok: false, message: expect.stringContaining('review.platform on') });
for (const files of [{ 'src/b.ts': 'export const b = 2;\n' }, { 'docs/results/summary.md': 'Ran it.\n' }] as Record<string, string>[]) {
const { dir, head } = repo({ review: { platform: 'on' } }, files);
const d = receiptReviewAtHead(dir, 'main', BIN, head).depth;
expect(withGh(head, () => platformReceiptDepth(dir, 'main', d, head, '99')).ok).toBe(false);
}
const { dir, head } = repo({ review: { platform: 'on' } }, { 'docs/howto.md': 'x\n' });
const d = receiptReviewAtHead(dir, 'main', BIN, head).depth;
expect(withGh('c'.repeat(40), () => platformReceiptDepth(dir, 'main', d, head, '99'))).toMatchObject({ ok: false, message: expect.stringContaining('capy-ai[bot]') });
for (const state of ['CHANGES_REQUESTED', 'DISMISSED']) expect(withGh(head, () => platformReceiptDepth(dir, 'main', d, head, '99'), state).ok).toBe(false);
expect(withGh(head, () => platformReceiptDepth(dir, 'main', d, head, '99'), 'APPROVED').ok).toBe(true);
expect(withGh(head, () => platformReviewAt(dir, head), 'CHANGES_REQUESTED').ok).toBe(false);
expect(withGh(head, () => platformReviewAt(dir, head))).toMatchObject({ ok: true, review: { id: '99' } });
expect(withGh(head, () => platformReviewAt(dir, head, { id: '98' })).ok).toBe(false);
});
test('the receipt records the risk_paths class and the panel mode the base policy gives', () => {
const { dir, head } = repo({ review: { risk_paths: ['src/balance/**'] } }, { 'src/balance/x.ts': 'export const x = 1;\n' });
const d = receiptReviewAtHead(dir, 'main', BIN, head).depth;
expect([d.depth, d.risk, d.mode]).toEqual(['full', ['policy'], 'panel']);
});
});
@@ -0,0 +1,4 @@
{
"bytes": 580,
"why": "GSTA-258 C: risk-scaled review depth (single/merged reader lines, NEXT: platform planned skip with its PLATFORM_REVIEW id, round 2 on risk class) and GST-1129 --declare-risk"
}