From b553ec96dfcdeef3efdc13daffeb2e116fe4f827 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 31 Oct 2026 12:42:57 +0000 Subject: [PATCH] fix: pre-landing review fixes (Step 11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- lib/review-pass.ts | 5 ++++- ship/DRIVER.md | 2 +- ship/DRIVER.md.tmpl | 2 +- test/review-pass.test.ts | 7 +++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/review-pass.ts b/lib/review-pass.ts index c11e87745..17e60376f 100644 --- a/lib/review-pass.ts +++ b/lib/review-pass.ts @@ -42,6 +42,7 @@ export interface Manifest { export interface Extra { name: string; prompt_file: string; first?: boolean; model?: PassModel } /** Step 9.2.1's own first line: its probes are the parent's, so a QA subagent only refuses (C4-36, C5-36: "parent-only"). */ export const PARENT_ONLY_QA = 'Only the parent runs report-only discovery.'; +const qaRefusal = (name: string) => new BriefError(`Step 9.2.1 is the parent's report-only discovery (ship/sections/review-army.md: "${PARENT_ONLY_QA}"), never an audit reader: ${name}`); /** Brief characters per transport chunk: even with every character escaped, one chunk line stays far under 1 MiB. */ export const CHUNK_CHARS = 65_536; @@ -89,7 +90,9 @@ export function buildManifest(o: { } for (const e of o.extras ?? []) { if (!/^[a-z0-9-]+$/.test(e.name)) throw new BriefError(`an audit reader's name is lowercase letters, digits and dashes: ${e.name}`); - if (/(^|-)qa(-|$)/.test(e.name) || (fs.existsSync(e.prompt_file) && fs.readFileSync(e.prompt_file, 'utf8').includes(PARENT_ONLY_QA))) throw new BriefError(`Step 9.2.1 is the parent's report-only discovery (ship/sections/review-army.md: "${PARENT_ONLY_QA}"), never an audit reader: ${e.name}`); + let brief = ''; + try { brief = fs.readFileSync(e.prompt_file, 'utf8'); } catch { brief = ''; } + if (/(^|-)qa(-|$)/.test(e.name) || brief.includes(PARENT_ONLY_QA)) throw qaRefusal(e.name); readers.push({ reader: `audit/${e.name}`, stage: e.first ? 'first' : 'specialists', title: `ship ${o.pass} audit/${e.name}`, state: 'planned', prompt_file: path.resolve(e.prompt_file), model: e.model }); } if (new Set(readers.map(r => r.reader)).size !== readers.length) throw new BriefError('two readers share a key in this pass'); diff --git a/ship/DRIVER.md b/ship/DRIVER.md index 745dfd64e..b33039e85 100644 --- a/ship/DRIVER.md +++ b/ship/DRIVER.md @@ -17,7 +17,7 @@ there, come back. Unattended, a question with a recommended option takes it (log disposition and every approval stay pending and end the run. Keep a private invocation record (the `--record` file): write its six headings at Phase 1, then fill each as its phase ends: **Checks** (Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Exploratory QA:`, `Scope Check:`), **Decisions** (any -phase: each answered or auto-taken question), **Reviews** (Phases 3-5: the plan line, +phase: each answered or auto-taken question, in its words), **Reviews** (Phases 3-5: the plan line, start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phases 5-7: version, CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** (every phase: the next act). diff --git a/ship/DRIVER.md.tmpl b/ship/DRIVER.md.tmpl index 15ead042e..97b8082d2 100644 --- a/ship/DRIVER.md.tmpl +++ b/ship/DRIVER.md.tmpl @@ -15,7 +15,7 @@ there, come back. Unattended, a question with a recommended option takes it (log disposition and every approval stay pending and end the run. Keep a private invocation record (the `--record` file): write its six headings at Phase 1, then fill each as its phase ends: **Checks** (Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Exploratory QA:`, `Scope Check:`), **Decisions** (any -phase: each answered or auto-taken question), **Reviews** (Phases 3-5: the plan line, +phase: each answered or auto-taken question, in its words), **Reviews** (Phases 3-5: the plan line, start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phases 5-7: version, CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** (every phase: the next act). diff --git a/test/review-pass.test.ts b/test/review-pass.test.ts index 664194d7e..efd14911f 100644 --- a/test/review-pass.test.ts +++ b/test/review-pass.test.ts @@ -87,6 +87,13 @@ describe('pass manifest', () => { expect(m.readers[0]).toMatchObject({ state: 'created', path: '4' }); }); + // Value: protects=an unreadable extra prompt stops as a BriefError naming the reader; fails_when=a directory prompt_file throws a raw + // EISDIR from the QA guard's read (GST-450 adversarial #4); seam=none + test('a directory extra prompt stops as an unreadable prompt file, not a raw read error', () => { + const d = manifest([spec('testing')], { extras: [{ name: 'plan', prompt_file: tmp('rp-qa-') }] }); + expect(() => renderStage(d, 'specialists', texts, () => '')).toThrow(/cannot read audit\/plan's prompt file/); + }); + test('a brief over 1 MiB with invalid UTF-8 in its diff travels in bounded chunks that rejoin to the rendered sha', () => { const m = manifest([{ role: 'outside-voice', name: 'outside-voice', effort: 'host', exclude: [] }]); const diff = Buffer.concat([Buffer.from('+line \u00e9\n'.repeat(90_000)), Buffer.from([0xff, 0xfe, 0x0a]), Buffer.from('+"quoted" \\ back\n'.repeat(20_000))]).toString('utf8');