Merge remote-tracking branch 'origin/main' into gsta-155-autoplan-carry

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

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
garrytan committed 2026-10-29 09:45:48 +00:00
commit ba42ed27a9
111 files changed
+4342 -498

No files matched your search

+26
View File
@@ -393,6 +393,32 @@ describe('capy skill: the session hands the installer GSTACK_CAPY_BEHIND', () =>
}
});
// #3571: /ship's docs step ran install.sh --for document-release from inside the run that
// holds the lease, the upgrader excluded its own ancestry, and the checkout moved mid-/ship.
// On Capy the leased pid is the long-lived capy-server, so the deferral ends by the lease's age.
test('install mode inside a run that holds a fresh workflow lease: the self-upgrade is deferred, HEAD stays and the trailer stays ok; once that lease is 120 minutes old the same install upgrades', () => {
const f = fixture({ cloneAt: 'B' });
write(path.join(f.root, 'bin/gstack-capy-install'), ENV_INSTALLER, 0o755);
f.git(['update-index', '--assume-unchanged', 'bin/gstack-capy-install']);
const leaseDir = path.join(f.host.base, 'leases');
const script = [
'bash "$1" touch --pid $$ --root "$2" || exit 9',
'bash "$3" --for document-release; echo "exit=$?"',
'git -C "$2" rev-parse HEAD',
'"$5" -e "const t = Date.now() / 1000 - 121 * 60; require(\'fs\').utimesSync(process.argv[1], t, t)" "$4/$$"',
'bash "$3" --for document-release; echo "exit=$?"',
].join('\n');
const r = spawnSync('bash', ['-c', script, 'run', path.join(f.root, 'bin/gstack-lease'), f.root, BOOT, leaseDir, process.execPath], { cwd: f.host.home, encoding: 'utf8', env: { ...f.host.env, GSTACK_CAPY_ROOT: f.root, GSTACK_CAPY_REPO: `file://${f.bare}`, WRAP_LOG: path.join(f.host.base, 'wrap.log'), GSTACK_LEASE_DIR: leaseDir }, timeout: 120_000 });
const [inRun, aged] = r.stdout.split(/^exit=0$/m);
expect(r.stdout.match(/^exit=0$/gm), r.stdout + r.stderr).toHaveLength(2);
expect(inRun).toMatch(/^SELF_UPGRADE: skipped \(a gstack run in this session holds a workflow lease on this installation: pid \d+ \(0m bash\), so the upgrade waits until the lease is 120 minutes old; fix: .+bin\/gstack-upgrade once the run ends \(UPGRADE_BUSY\)\)$/m);
expect(inRun).not.toContain('SELF_UPGRADE: done');
expect(inRun).toMatch(/blocked=a gstack run in this session holds a workflow lease .+ \(UPGRADE_BUSY\)$/m);
expect(aged).toContain(f.b);
expect(aged).toMatch(/^SELF_UPGRADE: done 1\.0\.1 -> 1\.0\.2$/m);
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.c);
});
test('an older bootstrapper handing --for none to the session in install mode: the session certifies nothing and moves nothing (no self-upgrade, HEAD unchanged), and reports behind like --check', () => {
const f = fixture({ cloneAt: 'B' });
write(path.join(f.root, 'bin/gstack-capy-install'), ENV_INSTALLER, 0o755);
+49 -3
View File
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { afterEach, describe, expect, spyOn, test } from 'bun:test';
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
@@ -11,7 +11,8 @@ import { selectTests } from './helpers/test-selection';
import { E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES } from './helpers/touchfiles-data';
import { selectPrProfile } from '../scripts/test-pr-profile';
import { JUDGE_MS } from './helpers/eval-budgets';
import { JudgeRefusalError, DEFAULT_JUDGE_MAX_TOKENS, judgePanel, judgePanelMean, judgePanelMedian, judgePanelReasoning, JUDGE_SCORE_DIMENSIONS, JUDGE_PANEL_SAMPLES } from './helpers/llm-judge';
import { Messages } from '@anthropic-ai/sdk/resources/messages';
import { callJudge, type CallJudgeOptions, JudgeRefusalError, DEFAULT_JUDGE_MAX_TOKENS, judgePanel, judgePanelMean, judgePanelMedian, judgePanelReasoning, JUDGE_SCORE_DIMENSIONS, JUDGE_PANEL_SAMPLES } from './helpers/llm-judge';
import { COOKIE_MANUAL_REVIEW_FILE, getCookieWorkflowManualReview, isManualReviewEntry } from './helpers/cookie-workflow-manual-review';
const ROOT = resolve(import.meta.dir, '..');
@@ -51,6 +52,7 @@ const passingScore: FixtureScore = { ...COOKIE_WORKFLOW_JUDGE.thresholds, reason
function actualCookieCallback(root: string, overrides: {
judge?: () => Promise<FixtureScore>;
callJudge?: (prompt: string, model: string | undefined, options: CallJudgeOptions) => Promise<FixtureScore>;
clock?: () => number;
budget?: number;
setTimer?: typeof setTimeout;
@@ -74,7 +76,11 @@ function actualCookieCallback(root: string, overrides: {
(_suite: string, names: string[], run: () => void) => { expect(names).toEqual([NAME]); run(); },
(name: string, run: () => Promise<void>, budget: number) => { expect(name).toBe(NAME); expect(budget).toBe(JUDGE_MS + 10_000); callback = run; },
root, buildCookieWorkflowJudgeInput, (_kind: string, explicit?: string) => explicit ?? 'fixture-model',
async (prompt: string, model: string | undefined, options: { signal: AbortSignal }) => { requests.push({ prompt, model, signal: options.signal }); return overrides.judge ? overrides.judge() : passingScore; },
async (prompt: string, model: string | undefined, options: CallJudgeOptions & { signal: AbortSignal }) => {
requests.push({ prompt, model, signal: options.signal });
if (overrides.callJudge) return overrides.callJudge(prompt, model, options);
return overrides.judge ? overrides.judge() : passingScore;
},
COOKIE_WORKFLOW_JUDGE, overrides.budget ?? JUDGE_MS, JUDGE_MS + 10_000, 5_000,
{ addTest: (record: EvalTestEntry) => records.push(record) }, expect, { log() {} },
readWorkflowJudgeInput, buildWorkflowJudgePrompt,
@@ -269,6 +275,46 @@ describe('cookie workflow judge input', () => {
expect(selectTests(['README.md'], LLM_JUDGE_TOUCHFILES).selected).not.toContain(NAME);
});
test('the registered cookie judge sends the response schema, so a free-text reply like train 42\'s is not what gets parsed', async () => {
const { replies } = JSON.parse(readFileSync(join(ROOT, 'test/fixtures/cookie-judge-train42-replies.json'), 'utf8')) as {
replies: Array<{ id: string; model: string; stop_reason: string; text: string }> };
expect(replies.map(reply => reply.id)).toEqual(['msg_011CgVnRtVj5vyLvb56m6KJH', 'msg_011CgVne5GStZ6f9pRoCvgWE']);
const original = process.env.ANTHROPIC_API_KEY;
process.env.ANTHROPIC_API_KEY = 'fake';
const create = spyOn(Messages.prototype, 'create');
try {
for (const reply of replies) {
expect(() => JSON.parse(reply.text)).toThrow();
const marker = '"reasoning": "';
const judgment: FixtureScore = {
clarity: Number(/"clarity": (\d)/.exec(reply.text)![1]),
completeness: Number(/"completeness": (\d)/.exec(reply.text)![1]),
actionability: Number(/"actionability": (\d)/.exec(reply.text)![1]),
reasoning: reply.text.slice(reply.text.indexOf(marker) + marker.length, reply.text.lastIndexOf('"}')),
};
expect(judgment.reasoning).toContain('"one --domain per run"');
create.mockReset();
create.mockResolvedValue({ id: reply.id, model: reply.model, stop_reason: 'end_turn',
content: [{ type: 'text', text: JSON.stringify(judgment) }], usage: { input_tokens: 1, output_tokens: 1 } } as never);
const root = fixture();
const h = actualCookieCallback(root, { callJudge: (prompt, model, options) => callJudge<FixtureScore>(prompt, model, options) });
await h.run();
expect(create).toHaveBeenCalledTimes(JUDGE_PANEL_SAMPLES);
for (const [request, options] of create.mock.calls as unknown as Array<[Record<string, unknown>, { signal: AbortSignal }]>) {
expect(request).toEqual({ model: COOKIE_WORKFLOW_JUDGE.model, max_tokens: DEFAULT_JUDGE_MAX_TOKENS,
output_config: { format: { type: 'json_schema', schema: WORKFLOW_JUDGE_RESPONSE_SCHEMA } },
messages: [{ role: 'user', content: buildCookieWorkflowJudgeInput(root).prompt }] });
expect(options.signal).toBeInstanceOf(AbortSignal);
}
expect(h.records[0]).toMatchObject({ name: NAME, passed: true, execution: 'executed',
judge_scores: { clarity: judgment.clarity, completeness: judgment.completeness, actionability: judgment.actionability } });
}
} finally {
create.mockRestore();
if (original === undefined) delete process.env.ANTHROPIC_API_KEY; else process.env.ANTHROPIC_API_KEY = original;
}
});
test('the managed callback sends and records exact custom input without borrowing a normal cache identity', async () => {
const root = fixture();
const input = buildCookieWorkflowJudgeInput(root);
+21
View File
@@ -367,6 +367,27 @@ describe('uncommitted work is visible (#2299)', () => {
expect(vars.SCOPE_BACKEND).toBe('true');
});
// Value: protects=--head scopes one committed range from any checkout, the input the receipt writer and the merge queue
// share; fails_when=--head still reads the working tree or untracked files, or an unknown head reads as all-false; seam=none
test('--head <sha> scopes only the committed range: working-tree and untracked files are ignored, an unknown head is an error', () => {
const dir = createRepo(['lib/auth.ts']);
const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: dir, stdio: 'pipe' }).stdout.toString().trim();
mkdirSync(join(dir, 'db', 'migrate'), { recursive: true });
writeFileSync(join(dir, 'db', 'migrate', '001.rb'), '# untracked\n');
writeFileSync(join(dir, 'styles.css'), 'a{}\n');
const scope = (args: string[]) => spawnSync('bash', [SCRIPT, 'main', ...args], { cwd: dir, stdio: 'pipe', timeout: 5000 });
const ranged = scope(['--head', head]);
expect(ranged.status).toBe(0);
const out = ranged.stdout.toString();
expect(out).toContain('SCOPE_AUTH=true');
expect(out).toContain('SCOPE_MIGRATIONS=false');
expect(out).toContain('SCOPE_FRONTEND=false');
expect(runScopeFull(dir).vars.SCOPE_MIGRATIONS).toBe('true');
const unknown = scope(['--head', 'f'.repeat(40)]);
expect(unknown.status).toBe(2);
expect(unknown.stdout.toString()).toContain('SCOPE_ERROR=no_head');
});
test('non-ASCII path still matches extension globs (NUL-safe file listing, #2526)', () => {
const dir = createRepo(['docs/M2 — Notes.md']);
const { vars, status } = runScopeFull(dir);
+13
View File
@@ -163,3 +163,16 @@ test('calibration keeps its future hook but no shipped source enables the absent
}
expect(mentions).toEqual(['scripts/resolvers/gbrain.ts']);
});
test('the judged Eng bundle carries the shared Voice once and states its em-dash precedence in the Scope gate (paid screen 40634787316)', () => {
// Five of six samples (red panel and rerun) named Eng's near-copy of the shared Voice, whose only difference was
// its em-dash clause, as a rule the agent must reconcile; the Scope gate's Format precedence already owned it.
// fails_when=a second Voice section returns to the judged bundle or the precedence stops naming em dashes.
const input = readWorkflowJudgeInput({root: path.join(import.meta.dir, '..'), skillPath: 'plan-eng-review/SKILL.md',
startMarker: '# Plan Review Mode', endMarker: null});
expect(input.text.match(/^## Voice$/gm)).toHaveLength(1);
expect(input.text).not.toContain('Do not add em dashes in prose you compose');
expect(scope).toContain('**Format precedence:** Copy required command, output and question formats exactly, em dashes included. Apply Voice to newly composed prose.');
const startup = input.files[0]!.content;
expect(startup.indexOf('**Format precedence:**')).toBeLessThan(startup.indexOf('## Shared preamble (load first)'));
});
+17
View File
@@ -0,0 +1,17 @@
{
"source": "train 42 gate run 40809051510 (paid-logs-slice-5-a1, rerun-logs-1-a1): llm-judge-response-parse-error events",
"replies": [
{
"id": "msg_011CgVnRtVj5vyLvb56m6KJH",
"model": "claude-sonnet-5-5",
"stop_reason": "end_turn",
"text": "{\"clarity\": 4, \"completeness\": 4, \"actionability\": 4, \"reasoning\": \"The workflow is well ordered: check Aside, collect decisions up front, prepare the browser, import, and report. Consent for storage reset and --all is explicit, and the guidance separates copied cookies from verified sign-in. Error tables, platform limits, and the privacy rules are clear. Weaknesses: (1) the picker-versus-direct choice is underspecified, because the picker is used when browser, profile or domains are open and direct import when they are named, but the target-URL, `--all`, and verify-auth flows under each are not fully mapped. (2) The `--all` handling is thin. SKILL.md does not give direct-import syntax for it and does not say whether `$B goto` is still needed. (3) Verification setup has an ordering wrinkle: the env vars must be set before the daemon starts, while the Aside and `$B status` checks come first, and the instructions only partly say when to stop and restart the daemon. (4) A few edge cases are not defined, such as what to do when the reset or verification fails, how to count partial results, and how to proceed after the failure-reason handoff. (5) The multi-domain instruction is \"one --domain per run\", but it is unclear whether to repeat runs or how to aggregate the receipts. (6) The \"Windows App-Bound Encryption\" recovery depends on a display, with no alternative if none is available. These gaps are minor, and the workflow is mostly executable.\"}"
},
{
"id": "msg_011CgVne5GStZ6f9pRoCvgWE",
"model": "claude-sonnet-5-5",
"stop_reason": "end_turn",
"text": "{\"clarity\": 4, \"completeness\": 4, \"actionability\": 4, \"reasoning\": \"The workflow is well ordered: probe Aside, collect all decisions up front, set up the browser, import, then report three honest lines. Consent boundaries for verification, storage clearing and --all are explicit, the sign-in-versus-cookie-copy distinction is stated repeatedly, and the error and receipt tables give recovery paths. Gaps: (1) Step 2 asks for decisions before `$B` exists, yet the picker-versus-direct choice is tied to browser, profile and domains rather than to the user's wording, and the step 3.1 env vars come before the daemon starts. (2) The conditional step 3.3 (Mode: cdp) and the `$B stop` guidance could be cleaner. (3) The direct-import syntax for `--all` is not shown, and the \"one --domain per run\" rule is stated without saying what to do for several domains (loop, with the tab open on each). (4) The verification-missing return path says to return to step 3 but doesn't say how to handle the already-running daemon beyond `$B stop`. (5) Handling of partial and failed receipts, and the handoff for non-Windows cases, is thin. (6) How the agent learns the profile directory when the user only knows a display name is left to the picker. (7) The root-selection logic with \"never a generated host stub\" is a bit dense. These are minor; the workflow is mostly executable.\"}"
}
]
}
+5 -5
View File
@@ -202,11 +202,11 @@ token; `--finish` stamps the binding fields automatically. Never borrow or repla
`gstack-wtree` prints a Git tree hash covering tracked and non-ignored untracked files,
not a commit ID. Use `git diff <old-tree> <new-tree>` to compare these snapshots.
**Review policy** (docs/review-policy.md): a finding blocks another review pass
only when it would fail a test, break production, or lose data or secrets;
everything below that floor ships as a named note in the PR body. Pass 1 reviews
the diff; a later pass reads only the range; two full passes plus one fix-check
is the cap. `NO FINDINGS` is a complete reviewer result. `ship: review_scope` (Step 3)
**Review policy** (docs/review-policy.md): only a finding that would fail a test,
break production, or lose data or secrets blocks a pass; the rest is a PR-body
note. Step 9's plan sets both reviews' depth (`--depth-floor standard|full` only
raises it). Pass 1 reads the diff, later passes the range; the cap is two full
passes and a fix-check. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
is the knob; the receipt's `review_loop` records the outcome.
### Ship control flow
+5 -5
View File
@@ -202,11 +202,11 @@ token; `--finish` stamps the binding fields automatically. Never borrow or repla
`gstack-wtree` prints a Git tree hash covering tracked and non-ignored untracked files,
not a commit ID. Use `git diff <old-tree> <new-tree>` to compare these snapshots.
**Review policy** (docs/review-policy.md): a finding blocks another review pass
only when it would fail a test, break production, or lose data or secrets;
everything below that floor ships as a named note in the PR body. Pass 1 reviews
the diff; a later pass reads only the range; two full passes plus one fix-check
is the cap. `NO FINDINGS` is a complete reviewer result. `ship: review_scope` (Step 3)
**Review policy** (docs/review-policy.md): only a finding that would fail a test,
break production, or lose data or secrets blocks a pass; the rest is a PR-body
note. Step 9's plan sets both reviews' depth (`--depth-floor standard|full` only
raises it). Pass 1 reads the diff, later passes the range; the cap is two full
passes and a fix-check. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
is the knob; the receipt's `review_loop` records the outcome.
### Ship control flow
+5 -5
View File
@@ -200,11 +200,11 @@ token; `--finish` stamps the binding fields automatically. Never borrow or repla
`gstack-wtree` prints a Git tree hash covering tracked and non-ignored untracked files,
not a commit ID. Use `git diff <old-tree> <new-tree>` to compare these snapshots.
**Review policy** (docs/review-policy.md): a finding blocks another review pass
only when it would fail a test, break production, or lose data or secrets;
everything below that floor ships as a named note in the PR body. Pass 1 reviews
the diff; a later pass reads only the range; two full passes plus one fix-check
is the cap. `NO FINDINGS` is a complete reviewer result. `ship: review_scope` (Step 3)
**Review policy** (docs/review-policy.md): only a finding that would fail a test,
break production, or lose data or secrets blocks a pass; the rest is a PR-body
note. Step 9's plan sets both reviews' depth (`--depth-floor standard|full` only
raises it). Pass 1 reads the diff, later passes the range; the cap is two full
passes and a fix-check. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
is the knob; the receipt's `review_loop` records the outcome.
### Ship control flow
+5 -5
View File
@@ -202,11 +202,11 @@ token; `--finish` stamps the binding fields automatically. Never borrow or repla
`gstack-wtree` prints a Git tree hash covering tracked and non-ignored untracked files,
not a commit ID. Use `git diff <old-tree> <new-tree>` to compare these snapshots.
**Review policy** (docs/review-policy.md): a finding blocks another review pass
only when it would fail a test, break production, or lose data or secrets;
everything below that floor ships as a named note in the PR body. Pass 1 reviews
the diff; a later pass reads only the range; two full passes plus one fix-check
is the cap. `NO FINDINGS` is a complete reviewer result. `ship: review_scope` (Step 3)
**Review policy** (docs/review-policy.md): only a finding that would fail a test,
break production, or lose data or secrets blocks a pass; the rest is a PR-body
note. Step 9's plan sets both reviews' depth (`--depth-floor standard|full` only
raises it). Pass 1 reads the diff, later passes the range; the cap is two full
passes and a fix-check. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
is the knob; the receipt's `review_loop` records the outcome.
### Ship control flow
+5 -1
View File
@@ -51,7 +51,11 @@ reservation; its 14 cases now send `WORKFLOW_JUDGE_RESPONSE_SCHEMA` without the
compact-reasoning validator (still only on `ship`). The frontier (`review`) configuration has no corpus and stays on today's request. A $0.12 two-call pilot sized
output tokens before the manifest was priced. Total W2 spend: $90.47 of $115.
`cookie-workflow` (GSTA-91, separately funded) is held and stays on today's request. Its one
`cookie-workflow` (GSTA-91, separately funded) was held, and the hold was released on
GStack Minder's ruling of 2026-10-29: "GStack Minder, 2026-10-29: release the GSTA-91 hold; a07 is
matched by old-b; free-text parse errors cost train 42" (`cookie-workflow/release.json`). The
cookie workflow judge now sends `WORKFLOW_JUDGE_RESPONSE_SCHEMA`, with its thresholds, model and
samples unchanged; the comparison below stays recorded as held. Its one
new false pass, a07, scored 4/4/4 on all three new samples and on all three `old-b` samples;
`old-a`'s a07 panel is incomplete because one old sample returned invalid JSON (the defect the
schema transport removes), so the comparison counts it against the new request only. Both
@@ -0,0 +1,16 @@
{
"config": "cookie-workflow",
"released": true,
"date": "2026-10-29",
"by": "GStack Minder",
"ruling": "GStack Minder, 2026-10-29: release the GSTA-91 hold; a07 is matched by old-b; free-text parse errors cost train 42",
"scope": "cookie workflow judge transport only",
"recorded_result": "runs/comparison-1.result.json",
"train_red": {
"gate_run": "https://github.com/garrytan/gstack/actions/runs/40809051510",
"responses": [
"msg_011CgVnRtVj5vyLvb56m6KJH",
"msg_011CgVne5GStZ6f9pRoCvgWE"
]
}
}
+3
View File
@@ -1,5 +1,8 @@
{
"newModules": [
{ "file": "scripts/modules.ts", "movedFrom": "(new: module loadouts catalog and selection, GSTA-140)" },
{ "file": "scripts/modules-render.ts", "movedFrom": "(new: module loadout render and lock, GSTA-140)" },
{ "file": "scripts/modules-cli.ts", "movedFrom": "(new: bin/gstack-modules CLI, GSTA-140)" },
{ "file": "lib/harness-register.ts", "movedFrom": "(new: gstack-harness registrar, GSTA-135)" },
{ "file": "lib/harness-qm.ts", "movedFrom": "(new: gstack-harness QM skill-pack target, GSTA-135)" },
{ "file": "lib/design-lint/render.ts", "movedFrom": "(new: the bundled design lint, GSTA-105; ported from hallmark sloplint.mjs)" },
+19 -19
View File
@@ -5,50 +5,50 @@
"agy/gstack-autoplan": 109810,
"agy/gstack-plan-design-review": 110044,
"agy/gstack-plan-devex-review": 116496,
"agy/gstack-plan-eng-review": 147241,
"agy/gstack-review": 103755,
"agy/gstack-plan-eng-review": 144907,
"agy/gstack-review": 103743,
"codex/gstack-autoplan": 108553,
"codex/gstack-land-and-deploy": 100284,
"codex/gstack-plan-design-review": 111116,
"codex/gstack-plan-devex-review": 118004,
"codex/gstack-plan-eng-review": 148406,
"codex/gstack-review": 103278,
"codex/gstack-plan-eng-review": 146072,
"codex/gstack-review": 103266,
"copilot/gstack-autoplan": 109253,
"copilot/gstack-plan-design-review": 110355,
"copilot/gstack-plan-devex-review": 115958,
"copilot/gstack-plan-eng-review": 147406,
"copilot/gstack-review": 103336,
"copilot/gstack-plan-eng-review": 145072,
"copilot/gstack-review": 103324,
"cursor/gstack-autoplan": 108485,
"cursor/gstack-plan-design-review": 108900,
"cursor/gstack-plan-devex-review": 115228,
"cursor/gstack-plan-eng-review": 145996,
"cursor/gstack-review": 121626,
"cursor/gstack-plan-eng-review": 143662,
"cursor/gstack-review": 121614,
"factory/gstack-autoplan": 110763,
"factory/gstack-land-and-deploy": 100105,
"factory/gstack-plan-design-review": 111675,
"factory/gstack-plan-devex-review": 118591,
"factory/gstack-plan-eng-review": 149104,
"factory/gstack-review": 125399,
"factory/gstack-plan-eng-review": 146770,
"factory/gstack-review": 125387,
"gbrain/gstack-autoplan": 108892,
"gbrain/gstack-plan-eng-review": 128623,
"gbrain/gstack-plan-eng-review": 126289,
"hermes/gstack-autoplan": 108982,
"hermes/gstack-plan-eng-review": 128708,
"hermes/gstack-plan-eng-review": 126374,
"kiro/gstack-autoplan": 108259,
"kiro/gstack-plan-design-review": 108752,
"kiro/gstack-plan-devex-review": 115080,
"kiro/gstack-plan-eng-review": 145858,
"kiro/gstack-review": 121450,
"kiro/gstack-plan-eng-review": 143524,
"kiro/gstack-review": 121438,
"openclaw/gstack-autoplan": 108715,
"openclaw/gstack-plan-eng-review": 127138,
"openclaw/gstack-plan-eng-review": 124804,
"opencode/gstack-autoplan": 109234,
"opencode/gstack-plan-design-review": 110187,
"opencode/gstack-plan-devex-review": 115836,
"opencode/gstack-plan-eng-review": 147265,
"opencode/gstack-review": 122248,
"opencode/gstack-plan-eng-review": 144931,
"opencode/gstack-review": 122236,
"slate/gstack-autoplan": 108372,
"slate/gstack-plan-design-review": 108826,
"slate/gstack-plan-devex-review": 115154,
"slate/gstack-plan-eng-review": 145927,
"slate/gstack-review": 121538
"slate/gstack-plan-eng-review": 143593,
"slate/gstack-review": 121526
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ describe('gen-skill-docs stale-render prune', () => {
const source = path.join(out, 'source');
fs.mkdirSync(path.join(source, 'scripts'), { recursive: true });
fs.copyFileSync(path.join(ROOT, 'scripts', 'gen-skill-docs.ts'), path.join(source, 'scripts', 'gen-skill-docs.ts'));
for (const file of ['discover-skills.ts', 'external-skill-names.ts', 'gen-llms-txt.ts', 'gen-agents-digest.ts', 'gen-doctor-components.ts', 'models.ts']) {
for (const file of ['discover-skills.ts', 'external-skill-names.ts', 'gen-llms-txt.ts', 'gen-agents-digest.ts', 'gen-doctor-components.ts', 'models.ts', 'modules.ts']) {
fs.symlinkSync(path.join(ROOT, 'scripts', file), path.join(source, 'scripts', file), 'file');
}
fs.symlinkSync(path.join(ROOT, 'scripts', 'resolvers'), path.join(source, 'scripts', 'resolvers'), 'dir');
+16 -1
View File
@@ -9,7 +9,7 @@ import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { missingDependencies, parseRegistry, selectEntries, validateRegistry } from '../lib/regen';
import { missingDependencies, parseRegistry, runRegen, selectEntries, validateRegistry } from '../lib/regen';
import { BASE_REGISTRY, cleanup, commit, makeFixtureRepo, readFile, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
@@ -135,6 +135,21 @@ describe('gstack-regen (bin)', () => {
expect(parsed.wtree).toMatch(/^[0-9a-f]{40}$/);
});
// Value: protects=the receipt carry's generated class (only outputs a generator recreates) and a generator that reads
// its own registered output (gen-skill-docs and qa/templates/functional-report-template.md); fails_when=the proof
// deletes outputs before the verdict run, or proves a path the command never wrote; seam=runRegen; no_claim=-
test('prove: a writer proves its touched output; a generator that reads its own output stays fresh and proves nothing', () => {
const registryOf = (repo: FixtureRepo) => parseRegistry(fs.readFileSync(path.join(repo.work, '.gstack/generated.json'), 'utf8')).registry;
const writer = fixture();
commit(writer, { 'src/version.js': "module.exports = { VERSION: '1.1.0' };\n", 'generated/digest.md': '# digest v1.1.0\n', 'generated/notes.md': '# hand-written\n' });
expect(runRegen({ repoRoot: writer.work, registry: registryOf(writer), mode: 'check', only: ['digest'], prove: ['generated/digest.md', 'generated/notes.md', 'src/version.js'] }).results)
.toMatchObject([{ id: 'digest', status: 'fresh', proven: ['generated/digest.md'] }]);
const selfRead = fixture({ registry: { entries: [{ id: 'self', command: 'node selfread.js', outputs: ['generated/**'], inputs: ['selfread.js'] }] }, baseFiles: { 'selfread.js': "const fs=require('fs');fs.writeFileSync('generated/digest.md',fs.readFileSync('generated/digest.md','utf8'));\n" } });
commit(selfRead, { 'generated/digest.md': '# hand edit\n' });
expect(runRegen({ repoRoot: selfRead.work, registry: registryOf(selfRead), mode: 'check', prove: ['generated/digest.md'] }).results)
.toMatchObject([{ id: 'self', status: 'fresh', proven: [] }]);
});
test('a failing command is failed (exit 1, REGEN_COMMAND_FAILED, status incomplete); an output outside the declared globs is stale', () => {
const repo = fixture({ registry: { entries: [{ id: 'boom', command: 'node -e "process.exit(7)"', outputs: ['generated/**'] }, { id: 'leak', command: 'node -e "require(\'fs\').writeFileSync(\'docs/extra.md\',\'x\')"', outputs: ['generated/**'] }] } });
const r = runBin(repo, 'gstack-regen', ['check', '--base', 'main', '--allow-repo-commands']);
+4 -2
View File
@@ -44,12 +44,14 @@ describe('gstack-ship-receipt write', () => {
'--session-kind', 'unattended', '--artifacts-consumed', '2/3', '--run', 'run-1', '--thread', 'jam_1', '--queue-mode', 'stamp-at-merge',
'--preregistration', 'none (no preregistration_shas)', '--history', 'squash-ok', '--body', body, '--out', path.join(repo.root, 'receipt.md')]);
expect(ok.status).toBe(EXIT.ok);
expect(ok.err.trim()).toBe(`SHIP_RECEIPT: pr=42 head=${head.slice(0, 12)} version=1.2.4.0 base=main gated_tree=${repo.git('rev-parse', 'HEAD^{tree}').slice(0, 12)} tree=same-modulo-stamps gate-reuse=eligible (reusable: tests; rerun: -) predecessor=${'b'.repeat(12)} session_kind=unattended artifacts_consumed=2/3`);
expect(ok.err.trim()).toBe(`SHIP_RECEIPT: pr=42 head=${head.slice(0, 12)} version=1.2.4.0 base=main gated_tree=${repo.git('rev-parse', 'HEAD^{tree}').slice(0, 12)} tree=same-modulo-stamps gate-reuse=eligible (reusable: tests; rerun: -) predecessor=${'b'.repeat(12)} session_kind=unattended artifacts_consumed=2/3 depth=light`);
const parsed = extractReceipt(ok.out);
expect(parsed.found).toBe(true);
expect(parsed.errors).toEqual([]);
expect(Object.keys(parsed.receipt!)).toEqual(['schema_version', 'pr', 'head', 'version', 'base', 'gated_tree', 'predecessor', 'tree', 'gate_reuse', 'gate', 'ci', 'spend_usd', 'session_kind', 'artifacts_consumed', 'run', 'thread', 'policy', 'queue_mode', 'preregistration', 'history']);
expect(Object.keys(parsed.receipt!)).toEqual(['schema_version', 'pr', 'head', 'version', 'base', 'gated_tree', 'predecessor', 'tree', 'gate_reuse', 'gate', 'ci', 'spend_usd', 'session_kind', 'artifacts_consumed', 'run', 'thread', 'policy', 'queue_mode', 'preregistration', 'history', 'head_tree', 'product_patch_id', 'gstack_version', 'review_depth']);
expect(parsed.receipt).toMatchObject({ schema_version: 1, pr: 42, head, version: '1.2.4.0', base: 'main', tree: 'same-modulo-stamps', spend_usd: 0.42, ci: { checks: { pass: 3 } } });
expect(parsed.receipt!.head_tree).toBe(repo.git('rev-parse', 'HEAD^{tree}'));
expect(parsed.receipt!.product_patch_id).toMatch(/^([0-9a-f]{40}|none)$/);
expect(parsed.receipt!.policy).toMatch(/^origin\/main@[0-9a-f]{12}:\.gstack\/ship-policy\.json$/);
const written = fs.readFileSync(body, 'utf8');
expect(written.startsWith('```gstack-ship-receipt\n{\n "schema_version": 1,\n "pr": 42,\n "head": "' + head + '"')).toBe(true);
+16
View File
@@ -84,6 +84,22 @@ describe('write (FRESH bump)', () => {
expect(code).toBe(2);
});
test('--manifest-form full keeps a four-part manifest and its lockfile as written', () => {
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'vbump-full-'));
fs.writeFileSync(path.join(d2, 'VERSION'), '0.60.482.0\n');
fs.writeFileSync(path.join(d2, 'package.json'), JSON.stringify({ name: 'g', version: '0.60.482.0' }, null, 2) + '\n');
fs.writeFileSync(path.join(d2, 'package-lock.json'), JSON.stringify({ name: 'g', version: '0.60.482.0', lockfileVersion: 3, packages: { '': { version: '0.60.482.0' } } }, null, 2) + '\n');
const out = JSON.parse(execFileSync('bun', [BIN, 'write', '--version', '0.60.483.0', '--manifest-form', 'full'], { cwd: d2, timeout: 30_000 }).toString());
expect(out.packageJsonVersion).toBe('0.60.483.0');
expect(JSON.parse(fs.readFileSync(path.join(d2, 'package.json'), 'utf-8')).version).toBe('0.60.483.0');
const lock = JSON.parse(fs.readFileSync(path.join(d2, 'package-lock.json'), 'utf-8'));
expect([lock.version, lock.packages[''].version]).toEqual(['0.60.483.0', '0.60.483.0']);
const bad = spawnSync('bun', [BIN, 'write', '--version', '0.60.484.0', '--manifest-form', 'wide'], { cwd: d2, timeout: 30_000 });
expect(bad.status).toBe(2);
expect(fs.readFileSync(path.join(d2, 'VERSION'), 'utf-8').trim()).toBe('0.60.483.0');
fs.rmSync(d2, { recursive: true, force: true });
});
test('VERSION-only repo (no package.json) writes just VERSION', () => {
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'vbump-noPkg-'));
fs.writeFileSync(path.join(d2, 'VERSION'), '0.1.0.0\n');
+1
View File
@@ -62,6 +62,7 @@ export const FREE_FIXTURES: Readonly<Record<string, readonly string[]>> = {
'test/fixtures/context-bill/**': ['test/context-bill.test.ts', 'test/context-budget-ratchet.test.ts'],
'test/fixtures/context-budget.json': ['test/helpers/capture-context-budget.ts', 'test/test-value-bar.test.ts'],
'test/fixtures/context-restore-fragment-match-captures.json': ['test/context-restore-fragment-verdict.test.ts'],
'test/fixtures/cookie-judge-train42-replies.json': ['test/cookie-workflow-judge-input.test.ts'],
'test/fixtures/coverage-audit-cd-chain.json': ['test/coverage-audit-evidence.test.ts'],
'test/fixtures/coverage-audit-parallel-column.json': ['test/coverage-audit-evidence.test.ts'],
'test/fixtures/coverage-audit-separator-collision.json': ['test/coverage-audit-evidence.test.ts'],
+63 -21
View File
@@ -135,7 +135,21 @@ export async function callJudge<T>(
// could be exhausted before a frontier judge emitted any JSON.
const resolvedModel = resolveEvalModel('judge', model);
const maxTokens = opts?.max_tokens ?? DEFAULT_JUDGE_MAX_TOKENS;
const client = new Anthropic();
// What a cancelled request had received, so a judge deadline names rate-limit
// waits, SDK retries, a stream with no events, or a long thinking phase. Counts
// only, never content; an observer that throws never changes the judge outcome.
const started = performance.now();
const elapsedMs = () => Math.round(performance.now() - started);
const progress = { httpRequests: 0, httpStatuses: [] as number[], retryWaitsMs: [] as number[], connectedMs: null as number | null,
firstEventMs: null as number | null, lastEventMs: null as number | null, block: null as string | null, thinkingChars: 0,
textChars: 0, messageId: null as string | null, requestId: null as string | null };
const observe = (record: () => void) => { try { record(); } catch {} };
const client = new Anthropic({ fetch: async (...args: Parameters<typeof fetch>) => {
progress.httpRequests += 1;
const response = await globalThis.fetch(...args);
progress.httpStatuses.push(response.status);
return response;
} });
const request = {
model: resolvedModel,
@@ -146,34 +160,62 @@ export async function callJudge<T>(
...(opts?.effort === undefined ? {} : { effort: opts.effort }) } }),
messages: [{ role: 'user' as const, content: prompt }],
};
const makeRequest = () => opts?.stream
? client.messages.stream(request, signal ? { signal } : undefined).finalMessage()
: client.messages.create(request, signal ? { signal } : undefined);
const makeRequest = () => {
if (!opts?.stream) return client.messages.create(request, signal ? { signal } : undefined);
const stream = client.messages.stream(request, signal ? { signal } : undefined);
stream.on('connect', () => observe(() => {
progress.connectedMs ??= elapsedMs();
progress.requestId ??= stream.request_id ?? null;
}));
stream.on('streamEvent', event => observe(() => {
progress.firstEventMs ??= elapsedMs();
progress.lastEventMs = elapsedMs();
progress.requestId ??= stream.request_id ?? null;
if (event.type === 'message_start') progress.messageId = event.message.id;
if (event.type === 'content_block_start') progress.block = event.content_block.type;
if (event.type === 'content_block_delta' && event.delta.type === 'thinking_delta') progress.thinkingChars += event.delta.thinking.length;
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') progress.textChars += event.delta.text.length;
}));
return stream.finalMessage();
};
// 429s under CI concurrency: jittered exponential backoff over 3 retries
// (~1s/4s/16s + jitter), honoring the server's retry-after when present.
// The old single fixed 1s retry lost races reliably at 40-way concurrency.
let response;
let attempt = 0;
for (;;) {
try {
signal?.throwIfAborted();
response = await makeRequest();
signal?.throwIfAborted();
break;
} catch (err: any) {
signal?.throwIfAborted();
if (err?.status !== 429 || attempt >= 3) throw err;
const retryAfterSecs = Number(err?.headers?.['retry-after']);
const baseMs = Number.isFinite(retryAfterSecs) && retryAfterSecs > 0
? retryAfterSecs * 1000
: 1000 * 4 ** attempt;
await delay(baseMs + Math.random() * 500, undefined, { signal }).catch(error => {
try {
for (;;) {
try {
signal?.throwIfAborted();
throw error;
});
attempt += 1;
response = await makeRequest();
signal?.throwIfAborted();
break;
} catch (err: any) {
signal?.throwIfAborted();
if (err?.status !== 429 || attempt >= 3) throw err;
const retryAfterSecs = Number(err?.headers?.['retry-after']);
const baseMs = Number.isFinite(retryAfterSecs) && retryAfterSecs > 0
? retryAfterSecs * 1000
: 1000 * 4 ** attempt;
const waitMs = baseMs + Math.random() * 500;
progress.retryWaitsMs.push(Math.round(waitMs));
await delay(waitMs, undefined, { signal }).catch(error => {
signal?.throwIfAborted();
throw error;
});
attempt += 1;
}
}
} catch (error) {
if (signal?.aborted) {
console.error(JSON.stringify({
type: 'llm-judge-aborted', model: resolvedModel, stream: opts?.stream === true, elapsedMs: elapsedMs(),
attempts: attempt + 1, ...progress,
reason: (signal.reason instanceof Error ? signal.reason.message : String(signal.reason)).slice(0, 200),
}));
}
throw error;
}
opts?.onResponse?.({
+6 -2
View File
@@ -321,17 +321,21 @@ describe('landed schemas match the recorded calibration', () => {
expect(recorded.complete).toBe(true);
const cookie = result('cookie-workflow');
expect(cookie.complete).toBe(true);
expect(cookie.landing.lands).toBe(false);
const release = JSON.parse(fs.readFileSync(path.join(import.meta.dir, 'fixtures', 'judge-calibration', 'cookie-workflow', 'release.json'), 'utf8'));
expect(release).toMatchObject({ config: 'cookie-workflow', released: true, by: 'GStack Minder', scope: 'cookie workflow judge transport only',
ruling: 'GStack Minder, 2026-10-29: release the GSTA-91 hold; a07 is matched by old-b; free-text parse errors cost train 42' });
const registrations = [...evalSource.matchAll(/await runWorkflowJudge\(\{([\s\S]*?)\n \}\);/g)].map(match => match[1]!);
expect(registrations.length).toBeGreaterThan(14);
for (const body of registrations) {
const testName = /testName: '([^']+)'/.exec(body)![1];
const defaultConfig = !/^\s+(?:agentCapability|model|readInput|thresholds|maxTokens|stream|effort):|\.\.\.COOKIE_WORKFLOW_JUDGE/m.test(body);
const expected = testName === 'ship/SKILL.md workflow' || (defaultConfig && recorded.landing.lands)
|| (testName === 'setup-browser-cookies/SKILL.md workflow' && cookie.landing.lands);
|| testName === 'setup-browser-cookies/SKILL.md workflow';
expect(body.includes('schemaTransport: true'), testName).toBe(expected);
expect(body.includes('compactReasoning: true'), testName).toBe(testName === 'ship/SKILL.md workflow');
}
expect(registrations.filter(body => body.includes('schemaTransport: true'))).toHaveLength(cookie.landing.lands ? 16 : 15);
expect(registrations.filter(body => body.includes('schemaTransport: true'))).toHaveLength(16);
const cookieBody = registrations.find(body => body.includes("testName: 'setup-browser-cookies/SKILL.md workflow'"))!;
expect(cookieBody).toContain('...COOKIE_WORKFLOW_JUDGE');
expect(cookieBody).not.toMatch(/^\s+(?:agentCapability|model|thresholds|maxTokens|stream|effort):/m);
+19
View File
@@ -85,6 +85,25 @@ describe('callJudge cancellation', () => {
expect(create).toHaveBeenCalledTimes(1);
}, 3000);
test('an abort during a 429 wait retains the wait it interrupted', async () => {
const diagnostics = spyOn(console, 'error').mockImplementation(() => {});
try {
const controller = new AbortController();
create.mockRejectedValue({ status: 429, headers: { 'retry-after': '30' } });
const pending = callJudge('Rate-limited request.', 'test-judge-model', { signal: controller.signal });
await new Promise<void>(resolve => setImmediate(resolve));
controller.abort(new Error('Expired during rate-limit backoff.'));
await expect(pending).rejects.toThrow('Expired during rate-limit backoff.');
expect(diagnostics).toHaveBeenCalledTimes(1);
expect(JSON.parse(diagnostics.mock.calls[0]![0])).toMatchObject({ type: 'llm-judge-aborted', model: 'test-judge-model',
stream: false, attempts: 1, httpRequests: 0, retryWaitsMs: [30000], firstEventMs: null, reason: 'Expired during rate-limit backoff.' });
create.mockResolvedValue(response as any);
diagnostics.mockClear();
expect(await callJudge('Judge this.', undefined, { signal: new AbortController().signal })).toEqual({ passed: true });
expect(diagnostics).not.toHaveBeenCalled();
} finally { diagnostics.mockRestore(); }
}, 3000);
test('keeps three 429 retries for callers without a signal', async () => {
const rateLimit = { status: 429, headers: { 'retry-after': '0.001' } };
create.mockRejectedValue(rateLimit);
+120
View File
@@ -103,3 +103,123 @@ test('streaming retains the caller abort signal instead of extending its deadlin
expect(await rejected).toBe(failure);
expect(transport).toHaveBeenCalledTimes(1);
});
test('a judge aborted mid-stream retains what it had received, as counts without content', async () => {
const encoder = new TextEncoder();
const partial = [
{ type: 'message_start', message: { id: 'msg_stalled', type: 'message', role: 'assistant', model: 'claude-fable-5-1',
content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 99023, output_tokens: 1 } } },
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '', signature: '' } },
{ type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'PRIVATE_THINKING' } },
];
let delivered!: () => void;
const received = new Promise<void>(resolve => { delivered = resolve; });
transport.mockImplementation(async (_url: string, init: RequestInit) => {
const chunks = partial.map(event => encoder.encode(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`));
return new Response(new ReadableStream({
start(stream) {
init.signal!.addEventListener('abort', () => stream.error(init.signal!.reason), { once: true });
},
pull(stream) {
const chunk = chunks.shift();
if (chunk) stream.enqueue(chunk);
else delivered();
},
}, { highWaterMark: 0 }), { headers: { 'content-type': 'text/event-stream', 'request-id': 'req_stalled' } });
});
const controller = new AbortController();
const rejected = callJudge('score the whole bundle', 'claude-fable-5-1', {
max_tokens: 65_536, stream: true, jsonSchema: WORKFLOW_JUDGE_RESPONSE_SCHEMA, signal: controller.signal,
}).catch(error => error);
await received;
const failure = new Error('ship/SKILL.md workflow exceeded its workflow judge work deadline');
controller.abort(failure);
expect(await rejected).toBe(failure);
expect(diagnostics).toHaveBeenCalledTimes(1);
const diagnostic = diagnostics.mock.calls[0]![0];
expect(diagnostic).not.toContain('PRIVATE_');
const event = JSON.parse(diagnostic);
expect(event).toMatchObject({ type: 'llm-judge-aborted', model: 'claude-fable-5-1', stream: true, attempts: 1,
httpRequests: 1, httpStatuses: [200], retryWaitsMs: [], block: 'thinking', thinkingChars: 'PRIVATE_THINKING'.length, textChars: 0,
messageId: 'msg_stalled', requestId: 'req_stalled', reason: failure.message });
expect(event.firstEventMs).toBeGreaterThanOrEqual(0);
expect(event.lastEventMs).toBeGreaterThanOrEqual(event.firstEventMs); // gstack-timing-ok: ordering of two recorded offsets, not an elapsed-time bound
expect(event.elapsedMs).toBeGreaterThanOrEqual(event.lastEventMs); // gstack-timing-ok: ordering of two recorded offsets, not an elapsed-time bound
});
test('a stream that never sent an event is retained as such, and provider errors without an abort are not', async () => {
let sent!: () => void;
const requested = new Promise<void>(resolve => { sent = resolve; });
transport.mockImplementation((_url: string, init: RequestInit) => new Promise((_resolve, reject) => {
init.signal!.addEventListener('abort', () => reject(init.signal!.reason), { once: true });
sent();
}));
const controller = new AbortController();
const rejected = callJudge('score', 'claude-fable-5-1', { max_tokens: 65_536, stream: true, signal: controller.signal }).catch(error => error);
await requested;
controller.abort(new Error('deadline'));
await rejected;
expect(JSON.parse(diagnostics.mock.calls[0]![0])).toMatchObject({ type: 'llm-judge-aborted', httpRequests: 1,
httpStatuses: [], connectedMs: null, firstEventMs: null, lastEventMs: null, block: null, thinkingChars: 0, messageId: null, requestId: null, reason: 'deadline' });
diagnostics.mockClear();
transport.mockResolvedValue(new Response(JSON.stringify({ type: 'error', error: { type: 'api_error', message: 'boom' } }),
{ status: 500, headers: { 'content-type': 'application/json', 'x-should-retry': 'false' } }));
await expect(callJudge('score', 'claude-fable-5-1', { max_tokens: 65_536, stream: true, signal: new AbortController().signal })).rejects.toThrow();
expect(diagnostics).not.toHaveBeenCalled();
});
test('a deadline after headers but before any event retains the connection and its request id', async () => {
let reading!: () => void;
const connected = new Promise<void>(resolve => { reading = resolve; });
transport.mockImplementation(async (_url: string, init: RequestInit) => new Response(new ReadableStream({
start(stream) { init.signal!.addEventListener('abort', () => stream.error(init.signal!.reason), { once: true }); },
pull() { reading(); return new Promise<void>(() => {}); },
}, { highWaterMark: 0 }), { headers: { 'content-type': 'text/event-stream', 'request-id': 'req_quiet' } }));
const controller = new AbortController();
const rejected = callJudge('score', 'claude-fable-5-1', { max_tokens: 65_536, stream: true, signal: controller.signal }).catch(error => error);
await connected;
controller.abort(new Error('deadline'));
await rejected;
const event = JSON.parse(diagnostics.mock.calls[0]![0]);
expect(event).toMatchObject({ httpRequests: 1, httpStatuses: [200], requestId: 'req_quiet', firstEventMs: null, block: null });
expect(event.connectedMs).toBeGreaterThanOrEqual(0);
});
test('SDK-internal 429 retries are counted, so throttling never reads as a silent stream', async () => {
let resent!: () => void;
const retried = new Promise<void>(resolve => { resent = resolve; });
transport.mockResolvedValueOnce(new Response(JSON.stringify({ type: 'error', error: { type: 'rate_limit_error', message: 'slow down' } }),
{ status: 429, headers: { 'content-type': 'application/json', 'retry-after-ms': '1' } }));
transport.mockImplementationOnce((_url: string, init: RequestInit) => new Promise((_resolve, reject) => {
init.signal!.addEventListener('abort', () => reject(init.signal!.reason), { once: true });
resent();
}));
const controller = new AbortController();
const rejected = callJudge('score', 'claude-fable-5-1', { max_tokens: 65_536, stream: true, signal: controller.signal }).catch(error => error);
await retried;
controller.abort(new Error('deadline'));
await rejected;
expect(JSON.parse(diagnostics.mock.calls[0]![0])).toMatchObject({ attempts: 1, httpRequests: 2, httpStatuses: [429],
retryWaitsMs: [], connectedMs: null, firstEventMs: null });
});
test('an event the diagnostic cannot read never changes the judge result', async () => {
const unreadable = response();
const body = (await unreadable.text()).replace('"thinking":"PRIVATE_THINKING"', '"thinking":null');
transport.mockResolvedValue(new Response(body, { headers: { 'content-type': 'text/event-stream' } }));
expect(await callJudge('score the whole bundle', 'claude-fable-5-1', {
max_tokens: 65_536, stream: true, jsonSchema: WORKFLOW_JUDGE_RESPONSE_SCHEMA,
})).toEqual(scores);
});
test('an abort reason is retained at most 200 characters long', async () => {
transport.mockImplementation((_url: string, init: RequestInit) => new Promise((_resolve, reject) => {
init.signal!.addEventListener('abort', () => reject(init.signal!.reason), { once: true });
}));
const controller = new AbortController();
const rejected = callJudge('score', 'claude-fable-5-1', { max_tokens: 65_536, stream: true, signal: controller.signal }).catch(error => error);
controller.abort(new Error('x'.repeat(500)));
await rejected;
expect(JSON.parse(diagnostics.mock.calls[0]![0]).reason).toBe('x'.repeat(200));
});
+301
View File
@@ -0,0 +1,301 @@
/**
* Module loadouts (GSTA-140, PR 1a): modules/loadouts.json, the derived
* catalog, --modules selection, the render filter, and gstack-modules render.
* Every rung loads `full` today, so the synthetic ad-hoc arms below are the
* only exclusion coverage; committed bytes are covered by Skill Docs Freshness.
*/
import { afterAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { bunIsolationArgs } from '../lib/bun-isolation';
import { MODEL_CATALOG } from '../lib/model-catalog';
import { runGeneration } from '../scripts/gen-skill-docs';
import { ModuleError, excludedSkillDirs, loadLoadouts, moduleCatalog, normalizeModelId, rungFor, selectModules, validateLoadouts, type ModuleErrorCode } from '../scripts/modules';
import { LOCK_FILE, renderLoadout, type LoadoutLock } from '../scripts/modules-render';
import { applySectionGuards } from '../scripts/resolvers/sections';
import { PREAMBLE_MODULE_IDS, preambleSections } from '../scripts/resolvers/preamble';
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
const ROOT = path.resolve(import.meta.dir, '..');
const manifest = loadLoadouts();
const catalog = moduleCatalog(manifest);
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'loadouts-'));
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
/** Model ids gstack runs on or GSTA-2 measures (Capy's model list, 2026-10-28). */
const CORPUS = [
...MODEL_CATALOG.map(e => e.model), 'gpt-6-astra',
'anthropic/claude-opus-5-5', 'anthropic/claude-haiku-5-5', 'anthropic/claude-sonnet-5-5', 'anthropic/claude-fable-5-1',
'openai/gpt-6.1-sol', 'openai/gpt-6-luna', 'openai/gpt-6.1-terra', 'deepseek/deepseek-v4.1-flash-1017', 'deepseek/deepseek-v4-pro',
'zai/glm-5.3', 'zai/glm-5.3-flash', 'google/gemini-4-argon', 'google/gemini-3.8-flash', 'xai/grok-4.7',
'moonshotai/kimi-k3.1-pro', 'qwen/qwen3.8-max', 'qwen/qwen3.8-flash', 'minimax/minimax-m3',
];
function codeOf(fn: () => unknown): ModuleErrorCode | 'none' {
try { fn(); return 'none'; } catch (error) { if (error instanceof ModuleError) return error.code; throw error; }
}
describe('modules/loadouts.json', () => {
test('validates against the derived catalog, with disjoint rungs over the model corpus', () => {
expect(validateLoadouts(manifest, catalog, CORPUS)).toEqual([]);
});
test('every rung and unknown loads full until GSTA-2 measures a smaller loadout', () => {
expect(Object.values(manifest.default)).toEqual(['full', 'full', 'full', 'full', 'full']);
});
test('the catalog derives every module kind from its source files', () => {
const ids = new Set(catalog.map(e => e.id));
expect(ids.has('skill:gstack')).toBe(true);
expect(ids.has('section:office-hours/second-opinion')).toBe(true);
expect(ids.has('overlay:opus-4-8')).toBe(true);
for (const id of PREAMBLE_MODULE_IDS) expect(ids.has(id)).toBe(true);
for (const entry of catalog) expect(fs.existsSync(path.join(ROOT, entry.source))).toBe(true);
expect(catalog.filter(e => e.pinned).map(e => e.id)).toEqual(expect.arrayContaining(['preamble:bash', 'skill:gstack', 'skill:gstack-upgrade', ...manifest.pinned]));
});
test('model ids normalize and map to rungs', () => {
expect(normalizeModelId(' Anthropic/Claude-Haiku-5-5[1m] ')).toBe('claude-haiku-5-5');
expect(rungFor(manifest, 'anthropic/claude-opus-5-5')).toBe('strong');
expect(rungFor(manifest, 'anthropic/claude-haiku-5-5')).toBe('mid');
expect(rungFor(manifest, 'deepseek/deepseek-v4.1-flash-1017')).toBe('light');
expect(rungFor(manifest, 'zai/glm-5.3-flash')).toBe('light');
expect(rungFor(manifest, 'zai/glm-5.3')).toBe('mid');
expect(rungFor(manifest, 'haiku')).toBe('mid');
expect(rungFor(manifest, 'some-new-model')).toBe('unknown');
expect(rungFor(manifest, 'constructor')).toBe('unknown');
for (const model of MODEL_CATALOG) expect(rungFor(manifest, model.model)).not.toBe('unknown');
});
test('a broken manifest names each problem', () => {
const bad = structuredClone(manifest);
bad.loadouts.lean = { base: 'missing', exclude: ['preamble:bash', 'section:nope/none'] };
bad.default.mid = 'ghost';
bad.default.light = 'toString';
bad.loadouts.noship = { base: 'constructor', exclude: ['skill:ship'] };
bad.rungs.light.push('claude-opus-5*');
const problems = validateLoadouts(bad, catalog, CORPUS).join('\n');
expect(problems).toContain('loadouts.lean.base names no loadout');
expect(problems).toContain('excludes pinned preamble:bash');
expect(problems).toContain('"section:nope/none", which is not a module');
expect(problems).toContain('default.mid names no loadout');
expect(problems).toContain('default.light names no loadout');
expect(problems).toContain('loadouts.noship.base names no loadout');
expect(problems).toContain('excludes skill:ship, which holds pinned section:ship/');
expect(problems).toContain('matches more than one rung');
});
});
describe('selectModules', () => {
test('full and an empty spec render today\'s bytes (no filter)', () => {
expect(selectModules({ loadout: 'full' })).toBeNull();
expect(selectModules({})).toBeNull();
expect(selectModules({ modules: 'full,-section:office-hours/second-opinion,+section:office-hours/second-opinion' })).toBeNull();
});
test('an ad-hoc spec records its name, normalized spec and hash', () => {
const s = selectModules({ modules: ' full , -skill:design-shotgun ' })!;
expect(s.name).toBe('adhoc');
expect(s.spec).toBe('full,-skill:design-shotgun');
expect(s.specSha256).toMatch(/^[0-9a-f]{64}$/);
expect(excludedSkillDirs(s)).toEqual(['design-shotgun']);
});
test('bad input is refused with a code, never a fallback to full', () => {
expect(codeOf(() => selectModules({ loadout: 'nope' }))).toBe('LOADOUT_UNKNOWN');
expect(codeOf(() => selectModules({ modules: 'full,skill:qa' }))).toBe('MODULE_SPEC_INVALID');
expect(codeOf(() => selectModules({ loadout: 'full', modules: 'full' }))).toBe('MODULE_SPEC_INVALID');
expect(codeOf(() => selectModules({ loadout: 'full,-skill:qa' }))).toBe('MODULE_SPEC_INVALID');
expect(codeOf(() => selectModules({ modules: 'full,-skill:nope' }))).toBe('MODULE_UNKNOWN');
expect(codeOf(() => selectModules({ modules: 'full,-preamble:bash' }))).toBe('MODULE_PINNED');
expect(codeOf(() => selectModules({ modules: 'full,-section:ship/adversarial' }))).toBe('MODULE_PINNED');
expect(codeOf(() => selectModules({ modules: 'full,-skill:ship' }))).toBe('MODULE_PINNED');
expect(codeOf(() => selectModules({ modules: 'full,-skill:autoplan' }))).toBe('MODULE_PINNED');
expect(codeOf(() => selectModules({ loadout: 'constructor' }))).toBe('LOADOUT_UNKNOWN');
});
test('every error code has a troubleshooting section', () => {
const doc = fs.readFileSync(path.join(ROOT, 'docs/troubleshooting.md'), 'utf-8');
const codes: ModuleErrorCode[] = ['LOADOUTS_INVALID', 'LOADOUT_UNKNOWN', 'MODULE_SPEC_INVALID', 'MODULE_UNKNOWN', 'MODULE_PINNED', 'MODULE_EXCLUSION_UNSAFE', 'RENDER_FAILED', 'RENDER_DEST_NOT_OWNED'];
for (const code of codes) {
const anchor = code.toLowerCase().replaceAll('_', '-');
const at = doc.indexOf(`<a id="${anchor}"></a>`);
expect(at).toBeGreaterThan(-1);
expect(doc.slice(at).split('\n')[1]).toMatch(new RegExp(`^### \`${code}`));
}
});
});
describe('render filter units', () => {
const ctx = (extra: Partial<TemplateContext> = {}): TemplateContext => ({
skillName: 'office-hours', tmplPath: 'office-hours/SKILL.md.tmpl', host: 'claude', paths: HOST_PATHS.claude, preambleTier: 3, model: 'claude', ...extra,
});
const filter = (excluded: string[], preambleTierMax?: number) => ({
name: 'test', excluded: new Set(excluded), pinned: new Set(catalog.filter(e => e.pinned).map(e => e.id)), preambleTierMax,
});
test('section guards vanish under full and drop their text when the section is excluded', () => {
const text = 'a\n{{IF_SECTION:second-opinion}}offer\n{{/IF_SECTION}}b';
expect(applySectionGuards(text, ctx())).toBe('a\noffer\nb');
expect(applySectionGuards(text, ctx({ modules: filter(['section:office-hours/second-opinion']) }))).toBe('a\nb');
expect(() => applySectionGuards('{{IF_SECTION:nope}}x{{/IF_SECTION}}', ctx())).toThrow('no section "nope"');
});
test('a tier cap drops unpinned pieces; pinned pieces follow full', () => {
const full = preambleSections(ctx()).join('\n');
const capped = preambleSections(ctx({ modules: filter([], 1) })).join('\n');
expect(full).toContain('## Writing Style');
expect(capped).not.toContain('## Writing Style');
expect(capped).toContain('## AskUserQuestion Format');
const tier1 = ctx({ preambleTier: 1 });
expect(preambleSections({ ...tier1, modules: filter([], 1) })).toEqual(preambleSections(tier1));
});
test('excluding a base overlay drops its text from the overlay that inherits it', () => {
const astra = ctx({ model: 'gpt-6-astra' as TemplateContext['model'] });
const withBase = preambleSections(astra).join('\n');
const withoutBase = preambleSections({ ...astra, modules: filter(['overlay:gpt']) }).join('\n');
expect(withBase.length).toBeGreaterThan(withoutBase.length);
expect(preambleSections({ ...astra, modules: filter(['overlay:gpt-6-astra']) }).join('\n')).not.toContain('Model-Specific Behavioral Patch');
});
});
describe('gstack-modules render', () => {
const spec = 'full,-section:office-hours/second-opinion,-section:office-hours/visual-design,-preamble:writing-style,-overlay:claude,-skill:design-shotgun';
const readLock = (dir: string) => JSON.parse(fs.readFileSync(path.join(dir, LOCK_FILE), 'utf-8')) as LoadoutLock;
const ensureArm = async (dir: string) => { if (!fs.existsSync(path.join(dir, LOCK_FILE))) await renderLoadout({ host: 'claude', selection: selectModules({ modules: spec }), outDir: dir }); };
test('claude: one arm excludes one module of each kind and locks every file', async () => {
const out = path.join(tmp, 'claude-arm');
const lock = await renderLoadout({ host: 'claude', selection: selectModules({ modules: spec }), outDir: out });
const skill = fs.readFileSync(path.join(out, 'office-hours/SKILL.md'), 'utf-8');
expect(skill).toContain('<!-- loadout: adhoc -->');
expect(skill).not.toContain('Want a second opinion');
expect(skill).not.toContain('## Phase 4.25: Visual design');
expect(skill).toContain('> Not loaded in the adhoc loadout: skip running the Phase 3.5 second opinion');
expect(skill).not.toContain('sections/second-opinion.md');
expect(fs.existsSync(path.join(out, 'office-hours/sections/second-opinion.md'))).toBe(false);
expect(fs.existsSync(path.join(out, 'office-hours/sections/design-and-handoff.md'))).toBe(true);
expect(skill).toContain(`${out}/office-hours/sections/design-and-handoff.md`);
const preamble = fs.readFileSync(path.join(out, 'office-hours/sections/preamble.md'), 'utf-8');
expect(preamble).not.toContain('## Writing Style');
expect(preamble).not.toContain('Model-Specific Behavioral Patch');
expect(preamble).toContain('## AskUserQuestion Format');
expect(fs.existsSync(path.join(out, 'design-shotgun'))).toBe(false);
expect(fs.readFileSync(path.join(out, 'gstack/llms.txt'), 'utf-8')).not.toContain('design-shotgun/SKILL.md');
expect(fs.readFileSync(path.join(out, 'SKILL.md'), 'utf-8')).toContain('Disabled on this install (never invoke or suggest them): `/design-shotgun`');
expect(lock).toMatchObject({ loadout: 'adhoc', spec, host: 'claude', model: 'claude' });
expect(lock.excluded).toEqual(spec.split(',').slice(1).map(t => t.slice(1)).sort());
expect(readLock(out).files.length).toBe(lock.files.length);
expect(lock.files.find(f => f.path === 'office-hours/SKILL.md')!.bytes).toBe(Buffer.byteLength(skill));
expect(fs.readdirSync(tmp).filter(n => n.includes('.stage-') || n.includes('.old-'))).toEqual([]);
});
test('codex: the same arm inlines nothing it excluded', async () => {
const out = path.join(tmp, 'codex-arm');
await renderLoadout({ host: 'codex', selection: selectModules({ modules: spec }), outDir: out, model: 'gpt-6-astra' });
const skill = fs.readFileSync(path.join(out, '.agents/skills/gstack-office-hours/SKILL.md'), 'utf-8');
expect(skill).not.toContain('Want a second opinion');
expect(skill).toContain('> Not loaded in the adhoc loadout');
expect(fs.existsSync(path.join(out, '.agents/skills/gstack-design-shotgun'))).toBe(false);
expect(readLock(out).model).toBe('gpt-6-astra');
});
test('a dangling reference is refused and the earlier render survives', async () => {
const out = path.join(tmp, 'claude-arm');
await ensureArm(out);
const before = readLock(out);
const unsafe = renderLoadout({ host: 'claude', selection: selectModules({ modules: 'full,-section:plan-design-review/review-sections' }), outDir: out });
await expect(unsafe).rejects.toMatchObject({ code: 'MODULE_EXCLUSION_UNSAFE' });
await expect(unsafe).rejects.toThrow('referenced by plan-design-review/sections/checklist.md');
await expect(renderLoadout({ host: 'claude', selection: selectModules({ modules: 'full,-skill:qa' }), outDir: out })).rejects.toThrow(/skill:qa: referenced by qa-only\//);
expect(readLock(out)).toEqual(before);
expect(fs.readdirSync(tmp).filter(n => n.includes('.stage-'))).toEqual([]);
});
test('a later render replaces an earlier one; a foreign directory is never replaced', async () => {
const out = path.join(tmp, 'claude-arm');
await ensureArm(out);
expect(readLock(out).loadout).toBe('adhoc');
const lock = await renderLoadout({ host: 'claude', selection: selectModules({ modules: 'full,-skill:design-shotgun' }), outDir: out });
expect(readLock(out).spec_sha256).toBe(lock.spec_sha256);
expect(fs.existsSync(path.join(out, 'office-hours/sections/second-opinion.md'))).toBe(true);
const foreign = path.join(tmp, 'foreign');
fs.mkdirSync(foreign);
fs.writeFileSync(path.join(foreign, 'notes.txt'), 'mine');
await expect(renderLoadout({ host: 'claude', selection: null, outDir: foreign })).rejects.toMatchObject({ code: 'RENDER_DEST_NOT_OWNED' });
expect(fs.readFileSync(path.join(foreign, 'notes.txt'), 'utf-8')).toBe('mine');
});
test('the full loadout regenerates the committed claude tree byte for byte', async () => {
const result = await runGeneration({ host: 'claude', dryRun: true, modules: selectModules({ loadout: 'full' }) });
expect(result.diagnostics.filter(d => d.kind === 'stale' || d.kind === 'error')).toEqual([]);
});
});
describe('modules/loadouts.schema.json', () => {
const schema = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/loadouts.schema.json'), 'utf-8'));
const raw = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/loadouts.json'), 'utf-8'));
test('the manifest carries every required key and no key the schema lacks', () => {
for (const key of schema.required) expect(raw).toHaveProperty(key);
for (const key of Object.keys(raw)) expect(Object.keys(schema.properties)).toContain(key);
expect(raw.version).toBe(schema.properties.version.const);
});
test('rung names and patterns agree between the schema and the manifest', () => {
const rungs = schema.properties.rungs.required as string[];
expect(Object.keys(raw.rungs).sort()).toEqual([...rungs].sort());
expect([...schema.properties.aliases.additionalProperties.enum].sort()).toEqual([...rungs].sort());
const pattern = new RegExp(schema.properties.rungs.properties.frontier.items.pattern);
for (const rung of rungs) for (const glob of raw.rungs[rung]) expect(glob).toMatch(pattern);
});
});
describe('bin/gstack-modules', () => {
const run = (...args: string[]) => {
const result = spawnSync(process.execPath, [...bunIsolationArgs(), path.join(ROOT, 'bin/gstack-modules'), ...args], { cwd: ROOT, encoding: 'utf-8', timeout: 60_000 });
return { code: result.status, stdout: result.stdout, stderr: result.stderr };
};
test('validate and list read the committed manifest', () => {
expect(run('validate')).toMatchObject({ code: 0, stdout: expect.stringContaining('LOADOUTS: ok') });
const listed = JSON.parse(run('list', '--json').stdout);
expect(listed.modules.map((m: { id: string }) => m.id)).toEqual(catalog.map(e => e.id));
const pinned = run('list', '--pinned').stdout;
expect(pinned).toContain('preamble:bash');
expect(pinned).not.toContain('skill:design-shotgun');
});
test('refused inputs exit 2 with their code; unknown commands print usage', () => {
const out = path.join(tmp, 'cli-refused');
expect(run('render', '--host', 'claude', '--modules', 'full,-skill:nope', '--out-dir', out)).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_UNKNOWN') });
expect(run('render', '--host', 'claude', '--modules', 'full,-preamble:bash', '--out-dir', out)).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_PINNED') });
expect(run('render', '--host', 'claude', '--loadout', 'full', '--out-dir', out, '--plan-mode', 'maybe')).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_SPEC_INVALID') });
expect(run('render', '--host', 'claude', '--loadout', 'full', '--out-dir', out, '--model', 'no-such-model')).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_SPEC_INVALID') });
expect(run('render', '--host', 'claude', '--out-dir', out)).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_SPEC_INVALID') });
expect(fs.existsSync(out)).toBe(false);
expect(run('bogus')).toMatchObject({ code: 2, stderr: expect.stringContaining('Usage:') });
expect(run('--help')).toMatchObject({ code: 0, stdout: expect.stringContaining('gstack-modules render') });
});
test('gen:skill-docs refuses an exclusion render into the committed tree', () => {
const before = spawnSync('git', ['status', '--porcelain'], { cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }).stdout;
for (const extra of [[], ['--out-dir', '.']]) {
const result = spawnSync(process.execPath, [...bunIsolationArgs(), path.join(ROOT, 'scripts/gen-skill-docs.ts'), '--host', 'claude', '--modules', 'full,-skill:design-shotgun', ...extra], { cwd: ROOT, encoding: 'utf-8', timeout: 120_000 });
expect(result.status).toBe(2);
expect(result.stderr).toContain('MODULE_SPEC_INVALID');
}
expect(spawnSync('git', ['status', '--porcelain'], { cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }).stdout).toBe(before);
});
test('render prints one RENDERED line and writes the lock', () => {
const out = path.join(tmp, 'cli-render');
const result = run('render', '--host', 'claude', '--modules', 'full,-skill:design-shotgun', '--out-dir', out);
expect(result.code).toBe(0);
expect(result.stdout).toMatch(/^RENDERED: loadout=adhoc host=claude files=\d+ bytes=\d+ excluded=1 lock=/);
expect(JSON.parse(fs.readFileSync(path.join(out, LOCK_FILE), 'utf-8')).excluded).toEqual(['skill:design-shotgun']);
});
});
+6
View File
@@ -193,6 +193,12 @@ describe('pregate config: sealed (CD4)', () => {
expect(codes({ sealed: { seed_patterns: ['('] } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.seed_patterns[0]']);
expect(codes({ sealed: { visibility: 'internal' } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.visibility']);
expect(codes({ sealed: { block: true } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.block']);
expect(codes({ sealed: { paths: ['**/sealed/**'], allow_paths: ['**/sealed/*/MANIFEST.json', '**/protected/MANIFEST.json'], case_insensitive: true } })).toEqual([]);
expect(codes({ sealed: { allow_paths: '**/sealed/*/MANIFEST.json' } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.allow_paths']);
expect(codes({ sealed: { allow_paths: ['../x/MANIFEST.json'] } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.allow_paths[0]']);
expect(codes({ sealed: { allow_paths: Array.from({ length: 65 }, (_, i) => `s${i}/MANIFEST.json`) } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.allow_paths']);
expect(codes({ sealed: { allow_paths: ['**', 'sealed/**', 'sealed/*', '**/*.json', 'sealed/*.*', '**/sealed/**.json', '**/sealed/*/MANIFEST.json', '**/sealed/*/transcript-*.jsonl', '.hidden'] } })).toEqual([0, 1, 2, 3, 4, 5].map(i => `PREGATE_CONFIG_INVALID:sealed.allow_paths[${i}]`));
expect(codes({ sealed: { case_insensitive: 'yes' } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.case_insensitive']);
});
});
@@ -0,0 +1,97 @@
/**
* Two MEDIUM false positives from the gstack-evals runs (gstack 1.91.299-301):
* pii.phone.e164 on `4294967296` (2^32, a PRNG constant) and pii.email on
* `terminal-bench@2.0.json` (a package@version filename). Each guard is
* pinned in both directions: the false positive is clean, and the real shape
* next to it still reports.
*/
import { describe, test, expect } from "bun:test";
import { scan } from "../lib/redact-engine";
import { isNumericConstant, looksLikeAtFilename } from "../lib/redact-patterns";
const flags = (id: string, s: string): boolean =>
scan(s, { repoVisibility: "private" }).findings.some((f) => f.id === id);
describe("pii.phone.e164 and bare numeric constants", () => {
// Value: protects=a bare power of two (or one less) and the common 32-bit hash/PRNG constants raise no phone finding; fails_when=the guard is
// removed and `4294967296` reports pii.phone.e164 again; why_new=no constant guard existed; seam=none (scan); no_claim=other constants not listed
test("powers of two, their max values and hash/PRNG constants are not phone numbers", () => {
for (const s of [
"const RANGE = 4294967296;",
"mask = 4294967295",
"INT32_MAX 2147483647",
"x % 2147483648",
"1073741824 bytes",
"2^49 is 562949953421312",
"h *= 2654435761",
"seed ^= 2654435769",
"a = 1103515245",
"basis 2166136261",
"mt init 1812433253",
"p = 4294967291",
]) {
expect([s, flags("pii.phone.e164", s)]).toEqual([s, false]);
}
expect(isNumericConstant("4294967296")).toBe(true);
expect(isNumericConstant("4294967297")).toBe(false);
});
// Value: protects=the same digits written as a phone number (a +, spaces, dashes, parens) and ordinary bare numbers still report; fails_when=the
// guard exempts by digits alone or by length, so +14294967296 or 429-496-7296 goes quiet; why_new=pins the narrowness of the new guard;
// seam=none (scan); no_claim=international formats beyond the ones listed
test("real phone numbers still report, including the constant's digits in phone form", () => {
for (const s of [
"tel: +14294967296",
"call 429-496-7296",
"reach +1 429 496 7296",
"phone (429) 496-7296",
"p 429.496.7296",
"call 4294967297 now",
"ring +44 20 7946 0958",
"office 4155550123",
]) {
expect([s, flags("pii.phone.e164", s)]).toEqual([s, true]);
}
});
});
describe("pii.email and package@version filenames", () => {
// Value: protects=a filename with an @ (package@version, or a name ending in a file extension that is not a TLD) raises no email finding;
// fails_when=the guard is removed and terminal-bench@2.0.json reports pii.email again; why_new=no filename guard existed; seam=none (scan);
// no_claim=extensions outside the closed list
test("package@version and @-named files are not email addresses", () => {
for (const s of [
"results in terminal-bench@2.0.json",
"npm pack wrote lodash@4.17.21.tgz",
"config eslint-config@next.yaml",
"cache react-dom@18.2.0.tar.gz",
"pin left-pad@1.3.0.lock",
"notes a@1.2.md",
]) {
expect([s, flags("pii.email", s)]).toEqual([s, false]);
}
expect(looksLikeAtFilename("terminal-bench@2.0.json")).toBe(true);
});
// Value: protects=real addresses still report, including numeric domain labels (163.com, 1.2.com), TLDs that double as file extensions (.sh, .md,
// .py, .zip) and a file named after an address; fails_when=the guard treats any digit label, any extension-like TLD or any trailing extension as a
// filename; why_new=pins the narrowness of the new guard; seam=none (scan); no_claim=every IANA TLD
test("real addresses still report, numeric domains and extension-shaped TLDs included", () => {
for (const s of [
"mail alex@acme-corp.com",
"reach li.wei@163.com",
"ops dev@acme.sh",
"docs ops@acme.md",
"py team@acme.py",
"files sales@acme.zip",
"contact jo@mail.acme.io",
"reach alice@1.2.com",
"export jane.doe@acme-corp.com.json",
"log at ops-team@acme.io.log",
"uploaded jdoe@acme-corp.com.PDF",
"archive li.wei@163.com.tar.gz",
]) {
expect([s, flags("pii.email", s)]).toEqual([s, true]);
}
});
});
+111
View File
@@ -0,0 +1,111 @@
/**
* A four-part package.json version survives gstack-restamp in a repo whose
* convention is a four-part manifest (gbrain: VERSION 0.60.483.0 and
* package.json "0.60.483.0", cross-checked by its CI version gate). The
* writer used to store the npm-valid three-part form there ("0.60.483").
* gstack's own shape (a three-part manifest outside the policy's stamp files)
* and the decision-11 migration of an unlisted four-part manifest are unchanged.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { cleanup, makeFixtureRepo, makePr, readFile, restamp, type FixtureRepo } from './helpers/restamp-fixture';
const repos: FixtureRepo[] = [];
const fixture = (opts?: Parameters<typeof makeFixtureRepo>[0]) => { const r = makeFixtureRepo(opts); repos.push(r); return r; };
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
const GBRAIN_POLICY = {
$schema: 'https://github.com/garrytan/gstack/docs/ship-policy.md#schema-v1',
bump: 'patch',
queue_mode: 'stamp-at-merge',
changelog: 'CHANGELOG.md',
stamp_files: ['VERSION', 'package.json', 'CHANGELOG.md'],
stamp_patterns: ['"version": "{version}"'],
};
const pkgVersion = (repo: FixtureRepo) => JSON.parse(readFile(repo, 'package.json')).version;
const setPkgVersion = (repo: FixtureRepo, version: string) => {
const pkg = JSON.parse(readFile(repo, 'package.json'));
fs.writeFileSync(path.join(repo.work, 'package.json'), JSON.stringify({ ...pkg, version }, null, 2) + '\n');
};
/** Rewrite the fixture's base as gbrain ships it: a four-part manifest mirroring VERSION, and bun, so no npm lockfile. */
function fourPartManifest(repo: FixtureRepo, version: string): void {
setPkgVersion(repo, version);
fs.rmSync(path.join(repo.work, 'package-lock.json'));
repo.git('add', '-A');
repo.git('commit', '-q', '-m', 'four-part manifest');
repo.git('push', '-q', 'origin', 'main');
}
describe('gstack-restamp keeps the repo\'s manifest width', () => {
test('gbrain-shaped repo: package.json keeps four parts and agrees with VERSION', () => {
const repo = fixture({ policy: GBRAIN_POLICY, version: '0.60.482.0' });
fourPartManifest(repo, '0.60.482.0');
makePr(repo, 'feat-a');
const r = restamp(repo, ['--next']);
expect(r.status).toBe(EXIT.ok);
expect(r.out).toContain('STAMP_VERSION: 0.60.483.0');
expect(r.out).toContain('stage version: done (wrote 0.60.483.0 (package.json keeps four parts');
expect(readFile(repo, 'VERSION').trim()).toBe('0.60.483.0');
expect(pkgVersion(repo)).toBe('0.60.483.0');
});
test('gbrain-shaped branch that already carries its stamp: the restamp leaves the four-part manifest as is', () => {
const repo = fixture({ policy: GBRAIN_POLICY, version: '0.60.482.0' });
fourPartManifest(repo, '0.60.482.0');
makePr(repo, 'feat-a');
fs.writeFileSync(path.join(repo.work, 'VERSION'), '0.60.483.0\n');
setPkgVersion(repo, '0.60.483.0');
repo.git('commit', '-q', '-am', 'stamp 0.60.483.0');
const r = restamp(repo, ['--next']);
expect(r.status).toBe(EXIT.ok);
expect(r.out).toContain('STAMP_VERSION: 0.60.483.0');
expect(r.out).not.toContain('diff --git a/package.json');
expect(pkgVersion(repo)).toBe('0.60.483.0');
});
test('a branch an older restamp cut to three parts heals: the base\'s four-part manifest sets the form', () => {
const repo = fixture({ policy: GBRAIN_POLICY, version: '0.60.482.0' });
fourPartManifest(repo, '0.60.482.0');
makePr(repo, 'feat-a');
setPkgVersion(repo, '0.60.482');
repo.git('commit', '-q', '-am', 'old restamp cut the manifest');
const r = restamp(repo, ['--next']);
expect(r.status).toBe(EXIT.ok);
expect(pkgVersion(repo)).toBe('0.60.483.0');
});
test('a manifest listed through stamp_paths keeps four parts too', () => {
const repo = fixture({ policy: { ...GBRAIN_POLICY, stamp_files: [], stamp_paths: ['package.json'] }, version: '0.60.482.0' });
fourPartManifest(repo, '0.60.482.0');
makePr(repo, 'feat-a');
const r = restamp(repo, ['--next']);
expect(r.status).toBe(EXIT.ok);
expect(pkgVersion(repo)).toBe('0.60.483.0');
});
test('gstack-shaped repo: the three-part manifest and its lockfile still get the npm-valid form', () => {
const repo = fixture();
makePr(repo, 'feat-a');
const r = restamp(repo, ['--next']);
expect(r.status).toBe(EXIT.ok);
expect(r.out).toContain('stage version: done (wrote 1.2.4.0)');
expect(readFile(repo, 'VERSION').trim()).toBe('1.2.4.0');
expect(pkgVersion(repo)).toBe('1.2.4');
const lock = JSON.parse(readFile(repo, 'package-lock.json'));
expect([lock.version, lock.packages[''].version]).toEqual(['1.2.4', '1.2.4']);
});
test('a four-part manifest the policy does not list as a stamp file still migrates to the npm-valid form (decision 11)', () => {
const repo = fixture({ policy: { ...GBRAIN_POLICY, stamp_files: ['VERSION', 'CHANGELOG.md'] }, version: '0.60.482.0' });
fourPartManifest(repo, '0.60.482.0');
makePr(repo, 'feat-a');
const r = restamp(repo, ['--next']);
expect(r.status).toBe(EXIT.ok);
expect(r.out).toContain('stage version: done (wrote 0.60.483.0)');
expect(pkgVersion(repo)).toBe('0.60.483');
});
});
+119
View File
@@ -0,0 +1,119 @@
import { afterEach, describe, expect, test } 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 { collectCommit, collectLive, loadRegistryAt } from '../lib/review-collect';
import { depthOfCommitInput } from '../lib/review-depth';
const dirs: string[] = [];
afterEach(() => { while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true }); });
const lines = (n: number, tag = 'v') => Array.from({ length: n }, (_, i) => `export const ${tag}${i} = ${i};`).join('\n') + '\n';
const REGISTRY = JSON.stringify({ entries: [{ id: 'docs', command: 'bun run gen', outputs: ['*/SKILL.md'] }, { id: 'manual', command: null, outputs: ['src/hand.ts'] }] });
function repo(baseFiles: Record<string, string | Buffer>) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'review-collect-'));
dirs.push(dir);
const git = (...a: string[]) => { const r = spawnSync('git', a, { cwd: dir, encoding: 'utf8', timeout: 10_000 }); if (r.status !== 0) throw new Error(`git ${a.join(' ')}: ${r.stderr}`); return r.stdout.trim(); };
const write = (files: Record<string, string | Buffer | null>) => { for (const [p, text] of Object.entries(files)) { const f = path.join(dir, p); if (text === null) { fs.rmSync(f); continue; } fs.mkdirSync(path.dirname(f), { recursive: true }); fs.writeFileSync(f, text); } };
git('init', '-q', '-b', 'main'); git('config', 'user.email', 't@t'); git('config', 'user.name', 't'); git('config', 'commit.gpgsign', 'false');
write({ 'README.md': '# x\n', ...baseFiles }); git('add', '-A'); git('commit', '-qm', 'base');
const baseSha = git('rev-parse', 'HEAD');
git('checkout', '-qb', 'feat');
const commit = (files: Record<string, string | Buffer | null>) => { write(files); git('add', '-A'); git('commit', '-qm', 'feat'); return git('rev-parse', 'HEAD'); };
return { dir, git, write, commit, baseSha };
}
describe('collectCommit: git objects only', () => {
// Value: protects=only a bare version bump in package.json is release metadata; fails_when=a version line that also
// carries scripts counts as stamp-only (docs class, light); seam=none
test('a package.json version line that also carries scripts is not stamp-only; a bare version bump is', () => {
const r = repo({ 'package.json': '{\n "name": "x",\n "version": "1.0.0"\n}\n' });
const bare = r.commit({ 'package.json': '{\n "name": "x",\n "version": "1.0.1"\n}\n' });
expect(collectCommit(r.dir, r.baseSha, bare).stampOnly).toEqual(['package.json']);
const riding = r.commit({ 'package.json': '{\n "name": "x",\n "version": "1.0.2", "scripts": { "postinstall": "node x.js" }\n}\n' });
expect(collectCommit(r.dir, r.baseSha, riding).stampOnly).toEqual([]);
});
test('a registered glob output is excluded, a command:null entry is not, and a .tmpl edit counts once', () => {
const r = repo({ '.gstack/generated.json': REGISTRY, 'ship/SKILL.md.tmpl': 'a\n', 'ship/SKILL.md': 'a\n' });
const head = r.commit({ 'ship/SKILL.md.tmpl': 'a\nb\nc\n', 'ship/SKILL.md': lines(120), 'src/hand.ts': lines(5) });
const c = collectCommit(r.dir, r.baseSha, head);
expect(c.errors).toEqual([]);
expect(c.registry).toEqual({ sha: r.baseSha, outputs: ['*/SKILL.md'] });
const d = depthOfCommitInput({ ...c, gateTagged: 0 });
expect([d.depth, d.runtimeLines]).toEqual(['light', 2 + 5]);
});
test('a head-only registry entry never excludes: the registry is the base commit\'s', () => {
const r = repo({ 'src/keep.ts': lines(1) });
const head = r.commit({ '.gstack/generated.json': JSON.stringify({ entries: [{ id: 'gen', command: 'gen', outputs: ['src/gen.ts'] }] }), 'src/gen.ts': lines(60) });
const c = collectCommit(r.dir, r.baseSha, head);
expect(c.registry).toEqual({ sha: r.baseSha, outputs: [] });
const d = depthOfCommitInput({ ...c, gateTagged: 0 });
expect(d.runtimeLines).toBeGreaterThanOrEqual(60);
expect(d.depth).toBe('full');
});
test('a rename into src/ counts the new path as runtime', () => {
const r = repo({ 'notes/a.txt': lines(30) });
fs.mkdirSync(path.join(r.dir, 'src'));
r.git('mv', 'notes/a.txt', 'src/a.ts');
r.git('commit', '-qm', 'mv');
const c = collectCommit(r.dir, r.baseSha, r.git('rev-parse', 'HEAD'));
expect(c.deleted).toEqual(['notes/a.txt']);
expect(c.perPath.find(e => e.path === 'src/a.ts')).toEqual({ path: 'src/a.ts', lines: 30, binary: false });
expect(depthOfCommitInput({ ...c, gateTagged: 0 })).toMatchObject({ depth: 'light', runtimeLines: 30 });
});
test('a binary code-class path is an error (full), never 0 lines; a deleted test is full', () => {
const r = repo({ 'test/old.test.ts': 'x\n' });
const head = r.commit({ 'lib/x.wasm': Buffer.from([0, 97, 115, 109, 0, 1, 2]) });
const d = depthOfCommitInput({ ...collectCommit(r.dir, r.baseSha, head), gateTagged: 0 });
expect(d.depth).toBe('full');
expect(d.errors).toEqual([expect.objectContaining({ code: 'binary_code_path', input: 'lib/x.wasm' })]);
const t = repo({ 'test/old.test.ts': 'x\n', 'test/keep.test.ts': 'x\n' });
const del = t.commit({ 'test/old.test.ts': null });
const deleted = depthOfCommitInput({ ...collectCommit(t.dir, t.baseSha, del), gateTagged: 0 });
expect([deleted.depth, deleted.runtimeLines, deleted.errors]).toEqual(['full', 0, []]);
});
test('an invalid base registry is an error with its result code; an unresolvable sha is unavailable', () => {
const r = repo({ '.gstack/generated.json': '{"entries": [{"id": "x"}]}' });
const head = r.commit({ 'src/a.ts': lines(3) });
const d = depthOfCommitInput({ ...collectCommit(r.dir, r.baseSha, head), gateTagged: 0 });
expect(d.depth).toBe('full');
expect(d.codes).toEqual(['REGEN_REGISTRY_INVALID']);
expect(d.errors[0]).toMatchObject({ code: 'registry_invalid', result: 'REGEN_REGISTRY_INVALID' });
expect(d.errors[0]!.repair).toContain('gstack-regen validate');
expect(loadRegistryAt(r.dir, 'f'.repeat(40)).error?.code).toBe('registry_unavailable');
const missing = collectCommit(r.dir, 'f'.repeat(40), head);
expect(missing.errors.map(e => e.code)).toContain('numstat_failed');
expect(depthOfCommitInput({ ...missing, gateTagged: 0 }).depth).toBe('full');
});
test('from a checkout at another HEAD with a dirty tree it equals the clean result at head', () => {
const r = repo({ '.gstack/generated.json': REGISTRY, 'src/a.ts': lines(2), 'ship/SKILL.md': 'a\n', 'autoplan/SKILL.md.tmpl': 'x\n' });
const head = r.commit({ 'src/a.ts': lines(40), 'ship/SKILL.md': lines(9), 'docs/b.md': '# b\n12% faster\n', 'autoplan/notes.md': 'n\n', 'package.json': '{"version":"1.0.1"}\n' });
const clean = collectCommit(r.dir, r.baseSha, head);
r.git('checkout', '-q', r.baseSha);
r.write({ 'src/a.ts': lines(500, 'dirty'), 'src/untracked.ts': lines(80), 'autoplan/SKILL.md.tmpl': null });
const dirty = collectCommit(r.dir, r.baseSha, head);
expect(dirty).toEqual(clean);
expect(depthOfCommitInput({ ...dirty, gateTagged: 0 })).toEqual(depthOfCommitInput({ ...clean, gateTagged: 0 }));
expect(clean.root).toEqual(['autoplan', 'ship']);
expect(clean.added['docs/b.md']).toEqual(['# b', '12% faster']);
});
});
describe('collectLive: the working tree', () => {
test('tracked uncommitted edits and untracked files count per path; scratch under .gstack/tmp/ does not', () => {
const r = repo({ 'src/a.ts': lines(1) });
r.commit({ 'docs/a.md': '# a\n' });
r.write({ 'src/a.ts': lines(11), 'src/new.ts': lines(20), '.gstack/tmp/x.ts': lines(500) });
const c = collectLive(r.dir, 'main');
expect(c.errors).toEqual([]);
expect(c.untracked).toEqual(['src/new.ts']);
expect(c.perPath.find(e => e.path === 'src/new.ts')).toEqual({ path: 'src/new.ts', lines: 20, binary: false });
expect(c.paths).toEqual(['docs/a.md', 'src/a.ts', 'src/new.ts']);
expect(depthOfCommitInput({ ...c, gateTagged: 0 })).toMatchObject({ depth: 'light', runtimeLines: 20 + 10 });
});
test('no merge base is a collector error the CLI refuses on', () => {
const r = repo({});
expect(collectLive(r.dir, 'no-such-branch').errors.map(e => e.code)).toEqual(['numstat_failed']);
});
});
+126 -4
View File
@@ -1,6 +1,9 @@
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, classifyPath, nextStep, planReview, scanNumbers, FULL_PASS_CAP, type PlanInput } from '../lib/review-depth';
import { classifyDiff, classifyPath, depthFor, depthOfCommitInput, diffCommand, nextStep, planReview, scanNumbers, DEPTH_RULES, FULL_PASS_CAP, type DepthError, type PlanInput } from '../lib/review-depth';
const ROOT = path.join(import.meta.dir, '..');
const none = scanNumbers({});
@@ -73,9 +76,10 @@ describe('planReview', () => {
const p = planReview(base({ paths: ['docs/bench.md'], scope: {}, numbers: scanNumbers({ 'docs/bench.md': ['3x faster'] }) }));
expect(roles(p)).toEqual(['outside-voice:outside-voice:host']);
});
test('tests class: testing specialist only', () => {
test('tests class: runtime 0 is light, core only (Behavior changes: no Testing specialist)', () => {
const p = planReview(base({ paths: ['test/a.test.ts'], scope: { SCOPE_TESTS: 'true' } }));
expect(roles(p)).toEqual(['specialist:testing:full']);
expect(p.depth).toBe('light');
expect(roles(p)).toEqual([]);
});
test('code class pass 1: every reviewer at full effort, the outside voice at its own', () => {
const p = planReview(base({}));
@@ -88,7 +92,7 @@ describe('planReview', () => {
});
test('code class honours hit-rate gating and small-diff skip', () => {
expect(roles(planReview(base({ gated: ['simplification'] })))).not.toContain('specialist:simplification:full');
expect(planReview(base({ diffLines: 20 })).dispatches.filter(d => d.role === 'specialist')).toEqual([]);
expect(planReview(base({ diffLines: 20 })).dispatches.filter(d => d.role === 'specialist').map(d => d.name)).toEqual(['batched']);
});
test('pass 2 reruns the army on the range but not the adversarial or outside voice once Step 11 ran', () => {
const p = planReview(base({ adversarialRan: true, history: { fullPasses: 1, last: 'full', fixedBlocking: true } }));
@@ -167,3 +171,121 @@ describe('GSTA-131 coverage: Red Team and specialist arms', () => {
expect(roles(planReview(base({ paths: ['docs/a.md'], force: ['security'] })))).toEqual(['specialist:security']);
});
});
const per = (o: Record<string, number>) => Object.entries(o).map(([p, lines]) => ({ path: p, lines, binary: false }));
const mixed = (o: Partial<PlanInput> = {}) => base({ paths: Object.keys(o.perPath ? Object.fromEntries(o.perPath.map(e => [e.path, 1])) : {}), ...o });
const err = (code: DepthError['code'], result?: DepthError['result']): DepthError => ({ code, detail: 'd', input: 'i', repair: 'r', ...(result ? { result } : {}) });
describe('review depth: the matrix', () => {
test('runtime 0 (tests and docs only) is light: core only, nothing dispatched', () => {
const p = planReview(mixed({ perPath: per({ 'test/a.test.ts': 300, 'docs/a.md': 40 }), diffLines: 340 }));
expect([p.depth, p.runtimeLines, p.dispatches]).toEqual(['light', 0, []]);
});
test('runtime 1-50 is light plus one batched specialist: the fixed set, scope picks and the Red Team in one context', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 48, 'test/a.test.ts': 190, 'docs/a.md': 9 }), diffLines: 247 }));
expect([p.depth, p.runtimeLines]).toEqual(['light', 48]);
expect(p.dispatches).toEqual([
{ role: 'specialist', name: 'batched', effort: 'full', exclude: [':(exclude)docs/a.md'], checklists: ['testing', 'maintainability', 'security', 'performance', 'red-team'] },
{ role: 'adversarial-native', name: 'adversarial', effort: 'full', exclude: [], reason: 'backtest inconclusive (GSTA-147 D5)' },
]);
expect(p.skipped.join('; ')).toContain('red-team (light depth');
expect(planReview(mixed({ perPath: per({ 'src/app.ts': 48 }), adversarialRan: true, history: { fullPasses: 1, last: 'full', touchedGate: true, fixedBelow: true } })).dispatches.some(d => d.role === 'adversarial-native')).toBe(false);
});
test('runtime 51 is standard: thresholds read runtime lines, reviewers read their own hunks', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 51, 'test/a.test.ts': 400, 'README.md': 3 }), diffLines: 454 }));
expect([p.depth, p.runtimeLines]).toEqual(['standard', 51]);
expect(roles(p)).toEqual(['specialist:testing:full', 'specialist:maintainability:full', 'specialist:performance:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
const ex = Object.fromEntries(p.dispatches.map(d => [d.name, d.exclude]));
expect(ex.testing).toEqual([':(exclude)README.md']);
expect(ex.maintainability).toEqual([':(exclude)README.md', ':(exclude)test/a.test.ts']);
expect([ex.adversarial, ex['outside-voice']]).toEqual([[], []]);
});
test.each([
['scope error', { scope: { SCOPE_ERROR: 'boom' } }, 'scope_error', []],
['gate scan unavailable', { gateTagged: null }, 'gate_scan_unavailable', []],
['base registry invalid', { registry: { sha: 'abc', outputs: [], error: err('registry_invalid', 'REGEN_REGISTRY_INVALID') } }, 'registry_invalid', ['REGEN_REGISTRY_INVALID']],
['base registry unavailable', { registry: { sha: null, outputs: [], error: err('registry_unavailable') } }, 'registry_unavailable', []],
['numstat failure', { errors: [err('numstat_failed')] }, 'numstat_failed', []],
['binary code path', { perPath: [...per({ 'src/app.ts': 5 }), { path: 'lib/x.wasm', lines: 0, binary: true }], paths: ['src/app.ts', 'lib/x.wasm'] }, 'binary_code_path', []],
] as const)('%s fails closed to full with its code', (_n, o, code, codes) => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 5 }), ...(o as Partial<PlanInput>) }));
expect(p.depth).toBe('full');
expect(p.errors.map(e => e.code)).toContain(code);
expect(p.codes).toEqual([...codes]);
expect(roles(p)).toContain('red-team:red-team:full');
});
test.each([
['a gate path', { paths: ['src/app.ts', '.github/workflows/ci.yml'] }],
['a gate tag', { gateTagged: 1 }],
['a deleted test', { deleted: ['test/old.test.ts'] }],
['an edit to the registry', { paths: ['.gstack/generated.json'] }],
['an edit to SMALL_DIFF_LINES', { paths: ['lib/review-depth.ts'] }],
['the collector', { paths: ['lib/review-collect.ts'] }],
['the plan CLI', { paths: ['bin/gstack-review-plan'] }],
['the queue', { paths: ['lib/ship-queue-train.ts'] }],
['the Step 9 template', { paths: ['ship/sections/review-army.md.tmpl'] }],
] as const)('%s is full', (_n, o) => {
const paths = (o as any).paths ?? ['src/app.ts'];
expect(planReview(mixed({ perPath: per(Object.fromEntries(paths.map((p: string) => [p, 2]))), ...(o as Partial<PlanInput>) })).depth).toBe('full');
});
test('a registered glob output is excluded from runtime; a .tmpl edit plus its generated SKILL.md counts the tmpl only', () => {
const p = planReview(mixed({ perPath: per({ 'ship/SKILL.md.tmpl': 30, 'ship/SKILL.md': 200 }), registry: { sha: 'abc', outputs: ['*/SKILL.md'] } }));
expect([p.depth, p.runtimeLines]).toEqual(['light', 30]);
expect(p.dispatches[0]!.exclude).toEqual([':(exclude)ship/SKILL.md']);
});
test('a path no base-registry entry names stays runtime', () => {
expect(planReview(mixed({ perPath: per({ 'src/gen.ts': 60 }), registry: { sha: 'abc', outputs: ['lib/dom.js'] } })).runtimeLines).toBe(60);
});
test('numbers in light dispatch the outside voice with an empty exclude', () => {
const p = planReview(mixed({ perPath: per({ 'docs/bench.md': 4 }), numbers: scanNumbers({ 'docs/bench.md': ['3x faster'] }) }));
expect(p.depth).toBe('light');
expect(p.dispatches).toEqual([{ role: 'outside-voice', name: 'outside-voice', effort: 'host', exclude: [] }]);
});
test('a fixed blocker in light raises the next full step to the standard set, Red Team included', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 30 }), history: { fullPasses: 1, last: 'full', fixedBlocking: true } }));
expect([p.depth, p.stepDepth]).toEqual(['light', 'standard']);
expect(roles(p)).toEqual(['specialist:testing:full', 'specialist:maintainability:full', 'specialist:performance:full', 'red-team:red-team:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
});
test('--depth-floor raises and never lowers', () => {
expect(depthFor({ cls: 'code', runtimeLines: 10, errors: [], floor: 'full' })).toBe('full');
expect(depthFor({ cls: 'code', runtimeLines: 10, errors: [], floor: 'standard' })).toBe('standard');
expect(depthFor({ cls: 'gate', runtimeLines: 10, errors: [], floor: 'standard' })).toBe('full');
expect(depthFor({ cls: 'code', runtimeLines: 80, errors: [] })).toBe('standard');
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 10 }), floor: 'full' }));
expect(p.depth).toBe('full');
expect(p.depthReasons).toContain('floor:full');
});
test('depthOfCommitInput gives the same depth as the plan for the same input', () => {
const i = { paths: ['src/app.ts', 'test/a.test.ts'], perPath: per({ 'src/app.ts': 51, 'test/a.test.ts': 9 }), gateTagged: 0 };
expect(depthOfCommitInput(i)).toMatchObject({ depth: 'standard', runtimeLines: 51, rules: DEPTH_RULES, errors: [] });
expect(depthOfCommitInput({ ...i, gateTagged: null }).depth).toBe('full');
});
});
describe('review depth: diff commands and the rules hash', () => {
test('DIFF_CMD shell-quotes each pathspec; a glob character makes the pathspec literal', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 10, "docs/my notes*.md": 3, "docs/it's.md": 1 }) }));
const ex = p.dispatches[0]!.exclude;
expect(ex).toEqual([':(exclude,literal)docs/my notes*.md', ":(exclude)docs/it's.md"]);
expect(diffCommand(p.dispatches[0]!)).toBe("git diff <diff-base> -- . ':(exclude,literal)docs/my notes*.md' ':(exclude)docs/it'\\''s.md'");
expect(diffCommand({ role: 'red-team', name: 'red-team', effort: 'full', exclude: [] })).toBe('git diff <diff-base>');
});
test('DEPTH_RULES hashes the deciding constants: a comment-only edit keeps it, a threshold edit changes it', () => {
expect(DEPTH_RULES).toMatch(/^[0-9a-f]{16}$/);
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'depth-rules-'));
try {
const src = fs.readFileSync(path.join(ROOT, 'lib/review-depth.ts'), 'utf8');
fs.symlinkSync(path.join(ROOT, 'lib/ship-policy.ts'), path.join(dir, 'ship-policy.ts'));
const hashOf = (text: string) => {
fs.writeFileSync(path.join(dir, 'review-depth.ts'), text);
fs.writeFileSync(path.join(dir, 'print.ts'), "import { DEPTH_RULES } from './review-depth';\nconsole.log(DEPTH_RULES);\n");
const r = spawnSync(process.execPath, ['--no-env-file', '--config=/dev/null', path.join(dir, 'print.ts')], { encoding: 'utf8', timeout: 30_000 });
expect(r.status, r.stderr).toBe(0);
return r.stdout.trim();
};
expect(hashOf('// a comment-only edit\n' + src)).toBe(DEPTH_RULES);
expect(hashOf(src.replace('SMALL_DIFF_LINES = 50', 'SMALL_DIFF_LINES = 60'))).not.toBe(DEPTH_RULES);
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
});
+1 -1
View File
@@ -34,7 +34,7 @@ describe('scripts/review-dispatch-compare.ts', () => {
const table = run(['--tokens', tok]);
expect(table).toContain(`TOKENS: measured (${tok})`);
expect(table).toMatch(/^SHIP_TOKEN_RATIO: median=0\.\d+ min=0\.\d+ max=\d\.\d+ \(n=10\)$/m);
expect(table).toMatch(/^SHIP_TOKEN_RATIO_BY_CLASS: docs median=0\.\d+ \(n=5\); code median=0\.\d+ \(n=5\)$/m);
expect(table).toMatch(/^SHIP_TOKEN_RATIO_BY_CLASS: docs median=0\.\d+ \(n=5\); gate median=0\.\d+ \(n=5\)$/m);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
+48
View File
@@ -109,3 +109,51 @@ describe('gstack-review-plan', () => {
expect(ran).toContain('SKIP: adversarial-native and outside-voice (run once per invocation)');
});
});
describe('gstack-review-plan: review depth', () => {
const code = (n: number) => Array.from({ length: n }, (_, i) => `export const v${i} = ${i};`).join('\n') + '\n';
test('prints REVIEW_DEPTH with runtime lines and the rules hash, and GATE_SCAN bound to the head', () => {
const r = repo({ 'src/a.ts': code(10), 'test/a.test.ts': code(200) });
const out = run(r, []).stdout;
const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: r.dir, encoding: 'utf8', timeout: 10_000 }).stdout.trim();
expect(out).toMatch(/^REVIEW_DEPTH: light runtime_lines=10 rules=[0-9a-f]{16} reasons=runtime=10<=50 codes=none$/m);
expect(out).toContain(`GATE_SCAN: tagged=0 head=${head}`);
expect(out).toContain('DISPATCH: specialist batched effort=full checklists=testing,maintainability,security,performance,state-machines,red-team');
expect(out).toContain('DISPATCH: adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)');
});
test('DIFF_CMD is shell-quoted per dispatch and excludes what the reviewer does not read; DIFF_EXCLUDE: none when empty', () => {
const r = repo({ 'src/a.ts': code(80), 'docs/my notes*.md': '# n\n', 'test/a.test.ts': code(3) });
const out = run(r, []).stdout;
expect(out).toContain("DIFF_CMD: testing git diff <diff-base> -- . ':(exclude,literal)docs/my notes*.md'");
expect(out).toContain("DIFF_CMD: maintainability git diff <diff-base> -- . ':(exclude,literal)docs/my notes*.md' ':(exclude)test/a.test.ts'");
expect(out).toContain('DIFF_CMD: adversarial git diff <diff-base>\nDIFF_EXCLUDE: adversarial none');
const cmd = /^DIFF_CMD: maintainability (.*)$/m.exec(out)![1]!.replace('<diff-base>', 'main');
const diff = spawnSync('bash', ['-c', cmd.replace('git diff main', 'git diff --name-only main')], { cwd: r.dir, encoding: 'utf8' });
expect(diff.stdout.trim().split('\n')).toEqual(['src/a.ts']);
});
test('--json carries depth, runtime_lines, rules, errors, gate_scan and every dispatch\'s exclude array', () => {
const r = repo({ 'src/a.ts': code(80), 'README.md': '# y\n' });
const plan = JSON.parse(run(r, ['--json']).stdout);
expect(plan).toMatchObject({ depth: 'standard', step_depth: 'standard', runtime_lines: 80, errors: [], codes: [], gate_scan: { tagged: 0 } });
expect(plan.rules).toMatch(/^[0-9a-f]{16}$/);
expect(plan.dispatches.find((d: any) => d.name === 'maintainability').exclude).toEqual([':(exclude)README.md']);
expect(plan.dispatches.find((d: any) => d.role === 'outside-voice').exclude).toEqual([]);
});
test('--depth-floor full raises a light diff, never lowers a full one, and an invalid floor is a usage error', () => {
const r = repo({ 'src/a.ts': code(5) });
expect(run(r, ['--depth-floor', 'full']).stdout).toMatch(/^REVIEW_DEPTH: full runtime_lines=5 .*reasons=runtime=5<=50,floor:full /m);
const gate = repo({ 'src/a.ts': code(5), '.github/workflows/ci.yml': 'on: push\n' });
expect(run(gate, ['--depth-floor', 'standard']).stdout).toMatch(/^REVIEW_DEPTH: full /m);
expect(run(r, ['--depth-floor', 'light']).status).toBe(2);
});
test('a conservative full plan exits 0 with its errors; a plan that cannot be built exits 1', () => {
const r = repo({ 'lib/x.wasm': '\u0000asm\u0000' });
const res = run(r, ['--json']);
expect(res.status).toBe(0);
expect(JSON.parse(res.stdout)).toMatchObject({ depth: 'full', errors: expect.arrayContaining([expect.objectContaining({ code: 'binary_code_path', input: 'lib/x.wasm' })]) });
expect(run(r, []).stdout).toContain('DEPTH_ERROR: binary_code_path input=lib/x.wasm');
const none = spawnSync(BIN, ['--base', 'no-such-branch', '--gate-tagged', '0'], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env: { ...process.env, GSTACK_STATE_ROOT: r.state } });
expect(none.status).toBe(1);
});
});
+15
View File
@@ -684,3 +684,18 @@ test('review suppressions allow a clean result and skip common false positives b
expectTokens(suppressions, ['"Function too long"', '"Possible null dereference"', '"N+1 query"', '"Hardcoded value"', '`Math.random()`'], 'suppressions');
expectAbsent(suppressions, [/missing input validation/i, /missing await/i], 'suppressions');
});
test('judged repeat-pass QA reruns and unresolved record actions each have one rule (paid screen 40617200857)', () => {
// Every sample of the red panel and its rerun named one of these: "post-expiry smoke rechecks are not-run" vs
// Step 5.8's rerun of superseded probes, materialize's once-per-PROBE_DIR limit vs reruns into the same report,
// and no record action for an unresolved non-gate defect (an unanswered question or an unproven repair).
// fails_when=the rerun stops being revalidation, materialize may run per pass, or such a defect has no action.
const flat = skill.replace(/\s+/g, ' ');
const repeat = flat.slice(flat.indexOf('2. On a repeat, execute Steps 3–5 in order.'), flat.indexOf('3. **Verify completed actions.**'));
expect(repeat).toContain('These reruns are revalidation, so they run even after smoke expiry; keep the PROBE_DIR. Earlier provisional sections link checkpoints; only the final pass runs its once-only `materialize`.');
expect(repeat.indexOf('rerun each probe the verdict relies on')).toBeLessThan(repeat.indexOf('These reruns are revalidation'));
const actions = flat.slice(flat.indexOf('Actions: `auto-fixed` (Step 5b)'), flat.indexOf('Advice is never `auto-fixed`'));
expect(actions).toContain('`fixed` (approved **and completed** in Step 5d)');
expect(actions).toContain('`skipped` (explicit Skip in Step 5c), `open` (any other defect, e.g. unanswered).');
expect(flat).toContain('Do not record an unanswered question as skipped');
});
+137
View File
@@ -195,6 +195,143 @@ describe('gstack-pregate sealed (the standalone preflight before any push or PR
});
});
const MANIFEST = { paths: ['**/sealed/**'], allow_paths: ['**/sealed/*/MANIFEST.json'], answer_markers: ['SEALED ANSWER:'] };
function preflight(s: Scratch, args: string[] = []) {
const r = spawnSync(PREGATE, ['sealed', '--base', 'main', ...args], { cwd: s.work, encoding: 'utf8', timeout: 60_000, env: { ...process.env, ...s.env } });
return { code: r.status, out: r.stdout ?? '', err: r.stderr ?? '' };
}
function landOnMain(s: Scratch, files: Record<string, string>): void {
sh(s.work, s.env, ['checkout', '-q', 'main']);
write(s.work, files);
sh(s.work, s.env, ['add', '-A']); sh(s.work, s.env, ['commit', '-q', '-m', 'base change']); sh(s.work, s.env, ['push', '-q', 'origin', 'main']);
sh(s.work, s.env, ['checkout', '-q', 'results']);
}
describe('sealed.allow_paths: base-branch exceptions applied after the deny globs', () => {
test('a deny with an allowed exception refuses the sealed file, lists the manifest as exempt, and a manifest-only push passes', () => {
const s = scratch({ '.gstack/pregate.json': policy(MANIFEST) });
commit(s, { 'sealed/s1/tasks.json': '{}\n', 'sealed/s1/MANIFEST.json': '{"n":1}\n', 'tasks/x/sealed/s2/MANIFEST.json': '{"n":2}\n' });
const r = hook(s);
expect(r.code).toBe(1);
expect(r.err).toMatch(/^ sealed\.path\s+sealed\/s1\/tasks\.json/m);
expect(r.err).not.toMatch(/^ sealed\.path\s+\S*MANIFEST\.json/m);
expect(r.err).toMatch(/exempt sealed\.path sealed\/s1\/MANIFEST\.json commit [0-9a-f]{12} \(sealed\.allow_paths \*\*\/sealed\/\*\/MANIFEST\.json\)/);
expect(r.err).toContain('exempt sealed.path tasks/x/sealed/s2/MANIFEST.json');
const only = scratch({ '.gstack/pregate.json': policy(MANIFEST) });
commit(only, { 'sealed/s1/MANIFEST.json': '{"n":1}\n' });
expect(hook(only).code).toBe(0);
const pre = preflight(only);
expect(pre.code).toBe(0);
expect(pre.out).toMatch(/^SEALED: clear findings=0 commits=1 visibility=unknown \(counted public\) waived=0 exempt=1 policy=/m);
const json = JSON.parse(preflight(only, ['--json']).out);
expect(json.exempt).toHaveLength(1);
expect(json.exempt[0]).toMatchObject({ rule: 'sealed.path', file: 'sealed/s1/MANIFEST.json', glob: '**/sealed/*/MANIFEST.json' });
});
test('an exception that does not match still refuses: another case and another depth', () => {
for (const file of ['sealed/s1/manifest.json', 'sealed/s1/x/MANIFEST.json']) {
const s = scratch({ '.gstack/pregate.json': policy(MANIFEST) });
commit(s, { [file]: '{}\n' });
const r = hook(s);
expect(r.code).toBe(1);
expect(r.err).toContain(`sealed.path ${file}`);
expect(r.err).not.toContain('exempt sealed.');
}
});
test('an exception added on the pushed head is ignored, on top of a base list too; a malformed pushed allow_paths refuses', () => {
const fresh = scratch({ '.gstack/pregate.json': policy({ paths: ['**/sealed/**'] }) });
commit(fresh, { '.gstack/pregate.json': policy(MANIFEST), 'sealed/s1/MANIFEST.json': '{}\n' });
const f = hook(fresh);
expect(f.code).toBe(1);
expect(f.err).toContain('sealed.path sealed/s1/MANIFEST.json');
const extra = scratch({ '.gstack/pregate.json': policy(MANIFEST) });
commit(extra, { '.gstack/pregate.json': policy({ ...MANIFEST, allow_paths: [...MANIFEST.allow_paths, '**/sealed/*/notes.md'] }), 'sealed/s1/notes.md': 'n\n' });
expect(hook(extra).err).toContain('sealed.path sealed/s1/notes.md');
const bad = scratch({ '.gstack/pregate.json': policy(MANIFEST) });
commit(bad, { '.gstack/pregate.json': policy({ ...MANIFEST, allow_paths: '**/sealed/*/MANIFEST.json' }), 'docs/x.md': 'x\n' });
const b = hook(bad);
expect(b.code).toBe(1);
expect(b.err).toContain('sealed.allow_paths');
});
test('an allowed manifest that quotes a sealed answer still refuses on the text rule', () => {
const s = scratch({ '.gstack/pregate.json': policy(MANIFEST) });
commit(s, { 'sealed/s1/MANIFEST.json': '{"note":"SEALED ANSWER: b"}\n' });
const r = hook(s);
expect(r.code).toBe(1);
expect(r.err).toMatch(/sealed\.answer\s+sealed\/s1\/MANIFEST\.json:1/);
expect(r.err).toContain('exempt sealed.path sealed/s1/MANIFEST.json');
});
test('an exception covers sealed.transcript too and is counted before a base sha waiver, never both', () => {
const s = scratch({ '.gstack/pregate.json': policy({ paths: ['**/sealed/**'], transcripts: ['**/transcript*.jsonl'], allow_paths: ['**/sealed/*/transcript-public.jsonl'] }) });
const sha = commit(s, { 'sealed/s1/transcript-public.jsonl': '{}\n' });
landOnMain(s, { '.gstack/sealed-allow.tsv': `sealed.transcript\tsealed/s1/transcript-public.jsonl\t${sha.slice(0, 12)}\tsynthetic fixture\n` });
const pre = preflight(s);
expect(pre.code).toBe(0);
expect(pre.err).toContain('exempt sealed.transcript sealed/s1/transcript-public.jsonl');
expect(pre.out).toMatch(/findings=0 .* waived=0 exempt=1 /);
});
test('a preflight error carries the same report shape: exit 1, SEALED: error with exempt=0, no stack trace', () => {
const s = scratch({ '.gstack/pregate.json': policy(MANIFEST) });
commit(s, { 'docs/x.md': 'x\n' });
const r = preflight(s, ['--range', 'nosuch..HEAD']);
expect(r.code).toBe(1);
expect(r.out).toMatch(/^SEALED: error findings=0 commits=0 .* waived=0 exempt=0 /m);
expect(r.err).not.toMatch(/\n\s+at /);
expect(JSON.parse(preflight(s, ['--range', 'nosuch..HEAD', '--json']).out).exempt).toEqual([]);
});
});
describe('sealed.case_insensitive: deny globs fold case, exceptions never do', () => {
test('off (the default), HELDOUT/ passes a lowercase deny glob; on, HELDOUT/, HeldOut/ and a Transcript refuse, and hold-out/ still passes', () => {
const off = scratch({ '.gstack/pregate.json': policy({ paths: ['**/heldout/**'] }) });
commit(off, { 'HELDOUT/q.json': '{}\n' });
expect(hook(off).code).toBe(0);
const on = scratch({ '.gstack/pregate.json': policy({ paths: ['**/heldout/**'], transcripts: ['runs/**/transcript*.jsonl'], case_insensitive: true }) });
commit(on, { 'HELDOUT/q.json': '{}\n', 'HeldOut/q.json': '{}\n', 'Runs/c1/Transcript.jsonl': '{}\n', 'hold-out/q.json': '{}\n' });
const r = hook(on);
expect(r.code).toBe(1);
expect(r.err).toContain('sealed.path HELDOUT/q.json');
expect(r.err).toContain('sealed.path HeldOut/q.json');
expect(r.err).toContain('sealed.transcript Runs/c1/Transcript.jsonl');
expect(r.err).not.toContain('hold-out/q.json');
});
test('on, allow_paths stay case-sensitive: MANIFEST.json is exempt and manifest.json refuses', () => {
const s = scratch({ '.gstack/pregate.json': policy({ ...MANIFEST, case_insensitive: true }) });
commit(s, { 'sealed/s1/MANIFEST.json': '{}\n', 'sealed/s1/manifest.json': '{}\n' });
const r = hook(s);
expect(r.code).toBe(1);
expect(r.err).toContain('exempt sealed.path sealed/s1/MANIFEST.json');
expect(r.err).toMatch(/^ sealed\.path\s+sealed\/s1\/manifest\.json/m);
});
test('set only on the pushed head, it still tightens (base OR pushed)', () => {
const s = scratch({ '.gstack/pregate.json': policy({ paths: ['**/heldout/**'] }) });
commit(s, { '.gstack/pregate.json': policy({ paths: ['**/heldout/**'], case_insensitive: true }), 'HELDOUT/q.json': '{}\n' });
expect(hook(s).err).toContain('sealed.path HELDOUT/q.json');
});
});
describe('root-anchoring (the docs/pregate.md example)', () => {
test('globs match from the repo root: sealed/** misses a nested sealed/ directory and **/sealed/** catches it at any depth', () => {
const docs = fs.readFileSync(path.join(ROOT, 'docs', 'pregate.md'), 'utf8');
expect(docs).toContain('`sealed/**` misses `tasks/x/sealed/a.json`');
expect(docs).toContain('write `**/sealed/**`');
const anchored = scratch({ '.gstack/pregate.json': policy({ paths: ['sealed/**'] }) });
commit(anchored, { 'tasks/x/sealed/a.json': '{}\n' });
expect(hook(anchored).code).toBe(0);
const any = scratch({ '.gstack/pregate.json': policy({ paths: ['**/sealed/**'] }) });
commit(any, { 'tasks/x/sealed/a.json': '{}\n', 'sealed/b.json': '{}\n' });
const a = hook(any);
expect(a.err).toContain('sealed.path tasks/x/sealed/a.json');
expect(a.err).toContain('sealed.path sealed/b.json');
});
});
describe('visibility (cache under the state root; unknown counts as public)', () => {
test('a public answer is cached; a cached private answer older than an hour is re-queried before it can downgrade; a failed lookup is unknown', () => {
const state = fs.mkdtempSync(path.join(os.tmpdir(), 'sealed-vis-'));
+159
View File
@@ -0,0 +1,159 @@
/**
* gstack-ship-receipt carry across rewritten history and regenerated outputs (GSTA-147 PR 1,
* docs/ship-policy.md#receipt-carry): a rebase carries only onto a tree the receipt covers with the
* product diff it recorded, and a base merge that changes only a registered output's diff carries when
* the pregate for HEAD regenerated that output fresh.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { EXIT } from '../lib/headless-artifacts';
import { extractReceipt, planTreeCarry } from '../lib/ship-receipt';
import { cleanup, commit, makeFixtureRepo, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
const receiptIn = (body: string) => extractReceipt(fs.readFileSync(body, 'utf8')).receipt!;
const writeReceipt = (repo: FixtureRepo, body: string, extra: string[] = []) =>
runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '7', '--base', 'main', '--version', '1.0.0', '--session-kind', 'unattended', '--body', body, ...extra]);
const carry = (repo: FixtureRepo, body: string, extra: string[] = [], cwd = repo.work) =>
runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main', ...extra], {}, cwd);
function moveMain(repo: FixtureRepo, files: Record<string, string>, message = 'main moves'): void {
repo.git('checkout', '-q', 'main');
for (const [rel, text] of Object.entries(files)) write(repo.work, rel, text);
repo.git('add', '-A'); repo.git('commit', '-qm', message); repo.git('push', '-q', 'origin', 'main');
repo.git('checkout', '-q', 'feature');
}
describe('carry across a rebase', () => {
// Value: protects=a rebase onto the reviewed tree keeps its receipt (Harbor paid ~60M for a byte-identical rebase);
// fails_when=carry demands ancestry, or needs the old commit object; seam=none; no_claim=gh pr edit (--body stands in)
test('a rebase onto the tree the receipt covers carries, from a fresh clone that never saw the old head', () => {
const repo = makeFixtureRepo(); repos.push(repo);
commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
const reviewed = receiptIn(body);
expect(reviewed.head_tree).toBe(repo.git('rev-parse', 'HEAD^{tree}'));
moveMain(repo, { 'docs/README.md': '# docs moved on main\n' });
repo.git('merge', '-q', '--no-edit', 'origin/main');
const merged = repo.git('rev-parse', 'HEAD');
expect(carry(repo, body).status).toBe(EXIT.ok);
expect(receiptIn(body)).toMatchObject({ head: merged, head_tree: repo.git('rev-parse', 'HEAD^{tree}'), carry: { ancestry: 'linear', trees: [reviewed.head_tree] } });
repo.git('checkout', '-q', '-B', 'feature', 'origin/main');
repo.git('cherry-pick', reviewed.head);
const rebased = repo.git('rev-parse', 'HEAD');
expect(repo.git('rev-parse', 'HEAD^{tree}')).toBe(repo.git('rev-parse', `${merged}^{tree}`));
repo.git('push', '-q', '-f', 'origin', 'feature');
const clone = path.join(repo.root, 'clone');
expect(spawnSync('git', ['clone', '-q', '--single-branch', '--branch', 'main', repo.origin, clone], { env: { ...process.env, ...repo.env }, timeout: 30_000 }).status).toBe(0);
expect(spawnSync('git', ['fetch', '-q', 'origin', 'feature:feature'], { cwd: clone, env: { ...process.env, ...repo.env }, timeout: 30_000 }).status).toBe(0);
expect(spawnSync('git', ['checkout', '-q', 'feature'], { cwd: clone, env: { ...process.env, ...repo.env }, timeout: 30_000 }).status).toBe(0);
expect(spawnSync('git', ['cat-file', '-e', `${merged}^{commit}`], { cwd: clone, timeout: 30_000 }).status).not.toBe(0);
const out = carry(repo, body, [], clone);
expect(out.status).toBe(EXIT.ok);
expect(out.out).toContain(`CARRY: carried from ${merged.slice(0, 12)} to ${rebased.slice(0, 12)} (rebase onto a reviewed tree)`);
expect(receiptIn(body)).toMatchObject({ head: rebased, carry: { from: reviewed.head, reason: 'base merge + rebase onto a reviewed tree', ancestry: 'rewritten', range_diff: 'tree-equal' } });
}, 90_000);
// Value: protects=a rebase that changed the code takes a fresh /ship; fails_when=any non-ancestor head with a receipt
// carries; seam=none; no_claim=-
test('a rebase onto a different tree is RECEIPT_CARRY_TREE_MISMATCH and leaves the receipt unchanged', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const reviewed = commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
repo.git('checkout', '-q', '-B', 'feature', 'origin/main');
const head = commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 1;\n" });
const out = carry(repo, body);
expect(out.status).toBe(EXIT.refused);
expect(out.out).toContain('CARRY: refused (RECEIPT_CARRY_TREE_MISMATCH)');
expect(out.err).toContain(`HEAD's tree ${repo.git('rev-parse', 'HEAD^{tree}').slice(0, 12)} is not a tree the receipt covers`);
expect(out.err).toContain(`run /ship on ${head.slice(0, 12)}`);
expect(receiptIn(body).head).toBe(reviewed);
}, 60_000);
// Value: protects=an identical tree on a moved base is not the reviewed diff (a stacked base closed unmerged);
// fails_when=tree equality alone carries; seam=planTreeCarry; no_claim=-
test('the reviewed tree on a base that moved under it is RECEIPT_CARRY_PRODUCT_CHANGED', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const reviewed = commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
moveMain(repo, { 'lib/retry.js': "exports.MAX_RETRIES = 4;\nexports.TIMEOUT_MS = 5000;\nexports.retry = (fn) => fn();\n" });
repo.git('checkout', '-q', '-B', 'feature', 'origin/main');
repo.git('checkout', reviewed, '--', '.');
repo.git('commit', '-qam', 'same tree, new base');
expect(repo.git('rev-parse', 'HEAD^{tree}')).toBe(receiptIn(body).head_tree ?? '');
const out = carry(repo, body);
expect(out.status).toBe(EXIT.refused);
expect(out.out).toContain('CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)');
expect(out.err).toContain('has the reviewed tree, but its product diff against the current base differs');
expect(receiptIn(body).head).toBe(reviewed);
expect(planTreeCarry({ from: 'a'.repeat(40), head: 'b'.repeat(40), headTree: 't1', coveredTrees: ['t1'], productPatchId: { recorded: 'p1', head: 'p1' } })).toMatchObject({ ok: true, carry: { ancestry: 'rewritten' } });
}, 60_000);
});
describe('carry across a base merge that changes a registered output', () => {
// Value: protects=U1's rerun (+28M): a base merge that changes only a generated output's diff carries once the regen
// check proves the output fresh; fails_when=outputs carry without regen evidence, or never carry; seam=none; no_claim=gh
test('the output is product without a regen-fresh pregate and metadata with one', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const reviewed = commit(repo, { 'src/version.js': "module.exports = { VERSION: '1.1.0' };\n", 'generated/digest.md': '# digest v1.1.0\n' });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
moveMain(repo, {
'gen.js': "const fs=require('fs');const {VERSION}=require('./src/version.js');fs.writeFileSync('generated/digest.md',`# Digest v${VERSION}\\n`);\n",
'generated/digest.md': '# Digest v1.0.0\n',
});
spawnSync('git', ['merge', '-q', '--no-edit', 'origin/main'], { cwd: repo.work, env: { ...process.env, ...repo.env }, timeout: 30_000 });
write(repo.work, 'generated/digest.md', '# Digest v1.1.0\n');
repo.git('add', '-A'); repo.git('commit', '-qm', 'merge main; regenerate digest');
const bare = carry(repo, body, ['--dry-run']);
expect(bare.status).toBe(EXIT.refused);
expect(bare.err).toContain('generated/digest.md');
expect(bare.out).toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
expect(runBin(repo, 'gstack-pregate', ['--base', 'main', '--allow-repo-commands']).status).toBe(EXIT.ok);
const pregateJson = path.join(repo.work, '.gstack/tmp/pregate.json');
const pregate = JSON.parse(fs.readFileSync(pregateJson, 'utf8'));
const regen = pregate.checks.find((c: { id: string }) => c.id === 'regen');
expect(regen).toMatchObject({ status: 'pass', proven: ['generated/digest.md'] });
const tampered = path.join(repo.root, 'pregate-tampered.json');
const label = regen.inputs.registry;
for (const [registry, proven] of [['head (unreviewed)', ['generated/digest.md']], [label, ['generated/*']]]) {
Object.assign(regen, { proven, inputs: { ...regen.inputs, registry } });
fs.writeFileSync(tampered, JSON.stringify(pregate));
const refused = carry(repo, body, ['--pregate', tampered]);
expect(refused.out).toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
expect(refused.err).toContain('generated/digest.md');
}
const out = carry(repo, body, ['--pregate', pregateJson]);
expect(out.status).toBe(EXIT.ok);
expect(receiptIn(body)).toMatchObject({ head: repo.git('rev-parse', 'HEAD'), carry: { from: reviewed } });
}, 120_000);
// Value: protects=a hand-written file under an output glob is reviewed code (red team, #3564 pass 1); fails_when=a
// regen pass that never wrote the path sets it aside as generated; seam=none; no_claim=-
test('a file that matches an output glob but that the generator never writes stays product', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const reviewed = commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
commit(repo, { 'generated/extra.md': '# hand-written after review\n' });
expect(runBin(repo, 'gstack-pregate', ['--base', 'main', '--allow-repo-commands']).status).toBe(EXIT.ok);
const pregateJson = path.join(repo.work, '.gstack/tmp/pregate.json');
const regen = JSON.parse(fs.readFileSync(pregateJson, 'utf8')).checks.find((c: { id: string }) => c.id === 'regen');
expect(regen.status).toBe('pass');
expect(regen.proven ?? []).not.toContain('generated/extra.md');
const out = carry(repo, body, ['--pregate', pregateJson]);
expect(out.status).toBe(EXIT.refused);
expect(out.out).toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
expect(out.err).toContain('generated/extra.md');
expect(receiptIn(body).head).toBe(reviewed);
}, 120_000);
});
+207
View File
@@ -0,0 +1,207 @@
/**
* The /ship receipt's review depth and review-loop exception (GSTA-147 PR 2,
* docs/review-policy.md#review-depth, #3559): write records the depth gstack
* computes at the checked-out head, the queue recomputes the depth the head
* needs and refuses a receipt below it, a carry rebinds the gate scan or
* refuses when the head needs more, and the review-loop exception is checked
* like the documentation exception.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { extractReceipt, renderReceiptBlock } from '../lib/ship-receipt';
import { bindShippedReceipt, receiptDepthVerdict, reviewLoopExceptionLine } from '../lib/ship-receipt-depth';
import { cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
const receiptIn = (body: string) => extractReceipt(fs.readFileSync(body, 'utf8')).receipt!;
const writeReceipt = (repo: FixtureRepo, body: string, extra: string[] = []) =>
runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '7', '--base', 'main', '--version', '1.0.0', '--session-kind', 'unattended', '--body', body, ...extra]);
const lines = (n: number) => Array.from({ length: n }, (_, i) => `exports.f${i} = () => ${i};`).join('\n') + '\n';
const rewrite = (body: string, edit: (r: Record<string, any>) => void) => {
const text = fs.readFileSync(body, 'utf8');
const r = extractReceipt(text).receipt as Record<string, any>;
edit(r);
fs.writeFileSync(body, text.replace(/```gstack-ship-receipt[\s\S]*?\n```\n/, renderReceiptBlock(r as any)));
return fs.readFileSync(body, 'utf8');
};
describe('write records the review depth at the checked-out head', () => {
// Value: protects=the receipt names the depth and a gate scan bound to the head it is written for, and never binds a
// tree it did not review; fails_when=write records no depth, or --head accepts a commit other than HEAD; seam=none
test('a light code diff records depth, runtime lines, the base and the gate scan at HEAD; --head of another commit writes nothing', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const older = commit(repo, { 'lib/util.js': lines(3) });
const head = commit(repo, { 'lib/more.js': lines(2) });
const body = path.join(repo.root, 'body.md');
const ok = writeReceipt(repo, body);
expect(ok.status).toBe(EXIT.ok);
expect(ok.err).toContain('depth=light');
const r = receiptIn(body);
expect(r.gstack_version).toMatch(/^\d+\.\d+\.\d+\.\d+$/);
const base = repo.git('rev-parse', 'origin/main');
expect(r.review_depth).toMatchObject({ depth: 'light', class: 'code', base, registry: base, gate_scan: { tagged: 0, head } });
expect(r.review_depth!.runtime_lines).toBeGreaterThanOrEqual(5);
expect(r.review_depth!.runtime_lines).toBeLessThanOrEqual(50);
const other = path.join(repo.root, 'other.md');
const bad = writeReceipt(repo, other, ['--head', older]);
expect(bad.status).toBe(EXIT.fail);
expect(bad.err).toContain(`--head ${older.slice(0, 12)} is not the checked-out HEAD ${head.slice(0, 12)}`);
expect(bad.err).toContain('(RECEIPT_INVALID)');
expect(fs.existsSync(other)).toBe(false);
}, 60_000);
});
describe('the queue refuses a receipt below the depth its head needs', () => {
// Value: protects=a receipt reviewed lighter than its head needs never READYs or seats, and the refusal says what to do;
// fails_when=the queue trusts the recorded depth, or prints no required depth, reasons and inputs; seam=bindShippedReceipt
test('light on a standard head, a missing depth from a current writer and an unbound gate scan refuse; a legacy receipt is accepted', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const head = commit(repo, { 'lib/big.js': lines(80) });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'standard', runtime_lines: 80 });
const base = repo.git('rev-parse', 'origin/main').slice(0, 12);
const bind = (text: string) => bindShippedReceipt(repo.work, 'origin/main', text, '7', [head]);
const good = bind(fs.readFileSync(body, 'utf8'));
expect(good.ok).toBe(true);
expect(good.ok && good.depthLine).toContain('REVIEW_DEPTH: receipt=standard required=standard runtime_lines=80 class=code');
const low = bind(rewrite(body, r => { r.review_depth.depth = 'light'; }));
expect(low.ok).toBe(false);
if (low.ok) return;
expect(low.code).toBe('QUEUE_RECEIPT_DEPTH_RAISED');
expect(low.depthLine).toContain('receipt=light required=standard runtime_lines=80');
expect(low.depthLine).toContain(`inputs=base@${base},registry@${base},gate_scan@${head.slice(0, 12)}`);
expect(low.message).toContain('runtime=80>50');
expect(low.message).toContain(`the receipt is unchanged; run /ship on ${head.slice(0, 12)}`);
const unbound = bind(rewrite(body, r => { r.review_depth.depth = 'standard'; r.review_depth.gate_scan.head = 'a'.repeat(40); }));
expect(unbound.ok).toBe(false);
expect(!unbound.ok && unbound.depthLine).toContain('required=full');
expect(!unbound.ok && unbound.message).toContain('gate_scan_unavailable');
const none = bind(rewrite(body, r => { delete r.review_depth; }));
expect(!none.ok && none.code).toBe('QUEUE_RECEIPT_DEPTH_RAISED');
expect(!none.ok && none.message).toContain('records no review_depth though its writer records one');
const legacy = rewrite(body, r => { delete r.gstack_version; });
const verdict = receiptDepthVerdict(repo.work, 'origin/main', extractReceipt(legacy).receipt!, head);
expect(verdict).toEqual({ ok: true, line: 'REVIEW_DEPTH: legacy receipt (written before receipts recorded review_depth); accepted' });
}, 60_000);
// Value: protects=the queue's required depth keeps the scope signals the writer used, and a gate scan binds only the full
// bound commit; fails_when=the queue recomputes without diff-scope (auth reads light) or binds an empty or short scan head
// by prefix; seam=bindShippedReceipt
test('an auth path needs full at the queue though the receipt says light; an empty or short gate scan head binds nothing', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const head = commit(repo, { 'lib/auth.js': lines(3) });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'full', class: 'gate' });
const bind = (text: string) => bindShippedReceipt(repo.work, 'origin/main', text, '7', [head]);
const light = bind(rewrite(body, r => { r.review_depth.depth = 'light'; }));
expect(!light.ok && light.code).toBe('QUEUE_RECEIPT_DEPTH_RAISED');
expect(!light.ok && light.depthLine).toContain('receipt=light required=full');
expect(!light.ok && light.depthLine).toContain('class=gate');
const plain = makeFixtureRepo(); repos.push(plain);
const plainHead = commit(plain, { 'lib/util.js': lines(3) });
const plainBody = path.join(plain.root, 'body.md');
expect(writeReceipt(plain, plainBody).status).toBe(EXIT.ok);
for (const scanHead of ['', plainHead.slice(0, 12)]) {
const r = bindShippedReceipt(plain.work, 'origin/main', rewrite(plainBody, x => { x.review_depth.gate_scan.head = scanHead; }), '7', [plainHead]);
expect(r.ok).toBe(false);
expect(!r.ok && r.depthLine).toContain('required=full');
expect(!r.ok && r.depthLine).toContain('gate_scan@missing');
}
}, 90_000);
});
describe('carry and the review depth', () => {
// Value: protects=a carried receipt keeps a gate scan bound to the head it now names, and a carry whose new head needs a
// deeper review (a skipped test) is refused; fails_when=carry keeps the old scan, or carries a gate-tagged test change; seam=none
test('a plain test change carries and rebinds the gate scan; a skipped test raises the depth and refuses the carry', () => {
const repo = makeFixtureRepo(); repos.push(repo);
commit(repo, { 'lib/util.js': lines(3), 'test/util.test.js': "test('adds', () => { expect(1).toBe(1); });\n" });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
expect(receiptIn(body).review_depth!.depth).toBe('light');
const plain = commit(repo, { 'test/util.test.js': "test('adds', () => { expect(1).toBe(1); });\ntest('more', () => { expect(2).toBe(2); });\n" });
const carried = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
expect(carried.status).toBe(EXIT.ok);
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'light', gate_scan: { tagged: 0, head: plain } });
const skipped = commit(repo, { 'test/util.test.js': "test.skip('adds', () => { expect(1).toBe(1); });\ntest('more', () => { expect(2).toBe(2); });\n" });
const refused = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
expect(refused.status).toBe(EXIT.refused);
expect(refused.out).toContain('CARRY: refused (RECEIPT_CARRY_REFUSED)');
expect(refused.err).toContain(`HEAD ${skipped.slice(0, 12)} needs full review depth`);
expect(refused.err).toContain("above the receipt's light");
expect(receiptIn(body).head).toBe(plain);
}, 90_000);
// Value: protects=a carry never binds a scan of one checkout to another commit, and never rebinds a receipt the queue would
// refuse; fails_when=carry --head from another checkout carries a skipped test at light, or a current receipt without
// review_depth carries; seam=none
test('carry --head from another checkout refuses; a receipt with gstack_version but no review_depth refuses', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const reviewed = commit(repo, { 'lib/util.js': lines(3), 'test/util.test.js': "test('a', () => {});\n" });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
const skipped = commit(repo, { 'test/util.test.js': "test.skip('a', () => {});\n" });
repo.git('checkout', '-q', reviewed);
const r = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main', '--head', skipped]);
expect(r.status).toBe(EXIT.refused);
expect(r.err).toContain(`--head ${skipped.slice(0, 12)} is not the checked-out HEAD ${reviewed.slice(0, 12)}`);
expect(receiptIn(body).head).toBe(reviewed);
repo.git('checkout', '-q', skipped);
rewrite(body, x => { delete x.review_depth; });
const noDepth = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
expect(noDepth.status).toBe(EXIT.refused);
expect(noDepth.err).toContain('records gstack_version but no review_depth');
}, 90_000);
});
describe('the review-loop exception (#3559)', () => {
// Value: protects=an accepted stop after a fix-check is a structured record the queue checks, not prose under REVIEW_LOOP;
// fails_when=write accepts it without a fix-check, with a foreign head or without the body line, or the queue ignores a
// body that dropped the line; seam=bindShippedReceipt
test('write refuses an exception with no fix-check, a foreign head or no body line, records it otherwise, and the queue checks the line', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const head = commit(repo, { 'lib/util.js': lines(3) });
const body = path.join(repo.root, 'body.md');
const exception = { accepted_by: 'Garry', date: '2026-10-28', via: 'GSTA-147 relay', reason: 'fix-check edit is a comment; scope frozen', fix_check: { head, findings: 1 } };
const arg = (e: unknown) => ['--review-loop-exception', JSON.stringify(e)];
const loop = (n: number) => ['--review-loop', JSON.stringify({ passes: 2, blocking_found: 1, deferred: 0, fix_checks: n })];
fs.writeFileSync(body, '## What changed\n');
for (const [extra, why] of [
[[...arg(exception)], 'needs --review-loop with fix_checks >= 1'],
[[...arg(exception), ...loop(0)], 'needs --review-loop with fix_checks >= 1'],
[[...arg({ ...exception, fix_check: { head: 'a'.repeat(40), findings: 1 } }), ...loop(1)], 'is not ' + head.slice(0, 12) + ' or an ancestor of it'],
[[...arg({ ...exception, extra: 1 }), ...loop(1)], 'unknown key "extra"'],
[[...arg(exception), ...loop(1)], 'does not carry the exception line verbatim'],
] as Array<[string[], string]>) {
const out = writeReceipt(repo, body, extra);
expect(out.status).toBe(EXIT.fail);
expect(out.err).toContain('(REVIEW_LOOP_EXCEPTION_INVALID;');
expect(out.err).toContain(why);
}
expect(extractReceipt(fs.readFileSync(body, 'utf8')).found).toBe(false);
const line = reviewLoopExceptionLine(exception);
expect(line).toBe(`Review-loop exception: accepted by Garry 2026-10-28 via GSTA-147 relay: fix-check edit is a comment; scope frozen (fix-check at ${head.slice(0, 12)}, 1 finding)`);
fs.writeFileSync(body, `## What changed\n\n${line}\n`);
expect(writeReceipt(repo, body, [...arg(exception), ...loop(1)]).status).toBe(EXIT.ok);
expect(receiptIn(body).review_loop_exception).toEqual(exception);
expect(bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8'), '7', [head]).ok).toBe(true);
const dropped = bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8').replace(line, ''), '7', [head]);
expect(!dropped.ok && dropped.code).toBe('QUEUE_RECEIPT_EXCEPTION_INVALID');
expect(!dropped.ok && dropped.message).toContain('does not carry the review-loop exception line verbatim');
const foreign = { ...exception, fix_check: { head: 'f'.repeat(40), findings: 1 } };
const forged = rewrite(body, x => { x.review_loop_exception = foreign; }).replace(line, reviewLoopExceptionLine(foreign));
const offBranch = bindShippedReceipt(repo.work, 'origin/main', forged, '7', [head]);
expect(!offBranch.ok && offBranch.code).toBe('QUEUE_RECEIPT_EXCEPTION_INVALID');
expect(!offBranch.ok && offBranch.message).toContain('is not the receipt head or an ancestor of it');
}, 90_000);
});
@@ -0,0 +1,4 @@
{
"bytes": 1008,
"why": "GSTA-147 review depth: Step 9 saves REVIEW_DEPTH/GATE_SCAN, passes --depth-floor, records depth fields and reads each reviewer's DIFF_CMD; the batched light specialist; Step 11's light note; the skeleton's Review policy paragraph names the depth and --depth-floor (trimmed to hold the skeleton cap); the pr-body section's --review-loop-exception instruction (#3559)"
}
+1
View File
@@ -792,6 +792,7 @@ describeIfSelected('Cookie setup workflow quality', ['setup-browser-cookies/SKIL
startMarker: '# Setup Browser Cookies',
endMarker: null,
...COOKIE_WORKFLOW_JUDGE,
schemaTransport: true,
readInput: () => buildCookieWorkflowJudgeInput(ROOT),
});
}, WORKFLOW_JUDGE_TEST_MS);
+2 -2
View File
@@ -410,7 +410,7 @@ test('Ship sends its authorized 64k cap and compact response contract through th
expect(WORKFLOW_JUDGE_RESPONSE_SCHEMA.properties.reasoning).not.toHaveProperty('maxLength');
expect(source.match(/compactReasoning: true/g)).toHaveLength(1);
const f = fixture();
const stream = spyOn(Messages.prototype, 'stream').mockReturnValue({ finalMessage: async () => ({
const stream = spyOn(Messages.prototype, 'stream').mockReturnValue({ on() { return this; }, finalMessage: async () => ({
stop_reason: 'end_turn', content: [{ type: 'text', text: JSON.stringify(scores) }],
}) } as any);
try {
@@ -516,7 +516,7 @@ test('the structured callback rejects incomplete, schema-invalid and below-thres
try {
for (const response of responses) {
const f = fixture();
stream.mockReturnValue({ finalMessage: async () => ({ stop_reason: response.stop_reason,
stream.mockReturnValue({ on() { return this; }, finalMessage: async () => ({ stop_reason: response.stop_reason,
content: [{ type: 'text', text: JSON.stringify(response.value) }] }) } as any);
const h = actualCallback(f, { judge: (prompt, model, request) => callJudge<typeof scores>(prompt, model, request) });
await expect(h.run({ ...h.options, schemaTransport: true, compactReasoning: true, maxTokens: 65_536, stream: true })).rejects.toThrow();