From 55fe76bc95f458e2725cb25ae0651bab6baf6177 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 31 Oct 2026 03:29:33 +0000 Subject: [PATCH] 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> --- bin/gstack-review-dispatch | 11 +++++++++++ lib/capy-review-pass.js | 12 +++++++++--- lib/review-pass.ts | 1 + test/review-pass.test.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/bin/gstack-review-dispatch b/bin/gstack-review-dispatch index bdb1833b3..edaea1d9d 100755 --- a/bin/gstack-review-dispatch +++ b/bin/gstack-review-dispatch @@ -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 --reader --state [--path

] [--output ] [--error ] mark --manifest --before , + mark --manifest --log-start --token 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 ] PROGRAM: v 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 --token \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); diff --git a/lib/capy-review-pass.js b/lib/capy-review-pass.js index 7a2d378aa..197a3e9ff 100644 --- a/lib/capy-review-pass.js +++ b/lib/capy-review-pass.js @@ -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])); diff --git a/lib/review-pass.ts b/lib/review-pass.ts index 211e6ab0c..49cdca9d9 100644 --- a/lib/review-pass.ts +++ b/lib/review-pass.ts @@ -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; } export interface Extra { name: string; prompt_file: string; first?: boolean; model?: PassModel } diff --git a/test/review-pass.test.ts b/test/review-pass.test.ts index 971ed87cb..fe20d9571 100644 --- a/test/review-pass.test.ts +++ b/test/review-pass.test.ts @@ -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);