mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 06:54:50 +01:00
fix: pre-landing review fixes (pass program reruns a failed review-log start and stops on a failed reader every run)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
f8c835e206
commit
55fe76bc95
4 files changed
+48
-3
No files matched your search
@@ -60,6 +60,7 @@ const HELP = `gstack-review-dispatch — verbatim reviewer briefs and loss-free
|
||||
BRIEF_CHUNK: reader=… index=i of=n sha256=…, then one JSON string line of at most 65,536 brief characters
|
||||
mark --manifest <f> --reader <r> --state <intent|created|queued|collected|failed> [--path <p>] [--output <f>] [--error <e>]
|
||||
mark --manifest <f> --before <tokens>,<credits>
|
||||
mark --manifest <f> --log-start <key> --token <t> records a review-log start token once, so a rerun starts only what is missing
|
||||
reader states move forward only; intent is recorded before the subagent is created, its path right after
|
||||
program [--ledger <f>]
|
||||
PROGRAM: v<n> sha256=… bytes=…, then lib/capy-review-pass.js as one JSON string line (see the gstack Capy skill)
|
||||
@@ -174,6 +175,16 @@ function passStep(cmd: string): number {
|
||||
return EXIT.ok;
|
||||
}
|
||||
if (cmd === 'mark') {
|
||||
const logStart = flag('--log-start');
|
||||
if (logStart) {
|
||||
const token = flag('--token');
|
||||
if (!token) { process.stderr.write('gstack-review-dispatch mark: --log-start <key> --token <t>\n'); return EXIT.usage; }
|
||||
m.log_starts ??= {};
|
||||
m.log_starts[logStart] ??= token;
|
||||
writeManifest(file, m);
|
||||
process.stdout.write(`LOG_START: ${logStart} token=${m.log_starts[logStart]}\n`);
|
||||
return EXIT.ok;
|
||||
}
|
||||
const before = flag('--before');
|
||||
if (before) {
|
||||
const [tokens, credits] = before.split(',').map(Number);
|
||||
|
||||
@@ -74,13 +74,19 @@ const active = input.stage === 'voices' ? ['voices'] : ['first', 'specialists',
|
||||
try {
|
||||
let m = await load();
|
||||
if (input.stage === 'voices' && !m.readers.some((r) => r.stage === 'voices')) return finish(['VOICES: none (the pass plan lists no Step 11 voice)', 'NEXT: judge']);
|
||||
const fresh = input.stage === 'voices' ? m.readers.every((r) => r.stage !== 'voices' || r.state === 'planned') : !resumed;
|
||||
for (const s of fresh ? input.logStart || [] : []) {
|
||||
for (const s of input.logStart || []) {
|
||||
const key = (input.stage === 'voices' ? 'voices:' : 'pass:') + s;
|
||||
if ((m.log_starts || {})[key]) continue;
|
||||
const l = await sh('gstack-review-log --start ' + s.split(' ').map(q).join(' ') + (input.reservation ? ' --reservation ' + q(input.reservation) : ''));
|
||||
if (l.code !== 0) return stop('gstack-review-log --start ' + s + ' exited ' + l.code + ': ' + l.out.slice(-500));
|
||||
lines.push('REVIEW_LOG_START: ' + s + ' token=' + l.out.trim());
|
||||
const token = l.out.trim();
|
||||
const k = await sh(bin + ' mark --manifest ' + q(mf) + ' --log-start ' + q(key) + ' --token ' + q(token));
|
||||
if (k.code !== 0) return stop('recording the ' + s + ' start token exited ' + k.code + ': ' + k.out.slice(-500));
|
||||
lines.push('REVIEW_LOG_START: ' + s + ' token=' + token);
|
||||
}
|
||||
for (;;) {
|
||||
const failed = m.readers.find((x) => active.includes(x.stage) && x.state === 'failed');
|
||||
if (failed) return stop(failed.reader + ' (subagent ' + failed.path + ') ' + (failed.error || 'failed') + '; its review is incomplete, so the pass cannot certify coverage; restore it with a fresh pass under a new --pass and --out');
|
||||
const list = await tools.subagent.list({});
|
||||
const byTitle = (t) => list.tasks.filter((x) => x.title === t);
|
||||
const byPath = new Map(list.tasks.map((x) => [x.path, x]));
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface Manifest {
|
||||
version: 1; pass: string; out: string; ledger: string; merged: string; base: string; diff_base: string;
|
||||
stack: string; test_fw: string; learnings: string | null; checklist_root: string;
|
||||
next: string; plan_lines: string[]; readers: PassReader[]; before?: { tokens: number; credits: number };
|
||||
log_starts?: Record<string, string>;
|
||||
}
|
||||
export interface Extra { name: string; prompt_file: string; first?: boolean; model?: PassModel }
|
||||
|
||||
|
||||
@@ -279,6 +279,33 @@ describe('Capy pass program (fake host, real bins)', () => {
|
||||
expect(fs.readFileSync(path.join(out, 'merged.jsonl'), 'utf8')).not.toContain('Coverage');
|
||||
}, 240_000);
|
||||
|
||||
test('a rerun after a failed review-log start runs the start again before any reader is sent', async () => {
|
||||
const { dir, env } = repo();
|
||||
const shim = path.join(dir, '.state/shim');
|
||||
fs.mkdirSync(shim, { recursive: true });
|
||||
fs.writeFileSync(path.join(shim, 'gstack-review-log'), '#!/bin/sh\necho refused >&2\nexit 1\n', { mode: 0o755 });
|
||||
const h = host(dir, { ...env, PATH: shim + path.delimiter + env.PATH });
|
||||
const input = { base: 'main', out: path.join(dir, '.state/run/p1'), pass: 'x11p1', planFlags: ['--depth-floor', 'full'], logStart: ['review'] };
|
||||
expect(await runPass(h, input)).toContain('STOP: gstack-review-log --start review exited 1');
|
||||
expect(h.creates).toHaveLength(0);
|
||||
fs.rmSync(path.join(shim, 'gstack-review-log'));
|
||||
const res = await runPass(h, input);
|
||||
expect(res).toMatch(/REVIEW_LOG_START: review token=\S+/);
|
||||
expect(res).toContain('NEXT: judge');
|
||||
expect(res.indexOf('REVIEW_LOG_START:')).toBeLessThan(res.indexOf('DISPATCHED:'));
|
||||
}, 240_000);
|
||||
|
||||
test('a rerun after a failed reader stops again instead of polling forever', async () => {
|
||||
const { dir, env } = repo();
|
||||
const h = host(dir, env, { fail: t => t.endsWith('specialist/performance') });
|
||||
const input = { base: 'main', out: path.join(dir, '.state/run/p1'), pass: 'x12p1', planFlags: ['--depth-floor', 'full'] };
|
||||
expect(await runPass(h, input)).toMatch(/STOP: specialist\/performance \(subagent \d+\) failed/);
|
||||
const again = await runPass(h, input);
|
||||
expect(again).toMatch(/STOP: specialist\/performance \(subagent \d+\) failed; .*new --pass and --out/);
|
||||
expect(again).not.toContain('NEXT: poll');
|
||||
expect(h.creates.some(c => c.title.endsWith('red-team'))).toBe(false);
|
||||
}, 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