fix(gates): one --gates rule in every command, run through the shim, and no executor pause the executor lacks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-22 15:38:20 +02:00
commit af36578f12
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
11 changed files with 190 additions and 52 deletions

View file

@ -72,13 +72,7 @@ No cloud dependency. No GitHub requirement. Works on **Mac, Linux, and Windows**
### Autonomy mode (`--gates`, v3.4.0) ### Autonomy mode (`--gates`, v3.4.0)
All four pipeline commands accept `--gates {open|closed|adaptive}` to control how many autonomy checkpoints surface to the operator on the path from brief approval to main-merge. All four pipeline commands accept `--gates [true|false|closed|open]`. A bare `--gates`, `true` or `closed` turns gating on; `false`, `open` or no flag (the default) leaves it off. Any other next token is not the flag's value, so `/trekplan --gates --project <dir>` is a bare `--gates`. With gating on, `/trekbrief`, `/trekresearch` and `/trekplan` pause at their own boundaries; `/trekexecute` records the setting and pauses on nothing, because its one stop is the merge into main, which always asks. Details: [`docs/operations.md`](docs/operations.md) § Autonomy mode.
| Value | Behavior |
|-------|----------|
| `open` | Skip optional checkpoints. The pipeline runs end-to-end with the fewest interruptions. Suitable for trusted briefs in clean repos. |
| `closed` | Stop at every checkpoint. The operator confirms each transition. Suitable for high-stakes work or unfamiliar repos. |
| `adaptive` (default) | Stop only when the autonomy-gate state machine reports a meaningful boundary (manifest-audit FAIL, plan-critic BLOCKER, main-merge gate). Best balance of velocity and safety. |
Under the hood, `lib/util/autonomy-gate.mjs` runs a small state machine (`idle → gates_on | auto_running → paused_for_gate → completed`), and `lib/stats/event-emit.mjs` records named lifecycle events (the main-merge gate and its answer, among others) to `trekexecute-stats.jsonl` in the plugin data directory. The new `hooks/scripts/post-compact-flush.mjs` PostCompact hook re-injects `.session-state.local.json` after context compaction so multi-session work survives a compaction boundary. Under the hood, `lib/util/autonomy-gate.mjs` runs a small state machine (`idle → gates_on | auto_running → paused_for_gate → completed`), and `lib/stats/event-emit.mjs` records named lifecycle events (the main-merge gate and its answer, among others) to `trekexecute-stats.jsonl` in the plugin data directory. The new `hooks/scripts/post-compact-flush.mjs` PostCompact hook re-injects `.session-state.local.json` after context compaction so multi-session work survives a compaction boundary.

View file

@ -40,15 +40,27 @@ Parse `$ARGUMENTS`:
2. Otherwise: **mode = default**. Interview probes each section until the 2. Otherwise: **mode = default**. Interview probes each section until the
completeness gate (Phase 3) and brief-review gate (Phase 4) both pass. completeness gate (Phase 3) and brief-review gate (Phase 4) both pass.
3. `--gates` flag (autonomy control, may combine with any mode): when 3. `--gates` flag (autonomy control, may combine with any mode): `--gates`
present, set `gates_mode = true`. This re-enables approval pauses at takes the next token as its value only when it is `true`, `false`,
every phase boundary in the downstream pipeline (research, plan, `closed` or `open` (strip both); any other next token is not its value
execute) and at every wave in the executor. Default `gates_mode = false` (strip only the flag). Run the shim once to map the value, bare when
there is no value:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state idle --event start --gates [value]
```
`next_state: gates_on` sets `gates_mode = true`, `auto_running` sets it
false, and nothing else reads the state. On exit 1 or no JSON (a broken
shim; the four values cannot cause it), print the error in one line and
map in prose instead: bare, `true`, `closed` → true; `false`, `open` →
false. The run goes on either way. `gates_mode = true` re-enables
approval pauses at every phase boundary in the downstream pipeline
(research, plan). Default `gates_mode = false`
means auto mode runs continuously until the main-merge gate (which is means auto mode runs continuously until the main-merge gate (which is
the one boundary that ALWAYS pauses, regardless of `gates_mode`). Strip the one boundary that ALWAYS pauses, regardless of `gates_mode`). Strip
the flag from `$ARGUMENTS` before further parsing. The flag is consumed the flag (and its value, when it has one) from `$ARGUMENTS` before
by the autonomy-gate state machine via the CLI shim: further parsing.
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`.
4. `--approve <project-dir | brief-file>`: set **mode = approve**. No 4. `--approve <project-dir | brief-file>`: set **mode = approve**. No
interview, no drafting. Set `APPROVE_ARG` to the argument and resolve it: interview, no drafting. Set `APPROVE_ARG` to the argument and resolve it:

View file

