feat(skills): skill-scoped R56 cuts for /investigate and /review, plus /review blocker proof (GST-1088 lane C, candidate)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-03 04:03:30 +00:00
1 parent 106fd39446
commit bd24f76ece
26 files changed
+380 -248

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
/**
* R56 cut ledger ratchet (GST-1088 lane C). modules/cuts.json names the pieces
* cut from /investigate and /review in every loadout and on every host, and pins
* each targeted skill's always-loaded bytes (its Claude SKILL.md, frontmatter
* included). This test fails when a cut piece reappears in a generated SKILL.md,
* when a pin moves, or when a cut or a review-only change leaks into another skill.
*/
import { afterAll, describe, expect, test } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ALL_HOST_CONFIGS } from '../hosts/index';
import { CUT_LEDGER_FILE, loadCutLedger, preambleCuts, type CutLedger } from '../scripts/cut-ledger';
import { PREAMBLE_PIECES } from '../scripts/resolvers/preamble';
import { SHARED_PREAMBLE_FILE } from '../scripts/resolvers/preamble-hoist';
import { discoverTemplates } from '../scripts/discover-skills';
import { loadLoadouts, moduleCatalog, selectModules, validateLoadouts } from '../scripts/modules';
const ROOT = path.resolve(import.meta.dir, '..');
const TARGETED = ['investigate', 'review'];
/** Text only /review may render: the GST-1088 markers and the blocker-proof rule. */
const REVIEW_ONLY = ['Dispatching 0 specialists: (small diff)', 'Dispatching adversarial pass: native', 'Blocker proof'];
const skillOfDir = (dir: string) => (dir === '.' ? 'gstack' : dir.replace(/^gstack-/, ''));
/** Every violation of the ledger by these renders (key "host/skill") and Claude SKILL.md byte counts. */
export function cutViolations(renders: Record<string, string>, bytes: Record<string, number>, ledger: CutLedger): string[] {
const out: string[] = [];
for (const [key, text] of Object.entries(renders).sort(([a], [b]) => a.localeCompare(b))) {
const [host, skill] = key.split('/');
for (const cut of ledger.cuts.filter(c => c.skill === skill && text.includes(c.match))) {
out.push(`${host}/${skill}/SKILL.md: cut section "${cut.match}" reappeared (${CUT_LEDGER_FILE} ${cut.id}: ${cut.why}). `
+ `Fix: remove it from ${cut.source}, then bun run gen:skill-docs. Restoring it instead needs a measurement in the PR and ${ledger.approver}.`);
}
}
for (const [skill, pin] of Object.entries(ledger.pins).sort(([a], [b]) => a.localeCompare(b))) {
const actual = bytes[skill];
if (actual === undefined) { out.push(`pins.${skill} in ${CUT_LEDGER_FILE} names no ${skill}/SKILL.md. Delete the pin.`); continue; }
if (actual > pin) {
out.push(`${skill}/SKILL.md: ${actual} always-loaded bytes, pinned at ${pin} (+${actual - pin}). `
+ `Fix: edit ${skill}/SKILL.md.tmpl, then bun run gen:skill-docs. Re-pin: set pins.${skill} to ${actual} in ${CUT_LEDGER_FILE} with the measurement in the PR body; approver: ${ledger.approver}.`);
} else if (actual < pin) {
out.push(`${skill}/SKILL.md: ${actual} always-loaded bytes, pinned at ${pin} (-${pin - actual}). Re-pin: lower pins.${skill} to ${actual} in ${CUT_LEDGER_FILE}.`);
}
}
return out;
}
/** Each skill's SKILL.md plus its own section files (the shared preamble is the same file for every skill). */
function renderAllHosts(outDir: string): Record<string, string> {
const render = Bun.spawnSync(
['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'all', '--out-dir', outDir],
{ cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 180_000 },
);
if (render.exitCode !== 0) throw new Error(`gen-skill-docs --host all --out-dir failed (exit ${render.exitCode}):\n${render.stderr.toString()}`);
const renders: Record<string, string> = {};
for (const host of ALL_HOST_CONFIGS) {
const skillsRoot = host.name === 'claude' ? ROOT : path.join(outDir, host.hostSubdir, 'skills');
const dirs = fs.readdirSync(skillsRoot, { withFileTypes: true }).filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name);
if (host.name === 'claude') dirs.push('.');
for (const dir of dirs) {
const file = path.join(skillsRoot, dir, 'SKILL.md');
if (!fs.existsSync(file) || (host.name === 'claude' && !fs.existsSync(`${file}.tmpl`))) continue;
const sectionsDir = path.join(skillsRoot, dir, 'sections');
const sections = dir !== '.' && fs.existsSync(sectionsDir)
? fs.readdirSync(sectionsDir).filter(f => f.endsWith('.md') && f !== SHARED_PREAMBLE_FILE).sort().map(f => fs.readFileSync(path.join(sectionsDir, f), 'utf-8'))
: [];
renders[`${host.name}/${skillOfDir(dir)}`] = [fs.readFileSync(file, 'utf-8'), ...sections].join('\n');
}
}
return renders;
}
const ledger = loadCutLedger();
const claudeBytes = Object.fromEntries(TARGETED.map(s => [s, fs.statSync(path.join(ROOT, s, 'SKILL.md')).size]));
describe('cut ledger ratchet messages', () => {
const fixture: CutLedger = {
version: 1, note: '', approver: 'the owner',
cuts: [{ id: 'part:x/learn', skill: 'x', status: 'cut', source: 'x/SKILL.md.tmpl', match: '## Learn', why: 'unused', evidence: 'run 1' }],
pins: { x: 100, y: 50 },
};
test('names the file, the section and its entry, the bytes and the fix or re-pin with its approver', () => {
expect(cutViolations({ 'codex/x': 'a\n## Learn\n', 'codex/z': '## Learn' }, { x: 101, y: 49 }, fixture)).toEqual([
`codex/x/SKILL.md: cut section "## Learn" reappeared (${CUT_LEDGER_FILE} part:x/learn: unused). Fix: remove it from x/SKILL.md.tmpl, then bun run gen:skill-docs. Restoring it instead needs a measurement in the PR and the owner.`,
`x/SKILL.md: 101 always-loaded bytes, pinned at 100 (+1). Fix: edit x/SKILL.md.tmpl, then bun run gen:skill-docs. Re-pin: set pins.x to 101 in ${CUT_LEDGER_FILE} with the measurement in the PR body; approver: the owner.`,
`y/SKILL.md: 49 always-loaded bytes, pinned at 50 (-1). Re-pin: lower pins.y to 49 in ${CUT_LEDGER_FILE}.`,
]);
});
});
describe('modules/cuts.json', () => {
test('cuts and pins cover exactly /investigate and /review', () => {
expect([...new Set(ledger.cuts.map(c => c.skill))].sort()).toEqual(TARGETED);
expect(Object.keys(ledger.pins).sort()).toEqual(TARGETED);
});
test('each cut is a skill-qualified id with a real source, preamble piece and match', () => {
const pieces = new Set(PREAMBLE_PIECES.map(p => p.id));
const others = discoverTemplates(ROOT).map(t => path.join(ROOT, path.dirname(t.tmpl), 'SKILL.md'))
.filter(f => !TARGETED.some(s => f.endsWith(`/${s}/SKILL.md`)) && fs.existsSync(f)).map(f => fs.readFileSync(f, 'utf-8'));
expect(new Set(ledger.cuts.map(c => c.id)).size).toBe(ledger.cuts.length);
for (const cut of ledger.cuts) {
expect(cut.id).toMatch(new RegExp(`^part:${cut.skill}/[a-z0-9-]+$`));
expect(cut.status).toBe('cut');
expect(fs.existsSync(path.join(ROOT, cut.source)), `${cut.id} source ${cut.source}`).toBe(true);
if (cut.preamble_piece) expect(pieces.has(cut.preamble_piece), `${cut.id} preamble_piece`).toBe(true);
expect(others.some(text => text.includes(cut.match)), `${cut.id} match "${cut.match}" appears in no other skill, so it guards nothing`).toBe(true);
}
});
test('a preamble cut applies to its own skill alone', () => {
for (const { tmpl } of discoverTemplates(ROOT)) {
const skill = skillOfDir(path.dirname(tmpl));
expect([...preambleCuts(skill)]).toEqual(ledger.cuts.filter(c => c.skill === skill && c.preamble_piece).map(c => c.preamble_piece!));
}
});
});
describe('module catalog lists cuts', () => {
const manifest = loadLoadouts();
test('every cut id is a catalog entry with status cut', () => {
const catalog = moduleCatalog(manifest);
for (const cut of ledger.cuts) expect(catalog.find(e => e.id === cut.id)).toMatchObject({ kind: 'part', status: 'cut', pinned: false });
});
test('a loadout or a --modules spec cannot name a cut, since it is cut in every loadout', () => {
const id = ledger.cuts[0].id;
const bad = { ...manifest, loadouts: { ...manifest.loadouts, odd: { base: 'full', include: [id] } } };
expect(validateLoadouts(bad, moduleCatalog(manifest))).toContain(`loadouts.odd names ${id}, which is cut in every loadout (${CUT_LEDGER_FILE})`);
expect(() => selectModules({ modules: `full,+${id}` })).toThrow(/cut in every loadout/);
});
});
describe('cut ledger against every host render', () => {
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-cut-ledger-'));
afterAll(() => fs.rmSync(outDir, { recursive: true, force: true }));
const renders = renderAllHosts(outDir);
test('no cut piece reappears in its skill or its sections, and each pin matches its SKILL.md', () => {
for (const host of ALL_HOST_CONFIGS) for (const skill of TARGETED) expect(renders[`${host.name}/${skill}`], `${host.name}/${skill}`).toBeDefined();
const violations = cutViolations(renders, claudeBytes, ledger);
if (violations.length) throw new Error(`${violations.length} cut ledger violation(s):\n ${violations.join('\n ')}`);
});
test('review-only GST-1088 text renders in /review alone, on every host', () => {
const leaks = Object.entries(renders).filter(([key]) => !key.endsWith('/review'))
.flatMap(([key, text]) => REVIEW_ONLY.filter(s => text.includes(s)).map(s => `${key}: "${s}"`));
expect(leaks).toEqual([]);
expect(REVIEW_ONLY.filter(s => !renders['claude/review'].includes(s))).toEqual([]);
});
});
+1
View File
@@ -326,6 +326,7 @@ describe('parity with the measured prototype', () => {
/** Files the shipped function reaches beyond the prototype, each with the reviewed edge class behind it. */
const REVIEWED_SUPERSET: Record<string, string> = {
setup: './setup, $ROOT/setup and join(..., "setup") exec edges (plan A2-2); setup is a full-gate file, so the closure stops there',
'scripts/cut-ledger.ts': 'GST-1088: scripts/resolvers/preamble.ts imports it (the R56 cut ledger), so it reaches every consumer of the preamble',
'lib/bun-isolation.ts': 'GSTA-56: every bun launcher (script-invocation, restamp, qa-deadline, the hooks) imports it, so it reaches every consumer of those launchers',
'lib/ship-receipt-docs.ts': 'bin/gstack-ship-receipt imports it (the docs audit record and doc caps), so it reaches every consumer of the receipt binary',
'lib/ship-receipt-prose.ts': 'bin/gstack-ship-receipt imports it (read --check-prose), so it reaches every consumer of the receipt binary',
+9 -9
View File
@@ -6,52 +6,52 @@
"agy/gstack-plan-design-review": 109107,
"agy/gstack-plan-devex-review": 114948,
"agy/gstack-plan-eng-review": 143581,
"agy/gstack-review": 104182,
"agy/gstack-review": 100939,
"codex/gstack-autoplan": 108400,
"codex/gstack-plan-design-review": 110464,
"codex/gstack-plan-devex-review": 116456,
"codex/gstack-plan-eng-review": 144746,
"codex/gstack-review": 103705,
"codex/gstack-review": 100123,
"copilot/gstack-autoplan": 109100,
"copilot/gstack-plan-design-review": 109373,
"copilot/gstack-plan-devex-review": 114410,
"copilot/gstack-plan-eng-review": 143746,
"copilot/gstack-review": 103763,
"copilot/gstack-review": 100557,
"cursor/gstack-autoplan": 108332,
"cursor/gstack-plan-design-review": 107915,
"cursor/gstack-plan-devex-review": 113680,
"cursor/gstack-plan-eng-review": 142336,
"cursor/gstack-review": 122053,
"cursor/gstack-review": 118940,
"factory/gstack-autoplan": 110610,
"factory/gstack-plan-design-review": 111013,
"factory/gstack-plan-devex-review": 117043,
"factory/gstack-plan-eng-review": 145444,
"factory/gstack-review": 125826,
"factory/gstack-review": 121318,
"gbrain/gstack-autoplan": 108739,
"gbrain/gstack-plan-eng-review": 124963,
"gemini/gstack-autoplan": 108890,
"gemini/gstack-plan-design-review": 109201,
"gemini/gstack-plan-devex-review": 114238,
"gemini/gstack-plan-eng-review": 143579,
"gemini/gstack-review": 103585,
"gemini/gstack-review": 100384,
"hermes/gstack-autoplan": 108742,
"hermes/gstack-plan-eng-review": 124999,
"kiro/gstack-autoplan": 108106,
"kiro/gstack-plan-design-review": 107761,
"kiro/gstack-plan-devex-review": 113532,
"kiro/gstack-plan-eng-review": 142198,
"kiro/gstack-review": 121877,
"kiro/gstack-review": 118778,
"openclaw/gstack-autoplan": 108562,
"openclaw/gstack-plan-eng-review": 123478,
"opencode/gstack-autoplan": 109081,
"opencode/gstack-plan-design-review": 109229,
"opencode/gstack-plan-devex-review": 114288,
"opencode/gstack-plan-eng-review": 143605,
"opencode/gstack-review": 122675,
"opencode/gstack-review": 119527,
"slate/gstack-autoplan": 108219,
"slate/gstack-plan-design-review": 107838,
"slate/gstack-plan-devex-review": 113606,
"slate/gstack-plan-eng-review": 142267,
"slate/gstack-review": 121965
"slate/gstack-review": 118859
}
}
-1
View File
@@ -67,7 +67,6 @@
"## Step 2: Read the checklist",
"## Step 2.5: Check for review bot comments",
"## Step 3: Get the diff",
"## Step 3.4: Workspace-aware queue status (advisory)",
"## Step 3.5: Diff scans",
"## Step 3.6: Gather review context",
"## Step 4: Critical pass (core review)",
+3 -3
View File
@@ -490,10 +490,10 @@ do not launch the downstream skill or open a browser.`,
gateAfterStop: undefined, // operational multi-STOP skill, like ship
},
behavioral: 'plan',
maxSkeletonBytes: 59_073, // GSTA-96 hoist (2026-10-26): lowered from 80_900 by the shared preamble that moved to sections/preamble.md, keeping the pre-hoist headroom of 319 bytes; measured 58,754. Before: Shared-code identity/skip/action rules + critical-severity validation; measured 74,493 (2026-09-17). + v1.91.12.0 merge of #2999 (review clarity repairs: await reads, research alongside dispatch, /review deadline and setup authority, findings sources) with #3002 (guarded state-root lines, plan-check checkpoints); each fit alone; measured 74,881 (2026-10-01). + Oct 8 wave: shared voice directive bans "load-bearing" and adds the reply-language rule, offset by tightening the bounded closer (net +17 bytes per tier-2+ skill); measured 74,884 (2026-10-08). + gate integrity (HONEST_WORK_GATE_INTEGRITY W2): Step 3.5 Diff scans with the shared GATE_SCAN_BLOCK, the Step 4 carry, the four-option `review-gate-disposition` question in 5c, gate record fields in 5.8 and the Gate edits report line; measured 80,054 (2026-10-10). + Oct 10 multi-agent wave PR D2 (sideways review): Step 4's enum-only paragraph becomes the sideways sweep with the forced-CRITICAL list, plus the flake-fix number sentence in the Gate Integrity paragraph; measured skeleton 80,812 (2026-10-10).
maxSkeletonBytes: 55_146, // GST-1088 lane C diet (R56 cut ledger modules/cuts.json: learnings search and capture, repo ownership, queue status; plus the blocker-proof rule): lowered from 59,073 to measured, no headroom; measured 55,146 (2026-11-03). Before: GSTA-96 hoist (2026-10-26): lowered from 80_900 by the shared preamble that moved to sections/preamble.md, keeping the pre-hoist headroom of 319 bytes; measured 58,754. Before: Shared-code identity/skip/action rules + critical-severity validation; measured 74,493 (2026-09-17). + v1.91.12.0 merge of #2999 (review clarity repairs: await reads, research alongside dispatch, /review deadline and setup authority, findings sources) with #3002 (guarded state-root lines, plan-check checkpoints); each fit alone; measured 74,881 (2026-10-01). + Oct 8 wave: shared voice directive bans "load-bearing" and adds the reply-language rule, offset by tightening the bounded closer (net +17 bytes per tier-2+ skill); measured 74,884 (2026-10-08). + gate integrity (HONEST_WORK_GATE_INTEGRITY W2): Step 3.5 Diff scans with the shared GATE_SCAN_BLOCK, the Step 4 carry, the four-option `review-gate-disposition` question in 5c, gate record fields in 5.8 and the Gate edits report line; measured 80,054 (2026-10-10). + Oct 10 multi-agent wave PR D2 (sideways review): Step 4's enum-only paragraph becomes the sideways sweep with the forced-CRITICAL list, plus the flake-fix number sentence in the Gate Integrity paragraph; measured skeleton 80,812 (2026-10-10).
minUnionBytes: 89_000, // Phase 4 wave 1; measured union 93,357
mustContain: ['confidence', 'P1', 'P2', 'Review Army', 'adversarial'],
maxSizeRatio: 1.3180, // + GSTA-167 variant G part 1 (plan approved by Garry 2026-10-30, autoplan-20261030161754 "Do it", which names about +0.3k prompt tokens per voice): the native adversarial and outside-voice prompts carry the blocking floor and the Severity and Blocks lines, +537 bytes; measured 1.3180 (2026-10-30). Before: 1.3136. GSTA-96 hoist merged with GSTA-92 phase 2 (2026-10-26): both owner-approved raises together, measured 142,550 / 108,523 = 1.3135. Before: Shared-code feature + critical-severity validation: 128,042 union bytes / 108,523 baseline = 1.1799; preserves content floors. + v1.91.12.0 merge of #2999 (above, plus plan-completion fallback intent and specialist checklist-by-path) with #3002; measured 1.1843 (2026-10-01). + v1.91.19.0 wave: verdict-form Codex callers with sandbox preflight (INV-1/B1), P0/P1 structured gate (B1b), bound-plan discovery and data-not-instructions guard (F1/D3), visible LEARNINGS failures (B5); measured 129,941 / 108,523 = 1.1974 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 130,025 / 108,523 = 1.1981 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051); measured 130,359 / 108,523 = 1.2012 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 131,314 / 108,523 = 1.2100 (2026-10-06). Combined Oct 6 wave head (A and B): measured 131,966 / 108,523 = 1.2160 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 132,199 / 108,523 = 1.2182 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 132,258 / 108,523 = 1.2187 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 132,233 / 108,523 = 1.2185 (2026-10-07). Merged with the guard area: measured 132,522 / 108,523 = 1.2211 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 133,902 / 108,523 = 1.2339 (2026-10-08). + gate integrity W2 (above); measured 1.282 (2026-10-10). + Oct 10 multi-agent wave PR D2 (sideways review): sideways sweep and flake-fix number in Step 4; `Diff read` and `Undeclared behavior changes` in the review-mode Scope Check (plan-completion section); measured 140,497 / 108,523 = 1.2946 (2026-10-10). + harness lane E (2026-10-26): the lead-judgment stage in the finding merge and the `cause` anchor in the specialist prompts, shared with /ship through scripts/resolvers/review-army.ts; measured 141,949 / 108,523 = 1.3080.
maxSizeRatio: 1.2849, // GST-1088 lane C diet (same cuts as maxSkeletonBytes, plus the blocker-proof rule and the two dispatch markers): lowered from 1.3180 to measured; measured 139,441 / 108,523 = 1.2849 (2026-11-03). Before: + GSTA-167 variant G part 1 (plan approved by Garry 2026-10-30, autoplan-20261030161754 "Do it", which names about +0.3k prompt tokens per voice): the native adversarial and outside-voice prompts carry the blocking floor and the Severity and Blocks lines, +537 bytes; measured 1.3180 (2026-10-30). Before: 1.3136. GSTA-96 hoist merged with GSTA-92 phase 2 (2026-10-26): both owner-approved raises together, measured 142,550 / 108,523 = 1.3135. Before: Shared-code feature + critical-severity validation: 128,042 union bytes / 108,523 baseline = 1.1799; preserves content floors. + v1.91.12.0 merge of #2999 (above, plus plan-completion fallback intent and specialist checklist-by-path) with #3002; measured 1.1843 (2026-10-01). + v1.91.19.0 wave: verdict-form Codex callers with sandbox preflight (INV-1/B1), P0/P1 structured gate (B1b), bound-plan discovery and data-not-instructions guard (F1/D3), visible LEARNINGS failures (B5); measured 129,941 / 108,523 = 1.1974 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 130,025 / 108,523 = 1.1981 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051); measured 130,359 / 108,523 = 1.2012 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 131,314 / 108,523 = 1.2100 (2026-10-06). Combined Oct 6 wave head (A and B): measured 131,966 / 108,523 = 1.2160 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 132,199 / 108,523 = 1.2182 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 132,258 / 108,523 = 1.2187 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 132,233 / 108,523 = 1.2185 (2026-10-07). Merged with the guard area: measured 132,522 / 108,523 = 1.2211 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 133,902 / 108,523 = 1.2339 (2026-10-08). + gate integrity W2 (above); measured 1.282 (2026-10-10). + Oct 10 multi-agent wave PR D2 (sideways review): sideways sweep and flake-fix number in Step 4; `Diff read` and `Undeclared behavior changes` in the review-mode Scope Check (plan-completion section); measured 140,497 / 108,523 = 1.2946 (2026-10-10). + harness lane E (2026-10-26): the lead-judgment stage in the finding merge and the `cause` anchor in the specialist prompts, shared with /ship through scripts/resolvers/review-army.ts; measured 141,949 / 108,523 = 1.3080.
},
codex: {
skill: 'codex',
@@ -895,7 +895,7 @@ do not launch the downstream skill or open a browser.`,
},
behavioral: 'prompt',
maxSkeletonBytes: 30_210, // GSTA-96 hoist (2026-10-27): lowered from 52_000 by the shared preamble that moved to sections/preamble.md, keeping main's headroom of 377 bytes; measured 29,833 on the v1.91.273.0 merge. Before: D3 carve: Arguments table, Section index, Phase 1 call-site and update-index additions, five STOP pointers; measured 51,532 (2026-10-10). + pstack item 2(b)/8 (Garry 2026-10-26 "do it"): the blast-radius STOP pointer and index row plus the shared-premise red flag; measured 51,623 (2026-10-26), inside the existing cap.
minUnionBytes: 55_000, // D3 carve: skeleton + five recipes; measured union 61,513 (2026-10-10)
minUnionBytes: 53_779, // GST-1088 lane C: lowered from 55,000 to measured for the R56 cuts in modules/cuts.json (learnings search, capture learnings, investigation log), which test/cut-ledger.test.ts keeps out; measured union 53,779 (2026-11-03). Before: D3 carve: skeleton + five recipes; measured union 61,513 (2026-10-10)
mustContain: ['root cause', 'hypothes', 'Flake evidence', 'still_open', 'already_fixed', 'P(fail | regression)', 'the one fact it is safe because of'],
maxSizeRatio: 1.12, // the monolith's cap (parity-harness MONOLITH_INVARIANTS before the carve); measured 61,513 / 60,600 = 1.0151 (2026-10-10). + the blast-radius section (2.8 KB); measured 66,167 / 60,600 = 1.0919 (2026-10-26).
},
+11 -3
View File
@@ -11,14 +11,18 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { runGeneration } from '../scripts/gen-skill-docs';
import { loadCutLedger } from '../scripts/cut-ledger';
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-learnings-callers-'));
const out = path.join(tmp, 'render');
const BUN_MISSING = 'gstack-learnings-search: bun not found on PATH, so learnings could not be read. Fix: install Bun (https://bun.sh), then re-run ./setup.';
const CALLERS: Array<{ name: string; file: (host: string) => string; hosts: string[] }> = [
{ name: 'Prior Learnings (review)', file: h => (h === 'claude' ? 'review/SKILL.md' : '.agents/skills/gstack-review/SKILL.md'), hosts: ['claude', 'codex'] },
{ name: 'investigate prior learnings', file: h => (h === 'claude' ? 'investigate/SKILL.md' : '.agents/skills/gstack-investigate/SKILL.md'), hosts: ['claude', 'codex'] },
/** modules/cuts.json ids; a caller whose `cut` is listed must render no search fence at all. */
const CUT = new Set(loadCutLedger().cuts.map(c => c.id));
const CALLERS: Array<{ name: string; file: (host: string) => string; hosts: string[]; cut?: string }> = [
{ name: 'Prior Learnings (review)', file: h => (h === 'claude' ? 'review/SKILL.md' : '.agents/skills/gstack-review/SKILL.md'), hosts: ['claude', 'codex'], cut: 'part:review/learnings-search' },
{ name: 'investigate prior learnings', file: h => (h === 'claude' ? 'investigate/SKILL.md' : '.agents/skills/gstack-investigate/SKILL.md'), hosts: ['claude', 'codex'], cut: 'part:investigate/learnings-search' },
{ name: 'qa fix-loop search', file: h => (h === 'claude' ? 'qa/SKILL.md' : '.agents/skills/gstack-qa/SKILL.md'), hosts: ['claude', 'codex'] },
{ name: 'ship adversarial search', file: h => (h === 'claude' ? 'ship/sections/adversarial.md' : '.agents/skills/gstack-ship/sections/adversarial.md'), hosts: ['claude', 'codex'] },
{ name: 'review army specialist search', file: () => 'review/sections/review-army.md', hosts: ['claude'] },
@@ -50,6 +54,10 @@ describe('B5: learnings callers report a failed search', () => {
for (const host of caller.hosts) {
test(`${host}: ${caller.name}`, () => {
const fences = searchFences(caller.file(host));
if (caller.cut && CUT.has(caller.cut)) {
expect(fences).toEqual([]);
return;
}
expect(fences.length).toBeGreaterThan(0);
for (const fence of fences) {
const missing = run(fence, host, `echo "${BUN_MISSING}" >&2\necho "second line" >&2\nexit 127`);
+7
View File
@@ -3,6 +3,7 @@ import { execSync, ExecSyncOptionsWithStringEncoding } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { loadCutLedger } from '../scripts/cut-ledger';
const ROOT = path.resolve(import.meta.dir, '..');
const BIN = path.join(ROOT, 'bin');
@@ -137,8 +138,14 @@ describe('gstack-learnings-log', () => {
// Caller contract: investigate/SKILL.md.tmpl must emit type:"investigation"
// verbatim. Guards against the template drifting to an invalid type and
// silently breaking the log path. See codex review finding for #1423.
// While modules/cuts.json cuts part:investigate/investigation-log (GST-1088), the template
// must carry no learnings log at all; restoring the cut restores the contract.
test('investigate template emits type:"investigation" verbatim (caller contract)', () => {
const tmpl = fs.readFileSync(path.join(ROOT, 'investigate/SKILL.md.tmpl'), 'utf-8');
if (loadCutLedger().cuts.some(c => c.id === 'part:investigate/investigation-log')) {
expect(tmpl).not.toContain('gstack-learnings-log');
return;
}
// The invocation line must include "type":"investigation" exactly.
expect(tmpl).toContain('"type":"investigation"');
});
+3 -3
View File
@@ -186,8 +186,8 @@ describe('safe classification (A1-2)', () => {
describe('derived dependencies of this checkout', () => {
test('a bin script a skill template invokes selects that skill\'s cases', () => {
const consumers = real.e2e.get('bin/gstack-next-version') ?? new Set();
expect(consumers.has('review-sql-injection')).toBe(true);
expect(real.trace('review-sql-injection', 'bin/gstack-next-version')?.[0]).toBe('review/SKILL.md.tmpl');
expect(consumers.has('ship-docsync-missing-marker')).toBe(true);
expect(real.trace('ship-docsync-missing-marker', 'bin/gstack-next-version')?.[0]).toBe('ship/SKILL.md.tmpl');
});
test('a compiled design source reaches exactly the cases whose closure names the design binary', () => {
@@ -301,7 +301,7 @@ describe('deleted files (paths absent from the head tree)', () => {
expect(result.mode).toBe('dependents');
expect(result.e2e).toContain('review-sql-injection');
expect(result.derivedFiles[0]?.file).toBe('bin/gstack-next-version');
expect(real.referencers('bin/gstack-next-version')).toContain('review/SKILL.md.tmpl');
expect(real.referencers('bin/gstack-next-version')).toContain('ship/SKILL.md.tmpl');
});
test('a deleted file a full-gate input still references, or a deleted full-gate entry, restores the full gate', () => {
+6 -6
View File
@@ -109,7 +109,7 @@ test('review emits scope check after the plan audit and before the checklist', (
test('review composes confidence-tagged findings into one final report with explicit incomplete coverage', () => {
const flat = skill.replace(/\s+/g, ' ');
expect(flat).toContain('CRITICAL/INFORMATIONAL');
const report = flat.slice(flat.indexOf('### Report the final review'), flat.indexOf('{{LEARNINGS_LOG}}'));
const report = flat.slice(flat.indexOf('### Report the final review'), flat.indexOf('## Important Rules'));
expect(report).toMatch(/one final report/i);
expect(report).toMatch(/unresolved non-advisory defects/i);
expect(report).toMatch(/[Ss]tate INCOMPLETE if `COMPLETED` is false/);
@@ -167,7 +167,7 @@ test('caller QA runs charter and setup after resource loading and has a severity
test('review prepares context and deduplicates before classifying findings', () => {
const positions = [
'## Step 3.5: Diff scans', '## Step 3.6: Gather review context',
'{{LEARNINGS_SEARCH}}', '{{ASIDE_RESEARCH}}', '## Step 4: Critical pass',
'{{ASIDE_RESEARCH}}', '## Step 4: Critical pass',
'## Step 5: Fix-First Review', '{{CROSS_REVIEW_DEDUP}}',
'**Keep decisions through fix cycles:**', '### Step 5a: Classify each finding',
].map(marker => skill.indexOf(marker));
@@ -237,7 +237,7 @@ test('review section index follows the actual pre-fix execution order', () => {
});
test('review finalization ownership: initialize invocation state and capture the core token before reading', () => {
const start = skill.slice(skill.indexOf('## Step 3: Get the diff'), skill.indexOf('## Step 3.4:'));
const start = skill.slice(skill.indexOf('## Step 3: Get the diff'), skill.indexOf('## Step 3.5:'));
expect(start).toContain('CYCLES=0');
expect(skill.match(/CYCLES=0/g)).toHaveLength(1);
expect(start).toContain('gstack-review-log --start review\ngit diff "$DIFF_BASE"');
@@ -350,7 +350,7 @@ test('review scope notes remain provisional until the plan section emits the onl
// order, neither pins where the check is emitted; seam=none; no_claim=the judged clarity of the sentences.
test('the final Scope Check is emitted once, in Step 3 after the full diff read, and every owner names that point', () => {
const ctx: TemplateContext = { skillName: 'review', tmplPath: 'review/SKILL.md.tmpl', host: 'claude', paths: HOST_PATHS.claude };
const step3 = between(skill, '## Step 3: Get the diff', '## Step 3.4:');
const step3 = between(skill, '## Step 3: Get the diff', '## Step 3.5:');
expectOrdered(step3, ['git diff "$DIFF_BASE"', "5. First pass only: emit Step 1.5's final Scope Check; `Diff read` records this read."], 'step3');
expect(skill.split("emit Step 1.5's final Scope Check")).toHaveLength(2);
const audit = generatePlanCompletionAuditReview(ctx);
@@ -398,7 +398,7 @@ test('review confidence uses its severity labels without an undefined P0 excepti
});
test('review names the lifecycle and record owners before using their persistence rules', () => {
const start = skill.slice(skill.indexOf('## Step 3: Get the diff'), skill.indexOf('## Step 3.4:')).replace(/\s+/g, ' ');
const start = skill.slice(skill.indexOf('## Step 3: Get the diff'), skill.indexOf('## Step 3.5:')).replace(/\s+/g, ' ');
expect(start).toContain('REVIEW_START / PASS_START |');
expect(start).toContain('`review_binding` |');
expect(start).toContain('`snapshot_covered_paths` |');
@@ -673,7 +673,7 @@ test('review persists gate identity fields, counts open findings and reports one
['`open`', 'count', '`issues_found`'],
['logger', 'stamps', '`actor`'],
], 'review record');
const report = compact(between(skill, '### Report the final review', '{{LEARNINGS_LOG}}'));
const report = compact(between(skill, '### Report the final review', '## Important Rules'));
expectOrdered(report, ['`Gate edits: none detected (', '`Gate edits: N listed, M read, K findings —', '`Gate edits: partial (', '`Gate edits: UNAVAILABLE —'], 'final report');
expectMentions(report, [['unavailable', 'never', 'none detected']], 'final report');
});
+1 -1
View File
@@ -24,7 +24,7 @@ function fixture(copy: boolean) {
const shims = path.join(temp, 'shims');
// Real setup, generator, resolver and migration code; only dependency
// installation and binary compilation are stubbed to keep this test free.
for (const dir of ['scripts', 'hosts', 'lib', 'model-overlays', 'browse/src', 'design/src', 'openclaw/templates', 'qa']) {
for (const dir of ['scripts', 'hosts', 'lib', 'model-overlays', 'modules', 'browse/src', 'design/src', 'openclaw/templates', 'qa']) {
fs.cpSync(path.join(ROOT, dir), path.join(root, dir), { recursive: true });
}
fs.copyFileSync(path.join(ROOT, 'setup'), path.join(root, 'setup'));