mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 08:54:04 +01:00
feat(ship): batched specialists per shard, audit skip at zero code paths, gate-scan summary for the parent (GSTA-167)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
1a558ded87
commit
bde74d6f33
17 files changed
+334
-101
No files matched your search
+8
-8
@@ -6,29 +6,29 @@
|
||||
"agy/gstack-plan-design-review": 110044,
|
||||
"agy/gstack-plan-devex-review": 116496,
|
||||
"agy/gstack-plan-eng-review": 144907,
|
||||
"agy/gstack-review": 103743,
|
||||
"agy/gstack-review": 103737,
|
||||
"codex/gstack-autoplan": 108553,
|
||||
"codex/gstack-land-and-deploy": 100284,
|
||||
"codex/gstack-plan-design-review": 111116,
|
||||
"codex/gstack-plan-devex-review": 118004,
|
||||
"codex/gstack-plan-eng-review": 146072,
|
||||
"codex/gstack-review": 103266,
|
||||
"codex/gstack-review": 103260,
|
||||
"copilot/gstack-autoplan": 109253,
|
||||
"copilot/gstack-plan-design-review": 110355,
|
||||
"copilot/gstack-plan-devex-review": 115958,
|
||||
"copilot/gstack-plan-eng-review": 145072,
|
||||
"copilot/gstack-review": 103324,
|
||||
"copilot/gstack-review": 103318,
|
||||
"cursor/gstack-autoplan": 108485,
|
||||
"cursor/gstack-plan-design-review": 108900,
|
||||
"cursor/gstack-plan-devex-review": 115228,
|
||||
"cursor/gstack-plan-eng-review": 143662,
|
||||
"cursor/gstack-review": 121614,
|
||||
"cursor/gstack-review": 121608,
|
||||
"factory/gstack-autoplan": 110763,
|
||||
"factory/gstack-land-and-deploy": 100105,
|
||||
"factory/gstack-plan-design-review": 111675,
|
||||
"factory/gstack-plan-devex-review": 118591,
|
||||
"factory/gstack-plan-eng-review": 146770,
|
||||
"factory/gstack-review": 125387,
|
||||
"factory/gstack-review": 125381,
|
||||
"gbrain/gstack-autoplan": 108892,
|
||||
"gbrain/gstack-plan-eng-review": 126289,
|
||||
"hermes/gstack-autoplan": 108891,
|
||||
@@ -37,18 +37,18 @@
|
||||
"kiro/gstack-plan-design-review": 108752,
|
||||
"kiro/gstack-plan-devex-review": 115080,
|
||||
"kiro/gstack-plan-eng-review": 143524,
|
||||
"kiro/gstack-review": 121438,
|
||||
"kiro/gstack-review": 121432,
|
||||
"openclaw/gstack-autoplan": 108715,
|
||||
"openclaw/gstack-plan-eng-review": 124804,
|
||||
"opencode/gstack-autoplan": 109234,
|
||||
"opencode/gstack-plan-design-review": 110187,
|
||||
"opencode/gstack-plan-devex-review": 115836,
|
||||
"opencode/gstack-plan-eng-review": 144931,
|
||||
"opencode/gstack-review": 122236,
|
||||
"opencode/gstack-review": 122230,
|
||||
"slate/gstack-autoplan": 108372,
|
||||
"slate/gstack-plan-design-review": 108826,
|
||||
"slate/gstack-plan-devex-review": 115154,
|
||||
"slate/gstack-plan-eng-review": 143593,
|
||||
"slate/gstack-review": 121526
|
||||
"slate/gstack-review": 121520
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* The shared gate-scan block, run: the parent gets the GATE_SUMMARY line and one index
|
||||
* line per read-level hunk, never the hunk text (the reviewer packets and the diff carry
|
||||
* that; the artifact holds the full listing). The fixture repository lives under the OS
|
||||
* temp directory, outside this checkout, and every seed commit is asserted.
|
||||
*/
|
||||
import { afterEach, 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 { generateGateScanBlock } from '../scripts/resolvers/gate-scan';
|
||||
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
|
||||
import { between } from './helpers/prompt-structure';
|
||||
|
||||
const ROOT = path.join(import.meta.dir, '..');
|
||||
const dirs: string[] = [];
|
||||
afterEach(() => { while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true }); });
|
||||
const tmp = (p: string) => { const d = fs.mkdtempSync(path.join(os.tmpdir(), p)); dirs.push(d); return d; };
|
||||
|
||||
test('the block prints the summary and the hunk index; the hunk text stays out of the parent', () => {
|
||||
const dir = tmp('gate-scan-block-');
|
||||
expect(path.relative(ROOT, dir).startsWith('..')).toBe(true);
|
||||
const git = (...a: string[]) => spawnSync('git', a, { cwd: dir, encoding: 'utf8', timeout: 10_000 });
|
||||
const write = (text: string) => { fs.mkdirSync(path.join(dir, 'test'), { recursive: true }); fs.writeFileSync(path.join(dir, 'test', 'a.test.ts'), text); };
|
||||
git('init', '-q', '-b', 'main'); git('config', 'user.email', 'fixture@example.com'); git('config', 'user.name', 'Fixture'); git('config', 'commit.gpgsign', 'false');
|
||||
write("test('a', () => {\n expect(HUNK_BODY_MARKER).toBe(1);\n expect(2).toBe(2);\n});\n");
|
||||
git('add', '.');
|
||||
expect(git('commit', '-qm', 'base').status).toBe(0);
|
||||
git('checkout', '-qb', 'feat');
|
||||
write("test.skip('a', () => {\n expect(2).toBe(2);\n});\n");
|
||||
git('add', '.');
|
||||
expect(git('commit', '-qm', 'feat').status).toBe(0);
|
||||
|
||||
const ctx: TemplateContext = { skillName: 'ship', tmplPath: 'ship/SKILL.md.tmpl', host: 'claude', paths: HOST_PATHS.claude };
|
||||
const bash = between(generateGateScanBlock(ctx), '```bash', '\n```').slice('```bash'.length).replaceAll(HOST_PATHS.claude.binDir, path.join(ROOT, 'bin')).replaceAll('<base>', 'main');
|
||||
const r = spawnSync('bash', ['-ec', bash], { cwd: dir, encoding: 'utf8', timeout: 60_000, env: { ...process.env, GSTACK_STATE_ROOT: tmp('gate-scan-state-') } });
|
||||
expect(r.status).toBe(0);
|
||||
const out = r.stdout.trimEnd().split('\n');
|
||||
expect(out[0]).toMatch(/^GATE_SUMMARY: listed=\d+ eligible=[1-9]/);
|
||||
expect(out.slice(1).length).toBeGreaterThan(0);
|
||||
for (const l of out.slice(1)) expect(l).toMatch(/^\[[^\]]+\] \S+ test\/a\.test\.ts @@/);
|
||||
expect(r.stdout).not.toContain('HUNK_BODY_MARKER');
|
||||
expect(r.stdout).not.toContain('citation:');
|
||||
const artifact = /artifact=(\S+)/.exec(out[0]!)![1]!;
|
||||
expect(fs.readFileSync(artifact, 'utf8')).toContain('HUNK_BODY_MARKER');
|
||||
});
|
||||
+109
-5
@@ -9,7 +9,7 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import * as P from '../lib/review-prompts';
|
||||
import { shardChunks } from '../lib/review-packet';
|
||||
import { shardChunks, SPECIALIST_PACKING } from '../lib/review-packet';
|
||||
|
||||
const ROOT = path.join(import.meta.dir, '..');
|
||||
const BIN = path.join(ROOT, 'bin', 'gstack-review-packet');
|
||||
@@ -43,7 +43,7 @@ const checklist = (name: string) => fs.readFileSync(path.join(ROOT, 'review', 's
|
||||
describe('gstack-review-packet build', () => {
|
||||
test('a packet holds its instructions, contract, checklist, the diff and read budget, and no other repository content', () => {
|
||||
const r = repo({ 'src/unrelated.ts': 'export const UNRELATED_MARKER = 1;\n' }, { 'src/a.ts': lines(80, 'alpha') });
|
||||
const b = build(r);
|
||||
const b = build(r, ['--fanout', 'parallel']);
|
||||
expect(b.status).toBe(0);
|
||||
const all = packets(b.stdout);
|
||||
const testing = all.find(p => p.name === 'testing')!;
|
||||
@@ -61,7 +61,7 @@ describe('gstack-review-packet build', () => {
|
||||
|
||||
test('the defensive framing reaches the security and adversarial packets with fixtures as a stat; testing reads fixtures in full', () => {
|
||||
const r = repo({}, { 'src/auth/login.ts': lines(120, 'auth'), 'test/login.test.ts': 'PAYLOAD_MARKER\n' });
|
||||
const all = packets(build(r, ['--force', 'security']).stdout);
|
||||
const all = packets(build(r, ['--force', 'security', '--fanout', 'parallel']).stdout);
|
||||
for (const name of ['security', 'adversarial']) {
|
||||
const p = all.find(x => x.name === name)!;
|
||||
expect(p.text).toContain(P.DEFENSIVE_REVIEW_FRAMING);
|
||||
@@ -89,7 +89,7 @@ describe('gstack-review-packet build', () => {
|
||||
|
||||
test('exclusions are honored: maintainability leaves out docs and tests; testing keeps tests; untracked files are in', () => {
|
||||
const r = repo({}, { 'src/a.ts': lines(80, 'gamma'), 'docs/guide.md': '# Guide\nDOCS_MARKER\n', 'test/a.test.ts': 'TEST_MARKER\n' }, { 'src/new.ts': 'UNTRACKED_MARKER\n' });
|
||||
const all = packets(build(r).stdout);
|
||||
const all = packets(build(r, ['--fanout', 'parallel']).stdout);
|
||||
const m = all.find(p => p.name === 'maintainability')!;
|
||||
const t = all.find(p => p.name === 'testing')!;
|
||||
expect(m.text).not.toContain('+DOCS_MARKER');
|
||||
@@ -128,7 +128,7 @@ describe('gstack-review-packet build', () => {
|
||||
|
||||
test('the Red Team is deferred until collect; with --findings it reads them, the framing and fixtures in summary mode', () => {
|
||||
const r = repo({}, { 'src/a.ts': lines(260, 'delta'), 'test/fixture.test.ts': 'PAYLOAD_MARKER\n' });
|
||||
expect(build(r).stdout).toContain('PACKET_DEFERRED: red-team');
|
||||
expect(build(r, ['--fanout', 'parallel']).stdout).toContain('PACKET_DEFERRED: red-team');
|
||||
const merged = path.join(r.out, 'merged.jsonl');
|
||||
fs.writeFileSync(merged, JSON.stringify({ severity: 'CRITICAL', path: 'src/a.ts', line: 3, summary: 'KNOWN_FINDING', sources: ['testing'] }) + '\n');
|
||||
const red = packets(build(r, ['--only', 'red-team', '--findings', merged]).stdout)[0]!;
|
||||
@@ -166,6 +166,81 @@ describe('gstack-review-packet build', () => {
|
||||
expect(a.text).toContain('+export const zeta19 = 19;');
|
||||
expect(build(r, ['--audit', 'plan-completion']).status).toBe(2);
|
||||
});
|
||||
|
||||
const planDispatches = (r: Repo, extra: string[] = []) => {
|
||||
const plan = spawnSync(path.join(ROOT, 'bin', 'gstack-review-plan'), ['--base', 'main', '--gate-tagged', '0', ...extra], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env: { ...process.env, GSTACK_STATE_ROOT: r.state } });
|
||||
return [...plan.stdout.matchAll(/^DISPATCH: (\S+) (\S+)/gm)].map(m => ({ role: m[1]!, name: m[2]! }));
|
||||
};
|
||||
|
||||
test('standard depth: the pass\'s specialist lines are one batched packet inlining every selected checklist; red-team and adversarial stay separate', () => {
|
||||
const r = repo({}, { 'src/a.ts': lines(260, 'kappa'), 'test/a.test.ts': 'TEST_MARKER\n', 'docs/guide.md': '# Guide\nDOCS_MARKER\n' });
|
||||
const selected = planDispatches(r).filter(d => d.role === 'specialist').map(d => d.name);
|
||||
expect(selected.length).toBeGreaterThan(1);
|
||||
const b = build(r);
|
||||
expect(b.status).toBe(0);
|
||||
const all = packets(b.stdout);
|
||||
const specialists = all.filter(p => p.role === 'specialist');
|
||||
expect(specialists.map(p => p.name)).toEqual(['batched']);
|
||||
const batched = specialists[0]!;
|
||||
for (const c of selected) { expect(batched.text).toContain(`## Checklist: ${c}`); expect(batched.text).toContain(checklist(c)); }
|
||||
expect(batched.text).not.toContain('## Checklist: red-team');
|
||||
expect(batched.text).toContain('Tag each finding with the checklist that found it');
|
||||
expect(batched.text).toContain(`COVERED: ${selected.join(',')}`);
|
||||
expect(batched.text).toContain('+TEST_MARKER');
|
||||
expect(batched.text).not.toContain('+DOCS_MARKER');
|
||||
expect(batched.text).toContain('the parent runs `gstack-state-matrix check` on it');
|
||||
expect(b.stdout).toContain(`PACKED: specialist batched checklists=${selected.join(',')} shards=1`);
|
||||
expect(b.stdout).toContain('PACKET_DEFERRED: red-team');
|
||||
expect(all.some(p => p.role === 'adversarial-native')).toBe(true);
|
||||
const parallel = packets(build(r, ['--fanout', 'parallel']).stdout).filter(p => p.role === 'specialist');
|
||||
expect(parallel.map(p => p.name)).toEqual(selected);
|
||||
expect(build(r, ['--fanout', 'serial']).status).toBe(2);
|
||||
});
|
||||
|
||||
test('the default packing is the one switch SPECIALIST_PACKING names', () => {
|
||||
expect(SPECIALIST_PACKING).toBe('batched');
|
||||
});
|
||||
|
||||
test('full depth: every specialist batched, the diff unexcluded; the Red Team counts the batched checklists', () => {
|
||||
const r = repo({}, { 'src/a.ts': lines(260, 'lambda'), 'docs/guide.md': '# Guide\nDOCS_MARKER\n' });
|
||||
const selected = planDispatches(r, ['--depth-floor', 'full']).filter(d => d.role === 'specialist').map(d => d.name);
|
||||
const b = build(r, ['--depth-floor', 'full']);
|
||||
const specialists = packets(b.stdout).filter(p => p.role === 'specialist');
|
||||
expect(specialists.map(p => p.name)).toEqual(['batched']);
|
||||
expect(specialists[0]!.text).toContain('+DOCS_MARKER');
|
||||
const merged = path.join(r.out, 'merged.jsonl');
|
||||
fs.writeFileSync(merged, '');
|
||||
const red = packets(build(r, ['--depth-floor', 'full', '--only', 'red-team', '--findings', merged]).stdout)[0]!;
|
||||
expect(red.text).toContain(`reviewed by ${selected.length} specialists`);
|
||||
});
|
||||
|
||||
test('a batched diff past the cap is one batched packet per shard, each with every checklist', () => {
|
||||
const files: Record<string, string> = {};
|
||||
for (let i = 0; i < 30; i++) files[`src/f${i}.ts`] = lines(400, `w${i}_`);
|
||||
const r = repo({}, files);
|
||||
const shards = packets(build(r, ['--only', 'specialist']).stdout);
|
||||
expect(shards.length).toBeGreaterThan(1);
|
||||
expect(shards.map(s => s.name)).toEqual(shards.map((_, i) => `batched.${i + 1}of${shards.length}`));
|
||||
for (const s of shards) { expect(s.bytes).toBeLessThanOrEqual(200_000); expect(s.text).toContain(checklist('testing')); expect(s.text).toContain(checklist('maintainability')); }
|
||||
});
|
||||
|
||||
test('Step 7: a diff with no code path (docs and data only) builds no audit packet and says why', () => {
|
||||
const r = repo({}, { 'docs/guide.md': '# Guide\n', 'data/scores.json': '{"a":1}\n' });
|
||||
const b = build(r, ['--audit', 'test-coverage']);
|
||||
expect(b.status).toBe(0);
|
||||
expect(packets(b.stdout)).toEqual([]);
|
||||
expect(b.stdout).toMatch(/^AUDIT_SKIPPED: test-coverage code_paths=0 \(.*docs\/guide\.md.*\)/m);
|
||||
expect(fs.existsSync(path.join(r.out, 'test-coverage.packet.md'))).toBe(false);
|
||||
const code = repo({}, { 'src/a.ts': lines(5, 'mu'), 'data/scores.json': '{"a":1}\n' });
|
||||
expect(packets(build(code, ['--audit', 'test-coverage']).stdout).length).toBe(1);
|
||||
});
|
||||
|
||||
test('the coverage audit packet is read-only: no edits, no test runs, three calls, whatever generation the parent has', () => {
|
||||
const r = repo({}, { 'src/a.ts': lines(20, 'nu') });
|
||||
const a = packets(build(r, ['--audit', 'test-coverage', '--generation', 'allowed', '--passes-used', '0']).stdout)[0]!;
|
||||
expect(a.text).toContain('Generation: audit-only; passes used: 0 of 2.');
|
||||
expect(a.text).toContain(P.AUDIT_READ_ONLY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the packet prose and the generated SKILL.md prose are one text', () => {
|
||||
@@ -180,6 +255,17 @@ describe('the packet prose and the generated SKILL.md prose are one text', () =>
|
||||
expect(read('ship/sections/review-army.md')).toContain(P.packetBrief('<file>', '<out>'));
|
||||
expect(read('ship/sections/adversarial.md')).toContain(P.packetBrief('<file>', '<out>'));
|
||||
});
|
||||
test('/ship Step 9 dispatches the batched packet at every depth and logs its checklists; Step 7 skips a no-code audit and writes tests itself', () => {
|
||||
const army = read('ship/sections/review-army.md');
|
||||
expect(army).toContain('--fanout parallel');
|
||||
expect(army).toContain('"checklists":[');
|
||||
expect(army).toContain('PACKED:');
|
||||
expect(army).toMatch(/checklists include state-machines/);
|
||||
const coverage = read('ship/sections/test-coverage.md');
|
||||
expect(coverage).toContain('AUDIT_SKIPPED:');
|
||||
expect(coverage).not.toContain('--generation <allowed|audit-only>');
|
||||
expect(coverage).toMatch(/the parent writes and runs the tests/);
|
||||
});
|
||||
test('the outside voice boundary the safety-rule registry pins in the resolver is the packet\'s boundary', () => {
|
||||
const line = read('scripts/resolvers/outside-voice-steps.ts').split('\n').find(l => l.startsWith('const CODEX_BOUNDARY = '))!;
|
||||
expect(line).toBe(`const CODEX_BOUNDARY = '${P.CODEX_BOUNDARY_TEXT}\\\\n\\\\n';`);
|
||||
@@ -235,6 +321,24 @@ describe('gstack-review-packet collect', () => {
|
||||
expect(prose.stdout).toContain('REVIEWER: security status=invalid');
|
||||
});
|
||||
|
||||
test('a batched packet must end with COVERED naming every checklist it inlined; a missing one is incomplete coverage', () => {
|
||||
const d = (out: string) => {
|
||||
const x = dir([{ role: 'specialist', name: 'batched' }], { batched: out });
|
||||
const m = JSON.parse(fs.readFileSync(path.join(x, 'manifest.json'), 'utf8'));
|
||||
m.packets[0].checklists = ['testing', 'security'];
|
||||
fs.writeFileSync(path.join(x, 'manifest.json'), JSON.stringify(m));
|
||||
return x;
|
||||
};
|
||||
const ok = collect(d('NO FINDINGS\nCOVERED: testing, security'));
|
||||
expect(ok.status).toBe(0);
|
||||
expect(ok.stdout).toContain('REVIEWER: batched status=clean crit=0 info=0 covered=testing,security');
|
||||
const partial = collect(d(finding({ specialist: 'testing' }) + '\nCOVERED: testing'));
|
||||
expect(partial.status).toBe(3);
|
||||
expect(partial.stdout).toContain('REVIEWER: batched status=findings crit=0 info=1 covered=testing uncovered=security');
|
||||
expect(partial.stdout).toMatch(/batched=uncovered:security.*\(REVIEW_COVERAGE_INCOMPLETE\)$/m);
|
||||
expect(collect(d('NO FINDINGS')).status).toBe(3);
|
||||
});
|
||||
|
||||
test('INFORMATIONAL findings are counts in the record and lines in informational.md', () => {
|
||||
const d = dir(specialists, { testing: finding({ summary: 'INFO_ONLY_MARKER', fix: 'do it' }), security: 'NO FINDINGS' });
|
||||
const r = collect(d);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"bytes": -3324,
|
||||
"why": "GSTA-167 reviewer packets: Step 9.1 dispatches each reviewer by one packet file (gstack-review-packet build/collect) in place of the inlined specialist prompt; Red Team, fix-check, Step 11 and the Step 7/8 audits read packets too"
|
||||
"bytes": -2687,
|
||||
"why": "GSTA-167 reviewer packets: Step 9.1 dispatches each reviewer by one packet file (gstack-review-packet build/collect) in place of the inlined specialist prompt; Red Team, fix-check, Step 11 and the Step 7/8 audits read packets too. PR 3b: a pass's specialists are one batched packet per shard with a COVERED line, Step 7 skips the audit on a diff with no code path and its packet is read-only, the gate scan prints the summary and hunk index only"
|
||||
}
|
||||
Reference in new issue
Block a user