@ -56,10 +56,22 @@ Parse `$ARGUMENTS` for mode flags:
Set **mode = step**, **target-step = N**. Set **mode = step**, **target-step = N**.
7. If arguments contain `--session N` (N is a positive integer): extract N and the file path. 7. If arguments contain `--session N` (N is a positive integer): extract N and the file path.
Set **mode = session**, **target-session = N**. Set **mode = session**, **target-session = N**.
8. If arguments contain `--gates`: set `gates_mode = true`. Default 8. If arguments contain `--gates`: `--gates` takes the next token as its
`gates_mode = false`. The flag is consumed by the autonomy-gate state value only when it is `true`, `false`, `closed` or `open` (strip both);
machine via the CLI shim: any other next token is not its value (strip only the flag). Run the shim
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`. once to map the value, bare when there is no value:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state idle --event start --gates [value]
```
`next_state: gates_on` sets `gates_mode = true`, `auto_running` sets it
false, and nothing else reads the state. On exit 1 or no JSON (a broken
shim; the four values cannot cause it), print the error in one line and
map in prose instead: bare, `true`, `closed` → true; `false`, `open` →
false. The run goes on either way. Default `gates_mode = false`.
`/trekexecute` records `gates_mode`; no phase of it pauses on it. The one
stop is the merge into main, which always asks (Phase 8).
It no longer adds a question of the executor's own before a merge. The It no longer adds a question of the executor's own before a merge. The
wave merge-back (Phase 2.6 Step 3) and the merge into main (Phase 8) are wave merge-back (Phase 2.6 Step 3) and the merge into main (Phase 8) are
asked by the guard, which asks before every merge into main and every push, asked by the guard, which asks before every merge into main and every push,

View file

@ -124,12 +124,23 @@ Parse `$ARGUMENTS` for mode flags. Order of precedence:
7. **`--quick`** — set **mode = quick**. Skip agent swarm; use lightweight 7. **`--quick`** — set **mode = quick**. Skip agent swarm; use lightweight
Glob/Grep scan and go directly to planning + adversarial review. Glob/Grep scan and go directly to planning + adversarial review.
8. **`--gates`** — autonomy control. When present, set `gates_mode = true`. 8. **`--gates`** — autonomy control. `--gates` takes the next token as its
Pause for operator confirmation after Phase 5 (exploration), Phase 7 value only when it is `true`, `false`, `closed` or `open` (strip both);
(synthesis), and Phase 9 (adversarial review). Default `gates_mode = any other next token is not its value (strip only the flag). Run the shim
false` lets phases flow continuously. The flag is consumed by the once to map the value, bare when there is no value:
autonomy-gate state machine via the CLI shim:
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`. ```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state idle --event start --gates [value]
```
`next_state: gates_on` sets `gates_mode = true`, `auto_running` sets it
false, and nothing else reads the state. On exit 1 or no JSON (a broken
shim; the four values cannot cause it), print the error in one line and
map in prose instead: bare, `true`, `closed` → true; `false`, `open` →
false. The run goes on either way. With `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.
9. **`--min-brief-version <ver>`** — (optional) version floor. Set 9. **`--min-brief-version <ver>`** — (optional) version floor. Set
**min_brief_version = {ver}** (e.g. `2.2`). Forwarded to the brief-validator **min_brief_version = {ver}** (e.g. `2.2`). Forwarded to the brief-validator

View file

@ -72,12 +72,23 @@ Supported flags:
node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase research --brief-path "{dir}/brief.md" [--profile {profile}] --json node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase research --brief-path "{dir}/brief.md" [--profile {profile}] --json
``` ```
6. `--gates` — autonomy control. When present, set `gates_mode = true`. The 6. `--gates` — autonomy control. `--gates` takes the next token as its
value only when it is `true`, `false`, `closed` or `open` (strip both);
any other next token is not its value (strip only the flag). Run the shim
once to map the value, bare when there is no value:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state idle --event start --gates [value]
```
`next_state: gates_on` sets `gates_mode = true`, `auto_running` sets it
false, and nothing else reads the state. On exit 1 or no JSON (a broken
shim; the four values cannot cause it), print the error in one line and
map in prose instead: bare, `true`, `closed` → true; `false`, `open` →
false. The run goes on either way. With `gates_mode = true`, the
research command will pause after each topic completes ("Topic N research command will pause after each topic completes ("Topic N
complete. Proceed to topic N+1? (yes/no)"). Default `gates_mode = false` complete. Proceed to topic N+1? (yes/no)"). Default `gates_mode = false`
means topics run continuously. The flag is consumed by the autonomy-gate means topics run continuously.
state machine via the CLI shim:
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`.
7. `--min-brief-version <ver>` — (optional) version floor for an attached 7. `--min-brief-version <ver>` — (optional) version floor for an attached
`--project` brief. Set **min_brief_version = {ver}** (e.g. `2.2`). Forwarded `--project` brief. Set **min_brief_version = {ver}** (e.g. `2.2`). Forwarded

View file

