test(review): one file per review and harness pin, so independent repins merge cleanly (GST-1084)

The reviewer-brief pins move from the table in test/review-packets-removed.test.ts to
test/fixtures/review-pins/{files,replays,brief-bodies}/*.json, and the harness pins from
scripts/harness-version.json to scripts/harness-pins/<path>.json. Every entry is ported
unchanged. New tests prove a stale pin still fails and that two repins of different pins
merge cleanly with a real git merge-tree.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-03 01:55:27 +00:00
1 parent 14d7414af3
commit 12f820954a
97 files changed
+371 -139

No files matched your search

@@ -0,0 +1,8 @@
### Changed
- **The review pins and the harness pins are one file per pin, so PRs that repin different files no longer conflict (GST-1084).** The reviewer-brief pins that lived in one table in `test/review-packets-removed.test.ts` now live under `test/fixtures/review-pins/`: `files/<pinned path>.json` holds a file's sha and the replay that admits it, `replays/<slug>.json` holds one replay's written no-loss justification, and `brief-bodies/<name>.json` holds one brief body's sha. The harness pins in `scripts/harness-version.json` moved to `scripts/harness-pins/<path>.json`; the manifest keeps only the version and the patterns, so `HARNESS_VERSION` and its readers are unchanged. Before, two PRs that repinned adjacent entries conflicted on the shared table (at least five main-merge conflicts on 2026-11-02, two of them on #3852). Every entry is ported unchanged, including the combined GST-508 entries from #3852.
### For contributors
- The checks are at least as strict as before. Every pin still needs its sha and a replay whose written justification names a no-loss replay or re-score, and the brief-body check still guards the bodies. A malformed or misnamed pin file, a stray file in a pin directory, or a `files` block back in `scripts/harness-version.json` now fails too. New tests prove that a stale pin still fails, and that two branches repinning different, adjacent pins merge cleanly with a real `git merge-tree`, while two repins of one pin still conflict.
- `bun run scripts/bump-harness-version.ts` writes and deletes pin files itself. Repin a review file by editing its `files/` pin and adding the replay file it names.
+6 -4
View File
@@ -1001,8 +1001,10 @@ paid test file, its fixtures and its skills' prompt files, including the
generated SKILL.md) plus `HARNESS_VERSION`, keyed with model, Claude CLI version
and policy version (`scripts/eval-trial-series.ts`). Each trial also records the
full consumed-input fingerprint as provenance. `scripts/harness-version.json`
pins every shared-harness file (`GLOBAL_TOUCHFILES` plus the session runners, the
PTY harness, the e2e gate and the LLM judge) by blob hash. When
holds the version and the harness patterns, and `scripts/harness-pins/<path>.json`
pins each shared-harness file (`GLOBAL_TOUCHFILES` plus the session runners, the
PTY harness, the e2e gate and the LLM judge) by blob hash, one file per pin, so
two PRs that record decisions for different harness files never conflict. When
`test/harness-version.test.ts` fails, run one of:
```bash
@@ -1010,8 +1012,8 @@ bun run scripts/bump-harness-version.ts --non-behavioral "<why no verdict can ch
bun run scripts/bump-harness-version.ts --bump "<what behavior changed>" # new series for every case
```
On a merge conflict in the manifest, take the higher version, then rerun the
script.
On a merge conflict in the manifest or a pin file, take the higher version, then
rerun the script.
## Ubicloud VMs (`bun run test:ubicloud`)
+37 -13
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* Keep HARNESS_VERSION honest (CEO-27/ENG-3/DX-6). scripts/harness-version.json
* pins the blob of every shared-harness file (GLOBAL_TOUCHFILES plus its
* `patterns`). test/harness-version.test.ts fails when a harness file changes
* holds the version and the harness `patterns`; scripts/harness-pins/<path>.json
* pins the blob of every shared-harness file (GLOBAL_TOUCHFILES plus those
* patterns). test/harness-version.test.ts fails when a harness file changes
* without a decision, and this script records the decision:
*
* --bump "<reason>" the change alters how cases behave: version + 1,
@@ -12,9 +13,10 @@
* compatible and no series resets
* --check print drift and the fix; exit 1 on drift
*
* Entries are one line per file, so two PRs touching different harness files
* merge cleanly. When both bump, or both touch one file, git conflicts on the
* manifest: take the higher version, then rerun this script.
* Each pin is its own file (GST-1084), so two PRs touching different harness
* files never edit the same file and merge cleanly. When both bump, or both
* touch one file, git conflicts on the manifest or that pin: take the higher
* version, then rerun this script.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
@@ -23,14 +25,31 @@ import { harnessFiles, treeEntries, type TreeEntry } from './eval-trial-series';
const ROOT = path.resolve(import.meta.dir, '..');
export const MANIFEST = 'scripts/harness-version.json';
export const PINS_DIR = 'scripts/harness-pins';
export const DOC_ANCHOR = 'docs/TESTING_INTERNALS.md#harness-version';
export interface ManifestEntry { blob: string; since: number; nonBehavioral?: string }
export interface HarnessManifest { version: number; patterns: string[]; files: Record<string, ManifestEntry> }
export interface Drift { changed: string[]; added: string[]; removed: string[] }
const pinPath = (root: string, file: string) => path.join(root, PINS_DIR, `${file}.json`);
/** The pin files under root's PINS_DIR, keyed by pinned path; any file that is not <path>.json is an error. */
function readPins(root: string): Record<string, ManifestEntry> {
const dir = path.join(root, PINS_DIR);
const files: Record<string, ManifestEntry> = {};
for (const rel of (fs.readdirSync(dir, { recursive: true }) as string[]).map(f => f.split(path.sep).join('/')).sort()) {
if (fs.statSync(path.join(dir, rel)).isDirectory()) continue;
if (!rel.endsWith('.json')) throw new Error(`${PINS_DIR}/${rel}: every harness pin is <pinned path>.json`);
files[rel.slice(0, -'.json'.length)] = JSON.parse(fs.readFileSync(path.join(dir, rel), 'utf8'));
}
return files;
}
export function readManifest(root = ROOT): HarnessManifest {
return JSON.parse(fs.readFileSync(path.join(root, MANIFEST), 'utf8'));
const head = JSON.parse(fs.readFileSync(path.join(root, MANIFEST), 'utf8'));
if (Object.keys(head).sort().join() !== 'patterns,version') throw new Error(`${MANIFEST} holds only version and patterns; pins live in ${PINS_DIR}/<path>.json`);
return { version: head.version, patterns: head.patterns, files: readPins(root) };
}
/** The tracked harness files with blobs of their working-tree bytes (an unstaged edit counts; a deleted file drops out). */
@@ -64,7 +83,7 @@ export function formatDrift(drift: Drift, version: number): string {
'Fix (pick one):',
' bun run scripts/bump-harness-version.ts --non-behavioral "<why no verdict can change>" # refactor/comment/logging: no series reset',
' bun run scripts/bump-harness-version.ts --bump "<what behavior changed>" # starts a new pass-rate series for every case',
`Merge conflict in ${MANIFEST}: take the higher version, then rerun the script. See ${DOC_ANCHOR}.`,
`Merge conflict in ${MANIFEST} or ${PINS_DIR}/: take the higher version, then rerun the script. See ${DOC_ANCHOR}.`,
].join('\n');
}
@@ -82,11 +101,16 @@ export function decide(manifest: HarnessManifest, files: TreeEntry[], decision:
: manifest.files[entry.file]!])) };
}
/** Stable, one-entry-per-line JSON so concurrent edits to different files merge cleanly. */
export function serializeManifest(manifest: HarnessManifest): string {
const files = Object.keys(manifest.files).sort().map(file => ` ${JSON.stringify(file)}: ${JSON.stringify(manifest.files[file])}`);
return `{\n "version": ${manifest.version},\n "patterns": [\n${manifest.patterns.map(pattern => ` ${JSON.stringify(pattern)}`).join(',\n')}\n ],\n`
+ ` "files": {\n${files.join(',\n')}\n }\n}\n`;
/** Write the manifest head and one pin file per harness file, rewriting only pins whose bytes change and deleting dropped ones. */
export function writeManifest(root: string, manifest: HarnessManifest): void {
fs.writeFileSync(path.join(root, MANIFEST), `${JSON.stringify({ version: manifest.version, patterns: manifest.patterns }, null, 2)}\n`);
for (const file of Object.keys(readPins(root))) if (!manifest.files[file]) fs.rmSync(pinPath(root, file));
for (const [file, entry] of Object.entries(manifest.files)) {
const target = pinPath(root, file), text = `${JSON.stringify(entry)}\n`;
if (fs.existsSync(target) && fs.readFileSync(target, 'utf8') === text) continue;
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, text);
}
}
if (import.meta.main) {
@@ -106,7 +130,7 @@ if (import.meta.main) {
}
if (mode === '--non-behavioral' && !hasDrift(drift)) { console.log('no harness drift: nothing to record'); process.exit(0); }
const next = decide(manifest, files, mode === '--bump' ? 'bump' : 'non-behavioral', argv[1] ?? '');
fs.writeFileSync(path.join(ROOT, MANIFEST), serializeManifest(next));
writeManifest(ROOT, next);
console.log(mode === '--bump'
? `HARNESS_VERSION ${manifest.version} -> ${next.version}: every case starts a new pass-rate series (reason: ${argv[1]})`
: `recorded ${drift.changed.length + drift.added.length} file(s) as non-behavioral at HARNESS_VERSION ${next.version}${drift.removed.length ? `, dropped ${drift.removed.length} removed file(s)` : ''}`);
@@ -0,0 +1 @@
{"blob":"f862cf5241580c7593b9ee62eacf5ef046248d26","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"}
@@ -0,0 +1 @@
{"blob":"e8ce3cb98474dded38c5128a631d283b0ca35b63","since":6,"nonBehavioral":"F1 rule-16 flaky rerun: report aggregation, rerun planning and the gate slice exit code; no case's execution, trial outcome, trial record or panel verdict changes, so no pass-rate series resets"}
@@ -0,0 +1 @@
{"blob":"e0d18da4ea02ac443835a68992abeea2b47f22fc","since":6,"nonBehavioral":"the headline adds only streamed session cost; a billed session's cost is already in its collector total; cache writes without a 5m key fall back to the total minus 1h; cost fields only"}
@@ -0,0 +1 @@
{"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":6}
@@ -0,0 +1 @@
{"blob":"dc714d7d897aed185c0ec3d2c92ad22d954039e5","since":6,"nonBehavioral":"createShardSandbox drops the Capy launch markers (GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL, GSTACK_INSTALL_VERDICT) from shard environments; CI never sets them, so no case's input or verdict changes there, and on a Capy machine fixtures now see the session kind they set themselves instead of the installer's"}
@@ -0,0 +1 @@
{"blob":"5439d0a8f6e1569fa7c924af20d67c30c81d5d30","since":6,"nonBehavioral":"credential rejections are classed infra instead of assertion: no case's pass or fail changes; only failed trials whose provider rejected the run's key move class, so no rerun spends on a rejected key"}
@@ -0,0 +1 @@
{"blob":"dd0ca92bcf8493fd5c1e9d399cc3bc74ec70c469","since":6,"nonBehavioral":"FREE_ONLY_PR_FILES gains USING_GBRAIN_WITH_GSTACK.md (a root guide no skill prompt or paid fixture reads); it changes only which PR paths select paid cases, never how a case runs or is judged (GST-1027)"}
@@ -0,0 +1 @@
{"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":6}
@@ -0,0 +1 @@
{"blob":"d66dac458b8c13250973e550edf8087d4044cd0b","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets. Also: runAgentSdkTest now validates systemPrompt before starting; every live caller already passed the claude_code preset, so the prompt the model sees is unchanged and no verdict can change"}
@@ -0,0 +1 @@
{"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":6}
@@ -0,0 +1 @@
{"blob":"ce7a5eaa66e502a052ea9c53348dd2ab25db6148","since":6,"nonBehavioral":"Removes the plan-design 0D focus picker from the shared harness; floor.ts gains an optional pickSetupChoice hook that no other caller passes, so no other case's input or verdict can change. plan-design-finding-floor, the only case whose seed raises that menu, passes the picker from its owned fixture test/fixtures/design-floor-focus-picker.ts, so its owned bytes start its own new series."}
@@ -0,0 +1 @@
{"blob":"bff225017e235f0be6ece29f04971ac04b95b544","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"}
@@ -0,0 +1 @@
{"blob":"c040ebbfa815b8cce19ef80c6133bd5a5474149a","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"}
@@ -0,0 +1 @@
{"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":6}
@@ -0,0 +1 @@
{"blob":"91996ff2bc303ee29f4e6f1705d1468b2536b32e","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"}
@@ -0,0 +1 @@
{"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":6}
@@ -0,0 +1 @@
{"blob":"4085ad18cd99698f675deadad2a76a796427b693","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"}
@@ -0,0 +1 @@
{"blob":"cc072895461dc52ea317d1beff72121342420d30","since":6}
@@ -0,0 +1 @@
{"blob":"ff32d03f32e4af76c6c7314a79cd1b35263ed0dd","since":6,"nonBehavioral":"llm-judge.ts adds a pass-through byte counter (bytesReceived, lastByteMs) to the aborted-judge diagnostic; the request, response bytes, deadline and scoring are unchanged. GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading so the review E2E cases extract the same section (now the skill's own reworded bytes); llm-judge.ts changes one bullet of the cross-skill consistency judge's intended architecture to name review-bot-history.md beside the older greptile-history.md name, so its calibration corpus (greptile-history inputs) stays valid. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own series"}
@@ -0,0 +1 @@
{"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":6}
@@ -0,0 +1 @@
{"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":6}
@@ -0,0 +1 @@
{"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":6}
@@ -0,0 +1 @@
{"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":6}
@@ -0,0 +1 @@
{"blob":"c09b69611fb22ddf093adb2baa83434494e5f8fa","since":6,"nonBehavioral":"Widens the two plan-eng-review boundary predicates' qid regex to also accept the optional ;signal=<key> hint suffix (#3442). Only a question carrying that suffix can change verdict, and only the changed question-tuning resolver asks for it; every case that runs a skill with that resolver owns its generated SKILL.md, which this change also edits, so those cases already start their own series. No other case's input or verdict can change."}
@@ -0,0 +1 @@
{"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":6}
@@ -0,0 +1 @@
{"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":6}
@@ -0,0 +1 @@
{"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":6}
@@ -0,0 +1 @@
{"blob":"4f0731ef39bc3a6a7c5c592ce98690a24cb1ed7f","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
@@ -0,0 +1 @@
{"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":6}
@@ -0,0 +1 @@
{"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":6}
@@ -0,0 +1 @@
{"blob":"a9938cb98f285474ebc4860cd61012504a36466e","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"}
@@ -0,0 +1 @@
{"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":6}
@@ -0,0 +1 @@
{"blob":"1384f765e48e511d5f843df2660b2ec70bbcb6b2","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"}
@@ -0,0 +1 @@
{"blob":"8fd1749a62f24dc37ae186c8a21c2165f7f7645e","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
@@ -0,0 +1 @@
{"blob":"08f7ccbdbd9d71a5dc86edd2f8fced423a78ad9a","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
@@ -0,0 +1 @@
{"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":6}
@@ -0,0 +1 @@
{"blob":"c99baaaa0a77a5a72384493fd8a85a4253ecb05d","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
@@ -0,0 +1 @@
{"blob":"c73b2596d9eb5377530a6bf1d284e0b6ea1556a0","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"}
@@ -0,0 +1 @@
{"blob":"ce7609eacc7a8265a4a211358d1b4fb49ea85bc5","since":6,"nonBehavioral":"GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading ('Check for review bot comments') so the review E2E cases extract the same section, now the skill's own reworded bytes; llm-judge.ts updates only the cross-skill consistency judge's intended architecture to the renamed review-bot-history.md and its record helper. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own new series"}
@@ -0,0 +1 @@
{"blob":"a8d21e3817c3f35a1d06af325c92fde7d420e377","since":6,"nonBehavioral":"readSkillEffective also appends a variant's added ### subsections (office-hours' Open-ended questions); readSkillStartup, readSharedSections and readSkillEffective return bytes identical to the pinned helper for every tier 2+ render without added subsections (735 across all hosts) and for office-hours' SKILL.md before this PR, so office-hours' fixture input changes only because its prompt changed in this PR, not because the helper did"}
@@ -0,0 +1 @@
{"blob":"e1b54ad5e3d3eadd8027de1b44e57554d6d99a41","since":6,"nonBehavioral":"selection only: fragment exemption narrowed to .md files directly in the fragments dir; changed-file lists include a rename's old path; no case prompt, fixture, judge or verdict logic changes (GST-400)"}
@@ -0,0 +1 @@
{"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":6}
+1 -45
View File
@@ -9,49 +9,5 @@
"test/helpers/pty-screen.ts",
"test/helpers/e2e-gate.ts",
"test/helpers/llm-judge.ts"
],
"files": {
"scripts/lib/paid-cases.ts": {"blob":"f862cf5241580c7593b9ee62eacf5ef046248d26","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"},
"scripts/lib/paid-plan.ts": {"blob":"e8ce3cb98474dded38c5128a631d283b0ca35b63","since":6,"nonBehavioral":"F1 rule-16 flaky rerun: report aggregation, rerun planning and the gate slice exit code; no case's execution, trial outcome, trial record or panel verdict changes, so no pass-rate series resets"},
"scripts/lib/paid-report.ts": {"blob":"e0d18da4ea02ac443835a68992abeea2b47f22fc","since":6,"nonBehavioral":"the headline adds only streamed session cost; a billed session's cost is already in its collector total; cache writes without a 5m key fall back to the total minus 1h; cost fields only"},
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":6},
"scripts/lib/shard-engine.ts": {"blob":"dc714d7d897aed185c0ec3d2c92ad22d954039e5","since":6,"nonBehavioral":"createShardSandbox drops the Capy launch markers (GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL, GSTACK_INSTALL_VERDICT) from shard environments; CI never sets them, so no case's input or verdict changes there, and on a Capy machine fixtures now see the session kind they set themselves instead of the installer's"},
"scripts/test-paid-shards.ts": {"blob":"5439d0a8f6e1569fa7c924af20d67c30c81d5d30","since":6,"nonBehavioral":"credential rejections are classed infra instead of assertion: no case's pass or fail changes; only failed trials whose provider rejected the run's key move class, so no rerun spends on a rejected key"},
"scripts/test-pr-profile.ts": {"blob":"dd0ca92bcf8493fd5c1e9d399cc3bc74ec70c469","since":6,"nonBehavioral":"FREE_ONLY_PR_FILES gains USING_GBRAIN_WITH_GSTACK.md (a root guide no skill prompt or paid fixture reads); it changes only which PR paths select paid cases, never how a case runs or is judged (GST-1027)"},
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":6},
"test/helpers/agent-sdk-runner.ts": {"blob":"d66dac458b8c13250973e550edf8087d4044cd0b","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets. Also: runAgentSdkTest now validates systemPrompt before starting; every live caller already passed the claude_code preset, so the prompt the model sees is unchanged and no verdict can change"},
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":6},
"test/helpers/claude-pty-runner.ts": {"blob":"ce7a5eaa66e502a052ea9c53348dd2ab25db6148","since":6,"nonBehavioral":"Removes the plan-design 0D focus picker from the shared harness; floor.ts gains an optional pickSetupChoice hook that no other caller passes, so no other case's input or verdict can change. plan-design-finding-floor, the only case whose seed raises that menu, passes the picker from its owned fixture test/fixtures/design-floor-focus-picker.ts, so its owned bytes start its own new series."},
"test/helpers/codex-host-runner.ts": {"blob":"bff225017e235f0be6ece29f04971ac04b95b544","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"},
"test/helpers/codex-session-runner.ts": {"blob":"c040ebbfa815b8cce19ef80c6133bd5a5474149a","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"},
"test/helpers/e2e-gate.ts": {"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":6},
"test/helpers/e2e-helpers.ts": {"blob":"91996ff2bc303ee29f4e6f1705d1468b2536b32e","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"},
"test/helpers/eval-budgets.ts": {"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":6},
"test/helpers/eval-store.ts": {"blob":"4085ad18cd99698f675deadad2a76a796427b693","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"},
"test/helpers/hermetic-env.ts": {"blob":"cc072895461dc52ea317d1beff72121342420d30","since":6},
"test/helpers/llm-judge.ts": {"blob":"ff32d03f32e4af76c6c7314a79cd1b35263ed0dd","since":6,"nonBehavioral":"llm-judge.ts adds a pass-through byte counter (bytesReceived, lastByteMs) to the aborted-judge diagnostic; the request, response bytes, deadline and scoring are unchanged. GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading so the review E2E cases extract the same section (now the skill's own reworded bytes); llm-judge.ts changes one bullet of the cross-skill consistency judge's intended architecture to name review-bot-history.md beside the older greptile-history.md name, so its calibration corpus (greptile-history inputs) stays valid. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own series"},
"test/helpers/paid-test-set.ts": {"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":6},
"test/helpers/pty-screen.ts": {"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":6},
"test/helpers/pty/auq.ts": {"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":6},
"test/helpers/pty/binary.ts": {"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":6},
"test/helpers/pty/boundaries.ts": {"blob":"c09b69611fb22ddf093adb2baa83434494e5f8fa","since":6,"nonBehavioral":"Widens the two plan-eng-review boundary predicates' qid regex to also accept the optional ;signal=<key> hint suffix (#3442). Only a question carrying that suffix can change verdict, and only the changed question-tuning resolver asks for it; every case that runs a skill with that resolver owns its generated SKILL.md, which this change also edits, so those cases already start their own series. No other case's input or verdict can change."},
"test/helpers/pty/classify.ts": {"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":6},
"test/helpers/pty/fake-session.ts": {"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":6},
"test/helpers/pty/judge.ts": {"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":6},
"test/helpers/pty/launch.ts": {"blob":"4f0731ef39bc3a6a7c5c592ce98690a24cb1ed7f","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
"test/helpers/pty/plan-native.ts": {"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":6},
"test/helpers/pty/runners/counting.ts": {"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":6},
"test/helpers/pty/runners/floor.ts": {"blob":"a9938cb98f285474ebc4860cd61012504a36466e","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"},
"test/helpers/pty/runners/observation.ts": {"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":6},
"test/helpers/pty/screen.ts": {"blob":"1384f765e48e511d5f843df2660b2ec70bbcb6b2","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"},
"test/helpers/pty/session.ts": {"blob":"8fd1749a62f24dc37ae186c8a21c2165f7f7645e","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
"test/helpers/pty/transcript-cost.ts": {"blob":"08f7ccbdbd9d71a5dc86edd2f8fced423a78ad9a","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
"test/helpers/session-drain-policy.ts": {"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":6},
"test/helpers/session-ledger.ts": {"blob":"c99baaaa0a77a5a72384493fd8a85a4253ecb05d","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
"test/helpers/session-runner.ts": {"blob":"c73b2596d9eb5377530a6bf1d284e0b6ea1556a0","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"},
"test/helpers/skill-fixture.ts": {"blob":"ce7609eacc7a8265a4a211358d1b4fb49ea85bc5","since":6,"nonBehavioral":"GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading ('Check for review bot comments') so the review E2E cases extract the same section, now the skill's own reworded bytes; llm-judge.ts updates only the cross-skill consistency judge's intended architecture to the renamed review-bot-history.md and its record helper. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own new series"},
"test/helpers/skill-startup.ts": {"blob":"a8d21e3817c3f35a1d06af325c92fde7d420e377","since":6,"nonBehavioral":"readSkillEffective also appends a variant's added ### subsections (office-hours' Open-ended questions); readSkillStartup, readSharedSections and readSkillEffective return bytes identical to the pinned helper for every tier 2+ render without added subsections (735 across all hosts) and for office-hours' SKILL.md before this PR, so office-hours' fixture input changes only because its prompt changed in this PR, not because the helper did"},
"test/helpers/test-selection.ts": {"blob":"e1b54ad5e3d3eadd8027de1b44e57554d6d99a41","since":6,"nonBehavioral":"selection only: fragment exemption narrowed to .md files directly in the fragments dir; changed-file lists include a rename's old path; no case prompt, fixture, judge or verdict logic changes (GST-400)"},
"test/helpers/touchfiles.ts": {"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":6}
}
]
}
+1
View File
@@ -0,0 +1 @@
{"sha":"fbe52f3a9e0f4442eb755f1747a95944b373b439bd05c61f3eb68345d1061a5b"}
+1
View File
@@ -0,0 +1 @@
{"sha":"aad667b5ea4c6d23d930dafffa38178c04be388cd4364fb73759fc92af17bb63"}
+1
View File
@@ -0,0 +1 @@
{"sha":"a7cc08fb31b6ca65932a316d589f0b09677426d193d63f88ec74fee6d93ce053"}
+1
View File
@@ -0,0 +1 @@
{"sha":"bd96406da28b03742384560425a333b6d6d7ed2be95bd2026c90e0039632cee2"}
+1
View File
@@ -0,0 +1 @@
{"sha":"6dd670ea040535ca6cceab81c29a395bb67ce5a2ab8fa2b81e51bc4711e4aada"}
@@ -0,0 +1 @@
{"sha":"1ebe47ddb3291a9e4fe90b5a260eb57e328144959549bc95c119b68d23652cb5"}
@@ -0,0 +1 @@
{"sha":"d76a9afcfadfe0a95b15428c04e6f7a88c6b0fa75591201c45f023fec2bb90db","replay":"GST-508+R51+GST-911+GST-967"}
@@ -0,0 +1 @@
{"sha":"126eff0f1de6982e03dc24d8017c67e6807c91dfa518b18d2db3aa910aac911f","replay":"GST-508+R51+GST-911"}
@@ -0,0 +1 @@
{"sha":"a32963ef38d9d3613c58c805e8517cd2e593c63769b30dd7d40e2592fa48fc78","replay":"GSTA-224 H1"}
@@ -0,0 +1 @@
{"sha":"b9205f88313cc64902c0b13e9685722ab8138d3dcc58916154f47274abda7d83","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"f570db344c9a01df633bcaa281d6017f4f12420fd3e0aa3078c1ce06e0df8332","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"e6962fa25b78f362654f2954f79683e524d2ebd452d1494f8a2b099888e8e255","replay":"G1"}
@@ -0,0 +1 @@
{"sha":"78329b64640260cd8831ca81a9189f3f8dcade9c406431631d3f2cd798cd3967","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"263d23ac119dd601d315c191dbdbd503d47c00264c9c0ca81959559ca11d4e95","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"b6fd9eb229002ea598f8fe9ff53b1cd8821e3bd37a7aa7b07b5526556c71ebca","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"f8015d549e70297f782f0aabf6fe3a9fe31ec5ed61c311ab854e4a858282ed9b","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"545c294ae53638b4c8524e8cde08246a4ce3b5c287ec7c44e27f5556a3b0e8cc","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"9ea05149f5b13d6a19ecec26285142d09d259cf80a515af828abdd1f43320427","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"17bfe813eb3de22835af048ec359770720f1d7f371a26460169f7e32b1bcac9f","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"f33846b1dcf2fdeef82d0744ab2ef48fb4a33b34a737dafc33dd0e891756689f","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"597215e4438d85d9cc71b48097c61eef7cba4f4b09dc14c92b4b94893b3d3ea2","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"7f92a0d0a8319c3c0e076fb3b1a4689c45120b2be2c14dfc7d4cb03aafa9d6a5","replay":"GST-257"}
@@ -0,0 +1 @@
{"sha":"d6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08","replay":"#3638"}
@@ -0,0 +1 @@
{"sha":"0142d6bc3f2a5bceb9f8b2adef1d3a206e5836c99a0667fe995568da2e80a5a6","replay":"GST-508+GST-464+GST-967"}
@@ -0,0 +1 @@
{"sha":"de3e0d35f2af56492c42fd287b6868fbe19bce96b86321e74a4947ba909b74eb","replay":"GST-508+GST-464+GST-967"}
+1
View File
@@ -0,0 +1 @@
{"replay":"#3638","justification":"no-loss round 3: main restored the pre-#3621 reviewers (v1.91.331.0)"}
+1
View File
@@ -0,0 +1 @@
{"replay":"G1","justification":"GSTA-167 variant G Arm G1: live no-loss replay of both prose voices with part 1's prompts, 2 passes, normalizer v2, paired rule against pooled C' (parts 1 and 3; specialists at medium as in C and C2)"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-257","justification":"GST-257 PR 1 dispatch-superset no-loss check (test/review-targeted.test.ts): on every fixture diff the branch dispatches main's reviewers at 8be7f21 unchanged plus only the targeted reader, and over the last 100 non-release commits on main it lost none; adds the targeted role and brief to the G1 and L bytes, whose existing reviewer text is unchanged"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-464","justification":"GST-464 brief-identity no-loss check (the brief-body test below): every reviewer brief lib/review-brief.ts cuts from ship/sections/review-army.md and adversarial.md is byte-identical to the bodies at the L and GST-257 pins; only lead instructions outside the cut spans changed"}
@@ -0,0 +1 @@
{"replay":"GST-508+GST-464+GST-967","justification":"wave review-53cfb5bfe011 merge of main 68410828f into #3852: ship/sections/review-army.md and adversarial.md are main's GST-967 bytes plus exactly the GST-508+GST-464 lines (the merged file minus main equals the wave minus fc292e22f, line for line), so GST-967's dispatch-superset no-loss check and GST-508's no-loss replay both hold; merged-head checks in GStack Minder drive handoffs/GSTA-202/merge-6841-checks.md"}
@@ -0,0 +1 @@
{"replay":"GST-508+GST-464","justification":"wave review-53cfb5bfe011 merge of #3744 into main: ship/sections/review-army.md and adversarial.md carry the GST-508 no-loss-replayed exclude pathspec placeholders inside the brief bodies (handoffs/GSTA-202/noloss-results.md) and the GST-464 lead-instruction edits outside the cut spans, which its brief-identity no-loss check admitted"}
@@ -0,0 +1 @@
{"replay":"GST-508+R51+GST-911+GST-967","justification":"wave review-53cfb5bfe011 merge of main 68410828f into #3852: lib/review-depth.ts is main's GST-967 bytes plus exactly the GST-508+R51+GST-911 lines (the merged file minus main equals the wave minus fc292e22f, line for line); GST-967's dispatch-superset no-loss check (test/review-depth.test.ts) passes on the merged file, and the GST-508+R51+GST-911 checks are rerun at the merged head (GStack Minder drive handoffs/GSTA-202/merge-6841-checks.md)"}
@@ -0,0 +1 @@
{"replay":"GST-508+R51+GST-911","justification":"wave review-53cfb5bfe011 merge of main 3d99c2249 (#3848) into #3852: lib/review-depth.ts keeps GST-508's review_exclude_paths (no-loss replay, GStack Minder drive handoffs/GSTA-202/noloss-results.md), R51 with GST-850's dispatch-superset no-loss check, and GST-911's SVG drawings with GST-911-redos (no-loss replay: 0 of 100 main commits changed, reports/GSTA-249-911-decisions.tsv; test/review-planner-misreads.test.ts, test/review-risk-redos.test.ts); lib/review-risk.ts is main's GST-911 bytes. Merged-head checks (handoffs/GSTA-202/merge-3d99c22-checks.md): Arm P on unit 3593 differs only in the exclude pathspec and the disclosure line, the same brief diff as before the merge, and GST-911 panel unit f5cb6ec1e plans, dispatches and renders byte-identical briefs under 3d99c2249 and the merged head"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-508+R51","justification":"wave review-53cfb5bfe011 merge of #3744 into main: lib/review-depth.ts keeps both admitted changes, the GST-508 no-loss replay of review_exclude_paths (Arm V 33 of 41 blockers vs R 33 of 41, p_NI 0.0279; GStack Minder drive handoffs/GSTA-202/noloss-results.md) and the R51 risk-tier re-score with GST-850's dispatch-superset no-loss check: excluded paths sit beside the added and removed line maps, and the exclusion filter keeps the publishes-docs rule"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-508","justification":"GST-508 (#3744, GSTA-202) preregistered no-loss replay of review_exclude_paths at ef2760f: Arm V found 33 of 41 blockers vs R 33 of 41, exact sign flip with delta 4, p_NI 0.0279 (pass); Arm P identical depth, dispatch and reader sets on 26 of 26 units (GStack Minder drive handoffs/GSTA-202/noloss-results.md)"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-850","justification":"GST-850 dispatch-superset no-loss check: on the 26 GSTA-167 no-loss units the branch plan (wider security and money classes, write-up text in the batched diff) drops no reviewer role main dispatches, so no saved blocker can be lost, and over the last 100 main commits no commit with a gate or risk class drops depth (test/review-risk-routing.test.ts, test/review-risk-linear.test.ts); reviewer text is unchanged"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-898","justification":"GST-898 verdict-identity no-loss check: the linear rm rule, and the rmSync rule (which also raises security on any call with more than 500 characters before its closing parenthesis), give the same verdict as R51's on every changed line of the last 100 main commits (69,023 lines, a563f47 back 100 first-parent commits) and on 60,000 seeded lines per rule (test/review-risk-redos.test.ts), and no commit changes depth, class or risk class, so every diff dispatches main's reviewers unchanged; reviewer text is unchanged"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-911-redos","justification":"GST-911 ReDoS fix verdict-identity no-loss check (wave review-e7b551ebd865): SVG_REFUSED's url() entry, rewritten to run in linear time, refuses exactly the strings #3838's pattern refused (test/review-risk-redos.test.ts: 60,000 seeded strings and the hand-picked edges, and every SVG blob in the repository's history, with zero differences); SVG_MARKUP now refuses a DOCTYPE naming any document but W3C's SVG DTD (test/review-planner-misreads.test.ts), which only keeps more SVGs code and so drops no reviewer; GST-911's no-loss replay holds unchanged; reviewer text is unchanged"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-911","justification":"GST-911 no-loss replay (GSTA-249/13): the planner over the last 100 first-parent main commits to f5cb6ec1e, origin/main's code against this branch, gate scan fixed at 0, changes no commit's depth, class, risk classes, targeted classes or SCOPE_AUTH; the three misreads (a drawing-only SVG, a session-named data file, a bare base) are proven on fixtures (test/review-planner-misreads.test.ts), where every SVG that can run script stays code with the security class; reviewer text is unchanged"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-967","justification":"GST-967 dispatch-superset no-loss check (test/review-depth.test.ts, 'GST-967 dispatch-superset no-loss check'): over 1,440 planReview cases the branch plans exactly what main planned at 1022f4f except the 71 gate-class stops before Step 11, which add only Step 11's adversarial, outside and targeted voices, so no reviewer main dispatches is dropped or re-efforted; the brief bodies lib/review-brief.ts cuts from the ship sections hash to main's; only the lead's repeat-decision and Step 11 instructions changed; reviewer text is unchanged"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-986","justification":"GST-986 brief-identity no-loss check (the brief-body test below): every reviewer brief lib/review-brief.ts cuts from ship/sections/adversarial.md is byte-identical to the bodies at the GST-257 pin; only the lead's Step 11 instruction naming the targeted record's path changed, outside the cut spans"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GSTA-224 H1","justification":"GSTA-224 H1 preregistered no-loss run: the PR-type checklist against main's checklist and a no-gstack arm (Opus 5.5, 81 calls), 5 behavior cases, 10 Round 7 application blockers and 2 clean controls; every bar passed (handoffs/GSTA-224/h1-noloss-results.md)"}
+1
View File
@@ -0,0 +1 @@
{"replay":"L","justification":"GSTA-167 variant G Arm L: live no-loss replay of one batched reader (testing and security plus scoped checklists) and the Red Team on its findings, 2 passes, scored L∘G1 against G1 on the paired rule (part 2)"}
+1
View File
@@ -0,0 +1 @@
{"replay":"PR-a","justification":"GSTA-167 variant G Arm 4: structural plan identity at the 14 cohort heads and the free exact re-score of saved C, D and A' findings (parts 4a and 4b)"}
+1
View File
@@ -0,0 +1 @@
{"replay":"R51","justification":"R51 risk-tier routing no-loss re-score (GSTA-167): the saved panel findings (349 records over 24 of the 26 no-loss units) re-scored against the branch's plan lose no blocker whose only finders are roles it drops, and every gate, security, user-data and money fixture keeps main's reviewers plus the full panel (test/review-targeted.test.ts, test/review-risk-routing.test.ts); reviewer text is unchanged"}
+73 -5
View File
@@ -8,7 +8,7 @@
*/
import { describe, expect, test } from 'bun:test';
import * as path from 'node:path';
import { decide, formatDrift, harnessDrift, hasDrift, MANIFEST, readManifest, serializeManifest, workingHarnessFiles,
import { decide, formatDrift, harnessDrift, hasDrift, MANIFEST, PINS_DIR, readManifest, workingHarnessFiles, writeManifest,
type HarnessManifest } from '../scripts/bump-harness-version';
import { caseSeriesIdentitiesV2, globRegex, HARNESS_PATTERNS, HARNESS_VERSION, isCaseOwnedFile, skillDirs, stampTrialSeries, treeEntries,
type TreeEntry } from '../scripts/eval-trial-series';
@@ -18,12 +18,14 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import { LIVE_REGISTRY, type Registry } from '../scripts/eval-flake-rank';
import { matchGlob } from './helpers/test-selection';
import { mergePinEdits } from './helpers/pin-merge-fixture';
const ROOT = path.resolve(import.meta.dir, '..');
describe('HARNESS_VERSION guard', () => {
test('every pinned shared-harness file matches scripts/harness-version.json', () => {
expect(MANIFEST).toBe('scripts/harness-version.json');
expect(PINS_DIR).toBe('scripts/harness-pins');
const manifest = readManifest(ROOT);
expect(manifest.version).toBe(HARNESS_VERSION);
expect(manifest.patterns).toEqual([...HARNESS_PATTERNS]);
@@ -54,10 +56,76 @@ describe('HARNESS_VERSION guard', () => {
expect(() => decide(base, now, 'bump', 'short')).toThrow('reason');
});
test('the manifest serializes one entry per line, sorted, as valid JSON', () => {
const text = serializeManifest({ ...base, files: { 'z.ts': { blob: 'z', since: 1 }, 'a.ts': { blob: 'a', since: 1 } } });
expect(JSON.parse(text).files).toEqual({ 'a.ts': { blob: 'a', since: 1 }, 'z.ts': { blob: 'z', since: 1 } });
expect(text.split('\n').filter(line => line.includes('"blob"'))).toEqual([' "a.ts": {"blob":"a","since":1},', ' "z.ts": {"blob":"z","since":1}']);
const copyManifest = (into: string) => {
fs.cpSync(path.join(ROOT, MANIFEST), path.join(into, MANIFEST));
fs.cpSync(path.join(ROOT, PINS_DIR), path.join(into, PINS_DIR), { recursive: true });
};
test('the manifest writes one pin file per harness file under scripts/harness-pins and reads back unchanged (GST-1084)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-pins-'));
try {
copyManifest(dir);
expect(readManifest(dir)).toEqual(readManifest(ROOT));
const next: HarnessManifest = { version: 3, patterns: ['test/helpers/x/**'], files: { 'z.ts': { blob: 'z', since: 1 }, 'test/a.ts': { blob: 'a', since: 1, nonBehavioral: 'why' } } };
writeManifest(dir, next);
expect(readManifest(dir)).toEqual(next);
expect(fs.readFileSync(path.join(dir, MANIFEST), 'utf8')).toBe('{\n "version": 3,\n "patterns": [\n "test/helpers/x/**"\n ]\n}\n');
expect(fs.readFileSync(path.join(dir, PINS_DIR, 'test/a.ts.json'), 'utf8')).toBe('{"blob":"a","since":1,"nonBehavioral":"why"}\n');
expect(fs.existsSync(path.join(dir, PINS_DIR, 'test/helpers/eval-store.ts.json'))).toBe(false);
fs.writeFileSync(path.join(dir, PINS_DIR, 'z.ts.txt'), 'stray\n');
expect(() => readManifest(dir)).toThrow('every harness pin is <pinned path>.json');
fs.rmSync(path.join(dir, PINS_DIR, 'z.ts.txt'));
fs.writeFileSync(path.join(dir, MANIFEST), JSON.stringify({ version: 3, patterns: [], files: {} }));
expect(() => readManifest(dir)).toThrow('holds only version and patterns');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
// Value: protects=the one-file-per-pin layout still fails on every stale pin; fails_when=a pin with an old blob, a deleted pin
// or a pin for a file that is gone passes the guard; seam=readManifest and harnessDrift on the checkout's harness files
test('a stale, missing or orphaned harness pin fails the guard (GST-1084)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-stale-'));
try {
copyManifest(dir);
const live = readManifest(ROOT);
const current = workingHarnessFiles(ROOT, live.patterns);
const pin = (file: string) => path.join(dir, PINS_DIR, `${file}.json`);
fs.writeFileSync(pin('test/helpers/eval-store.ts'), `${JSON.stringify({ ...live.files['test/helpers/eval-store.ts'], blob: '0'.repeat(40) })}\n`);
fs.rmSync(pin('test/helpers/e2e-gate.ts'));
fs.writeFileSync(pin('test/helpers/gone.ts'), `${JSON.stringify({ blob: '1'.repeat(40), since: live.version })}\n`);
const drift = harnessDrift(readManifest(dir), current);
expect(drift).toEqual({ changed: ['test/helpers/eval-store.ts'], added: ['test/helpers/e2e-gate.ts'], removed: ['test/helpers/gone.ts'] });
expect(formatDrift(drift, live.version)).toContain(`Merge conflict in ${MANIFEST} or ${PINS_DIR}/: take the higher version, then rerun the script.`);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
// Value: protects=PRs that record decisions for different harness files merge without a conflict (the one-line-per-file manifest
// conflicted on adjacent lines and on the trailing comma); fails_when=two non-behavioral decisions on adjacent pins conflict in a
// real merge, or two decisions on one pin merge silently; seam=writeManifest and git merge-tree --write-tree on two branches
test('non-behavioral decisions on two different harness files merge cleanly in a real git merge (GST-1084)', () => {
const live = readManifest(ROOT);
const record = (file: string, blob: string, reason: string) => (root: string) => {
const manifest = readManifest(root);
const files = workingHarnessFiles(ROOT, live.patterns).map(entry => entry.file === file ? { file, blob } : entry);
writeManifest(root, decide(manifest, files, 'non-behavioral', reason));
};
const floor = 'test/helpers/pty/runners/floor.ts', observation = 'test/helpers/pty/runners/observation.ts';
const clean = mergePinEdits(copyManifest, record(floor, 'a'.repeat(40), 'floor runner logging only'), record(observation, 'b'.repeat(40), 'observation runner comment only'));
const conflict = mergePinEdits(copyManifest, record(floor, 'a'.repeat(40), 'floor runner logging only'), record(floor, 'c'.repeat(40), 'floor runner rename only'));
try {
expect(clean.output).not.toContain('CONFLICT');
expect(clean.clean).toBe(true);
expect(JSON.parse(clean.read(`${PINS_DIR}/${floor}.json`))).toEqual({ blob: 'a'.repeat(40), since: live.version, nonBehavioral: 'floor runner logging only' });
expect(JSON.parse(clean.read(`${PINS_DIR}/${observation}.json`))).toEqual({ blob: 'b'.repeat(40), since: live.version, nonBehavioral: 'observation runner comment only' });
expect(clean.read(MANIFEST)).toBe(fs.readFileSync(path.join(ROOT, MANIFEST), 'utf8'));
expect(conflict.clean).toBe(false);
expect(conflict.output).toContain(`CONFLICT (content): Merge conflict in ${PINS_DIR}/${floor}.json`);
} finally {
for (const merge of [clean, conflict]) fs.rmSync(merge.root, { recursive: true, force: true });
}
});
});
+1
View File
@@ -209,6 +209,7 @@ export const FREE_FIXTURES: Readonly<Record<string, readonly string[]>> = {
'test/fixtures/remote-identity-vectors.json': ['test/bin-context-windows-slug.test.ts', 'test/gstack-slug-parity.test.ts', 'test/remote-identity-vectors.test.ts'],
'test/fixtures/review-depth/**': ['test/review-dispatch-compare.test.ts'],
'test/fixtures/review-ledger/**': ['test/review-dispatch.test.ts'],
'test/fixtures/review-pins/**': ['test/review-packets-removed.test.ts'],
'test/fixtures/review-risk/gbrain-evals-289.json': ['test/review-risk-routing.test.ts'],
'test/fixtures/review-design-lite-reports-ci-36633323521.json': ['test/review-finalization-budget.test.ts'],
'test/fixtures/review-gate-integrity-opus-reports.json': ['test/gate-integrity-templates.test.ts'],
+35
View File
@@ -0,0 +1,35 @@
/**
* Two independent edits to a pin store, merged for real (GST-1084). seed(root) writes the base tree, edits a and b
* each change it on their own branch, and `git merge-tree --write-tree` says whether the two merge without a
* textual conflict. The repository is fresh under os.tmpdir(), outside any checkout, and git runs under gitEnv() so
* no global hook or config reaches it; the caller removes root.
*/
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 { gitEnv } from './git-env';
export interface PinMerge { root: string; clean: boolean; output: string; read: (rel: string) => string }
export function mergePinEdits(seed: (root: string) => void, a: (root: string) => void, b: (root: string) => void): PinMerge {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pin-merge-'));
const git = (...args: string[]) => spawnSync('git', ['-c', 'user.name=Pin fixture', '-c', 'user.email=pins@gstack.test', '-c', 'commit.gpgsign=false',
'-c', 'core.autocrlf=false', ...args], { cwd: root, encoding: 'utf8', timeout: 30_000, env: gitEnv() });
const must = (...args: string[]) => {
const result = git(...args);
if (result.status !== 0) throw new Error(`git ${args.join(' ')} failed (${result.status}): ${result.stderr}`);
return result.stdout;
};
must('init', '-q', '-b', 'base');
const commit = (edit: (root: string) => void, message: string) => { edit(root); must('add', '-A'); must('commit', '-q', '--no-verify', '-m', message); };
commit(seed, 'seed');
for (const [branch, edit] of [['a', a], ['b', b]] as const) {
must('checkout', '-q', '-b', branch, 'base');
commit(edit, branch);
}
const merged = git('merge-tree', '--write-tree', 'a', 'b');
if (merged.status !== 0 && merged.status !== 1) throw new Error(`git merge-tree failed (${merged.status}): ${merged.stderr}`);
const tree = merged.stdout.split('\n')[0]!;
return { root, clean: merged.status === 0, output: merged.stdout, read: rel => must('show', `${tree}:${rel}`) };
}
+1 -1
View File
@@ -149,7 +149,7 @@ describe('GST-967 invariant: no gate-class owner exit before Step 11\'s voices;
});
/**
* The GST-967 dispatch-superset no-loss check (crew rule 2026-10-30; replay 'GST-967' in test/review-packets-removed.test.ts).
* The GST-967 dispatch-superset no-loss check (crew rule 2026-10-30; replay 'GST-967' in test/fixtures/review-pins/replays/GST-967.json).
* On a 1,440-case grid (five diffs: light, standard, gate, docs, tests; passes 0-2; last none, full or fix-check; every mix of
* the four fix flags; Step 11 run or not), main's planReview at 1022f4f (v1.92.81.0) hashes to MAIN_PLANS. The branch's plan,
* with each voices step read back as main's stop (its reason is the parenthesised tail, no dispatch), hashes to the same value,
+121 -71
View File
@@ -11,6 +11,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { sectionTexts } from '../lib/review-brief';
import { mergePinEdits } from './helpers/pin-merge-fixture';
const ROOT = path.join(import.meta.dir, '..');
const REMOVED = [
@@ -49,69 +50,66 @@ describe('reviewer packets stay reverted (GSTA-167)', () => {
* reviewer sees or how its findings rate, so it merges only after its preregistered no-loss replay
* passes (GStack Minder crew rule, 2026-10-30); a cost replay alone is not enough (#3621 passed its
* cost replay, then its no-loss replay found 11 of 30 blockers). Every pin names the replay that
* admits its bytes: repin with that replay's id here and in the commit message, and merge only on
* admits its bytes: repin with that replay's id in the pin file and in the commit message, and merge only on
* its passing result.
*
* The pins live one file per pin under test/fixtures/review-pins (GST-1084), so two PRs that repin
* different files, or add different replays, never edit the same lines and merge without a conflict:
* files/<pinned path>.json {"sha", "replay"}: the sha256 of the file's LF-normalized bytes and the replay that admits them
* replays/<slug>.json {"replay", "justification"}: one replay id and its written no-loss justification,
* where <slug> is replaySlug(replay)
* brief-bodies/<name>.json {"sha"}: one brief body lib/review-brief.ts cuts from the ship sections (below)
*/
const NO_LOSS_REPLAYS = {
'#3638': 'no-loss round 3: main restored the pre-#3621 reviewers (v1.91.331.0)',
'PR-a': "GSTA-167 variant G Arm 4: structural plan identity at the 14 cohort heads and the free exact re-score of saved C, D and A' findings (parts 4a and 4b)",
'G1': "GSTA-167 variant G Arm G1: live no-loss replay of both prose voices with part 1's prompts, 2 passes, normalizer v2, paired rule against pooled C' (parts 1 and 3; specialists at medium as in C and C2)",
'L': "GSTA-167 variant G Arm L: live no-loss replay of one batched reader (testing and security plus scoped checklists) and the Red Team on its findings, 2 passes, scored L∘G1 against G1 on the paired rule (part 2)",
'GST-508': "GST-508 (#3744, GSTA-202) preregistered no-loss replay of review_exclude_paths at ef2760f: Arm V found 33 of 41 blockers vs R 33 of 41, exact sign flip with delta 4, p_NI 0.0279 (pass); Arm P identical depth, dispatch and reader sets on 26 of 26 units (GStack Minder drive handoffs/GSTA-202/noloss-results.md)",
'GST-257': "GST-257 PR 1 dispatch-superset no-loss check (test/review-targeted.test.ts): on every fixture diff the branch dispatches main's reviewers at 8be7f21 unchanged plus only the targeted reader, and over the last 100 non-release commits on main it lost none; adds the targeted role and brief to the G1 and L bytes, whose existing reviewer text is unchanged",
'GST-464': "GST-464 brief-identity no-loss check (the brief-body test below): every reviewer brief lib/review-brief.ts cuts from ship/sections/review-army.md and adversarial.md is byte-identical to the bodies at the L and GST-257 pins; only lead instructions outside the cut spans changed",
'GST-850': "GST-850 dispatch-superset no-loss check: on the 26 GSTA-167 no-loss units the branch plan (wider security and money classes, write-up text in the batched diff) drops no reviewer role main dispatches, so no saved blocker can be lost, and over the last 100 main commits no commit with a gate or risk class drops depth (test/review-risk-routing.test.ts, test/review-risk-linear.test.ts); reviewer text is unchanged",
'GSTA-224 H1': 'GSTA-224 H1 preregistered no-loss run: the PR-type checklist against main\'s checklist and a no-gstack arm (Opus 5.5, 81 calls), 5 behavior cases, 10 Round 7 application blockers and 2 clean controls; every bar passed (handoffs/GSTA-224/h1-noloss-results.md)',
'R51': "R51 risk-tier routing no-loss re-score (GSTA-167): the saved panel findings (349 records over 24 of the 26 no-loss units) re-scored against the branch's plan lose no blocker whose only finders are roles it drops, and every gate, security, user-data and money fixture keeps main's reviewers plus the full panel (test/review-targeted.test.ts, test/review-risk-routing.test.ts); reviewer text is unchanged",
'GST-508+R51': "wave review-53cfb5bfe011 merge of #3744 into main: lib/review-depth.ts keeps both admitted changes, the GST-508 no-loss replay of review_exclude_paths (Arm V 33 of 41 blockers vs R 33 of 41, p_NI 0.0279; GStack Minder drive handoffs/GSTA-202/noloss-results.md) and the R51 risk-tier re-score with GST-850's dispatch-superset no-loss check: excluded paths sit beside the added and removed line maps, and the exclusion filter keeps the publishes-docs rule",
'GST-508+GST-464': "wave review-53cfb5bfe011 merge of #3744 into main: ship/sections/review-army.md and adversarial.md carry the GST-508 no-loss-replayed exclude pathspec placeholders inside the brief bodies (handoffs/GSTA-202/noloss-results.md) and the GST-464 lead-instruction edits outside the cut spans, which its brief-identity no-loss check admitted",
'GST-986': "GST-986 brief-identity no-loss check (the brief-body test below): every reviewer brief lib/review-brief.ts cuts from ship/sections/adversarial.md is byte-identical to the bodies at the GST-257 pin; only the lead's Step 11 instruction naming the targeted record's path changed, outside the cut spans",
'GST-911-redos': "GST-911 ReDoS fix verdict-identity no-loss check (wave review-e7b551ebd865): SVG_REFUSED's url() entry, rewritten to run in linear time, refuses exactly the strings #3838's pattern refused (test/review-risk-redos.test.ts: 60,000 seeded strings and the hand-picked edges, and every SVG blob in the repository's history, with zero differences); SVG_MARKUP now refuses a DOCTYPE naming any document but W3C's SVG DTD (test/review-planner-misreads.test.ts), which only keeps more SVGs code and so drops no reviewer; GST-911's no-loss replay holds unchanged; reviewer text is unchanged",
'GST-911': "GST-911 no-loss replay (GSTA-249/13): the planner over the last 100 first-parent main commits to f5cb6ec1e, origin/main's code against this branch, gate scan fixed at 0, changes no commit's depth, class, risk classes, targeted classes or SCOPE_AUTH; the three misreads (a drawing-only SVG, a session-named data file, a bare base) are proven on fixtures (test/review-planner-misreads.test.ts), where every SVG that can run script stays code with the security class; reviewer text is unchanged",
'GST-508+R51+GST-911': "wave review-53cfb5bfe011 merge of main 3d99c2249 (#3848) into #3852: lib/review-depth.ts keeps GST-508's review_exclude_paths (no-loss replay, GStack Minder drive handoffs/GSTA-202/noloss-results.md), R51 with GST-850's dispatch-superset no-loss check, and GST-911's SVG drawings with GST-911-redos (no-loss replay: 0 of 100 main commits changed, reports/GSTA-249-911-decisions.tsv; test/review-planner-misreads.test.ts, test/review-risk-redos.test.ts); lib/review-risk.ts is main's GST-911 bytes. Merged-head checks (handoffs/GSTA-202/merge-3d99c22-checks.md): Arm P on unit 3593 differs only in the exclude pathspec and the disclosure line, the same brief diff as before the merge, and GST-911 panel unit f5cb6ec1e plans, dispatches and renders byte-identical briefs under 3d99c2249 and the merged head",
'GST-898': "GST-898 verdict-identity no-loss check: the linear rm rule, and the rmSync rule (which also raises security on any call with more than 500 characters before its closing parenthesis), give the same verdict as R51's on every changed line of the last 100 main commits (69,023 lines, a563f47 back 100 first-parent commits) and on 60,000 seeded lines per rule (test/review-risk-redos.test.ts), and no commit changes depth, class or risk class, so every diff dispatches main's reviewers unchanged; reviewer text is unchanged",
'GST-967': "GST-967 dispatch-superset no-loss check (test/review-depth.test.ts, 'GST-967 dispatch-superset no-loss check'): over 1,440 planReview cases the branch plans exactly what main planned at 1022f4f except the 71 gate-class stops before Step 11, which add only Step 11's adversarial, outside and targeted voices, so no reviewer main dispatches is dropped or re-efforted; the brief bodies lib/review-brief.ts cuts from the ship sections hash to main's; only the lead's repeat-decision and Step 11 instructions changed; reviewer text is unchanged",
'GST-508+GST-464+GST-967': "wave review-53cfb5bfe011 merge of main 68410828f into #3852: ship/sections/review-army.md and adversarial.md are main's GST-967 bytes plus exactly the GST-508+GST-464 lines (the merged file minus main equals the wave minus fc292e22f, line for line), so GST-967's dispatch-superset no-loss check and GST-508's no-loss replay both hold; merged-head checks in GStack Minder drive handoffs/GSTA-202/merge-6841-checks.md",
'GST-508+R51+GST-911+GST-967': "wave review-53cfb5bfe011 merge of main 68410828f into #3852: lib/review-depth.ts is main's GST-967 bytes plus exactly the GST-508+R51+GST-911 lines (the merged file minus main equals the wave minus fc292e22f, line for line); GST-967's dispatch-superset no-loss check (test/review-depth.test.ts) passes on the merged file, and the GST-508+R51+GST-911 checks are rerun at the merged head (GStack Minder drive handoffs/GSTA-202/merge-6841-checks.md)",
} as const;
type Replay = keyof typeof NO_LOSS_REPLAYS;
const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
'ship/sections/review-army.md': { sha: 'de3e0d35f2af56492c42fd287b6868fbe19bce96b86321e74a4947ba909b74eb', replay: 'GST-508+GST-464+GST-967' },
'ship/sections/adversarial.md': { sha: '0142d6bc3f2a5bceb9f8b2adef1d3a206e5836c99a0667fe995568da2e80a5a6', replay: 'GST-508+GST-464+GST-967' },
'review/sections/review-army.md': { sha: '78329b64640260cd8831ca81a9189f3f8dcade9c406431631d3f2cd798cd3967', replay: '#3638' },
'review/sections/adversarial.md': { sha: 'e6962fa25b78f362654f2954f79683e524d2ebd452d1494f8a2b099888e8e255', replay: 'G1' },
'review/checklist.md': { sha: 'a32963ef38d9d3613c58c805e8517cd2e593c63769b30dd7d40e2592fa48fc78', replay: 'GSTA-224 H1' },
'review/design-checklist.md': { sha: 'b9205f88313cc64902c0b13e9685722ab8138d3dcc58916154f47274abda7d83', replay: '#3638' },
'review/gate-integrity.md': { sha: 'f570db344c9a01df633bcaa281d6017f4f12420fd3e0aa3078c1ce06e0df8332', replay: '#3638' },
'review/specialists/api-contract.md': { sha: '263d23ac119dd601d315c191dbdbd503d47c00264c9c0ca81959559ca11d4e95', replay: '#3638' },
'review/specialists/data-migration.md': { sha: 'b6fd9eb229002ea598f8fe9ff53b1cd8821e3bd37a7aa7b07b5526556c71ebca', replay: '#3638' },
'review/specialists/maintainability.md': { sha: 'f8015d549e70297f782f0aabf6fe3a9fe31ec5ed61c311ab854e4a858282ed9b', replay: '#3638' },
'review/specialists/performance.md': { sha: '545c294ae53638b4c8524e8cde08246a4ce3b5c287ec7c44e27f5556a3b0e8cc', replay: '#3638' },
'review/specialists/red-team.md': { sha: '9ea05149f5b13d6a19ecec26285142d09d259cf80a515af828abdd1f43320427', replay: '#3638' },
'review/specialists/security.md': { sha: '17bfe813eb3de22835af048ec359770720f1d7f371a26460169f7e32b1bcac9f', replay: '#3638' },
'review/specialists/simplification.md': { sha: 'f33846b1dcf2fdeef82d0744ab2ef48fb4a33b34a737dafc33dd0e891756689f', replay: '#3638' },
'review/specialists/state-machines.md': { sha: '597215e4438d85d9cc71b48097c61eef7cba4f4b09dc14c92b4b94893b3d3ea2', replay: '#3638' },
'review/specialists/testing.md': { sha: 'd6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4', replay: '#3638' },
'review/specialists/targeted.md': { sha: '7f92a0d0a8319c3c0e076fb3b1a4689c45120b2be2c14dfc7d4cb03aafa9d6a5', replay: 'GST-257' },
'scripts/resolvers/fix-check.ts': { sha: '712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08', replay: '#3638' },
'lib/review-depth.ts': { sha: 'd76a9afcfadfe0a95b15428c04e6f7a88c6b0fa75591201c45f023fec2bb90db', replay: 'GST-508+R51+GST-911+GST-967' },
'lib/review-risk.ts': { sha: '126eff0f1de6982e03dc24d8017c67e6807c91dfa518b18d2db3aa910aac911f', replay: 'GST-508+R51+GST-911' },
};
const PINS = path.join(ROOT, 'test/fixtures/review-pins');
const replaySlug = (replay: string) => replay.replace(/[^A-Za-z0-9+-]+/g, '-').replace(/^-+|-+$/g, '');
/**
* The reviewer brief bodies lib/review-brief.ts cuts out of the ship sections, at the bytes replays L and GST-257
* admitted, except fixtures, redTeam and native, which carry the exclude pathspec placeholders the GST-508 replay
* admitted (byte-identical to #3744 at 995e442). A section repin under GST-464 or GST-986 is valid only while every
* body still hashes here: the section may change outside the cut spans, never inside them.
*/
const BRIEF_BODIES: Record<string, string> = {
specialist: '1ebe47ddb3291a9e4fe90b5a260eb57e328144959549bc95c119b68d23652cb5',
framing: 'aad667b5ea4c6d23d930dafffa38178c04be388cd4364fb73759fc92af17bb63',
fixtures: 'fbe52f3a9e0f4442eb755f1747a95944b373b439bd05c61f3eb68345d1061a5b',
redTeam: '6dd670ea040535ca6cceab81c29a395bb67ce5a2ab8fa2b81e51bc4711e4aada',
native: 'a7cc08fb31b6ca65932a316d589f0b09677426d193d63f88ec74fee6d93ce053',
outside: 'bd96406da28b03742384560425a333b6d6d7ed2be95bd2026c90e0039632cee2',
};
/** Every pin file under dir, keyed by its path below dir without `.json`; any other file is an error. */
function readPinDir(dir: string): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const rel of (fs.readdirSync(dir, { recursive: true }) as string[]).map(f => f.split(path.sep).join('/')).sort()) {
if (fs.statSync(path.join(dir, rel)).isDirectory()) continue;
if (!rel.endsWith('.json')) throw new Error(`${dir}/${rel}: every pin file is <name>.json`);
out[rel.slice(0, -'.json'.length)] = JSON.parse(fs.readFileSync(path.join(dir, rel), 'utf8'));
}
return out;
}
/** The pin store at pins, with every file that is not exactly its kind's fields as non-empty strings, or is misnamed. */
function readPins(pins: string) {
const malformed: string[] = [];
const kind = <K extends string>(sub: string, keys: readonly K[]) => {
const out: Record<string, Record<K, string>> = {};
for (const [name, value] of Object.entries(readPinDir(path.join(pins, sub)))) {
const fields = typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
if (Object.keys(fields).sort().join() === [...keys].sort().join() && keys.every(k => typeof fields[k] === 'string' && fields[k] !== '')) out[name] = fields as Record<K, string>;
else malformed.push(`${sub}/${name}.json (expected exactly ${keys.join(', ')})`);
}
return out;
};
const replayFiles = kind('replays', ['replay', 'justification']);
for (const [name, r] of Object.entries(replayFiles)) if (name !== replaySlug(r.replay)) malformed.push(`replays/${name}.json (replay ${r.replay} belongs in replays/${replaySlug(r.replay)}.json)`);
return {
malformed,
replays: Object.fromEntries(Object.values(replayFiles).map(r => [r.replay, r.justification])),
files: kind('files', ['sha', 'replay']),
bodies: Object.fromEntries(Object.entries(kind('brief-bodies', ['sha'])).map(([name, body]) => [name, body.sha])),
};
}
type Pins = ReturnType<typeof readPins>;
/** Pinned files whose LF-normalized bytes under root no longer hash to their pin. */
const stalePins = (pins: Pins, root: string) => Object.entries(pins.files).filter(([file, pin]) => {
const text = fs.readFileSync(path.join(root, file), 'utf8').replace(/\r\n/g, '\n');
return createHash('sha256').update(text).digest('hex') !== pin.sha;
}).map(([file, pin]) => `${file} (a repin needs a passing no-loss replay; the current pin names ${pin.replay})`);
/** Pins naming no replay in the store, and replays whose justification names no no-loss replay or re-score. */
const unadmitted = (pins: Pins) => [
...Object.entries(pins.files).filter(([, pin]) => !(pin.replay in pins.replays)).map(([file, pin]) => `${file} names unknown replay ${pin.replay}`),
...Object.entries(pins.replays).filter(([, what]) => !/no-loss|re-score/.test(what)).map(([replay]) => `replay ${replay} is not a no-loss replay or re-score`),
];
const PINNED = readPins(PINS);
describe('reviewer briefs stay at their no-loss-tested bytes (GSTA-167)', () => {
// Value: protects=the GST-986 repin of ship/sections/adversarial.md admits only a lead-instruction edit: every brief a reviewer
@@ -119,34 +117,86 @@ describe('reviewer briefs stay at their no-loss-tested bytes (GSTA-167)', () =>
// section pin moves; seam=sectionTexts, the function every render uses; no_claim=the checklists (pinned by file hash below)
test('every reviewer brief cut from the ship sections is byte-identical to its pinned body', () => {
const hashes = (root: string) => Object.fromEntries(Object.entries(sectionTexts(root)).map(([k, v]) => [k, createHash('sha256').update(v).digest('hex')]));
expect(hashes(ROOT)).toEqual(BRIEF_BODIES);
expect(hashes(ROOT)).toEqual(PINNED.bodies);
const copy = fs.mkdtempSync(path.join(os.tmpdir(), 'brief-bodies-'));
try {
fs.cpSync(path.join(ROOT, 'ship', 'sections'), path.join(copy, 'ship', 'sections'), { recursive: true });
const adversarial = path.join(copy, 'ship', 'sections', 'adversarial.md');
fs.writeFileSync(adversarial, fs.readFileSync(adversarial, 'utf8').replace('Recommendation:', 'Recommendation (short):'));
expect(hashes(copy)).not.toEqual(BRIEF_BODIES);
expect(hashes(copy)).not.toEqual(PINNED.bodies);
} finally {
fs.rmSync(copy, { recursive: true, force: true });
}
});
test('every pinned reviewer brief, checklist and depth rule hashes to its pin', () => {
const drift = Object.entries(REVIEWER_BRIEFS).filter(([file, pin]) => {
const text = fs.readFileSync(path.join(ROOT, file), 'utf8').replace(/\r\n/g, '\n');
return createHash('sha256').update(text).digest('hex') !== pin.sha;
}).map(([file]) => `${file} (a repin needs a passing no-loss replay; the current pin names ${REVIEWER_BRIEFS[file]!.replay})`);
expect(drift).toEqual([]);
expect(PINNED.malformed).toEqual([]);
expect(stalePins(PINNED, ROOT)).toEqual([]);
});
test('every pin names the preregistered no-loss replay that admits its bytes', () => {
const unnamed = Object.entries(REVIEWER_BRIEFS).filter(([, pin]) => !(pin.replay in NO_LOSS_REPLAYS)).map(([file]) => file);
expect(unnamed).toEqual([]);
for (const what of Object.values(NO_LOSS_REPLAYS)) expect(what).toMatch(/no-loss|re-score/);
expect(unadmitted(PINNED)).toEqual([]);
});
test('the pins cover every specialist checklist', () => {
const specialists = fs.readdirSync(path.join(ROOT, 'review/specialists')).filter(f => f.endsWith('.md')).map(f => `review/specialists/${f}`);
expect(specialists.filter(f => !(f in REVIEWER_BRIEFS))).toEqual([]);
expect(specialists.filter(f => !(f in PINNED.files))).toEqual([]);
});
// Value: protects=the one-file-per-pin layout keeps every check the in-file table had; fails_when=a stale sha, an unknown or
// unjustified replay, a malformed or misnamed pin file, or a stray file passes; seam=readPins, stalePins and unadmitted, the
// functions the checks above call; no_claim=that a repin's replay actually passed (the commit and PR carry that)
test('a stale pin, an unknown or unjustified replay and a malformed pin file each fail (GST-1084)', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'review-pins-'));
try {
const src = path.join(tmp, 'src'), pins = path.join(tmp, 'pins');
for (const file of Object.keys(PINNED.files)) fs.cpSync(path.join(ROOT, file), path.join(src, file));
fs.cpSync(PINS, pins, { recursive: true });
expect(stalePins(readPins(pins), src)).toEqual([]);
fs.writeFileSync(path.join(src, 'review/checklist.md'), fs.readFileSync(path.join(src, 'review/checklist.md'), 'utf8').replace(/\n/g, '\r\n'));
expect(stalePins(readPins(pins), src)).toEqual([]);
fs.appendFileSync(path.join(src, 'review/checklist.md'), 'One more rule.\r\n');
expect(stalePins(readPins(pins), src)).toEqual([`review/checklist.md (a repin needs a passing no-loss replay; the current pin names ${PINNED.files['review/checklist.md']!.replay})`]);
const pin = (file: string, value: unknown) => fs.writeFileSync(path.join(pins, 'files', `${file}.json`), `${JSON.stringify(value)}\n`);
pin('review/design-checklist.md', { sha: PINNED.files['review/design-checklist.md']!.sha, replay: 'GST-0000' });
fs.writeFileSync(path.join(pins, 'replays', 'GST-0001.json'), `${JSON.stringify({ replay: 'GST-0001', justification: 'reviewed by hand' })}\n`);
expect(unadmitted(readPins(pins))).toEqual(['review/design-checklist.md names unknown replay GST-0000', 'replay GST-0001 is not a no-loss replay or re-score']);
pin('review/gate-integrity.md', { ...PINNED.files['review/gate-integrity.md'], note: 'extra' });
fs.writeFileSync(path.join(pins, 'replays', 'GST-0002.json'), `${JSON.stringify({ replay: 'GST-0003', justification: 'a no-loss replay' })}\n`);
expect(readPins(pins).malformed).toEqual(['replays/GST-0002.json (replay GST-0003 belongs in replays/GST-0003.json)',
'files/review/gate-integrity.md.json (expected exactly sha, replay)']);
fs.writeFileSync(path.join(pins, 'brief-bodies', 'specialist.txt'), 'stray\n');
expect(() => readPins(pins)).toThrow('every pin file is <name>.json');
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
// Value: protects=PRs that repin different review files merge without a conflict (five main-merge conflicts on 2026-11-02 came
// from the in-file table); fails_when=two repins of adjacent pins, each with its own new replay, conflict in a real merge, or
// two repins of one pin merge silently; seam=git merge-tree --write-tree on two branches; no_claim=semantic merge correctness
test('repins of two different files, each with its own new replay, merge cleanly in a real git merge (GST-1084)', () => {
const repin = (file: string, sha: string, replay: string) => (root: string) => {
fs.writeFileSync(path.join(root, 'files', `${file}.json`), `${JSON.stringify({ sha, replay })}\n`);
fs.writeFileSync(path.join(root, 'replays', `${replaySlug(replay)}.json`), `${JSON.stringify({ replay, justification: `${replay} no-loss replay` })}\n`);
};
const seed = (root: string) => fs.cpSync(PINS, root, { recursive: true });
const army = 'ship/sections/review-army.md', adversarial = 'ship/sections/adversarial.md';
const clean = mergePinEdits(seed, repin(army, 'a'.repeat(64), 'GST-9001+GST-967'), repin(adversarial, 'b'.repeat(64), 'GST-9002'));
const conflict = mergePinEdits(seed, repin(army, 'a'.repeat(64), 'GST-9001'), repin(army, 'c'.repeat(64), 'GST-9003'));
try {
expect(clean.output).not.toContain('CONFLICT');
expect(clean.clean).toBe(true);
expect(JSON.parse(clean.read(`files/${army}.json`))).toEqual({ sha: 'a'.repeat(64), replay: 'GST-9001+GST-967' });
expect(JSON.parse(clean.read(`files/${adversarial}.json`))).toEqual({ sha: 'b'.repeat(64), replay: 'GST-9002' });
expect(JSON.parse(clean.read('replays/GST-9001+GST-967.json')).replay).toBe('GST-9001+GST-967');
expect(JSON.parse(clean.read('replays/GST-9002.json')).replay).toBe('GST-9002');
expect(conflict.clean).toBe(false);
expect(conflict.output).toContain(`CONFLICT (content): Merge conflict in files/${army}.json`);
} finally {
for (const merge of [clean, conflict]) fs.rmSync(merge.root, { recursive: true, force: true });
}
});
});