feat(ultraplan-local): add --gates autonomy-control flag to all four pipeline commands

Single autonomy-control surface (--gates) added to ultrabrief, ultraresearch,
ultraplan, and ultraexecute. When present, sets gates_mode = true and
re-enables approval pauses at every phase boundary + every wave for
high-stakes runs. When absent (default in auto), the chain runs continuously
to the main-merge gate (which always pauses regardless of --gates — that
boundary is the one always-on safety stop).

ultrabrief:    pause after auto-mode confirmation; emit brief-approved event
ultraresearch: pause after each topic completes
ultraplan:     pause after Phases 5, 7, 9
ultraexecute:  pause after each wave's worktrees finish, before merge-back,
               AND before the main-merge gate (MAIN_MERGE_GATE)

All four commands invoke the autonomy-gate state machine via the CLI shim
node lib/util/autonomy-gate.mjs (built in S8). Test pin in
tests/lib/gates-flag-coverage.test.mjs locks the contract.

Also wires the brief-approved stats emission into ultrabrief Phase 5 auto
path (was the SC4 wiring requirement from plan-v2 Step 11).
This commit is contained in:
Kjell Tore Guttormsen 2026-05-04 07:54:30 +02:00
commit 34f62043f9
5 changed files with 100 additions and 2 deletions

View file

@ -42,6 +42,16 @@ Parse `$ARGUMENTS`:
2. Otherwise: **mode = default**. Interview probes each section until the
completeness gate (Phase 3) and brief-review gate (Phase 4) both pass.
3. `--gates` flag (autonomy control, may combine with any mode): when
present, set `gates_mode = true`. This re-enables approval pauses at
every phase boundary in the downstream pipeline (research, plan,
execute) and at every wave in the executor. Default `gates_mode = false`
means auto mode runs continuously until the main-merge gate (which is
the one boundary that ALWAYS pauses, regardless of `gates_mode`). Strip
the flag from `$ARGUMENTS` before further parsing. The flag is consumed
by the autonomy-gate state machine via the CLI shim:
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`.
If no task description is provided, output usage and stop:
```
@ -524,6 +534,24 @@ Stop. Do not continue to Phase 6.
Set `auto_research: true` in the brief's frontmatter (edit the file).
Emit the brief-approved lifecycle event so downstream observability sees
the pipeline kick off (consumed by `lib/stats/event-emit.mjs`):
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs \
--event brief-approved \
--payload "{\"project\":\"${PROJECT_DIR}\"}"
```
If `gates_mode == true`: pause here via `AskUserQuestion`
"Auto-mode confirmed. Proceed to research now? (yes/no)". If the user
answers no, fall back to the manual path output and stop. Otherwise
proceed to Phase 6.
If `gates_mode == false` (default in auto): proceed directly to Phase 6.
The chain stops only at the main-merge gate (see `commands/ultraexecute-local.md`
Phase 8).
Proceed to Phase 6.
## Phase 6 — Auto research dispatch (auto path only)

View file

@ -40,7 +40,15 @@ Parse `$ARGUMENTS` for mode flags:
Set **mode = step**, **target-step = N**.
7. If arguments contain `--session N` (N is a positive integer): extract N and the file path.
Set **mode = session**, **target-session = N**.
8. Otherwise: the entire argument string is the file path. Set **mode = execute**.
8. If arguments contain `--gates`: set `gates_mode = true`. Pause for
operator confirmation after each wave's worktrees finish but before
merge-back, AND before the main-merge gate. (The MAIN_MERGE_GATE in
Phase 8 ALWAYS pauses regardless of `gates_mode``--gates` re-enables
the per-wave pauses that auto mode otherwise skips.) Default
`gates_mode = false`. The flag is consumed by the autonomy-gate state
machine via the CLI shim:
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`.
9. Otherwise: the entire argument string is the file path. Set **mode = execute**.
If no path is provided (and `--project` was not used to derive one), output
usage and stop:

View file

@ -107,7 +107,14 @@ Parse `$ARGUMENTS` for mode flags. Order of precedence:
7. **`--quick`** — set **mode = quick**. Skip agent swarm; use lightweight
Glob/Grep scan and go directly to planning + adversarial review.
8. If neither `--brief` nor `--project` is present after flag parsing,
8. **`--gates`** — autonomy control. When present, set `gates_mode = true`.
Pause for operator confirmation after Phase 5 (exploration), Phase 7
(synthesis), and Phase 9 (adversarial review). Default `gates_mode =
false` lets phases flow continuously. The flag is consumed by the
autonomy-gate state machine via the CLI shim:
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`.
9. If neither `--brief` nor `--project` is present after flag parsing,
output usage and stop:
```

View file

@ -54,6 +54,13 @@ Supported flags:
```
Create `{dir}/research/` if it does not already exist.
6. `--gates` — autonomy control. When present, set `gates_mode = true`. The
research command will pause after each topic completes ("Topic N
complete. Proceed to topic N+1? (yes/no)"). Default `gates_mode = false`
means topics run continuously. The flag is consumed by the autonomy-gate
state machine via the CLI shim:
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`.
Flags can be combined:
- `--local` — local-only research
- `--external --quick` — external-only, lightweight

View file

@ -0,0 +1,48 @@
// tests/lib/gates-flag-coverage.test.mjs
// Step 11 (plan-v2) — pin that all four pipeline commands document the
// --gates autonomy-control flag and consume the autonomy-gate state
// machine via the lib/util/autonomy-gate.mjs CLI shim.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
function read(rel) { return readFileSync(join(ROOT, rel), 'utf-8'); }
const COMMANDS = [
'commands/ultrabrief-local.md',
'commands/ultraresearch-local.md',
'commands/ultraplan-local.md',
'commands/ultraexecute-local.md',
];
for (const cmdPath of COMMANDS) {
test(`${cmdPath} documents the --gates flag`, () => {
const text = read(cmdPath);
assert.ok(
text.includes('--gates'),
`${cmdPath} should document the --gates autonomy-control flag (Step 11)`,
);
});
test(`${cmdPath} wires the autonomy-gate.mjs CLI shim`, () => {
const text = read(cmdPath);
assert.ok(
text.includes('autonomy-gate.mjs'),
`${cmdPath} should reference lib/util/autonomy-gate.mjs as the state-machine implementation`,
);
});
}
test('commands/ultraexecute-local.md mentions MAIN_MERGE_GATE', () => {
const text = read('commands/ultraexecute-local.md');
assert.ok(
text.includes('MAIN_MERGE_GATE'),
'commands/ultraexecute-local.md should name MAIN_MERGE_GATE — the only boundary that always pauses regardless of --gates',
);
});