@ -42,7 +42,7 @@ Doc-consistency test at `tests/lib/doc-consistency.test.mjs` pins agent-table co
**Brief:** 7-phase workflow: Parse mode → Create project dir → Phase 3 completeness loop (section-driven, no question cap) → Phase 3.5 per-phase effort dialog (v5.1) → Phase 4 draft/review/revise with `brief-reviewer` as stop-gate (max 3 iterations; gate = all dimensions ≥ 4 and research plan = 5) → Finalize (`brief.md` on pass, or `brief_quality: partial` on cap/force-stop) → Manual/auto opt-in → Stats. Always interactive. Auto mode runs research + plan inline in the main context (v2.4.0). **Brief:** 7-phase workflow: Parse mode → Create project dir → Phase 3 completeness loop (section-driven, no question cap) → Phase 3.5 per-phase effort dialog (v5.1) → Phase 4 draft/review/revise with `brief-reviewer` as stop-gate (max 3 iterations; gate = all dimensions ≥ 4 and research plan = 5) → Finalize (`brief.md` on pass, or `brief_quality: partial` on cap/force-stop) → Manual/auto opt-in → Stats. Always interactive. Auto mode runs research + plan inline in the main context (v2.4.0).
**Phase 3.5 (v5.1) — adaptive-depth signals:** Between Phase 3 completeness exit and Phase 4 draft, the operator commits an effort level (`low | standard | high`) and an optional `model` (`sonnet | opus | fable`) per downstream phase (`research`, `plan`, `execute`, `review`) via 4 tier-coupled `AskUserQuestion` calls. The choices land in `brief.md` frontmatter as `phase_signals:` (a list of `{phase, effort?, model?}` entries) when committed, or `phase_signals_partial: true` when the operator force-stops. `brief_version: 2.1` activates the **sequencing gate**: validator emits `BRIEF_V51_MISSING_SIGNALS` if a 2.1-versioned brief lacks both fields. Downstream commands surface a friendly hint pointing back to `/trekbrief` — enforcement is validator-only. Composition is documented prose in each downstream command's `## Composition rule (v5.1)` section: `brief.phase_signals[phase] > profile.phase_models[phase]`. The brief signal wins per-phase when present; the profile fills gaps. `effort == low` activates each command's existing `--quick`-equivalent code-path (`/trekexecute` low-effort = `--gates open` + sequential-only). High-effort behavior is deferred to v5.1.1 per brief Non-Goal. **Phase 3.5 (v5.1) — adaptive-depth signals:** Between Phase 3 completeness exit and Phase 4 draft, the operator commits an effort level (`low | standard | high`) and an optional `model` (`sonnet | opus | fable`) per downstream phase (`research`, `plan`, `execute`, `review`) via 4 tier-coupled `AskUserQuestion` calls. The choices land in `brief.md` frontmatter as `phase_signals:` (a list of `{phase, effort?, model?}` entries) when committed, or `phase_signals_partial: true` when the operator force-stops. `brief_version: 2.1` activates the **sequencing gate**: validator emits `BRIEF_V51_MISSING_SIGNALS` if a 2.1-versioned brief lacks both fields. Downstream commands surface a friendly hint pointing back to `/trekbrief` — enforcement is validator-only. Composition is documented prose in each downstream command's `## Composition rule (v5.1)` section: `brief.phase_signals[phase] > profile.phase_models[phase]`. The brief signal wins per-phase when present; the profile fills gaps. `effort == low` activates each command's existing `--quick`-equivalent code-path (`/trekexecute` low effort = one session at a time). High-effort behavior is deferred to v5.1.1 per brief Non-Goal.
**Research:** Foreground workflow (v2.4.0): Parse mode → Interview → Parallel research swarm (5 local + 4 external + 1 bridge, spawned from main context) → Follow-ups → Triangulation → Synthesis + brief → Stats. With `--project`, writes to `{dir}/research/NN-slug.md`. **Research:** Foreground workflow (v2.4.0): Parse mode → Interview → Parallel research swarm (5 local + 4 external + 1 bridge, spawned from main context) → Follow-ups → Triangulation → Synthesis + brief → Stats. With `--project`, writes to `{dir}/research/NN-slug.md`.

View file

