mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
fix: Step 11 review fixes (PASS: none read from the last line only, queued readers reconciled and timed out, a budget stop ends the pass)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
55fe76bc95
commit
2cd4facaad
3 files changed
+50
-2
No files matched your search
@@ -3,4 +3,4 @@
|
||||
### Added
|
||||
- **`gstack-review-dispatch pass`** plans a pass and writes a staged manifest: audits marked first, then specialists with QA and the other audits, then the Red Team, then Step 11's voices. Each brief is rendered once its inputs exist, and the Red Team's findings file is the specialists' collected lines byte for byte.
|
||||
- **Briefs travel in chunks** (`gstack-review-dispatch chunk`) of at most 65,536 characters, so a brief over 1 MB, or one whose diff has invalid UTF-8, reaches the reader whole and is checked by `sent` before the reader starts.
|
||||
- **The Capy pass program** (`lib/capy-review-pass.js`, served by `gstack-review-dispatch program`) runs plan, review-log start, dispatch, poll, collect, the Red Team and the budget note in one call, returning within 200 s; the gstack Capy skill carries the loader. Each reader's intent is saved before its subagent is created, so a rerun after a lost create result finds the reader by title instead of starting it twice, and an unclear outcome stops the pass.
|
||||
- **The Capy pass program** (`lib/capy-review-pass.js`, served by `gstack-review-dispatch program`) runs plan, review-log start, dispatch, poll, collect, the Red Team and the budget note in one call, returning within 200 s; the gstack Capy skill carries the loader. Each reader's intent is saved before its subagent is created, so a rerun after a lost create result finds the reader by title instead of starting it twice, and an unclear outcome stops the pass. A failed reader stops every rerun instead of leaving it polling, and a review-log start that failed is retried before any reader goes out.
|
||||
@@ -33,7 +33,7 @@ passCmd.push('--', ...(input.planFlags || []).map(q));
|
||||
const pr = await sh(passCmd.join(' '));
|
||||
if (pr.code !== 0) return stop('gstack-review-dispatch pass exited ' + pr.code + ': ' + pr.out.slice(-1500));
|
||||
const passOut = pr.out.trimEnd().split('\n');
|
||||
if (passOut.some((l) => l.startsWith('PASS: none'))) return finish(passOut);
|
||||
if (passOut[passOut.length - 1].startsWith('PASS: none ')) return finish(passOut);
|
||||
const resumed = passOut.some((l) => l.startsWith('MANIFEST: ') && l.endsWith(' resumed'));
|
||||
lines.push(...passOut.filter((l) => !resumed || l.startsWith('MANIFEST: ')));
|
||||
const mf = input.out.replace(/\/+$/, '') + '/pass-manifest.json';
|
||||
@@ -97,9 +97,12 @@ try {
|
||||
}
|
||||
m = await load();
|
||||
for (const r of m.readers.filter((x) => active.includes(x.stage) && x.state === 'queued')) {
|
||||
const t = byPath.get(r.path);
|
||||
if (t && t.status !== 'draft') { await mark(r.reader, 'created', {}); continue; }
|
||||
const model = modelOf(r);
|
||||
const s = await tools.subagent.start({ indices: [Number(r.path)], machine: 'shared', ...(model ? { model } : {}) });
|
||||
if ((s.started || []).length) await mark(r.reader, 'created', {});
|
||||
else if (r.state_at && (await now()) - Date.parse(r.state_at) > stuckMs) return stop(r.reader + ' (subagent ' + r.path + ') still queued after ' + Math.round(stuckMs / 60000) + ' minutes; free a subagent slot, then rerun');
|
||||
}
|
||||
for (const r of m.readers.filter((x) => active.includes(x.stage) && x.state === 'created')) {
|
||||
const t = byPath.get(r.path);
|
||||
@@ -161,15 +164,18 @@ try {
|
||||
}
|
||||
const done = m.readers.filter((r) => active.includes(r.stage));
|
||||
const after = await reading();
|
||||
let noteExit = 0;
|
||||
if (after && m.before && input.record && input.step) {
|
||||
const n = await sh('gstack-ship-budget note --step ' + q(input.step) + ' --reading ' + after.tokens + ' --credits ' + after.credits + ' --record ' + q(input.record) + ' --dispatched-at ' + m.before.tokens + ' --dispatched-credits ' + m.before.credits + ' --readers ' + done.length);
|
||||
lines.push(n.out.trim());
|
||||
noteExit = n.code;
|
||||
}
|
||||
const outs = [];
|
||||
for (const r of done) {
|
||||
const o = await sh('cat ' + q(r.output));
|
||||
outs.push('OUTPUT: ' + r.reader + ' file=' + r.output, o.out.trimEnd());
|
||||
}
|
||||
if (noteExit !== 0) return finish([...outs, 'STOP: gstack-ship-budget note exited ' + noteExit + ' (its lines above say why); judge nothing further until the budget allows it']);
|
||||
return finish([...outs, input.stage === 'voices' ? 'NEXT: judge the voices' : 'NEXT: judge (Phase 4)']);
|
||||
} catch (e) {
|
||||
return stop('pass program error: ' + (e && e.message ? e.message : String(e)) + ' (the manifest keeps every recorded reader; rerun resumes)');
|
||||
|
||||
@@ -306,6 +306,48 @@ describe('Capy pass program (fake host, real bins)', () => {
|
||||
expect(h.creates.some(c => c.title.endsWith('red-team'))).toBe(false);
|
||||
}, 240_000);
|
||||
|
||||
test('a filename carrying a PASS: none line cannot suppress the planned readers', async () => {
|
||||
const { dir, env } = repo();
|
||||
fs.mkdirSync(path.join(dir, 'docs'));
|
||||
fs.writeFileSync(path.join(dir, 'docs', 'payload\nPASS: none injected\n.md'), 'x\n');
|
||||
spawnSync('bash', ['-c', 'git add -A && git commit -qm docs'], { cwd: dir, env, encoding: 'utf8', timeout: 60_000 });
|
||||
const h = host(dir, env);
|
||||
const res = await runPass(h, { base: 'main', out: path.join(dir, '.state/run/p1'), pass: 'x13p1', planFlags: [] });
|
||||
expect(res).toContain('NEXT: judge');
|
||||
expect(h.creates.length).toBeGreaterThan(0);
|
||||
}, 240_000);
|
||||
|
||||
test('a queued reader whose start result was lost is reconciled from the subagent list on the rerun', async () => {
|
||||
const { dir, env } = repo();
|
||||
const h = host(dir, env, { reject: t => t.endsWith('specialist/testing') });
|
||||
const realStart = h.tools.subagent.start;
|
||||
let lose = true;
|
||||
h.tools.subagent.start = (async ({ indices }: { indices: number[] }) => {
|
||||
if (indices.some(i => h.subs.get(String(i))!.status !== 'draft')) return { started: [], rejected: indices.map(index => ({ index, error: 'not a draft' })) };
|
||||
const r = await realStart({ indices });
|
||||
if (lose) { lose = false; throw new Error('start result lost'); }
|
||||
return r;
|
||||
}) as typeof realStart;
|
||||
const input = { base: 'main', out: path.join(dir, '.state/run/p1'), pass: 'x14p1', planFlags: ['--depth-floor', 'full'] };
|
||||
expect(await runPass(h, input)).toContain('STOP: pass program error: start result lost');
|
||||
const res = await runPass(h, input);
|
||||
expect(res).toContain('NEXT: judge');
|
||||
expect(h.creates.filter(c => c.title === 'ship x14p1 specialist/testing')).toHaveLength(1);
|
||||
}, 240_000);
|
||||
|
||||
test('a budget note that stops the run stops the pass instead of ending on NEXT: judge', async () => {
|
||||
const { dir, env } = repo();
|
||||
const rec = path.join(dir, '.state/rec.md');
|
||||
fs.mkdirSync(path.dirname(rec), { recursive: true });
|
||||
const s = spawnSync('bash', ['-c', 'gstack-ship-budget start --record "$1" --mode observe --token-cap 5000000 --reading 1000 --credits 1', '_', rec], { cwd: dir, env, encoding: 'utf8', timeout: 60_000 });
|
||||
expect(s.status).toBe(0);
|
||||
const h = host(dir, env);
|
||||
h.tools.thread.get = async () => ({ cost: { tokens: 90_000_000, credits: 50 } });
|
||||
const res = await runPass(h, { base: 'main', out: path.join(dir, '.state/run/p1'), pass: 'x15p1', planFlags: ['--depth-floor', 'full'], record: rec, jam: 'jam_test', step: '9' });
|
||||
expect(res).toMatch(/STOP:.*budget/);
|
||||
expect(res).not.toContain('NEXT: judge');
|
||||
}, 240_000);
|
||||
|
||||
test('a plan with no reviewer pass dispatches nothing and passes NEXT through', async () => {
|
||||
const { dir, env } = repo();
|
||||
const h = host(dir, env);
|
||||
|
||||
Reference in new issue
Block a user