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>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-02 01:47:15 +00:00
1 parent 81160bab70
commit 1bb983c0b3
3 files changed
+16 -2

No files matched your search

+3 -1
View File
@@ -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`,
+2 -1
View File
@@ -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);
+11
View File
@@ -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: ');