@ -8,7 +8,7 @@ Per-command flag tables, imported from `CLAUDE.md` via pointer.
|------|----------| |------|----------|
| _(default)_ | Question round sized by class (`small` / `bounded` / `architectural`), brief-review gate, approval → brief.md with research plan | | _(default)_ | Question round sized by class (`small` / `bounded` / `architectural`), brief-review gate, approval → brief.md with research plan |
| `--quick` | Proposes `small` as the class (still said aloud, overridable, raised when the task proves bigger) → brief.md with research plan | | `--quick` | Proposes `small` as the class (still said aloud, overridable, raised when the task proves bigger) → brief.md with research plan |
| `--gates {true\|false}` | (v3.4.0) Boolean autonomy-gate flag; present → gating on. Policy (`gates_mode`) detailed under `## Autonomy mode` in `docs/operations.md`. | | `--gates [true\|false\|closed\|open]` | (v3.4.0) Autonomy flag: bare, `true` or `closed` = on; `false` or `open` = off; any other next token is not its value. See `## Autonomy mode` in `docs/operations.md`. |
| `--profile <name>` | (v4.1.0) Model profile: `economy` / `balanced` / `premium` / `fable` / `<custom>`. Sets `phase_models` for the brief phase. See `## Profile system` in `docs/operations.md`. | | `--profile <name>` | (v4.1.0) Model profile: `economy` / `balanced` / `premium` / `fable` / `<custom>`. Sets `phase_models` for the brief phase. See `## Profile system` in `docs/operations.md`. |
| `--approve <project-dir \| brief-file>` | (veikart steg 1) No interview: show the existing brief's `## Intent` + `## Goal` verbatim, ask the operator to approve, and on "Approve" stamp `intent_approved_hash` + `intent_approved_at`, emit `brief-approved`, and write the trekbrief stats record (`"mode": "approve"`). The remedy `/trekplan` names when it halts on `BRIEF_INTENT_NOT_APPROVED` / `BRIEF_INTENT_APPROVAL_STALE`. | | `--approve <project-dir \| brief-file>` | (veikart steg 1) No interview: show the existing brief's `## Intent` + `## Goal` verbatim, ask the operator to approve, and on "Approve" stamp `intent_approved_hash` + `intent_approved_at`, emit `brief-approved`, and write the trekbrief stats record (`"mode": "approve"`). The remedy `/trekplan` names when it halts on `BRIEF_INTENT_NOT_APPROVED` / `BRIEF_INTENT_APPROVAL_STALE`. |
@ -24,7 +24,7 @@ Always interactive. Phase 3 puts the task in a size class (`ceremony:`, K1; heav
| `--local` | Only codebase analysis agents (skip external research) | | `--local` | Only codebase analysis agents (skip external research) |
| `--external` | Only external research agents (skip codebase analysis) | | `--external` | Only external research agents (skip codebase analysis) |
| `--fg` | No-op alias (foreground is default since v2.4.0) | | `--fg` | No-op alias (foreground is default since v2.4.0) |
| `--gates {true\|false}` | (v3.4.0) Boolean autonomy-gate flag; present → gating on. Policy (`gates_mode`) detailed under `## Autonomy mode` in `docs/operations.md`. | | `--gates [true\|false\|closed\|open]` | (v3.4.0) Autonomy flag: bare, `true` or `closed` = on; `false` or `open` = off; any other next token is not its value. See `## Autonomy mode` in `docs/operations.md`. |
| `--min-brief-version <ver>` | (S18) Warn — never block — if an attached `--project` brief declares a version below `<ver>` (e.g. `2.2`), i.e. sidesteps framing enforcement | | `--min-brief-version <ver>` | (S18) Warn — never block — if an attached `--project` brief declares a version below `<ver>` (e.g. `2.2`), i.e. sidesteps framing enforcement |
| `--profile <name>` | (v4.1.0) Model profile for the research phase. | | `--profile <name>` | (v4.1.0) Model profile for the research phase. |
| `--engine {swarm\|deep-research}` | (deep-research-engine) Opt-in external-research engine; `deep-research` delegates the external phase to Claude Code's built-in `/deep-research` workflow, which only works on `2.1.154 <= CC < 2.1.218` — from **2.1.218** `/deep-research` is operator-invoked only (the Skill tool refuses with `disable-model-invocation`), so the engine always falls back to `swarm`. Never hard-fails. Default `swarm`. | | `--engine {swarm\|deep-research}` | (deep-research-engine) Opt-in external-research engine; `deep-research` delegates the external phase to Claude Code's built-in `/deep-research` workflow, which only works on `2.1.154 <= CC < 2.1.218` — from **2.1.218** `/deep-research` is operator-invoked only (the Skill tool refuses with `disable-model-invocation`), so the engine always falls back to `swarm`. Never hard-fails. Default `swarm`. |
@ -59,7 +59,7 @@ protocol, thresholds, and the exact commands in
| `--min-brief-version <ver>` | (S18) Warn — never block — if the brief declares a version below `<ver>` (e.g. `2.2`), i.e. sidesteps framing enforcement | | `--min-brief-version <ver>` | (S18) Warn — never block — if the brief declares a version below `<ver>` (e.g. `2.2`), i.e. sidesteps framing enforcement |
| `--export headless <plan>` | Legacy alias for `--decompose` — the only remaining export format (the `pr` / `issue` / `markdown` variants were removed; Claude reformats a plan ad-hoc on request) | | `--export headless <plan>` | Legacy alias for `--decompose` — the only remaining export format (the `pr` / `issue` / `markdown` variants were removed; Claude reformats a plan ad-hoc on request) |
| `--decompose <plan>` | Split plan into self-contained session specs + `orders.md` (one starter line per session) | | `--decompose <plan>` | Split plan into self-contained session specs + `orders.md` (one starter line per session) |
| `--gates {true\|false}` | (v3.4.0) Boolean autonomy-gate flag; present → gating on. Policy (`gates_mode`) detailed under `## Autonomy mode` in `docs/operations.md`. | | `--gates [true\|false\|closed\|open]` | (v3.4.0) Autonomy flag: bare, `true` or `closed` = on; `false` or `open` = off; any other next token is not its value. See `## Autonomy mode` in `docs/operations.md`. |
| `--profile <name>` | (v4.1.0) Model profile for the plan phase (and others, since plan emits `profile:` to plan.md frontmatter). | | `--profile <name>` | (v4.1.0) Model profile for the plan phase (and others, since plan emits `profile:` to plan.md frontmatter). |
**Breaking change (v2.0):** one of `--brief` or `--project` is required. There is no interview inside `/trekplan`. The `--spec` flag has been removed — use `/trekbrief` to produce a brief instead. **Breaking change (v2.0):** one of `--brief` or `--project` is required. There is no interview inside `/trekplan`. The `--spec` flag has been removed — use `/trekbrief` to produce a brief instead.
@ -80,7 +80,7 @@ If `{project_dir}/architecture/overview.md` exists (typically produced by an opt
| `--step N` | Execute only step N | | `--step N` | Execute only step N |
| `--fg` | Force foreground — run all steps sequentially, ignore Execution Strategy | | `--fg` | Force foreground — run all steps sequentially, ignore Execution Strategy |
| `--session N` | Execute only session N from plan's Execution Strategy | | `--session N` | Execute only session N from plan's Execution Strategy |
| `--gates {true\|false}` | (v3.4.0) Boolean autonomy-gate flag; present → gating on. Policy (`gates_mode`) detailed under `## Autonomy mode` in `docs/operations.md`. It adds no question of its own before a merge: the guard asks there (operator decision 2026-10-08). | | `--gates [true\|false\|closed\|open]` | (v3.4.0) Autonomy flag: bare, `true` or `closed` = on; `false` or `open` = off; any other next token is not its value. See `## Autonomy mode` in `docs/operations.md`. `/trekexecute` records `gates_mode` and pauses on nothing; it adds no question of its own before a merge: the guard asks there (operator decision 2026-10-08). |
| `--profile <name>` | (v4.1.0) Model profile for the execute phase. Inherited from plan.md frontmatter `profile:` if present. | | `--profile <name>` | (v4.1.0) Model profile for the execute phase. Inherited from plan.md frontmatter `profile:` if present. |
Every mode that executes refuses a plan whose approval in the approval log does not cover its current sha256 (K2). A run ends with the review it starts by itself (`/simplify` → `/trekreview` → verdict gate); a BLOCK or an undecided verdict keeps the merge into main closed. `TREKEXECUTE_MAX_REVIEW_ROUNDS` (default 3) caps the reviews per run. Every mode that executes refuses a plan whose approval in the approval log does not cover its current sha256 (K2). A run ends with the review it starts by itself (`/simplify` → `/trekreview` → verdict gate); a BLOCK or an undecided verdict keeps the merge into main closed. `TREKEXECUTE_MAX_REVIEW_ROUNDS` (default 3) caps the reviews per run.

View file

@ -4,17 +4,9 @@ Imported from `CLAUDE.md` via pointer.
## Autonomy mode (`--gates`, v3.4.0) ## Autonomy mode (`--gates`, v3.4.0)
All four pipeline commands accept a boolean `--gates {true|false}` flag. Presence (`--gates true`, or bare `--gates`) turns gating **on** — the run pauses at autonomy boundaries for operator confirmation. Absence (or `--gates false`, the default) runs phases continuously without pausing. All four pipeline commands accept `--gates [true|false|closed|open]`. A bare `--gates`, `--gates true` or `--gates closed` turns gating **on**; `--gates false`, `--gates open` or no flag (the default) leaves it off. The next token is the flag's value only when it is one of those four; any other token stays the command's own argument, so `/trekplan --gates --project <dir>` and `/trekexecute --gates plan.md` are a bare `--gates`. Each command maps the value once through `lib/util/autonomy-gate.mjs`; a shim that fails falls back to the same mapping in prose, and the run goes on. With gating on, `/trekbrief` pauses before its auto chain, `/trekresearch` after each topic, and `/trekplan` after exploration, synthesis and adversarial review. `/trekexecute` records `gates_mode` and pauses on nothing: its one stop is the merge into main, which always asks (below).
`/trekexecute` additionally refines *how strict* the gating is via a `gates_mode` policy derived from the brief's effort signal (an explicit operator `--gates` flag takes precedence over the brief signal — see `commands/trekexecute.md` § High-effort behavior (v5.1.1)): Under the hood: `lib/util/autonomy-gate.mjs` runs the state machine `idle → gates_on | auto_running → paused_for_gate → completed`. `start` routes to `gates_on` when `--gates` is bare, `true` or `closed`, else `auto_running`; a `phase_boundary` from `gates_on` pauses at `paused_for_gate` (awaiting `resume`); `finish` reaches the terminal `completed`. The module is pure data with no I/O; the commands run only its `start` transition, to map the flag. Separately, `lib/stats/event-emit.mjs` records named lifecycle events (`brief-approved`, `main-merge-gate`, `main-merge-approved`, `main-merge-declined`, `user_input`) to `trekexecute-stats.jsonl` in the plugin data directory (`CLAUDE_PLUGIN_DATA`, else `~/.claude/plugins/data/voyage-ktg-plugin-marketplace/`, the directory `scripts/yardstick.mjs` reads); a write that fails is reported, never skipped. The main-merge gate is the final autonomy boundary before HEAD lands on `main`.
| `gates_mode` | Derived from (effort) | Behavior |
|--------------|-----------------------|----------|
| `adaptive` | standard / absent (default) | Stop only at meaningful boundaries (manifest-audit FAIL, plan-critic BLOCKER, main-merge gate) |
| `closed` | high | Stop at every autonomy boundary; operator confirms each transition |
| `open` | low | Skip optional checkpoints; trust manifests + verify gates only |
Under the hood: `lib/util/autonomy-gate.mjs` runs the state machine `idle → gates_on | auto_running → paused_for_gate → completed`. `start` routes to `gates_on` when `--gates true`, else `auto_running`; a `phase_boundary` from `gates_on` pauses at `paused_for_gate` (awaiting `resume`); `finish` reaches the terminal `completed`. The module is pure data with no I/O. Separately, `lib/stats/event-emit.mjs` records named lifecycle events (`brief-approved`, `main-merge-gate`, `main-merge-approved`, `main-merge-declined`, `gates_mode_flag_override`, `user_input`) to `trekexecute-stats.jsonl` in the plugin data directory (`CLAUDE_PLUGIN_DATA`, else `~/.claude/plugins/data/voyage-ktg-plugin-marketplace/`, the directory `scripts/yardstick.mjs` reads); a write that fails is reported, never skipped. The main-merge gate is the final autonomy boundary before HEAD lands on `main`.
**The main-merge gate is a lock, and the guard asks the one question** (operator decision 2026-10-08). The gate opens only when the run is `completed` and the review's verdict gate (`lib/review/verdict-gate.mjs`, K7) exits 0; BLOCK (1) and "could not decide" (2) keep it closed. When it opens, `/trekexecute` runs the merge into main itself and `pre-bash-executor` answers "ask" before it runs, in auto mode too, so that question is the only one. In the permission mode where Claude Code shows no prompts, the guard refuses the merge instead, and every other call it would ask about (push, tag or publish, destructive git), since no one can be asked (operator decisions 2026-10-09). The executor no longer asks "ready to merge?" first, and `--gates` no longer adds a pause before the wave merge-back: the guard asks there as well, once per session, because the push and the merge go in one call. Before this, one merge cost two questions in three places: the `--gates` pause before the wave merge-back, the gate's own question after the waves were merged, and the gate's question on the single-session path followed by the guard's at the merge itself. **The main-merge gate is a lock, and the guard asks the one question** (operator decision 2026-10-08). The gate opens only when the run is `completed` and the review's verdict gate (`lib/review/verdict-gate.mjs`, K7) exits 0; BLOCK (1) and "could not decide" (2) keep it closed. When it opens, `/trekexecute` runs the merge into main itself and `pre-bash-executor` answers "ask" before it runs, in auto mode too, so that question is the only one. In the permission mode where Claude Code shows no prompts, the guard refuses the merge instead, and every other call it would ask about (push, tag or publish, destructive git), since no one can be asked (operator decisions 2026-10-09). The executor no longer asks "ready to merge?" first, and `--gates` no longer adds a pause before the wave merge-back: the guard asks there as well, once per session, because the push and the merge go in one call. Before this, one merge cost two questions in three places: the `--gates` pause before the wave merge-back, the gate's own question after the waves were merged, and the gate's question on the single-session path followed by the guard's at the merge itself.

View file

@ -19,10 +19,10 @@
| schema_id | fields | writer_path | line_ref | v4.1 additive | PII | | schema_id | fields | writer_path | line_ref | v4.1 additive | PII |
|-----------|--------|-------------|----------|---------------|-----| |-----------|--------|-------------|----------|---------------|-----|
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1128-1152 | profile, phase_models, profile_source | none | | trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1140-1164 | profile, phase_models, profile_source | none |
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:782-808 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none | | trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:806-839 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1294-1322 | profile, phase_models, parallel_agents, profile_source | none | | trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1305-1333 | profile, phase_models, parallel_agents, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2909-2931 | profile, phase_models, profile_source | none | | trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2921-2943 | profile, phase_models, profile_source | none |
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none | | trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none |
| trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) | | trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) |
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:636-686 | profile, phase_models, profile_source | none | | trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:636-686 | profile, phase_models, profile_source | none |

