From 1bb983c0b3664258fc388077ccfca9cc6a470ff9 Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 2 Nov 2026 01:47:15 +0000 Subject: [PATCH] fix(autoplan): CEO-only runs leave main's open Critical/High finding items to the owner (no decision row) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- docs/unattended.md | 4 +++- lib/autoplan-ceo-only.ts | 3 ++- test/autoplan-ceo-only-driver.test.ts | 11 +++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/unattended.md b/docs/unattended.md index 0dd7ec258..822f38e73 100644 --- a/docs/unattended.md +++ b/docs/unattended.md @@ -635,7 +635,9 @@ voices as usual. When CEO closes, the run prints `REVIEWED:` and ends gate would have asked becomes a `decisions.jsonl` row of kind `auto` and status `auto_kept` (nobody answered: the plan's direction is kept, and a round-cap item only the owner can accept, such as a Critical left open, stays undecided with -no chosen option), and tasks are blocked on no decision. The driver never edits the plan, writes no `gate.json` and never +no chosen option), and tasks are blocked on no decision. An open Critical or +High finding, which a full gate asks about, gets no row: nobody decides it, and +it stays in the report's Findings and in `tasks.jsonl` for the owner. The driver never edits the plan, writes no `gate.json` and never approves, so `gstack-artifact validate` accepts the run as it stands. - The flag goes on the first `next` only. It refuses beside `--ui`, diff --git a/lib/autoplan-ceo-only.ts b/lib/autoplan-ceo-only.ts index db55d3836..d2ec44826 100644 --- a/lib/autoplan-ceo-only.ts +++ b/lib/autoplan-ceo-only.ts @@ -18,6 +18,7 @@ import * as crypto from 'node:crypto'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { buildGate, finishGate } from './autoplan-gate'; +import { FINDING_ORIGIN } from './autoplan-confirm'; import { carryLines } from './autoplan-carry'; import { aggregateFindings, exportRun } from './autoplan-export'; import { decisionRows, keepDirection, type GateItem } from './gate-list'; @@ -62,7 +63,7 @@ export function finishRun(state: RunState): string[] { const plan = at?.nlink === 1n ? [state.plan_source, state.plan_input, state.active_plan].find(p => p && fs.existsSync(p) && id(p).ino === at.ino && id(p).dev === at.dev) : undefined; if (plan) throw new RunError('CEO_ONLY_CONFLICT', `the plan ${plan} is the report path ${report}`, 3); const list = buildGate(state, 1); - const items = list.items.filter(i => i.id !== 'p1'); + const items = list.items.filter(i => i.id !== 'p1' && !i.origin?.endsWith(`|${FINDING_ORIGIN}`)); const kept = (item: GateItem) => (item.cap && item.kind === 'approval' ? null : keepDirection(item)); const rows: DecisionRow[] = decisionRows({ ...list, items }).map((row, i) => ({ ...row, kind: 'auto', status: 'auto_kept', chosen: kept(items[i]!), default_if_unanswered: kept(items[i]!), source: 'ceo-only' })); writeDecisions(state.out, rows); diff --git a/test/autoplan-ceo-only-driver.test.ts b/test/autoplan-ceo-only-driver.test.ts index 48f76cf2b..db7b56bfc 100644 --- a/test/autoplan-ceo-only-driver.test.ts +++ b/test/autoplan-ceo-only-driver.test.ts @@ -68,6 +68,17 @@ describe('next --ceo-only runs the CEO phase alone and ends reviewed', () => { for (const t of tasks) { expect(t.blocked_by).toEqual([]); expect(t.status).not.toBe('blocked_on_decision'); } }); + test('an open Critical or High finding is decided by nobody: no decision row, still in the report and the tasks for the owner', () => { + const decisions = rows(path.join(out, 'decisions.jsonl')); + expect(decisions.some(d => /^(Critical|High): /.test(d.title))).toBe(false); + const report = fs.readFileSync(path.join(out, 'ceo-review.md'), 'utf8'); + expect(report).toMatch(/\[Critical\] The install check lies by omission .*disposition: /); + expect(report).toMatch(/\[High\] Gate reuse on tree equality alone is unsound .*disposition: /); + expect(report).not.toMatch(/(Critical|High): .*kept/); + const tasks = rows(path.join(out, 'tasks.jsonl')); + expect(tasks.some(t => t.title === 'Gate reuse on tree equality alone is unsound' && t.priority === 'High')).toBe(true); + }); + test('the artifact set validates and the plan is untouched', () => { const v = spawnSync('bun', [path.join(ROOT, 'bin', 'gstack-artifact'), 'validate', path.join(out, 'run.json')], { cwd: ROOT, env: env(), encoding: 'utf8', timeout: 60_000 }); expect(v.stdout).toContain('ARTIFACT_VALID: ');