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:
garrytanandcapy-ai[bot] committed 2026-10-31 03:29:33 +00:00
1 parent f8c835e206
commit 55fe76bc95
4 files changed
+48 -3

No files matched your search

+11
View File
@@ -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);
+9 -3
View File
@@ -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]));
+1
View File
@@ -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 }
+27
View File
@@ -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);