View file

@ -395,15 +395,16 @@ test('operations.md custom-profile prose matches findProfilePath resolution orde
// --- S24 — anti-false-claim: operations.md + command-modes.md autonomy-gate truth-pass --- // --- S24 — anti-false-claim: operations.md + command-modes.md autonomy-gate truth-pass ---
// //
// autonomy-gate.mjs is a BOOLEAN gate: --gates {true|false} → gates_on|auto_running. // autonomy-gate.mjs is a BOOLEAN gate: --gates [true|false|closed|open] → gates_on|auto_running.
// State machine: idle → gates_on|auto_running → paused_for_gate → completed. // State machine: idle → gates_on|auto_running → paused_for_gate → completed.
// The old prose (operations.md:15 / command-modes.md gates rows) (1) fabricated a // The old prose (operations.md:15 / command-modes.md gates rows) (1) fabricated a
// state machine `idle → approved → executing → merge-pending → main-merged` — only // state machine `idle → approved → executing → merge-pending → main-merged` — only
// `idle` exists in autonomy-gate.mjs STATES; (2) claimed event-emit.mjs "records // `idle` exists in autonomy-gate.mjs STATES; (2) claimed event-emit.mjs "records
// each transition" — it emits 3 named lifecycle events and is decoupled from the // each transition" — it emits 3 named lifecycle events and is decoupled from the
// (pure, no-I/O) autonomy-gate; (3) advertised `--gates {open|closed|adaptive}` — // (pure, no-I/O) autonomy-gate; (3) advertised `--gates {open|closed|adaptive}` —
// open/closed/adaptive is a DERIVED gates_mode policy in the command layer, not a // there is no strictness policy and no `adaptive` value: the flag is boolean, and
// flag value (the CLI shim + all 4 command docs take boolean `--gates {true|false}`). // the CLI shim + all 4 command docs take `--gates [true|false|closed|open]`
// (bare, true, closed = on; false, open = off; fix round 4.5a, #1).
test('operations.md + command-modes.md describe the real autonomy-gate, not a fabricated one [S24]', () => { test('operations.md + command-modes.md describe the real autonomy-gate, not a fabricated one [S24]', () => {
const ops = read('docs/operations.md'); const ops = read('docs/operations.md');
const modes = read('docs/command-modes.md'); const modes = read('docs/command-modes.md');
@ -430,13 +431,13 @@ test('operations.md + command-modes.md describe the real autonomy-gate, not a fa
'docs/operations.md: event-emit records 3 named lifecycle events, not "each transition" of the (decoupled) autonomy-gate; fix the prose', 'docs/operations.md: event-emit records 3 named lifecycle events, not "each transition" of the (decoupled) autonomy-gate; fix the prose',
); );
// F3 — --gates is boolean; open/closed/adaptive is a derived gates_mode policy, not a flag enum // F3 — --gates is boolean; there is no open/closed/adaptive strictness enum
const GATES_ENUM_FORMS = ['{open|closed|adaptive}', '{open\\|closed\\|adaptive}']; const GATES_ENUM_FORMS = ['{open|closed|adaptive}', '{open\\|closed\\|adaptive}'];
for (const [label, txt] of [['operations.md', ops], ['command-modes.md', modes]]) { for (const [label, txt] of [['operations.md', ops], ['command-modes.md', modes]]) {
for (const bad of GATES_ENUM_FORMS) { for (const bad of GATES_ENUM_FORMS) {
assert.ok( assert.ok(
!txt.includes(bad), !txt.includes(bad),
`docs/${label}: --gates flag enum "${bad}" is false — flag is boolean {true|false}; open/closed/adaptive is a derived gates_mode; fix the prose, not this test`, `docs/${label}: --gates flag enum "${bad}" is false — the flag is boolean: bare, true or closed = on, false or open = off, and there is no adaptive; fix the prose, not this test`,
); );
} }
} }

View file

@ -523,3 +523,108 @@ test('keep: /trekbrief\'s auto path still requires an approved intent', () => {
assert.equal(partAt('commands/trekbrief.md', '**Auto requires an approved intent.**', toBlank), assert.equal(partAt('commands/trekbrief.md', '**Auto requires an approved intent.**', toBlank),
'**Auto requires an approved intent.** If Phase 4h ended "Leave unapproved", the auto chain would halt at `/trekplan` anyway — print the manual-path output instead and stop.'); '**Auto requires an approved intent.** If Phase 4h ended "Leave unapproved", the auto chain would halt at `/trekplan` anyway — print the manual-path output instead and stop.');
}); });
// --- #1 ----------------------------------------------------------------------------
// `--gates` meant one thing in the shim (a boolean), another in the docs (an
// open/closed/adaptive strictness policy), and /trekexecute promised pauses it
// never makes. Each of the four commands now holds one rule: the next token is
// the flag's value only when it is true, false, closed or open; the shim maps
// it, run once; a broken shim falls back to the same mapping in prose and the
// run goes on. /trekexecute records gates_mode and pauses on nothing.
const GATES_COMMANDS = ['trekbrief', 'trekresearch', 'trekplan', 'trekexecute'];
const GATES_RULE = [
'`--gates` takes the next token as its value only when it is `true`, `false`, `closed` or `open` (strip both); any other next token is not its value (strip only the flag).',
'Run the shim once to map the value, bare when there is no value:',
'`next_state: gates_on` sets `gates_mode = true`, `auto_running` sets it false, and nothing else reads the state.',
'On exit 1 or no JSON (a broken shim; the four values cannot cause it), print the error in one line and map in prose instead: bare, `true`, `closed` → true; `false`, `open` → false. The run goes on either way.',
];
/** The command's one shim line, `[value]` filled in with `value` (left out when ''). */
function gatesShim(cmd, value) {
const l = line(read(`commands/${cmd}.md`), 'util/autonomy-gate.mjs', '--event start');
assert.match(l, / --state idle --event start --gates \[value\]$/, `${cmd}: the shim line must end "--gates [value]"`);
return l.replace(' [value]', value ? ` ${value}` : '');
}
/** Run a shim line from the prose; its `next_state`. */
function shimNext(cmdLine) {
const r = bash(cmdLine, ROOT);
assert.equal(r.status, 0, `shim exited ${r.status}: ${r.stdout}${r.stderr}`);
return JSON.parse(r.stdout).next_state;
}
for (const cmd of GATES_COMMANDS) {
test(`[#1] commands/${cmd}.md holds the shared --gates rule and a shim line that maps closed to gates_on`, () => {
const text = read(`commands/${cmd}.md`);
const f = flat(text);
for (const s of GATES_RULE) assert.ok(f.includes(s), `${cmd}: missing the shared --gates sentence: ${s}`);
assert.doesNotMatch(text, /--state X --event Y/, `${cmd}: the placeholder shim line is still there`);
assert.doesNotMatch(f, /consumed by the autonomy-gate/, `${cmd}: still says the flag is consumed by the state machine`);
assert.equal(shimNext(gatesShim(cmd, 'closed')), 'gates_on');
});
}
// Review focus: `/trekplan --gates --project d` and `/trekexecute --gates plan.md`
// are a bare --gates (the next token is not a value), so the shim runs bare.
test('keep: every command\'s shim line maps a bare --gates to gates_on and open to auto_running', () => {
for (const cmd of GATES_COMMANDS) {
assert.equal(shimNext(gatesShim(cmd, '')), 'gates_on', `${cmd}: bare --gates`);
assert.equal(shimNext(gatesShim(cmd, 'open')), 'auto_running', `${cmd}: --gates open`);
}
});
test('[#1] sibling: commands/trekbrief.md promises no execute pause and no pause at every wave in the executor', () => {
const f = flat(read('commands/trekbrief.md'));
assert.doesNotMatch(f, /every wave in the executor/, 'trekbrief still promises a pause at every wave in the executor');
assert.doesNotMatch(f, /\(research, plan, execute\)/, 'trekbrief still promises an execute pause');
assert.match(f, /approval pauses at every phase boundary in the downstream pipeline \(research, plan\)\./);
});
test('[#1] sibling: commands/trekexecute.md records gates_mode and pauses only at the merge into main', () => {
const f = flat(read('commands/trekexecute.md'));
assert.doesNotMatch(f, /consumed by the autonomy-gate state machine/);
assert.match(f, /`\/trekexecute` records `gates_mode`; no phase of it pauses on it\. The one stop is the merge into main, which always asks \(Phase 8\)\./);
});
test('[#1] sibling: docs/operations.md gives the four values and the bare form, and no executor strictness table', () => {
const text = read('docs/operations.md');
const sec = flat(section(text, '## Autonomy mode (`--gates`, v3.4.0)'));
assert.doesNotMatch(text, /\{open\\?\|closed\\?\|adaptive\}/);
assert.doesNotMatch(sec, /\| `gates_mode` \| Derived from/, 'the executor strictness table is still there');
assert.doesNotMatch(sec, /\| `adaptive` \|/, 'the executor strictness table is still there');
assert.doesNotMatch(sec, /how strict/);
assert.doesNotMatch(sec, /gates_mode_flag_override/, 'still names an override record the executor does not write');
assert.match(sec, /accept `--gates \[true\|false\|closed\|open\]`/);
assert.match(sec, /A bare `--gates`, `--gates true` or `--gates closed` turns gating \*\*on\*\*/);
for (const s of ['gates_on', 'auto_running', 'paused_for_gate']) assert.ok(sec.includes(s), `the state ${s} stays named`);
});
test('[#1] sibling: docs/architecture.md does not call --gates open the executor\'s low effort', () => {
const text = read('docs/architecture.md');
assert.doesNotMatch(text, /--gates open/);
assert.match(text, /\(`\/trekexecute` low effort = one session at a time\)/);
});
test('[#1] sibling: docs/command-modes.md gives every --gates row the four values and no strictness policy', () => {
const text = read('docs/command-modes.md');
assert.doesNotMatch(text, /--gates \{true\\\|false\}/);
assert.doesNotMatch(text, /\{open\\?\|closed\\?\|adaptive\}/);
assert.doesNotMatch(text, /Policy \(`gates_mode`\)/, 'a row still points at a gates_mode strictness policy');
const rows = text.split('\n').filter((l) => l.startsWith('| `--gates'));
assert.equal(rows.length, 4, 'one --gates row per command');
for (const r of rows) {
assert.ok(r.startsWith('| `--gates [true\\|false\\|closed\\|open]` |'), `row does not give the four values: ${r}`);
assert.match(r, /`## Autonomy mode` in `docs\/operations\.md`/);
}
});
test('[#1] sibling: README.md gives the four values, and no strictness table', () => {
const text = read('README.md');
const sec = flat(section(text, '### Autonomy mode (`--gates`, v3.4.0)'));
assert.doesNotMatch(text, /\{open\|closed\|adaptive\}/);
assert.doesNotMatch(sec, /\| Value \| Behavior \|/, 'the strictness table is still there');
assert.doesNotMatch(sec, /`adaptive` \(default\)/);
assert.match(sec, /accept `--gates \[true\|false\|closed\|open\]`/);
});