mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 15:27:56 +01:00
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:
1 parent
81160bab70
commit
1bb983c0b3
3 files changed
+16
-2
No files matched your search
+3
-1
@@ -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
|
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
|
`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
|
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.
|
approves, so `gstack-artifact validate` accepts the run as it stands.
|
||||||
|
|
||||||
- The flag goes on the first `next` only. It refuses beside `--ui`,
|
- The flag goes on the first `next` only. It refuses beside `--ui`,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import * as crypto from 'node:crypto';
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { buildGate, finishGate } from './autoplan-gate';
|
import { buildGate, finishGate } from './autoplan-gate';
|
||||||
|
import { FINDING_ORIGIN } from './autoplan-confirm';
|
||||||
import { carryLines } from './autoplan-carry';
|
import { carryLines } from './autoplan-carry';
|
||||||
import { aggregateFindings, exportRun } from './autoplan-export';
|
import { aggregateFindings, exportRun } from './autoplan-export';
|
||||||
import { decisionRows, keepDirection, type GateItem } from './gate-list';
|
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;
|
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);
|
if (plan) throw new RunError('CEO_ONLY_CONFLICT', `the plan ${plan} is the report path ${report}`, 3);
|
||||||
const list = buildGate(state, 1);
|
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 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' }));
|
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);
|
writeDecisions(state.out, rows);
|
||||||
|
|||||||
@@ -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'); }
|
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', () => {
|
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 });
|
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: ');
|
expect(v.stdout).toContain('ARTIFACT_VALID: ');
|
||||||
|
|||||||
Reference in new issue
Block a user