feat(trekexecute): couple the executor to the review gate, the plan approval and the step checks
Wave 2, order 6. /trekexecute now: - refuses a plan whose logged approval does not cover its current sha256 (K2, approval-log verify --kind plan), before step 1; - runs the step check (K9) and the step's optional Regression: (K10, new lib/verification/regression-check.mjs through the criteria runner) at step end, before the checkpoint; exit 2 is "not measured", never green; - runs the architecture rules (K5) and the plan-file sweep (K6) after the last step; a violation or a file outside the plan turns completed into partial; - reviews by itself in the closing run (Phase 7.2): run the thing, /simplify, /trekreview, verdict-gate; a BLOCK adds the review's remediation steps after the plan validator accepts them and the operator re-approves the plan, capped at TREKEXECUTE_MAX_REVIEW_ROUNDS (3); - makes the main-merge gate a lock on the verdict: BLOCK or undecided keeps the merge closed; when open, the executor merges and the guard's question is the one question (operator decision 2026-10-08). The --gates pause before the wave merge-back is gone for the same reason. The guard refuses a merge into main and a push in the permission mode that shows no prompts, since no one can be asked (operator decision 2026-10-09; two mode-read exemptions approved by the operator). progress.json gains optional coupling fields (Handover 5); the validator rejects the runner's exitCode in plan_verification.failed_criteria (C15). Pointer lines name the skills test-first and debugging (S2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4ee711a6ac
commit
920fbd3c47
15 changed files with 1310 additions and 64 deletions
|
|
@ -2,7 +2,7 @@
|
|||
name: trekexecute
|
||||
description: Disciplined plan executor — single-session, or multi-session through session orders the user starts, with failure recovery
|
||||
argument-hint: "[--project <dir>] [--fg | --resume | --dry-run | --validate | --step N | --session N] [plan.md]"
|
||||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent
|
||||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent, Skill
|
||||
disallowed-tools: TeamCreate
|
||||
---
|
||||
|
||||
|
|
@ -16,8 +16,14 @@ Each plan step is built by **one fresh helper** (`voyage:step-implementer`)
|
|||
and judged by **one controller** that reads the step's diff. The executor
|
||||
itself writes no product code: it records the start point, launches the
|
||||
helper, runs the mechanical checks (proof of red, the floor guard, the
|
||||
manifest, the done line), and decides from exit codes and the controller's
|
||||
verdicts (Phase 6).
|
||||
manifest, the step check, the regression, the done line), and decides from
|
||||
exit codes and the controller's verdicts (Phase 6).
|
||||
|
||||
It starts only on a plan the operator approved as it is now (Phase 1, K2), and
|
||||
it ends with a review it starts by itself (Phase 7.2): `/simplify`, then
|
||||
`/trekreview`, then the verdict gate (K7). A BLOCK keeps the merge into main
|
||||
closed (Phase 8). The guard asks the one question before a merge; the executor
|
||||
never asks it again (operator decision 2026-10-08).
|
||||
|
||||
Voyage never starts a Claude Code session. Work that needs another session
|
||||
becomes an order plus a starter line the user gives a session they open
|
||||
|
|
@ -50,14 +56,17 @@ 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. 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
|
||||
8. If arguments contain `--gates`: set `gates_mode = true`. 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}`.
|
||||
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
|
||||
asked by the guard, which asks before every merge into main and every push,
|
||||
in auto mode too, and the executor never asks the same thing again
|
||||
(operator decision 2026-10-08: two questions for one merge became one). The
|
||||
MAIN_MERGE_GATE in Phase 8 is always on, regardless of `gates_mode`: it is
|
||||
a lock, and the guard's question is the one question.
|
||||
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
|
||||
|
|
@ -108,6 +117,53 @@ Project: {project_dir or "-"}
|
|||
File: {path}
|
||||
```
|
||||
|
||||
### Plan approval gate (K2)
|
||||
|
||||
**Runs in every mode that executes** (execute, resume, step N, session N).
|
||||
Dry-run and validate execute nothing and skip it. Before anything is parsed
|
||||
for execution, check that the operator approved THIS plan as it is now.
|
||||
`/trekplan` logs the approval bound to the plan file's sha256 (its Phase 11:
|
||||
`approval-log.mjs append --kind plan --sha256-of`), so an edit after the
|
||||
approval makes it stale. Nothing reads the log file directly (K2); the log's
|
||||
own CLI answers:
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/util/approval-log.mjs verify --json --kind plan --subject "{plan_path}" --sha256-of "{plan_path}"
|
||||
```
|
||||
|
||||
`{plan_path}` is the file resolved above. Exit 0 (`approved`) → continue and
|
||||
record `plan_approval = "approved"`; if `uncommitted` is above 0, say in one
|
||||
line that N approvals are in the log but not committed yet. Exit 1 → **halt**:
|
||||
no step, no helper, no progress file. Print `status` and `detail`:
|
||||
|
||||
| `status` | Meaning | What the operator does |
|
||||
|----------|---------|------------------------|
|
||||
| `not-logged` | The plan was never approved, or was approved before the log existed | `/trekplan --project {dir}` and answer "Approve" at its plan approval |
|
||||
| `mismatch` | The plan changed after its approval (the approval covers other content) | Same: approve the plan as it is now, or restore the approved text. A review loop interrupted between adding its steps and the approval left the approved text in `{report dir}/plan-approved-round-{R}.md` (Phase 7.2): copy it back |
|
||||
| `expired` | The approval carried an expiry that has passed | Same |
|
||||
| `broken` | A record in `.claude/voyage/approvals.jsonl` was edited, removed or reordered | Restore the log from git (`git checkout -- .claude/voyage/approvals.jsonl`), then re-run |
|
||||
| `diverged` | Committed approvals were rewritten | Same: restore from git |
|
||||
|
||||
Exit 2, or no JSON at all → **halt** and show the raw error: an approval check
|
||||
that could not run is never a pass.
|
||||
|
||||
**Not checked:** a session spec or a recovery spec (a file with
|
||||
`## Scope Fence`, Phase 2). Voyage writes those from an approved plan (the
|
||||
session decomposer, Phase 7.6), and they carry no approval of their own.
|
||||
Record `plan_approval = "not-checked"` and say "plan approval: not checked
|
||||
(session spec)" in the final report, never "approved". A `--session N` run of
|
||||
a trekplan's Execution Strategy executes the plan itself, so it IS checked:
|
||||
the log is committed, and Phase 2.55 Check 2 brings the plan into the worktree.
|
||||
|
||||
The remediation steps of the review loop (Phase 7.2) change the plan, so the
|
||||
loop asks for the plan's approval again before it builds them. Nothing else
|
||||
in this command edits the plan.
|
||||
|
||||
Borrowed: the form of `lib/validators/intent-approval.mjs` and the `/trekplan`
|
||||
intent gate (an approval bound to the approved content's hash, checked before
|
||||
the work starts); the log is ai-native-sdlc's gate ledger (MIT), with its two
|
||||
flaws corrected (header of `lib/util/approval-log.mjs`).
|
||||
|
||||
## Phase 2 — Detect file type and parse structure
|
||||
|
||||
Read the file. Determine whether it is an **trekplan** or a **session spec**:
|
||||
|
|
@ -135,7 +191,15 @@ Extract every `### Step N: {description}` heading (in order). For each step, ext
|
|||
span of the field. It is also the step's **test command**: proof of red
|
||||
(Sub-step B) and the done line (Sub-step G) run it, so it must be one
|
||||
test-runner invocation
|
||||
- **On failure** — recovery action (revert/retry/skip/escalate)
|
||||
- **On failure** — recovery action: the field starts with one of the words
|
||||
`revert`, `retry`, `skip`, `escalate` (K11; the plan validator warns
|
||||
otherwise), and the rest is the clause's detail
|
||||
- **Regression** — optional (K10): ONE command from the criteria runner's
|
||||
allowlist, without pipes or `&&`: the frozen regression suite the step must
|
||||
keep green. Sub-step D4 runs it before the checkpoint; a step without the
|
||||
field runs as before. The plan validator rejects a `Regression:` that cannot
|
||||
run once K10 is in it (wave 2, order 9); for a plan validated before that,
|
||||
Sub-step D4 is where an unrunnable one is caught, never read as green
|
||||
- **Checkpoint** — git commit command after success
|
||||
- **Manifest** — YAML block following Checkpoint (v1.7+)
|
||||
- **Interface in** / **Interface out** / **Review focus** — optional (K4):
|
||||
|
|
@ -666,6 +730,9 @@ Record `REPO_ROOT`, `PLAN_REL`, `SESSION_DIR`, `WORKTREE_DIR`, `ORDERS_DIR`,
|
|||
|
||||
```bash
|
||||
mkdir -p "$WORKTREE_DIR" "$ORDERS_DIR"
|
||||
# The run's start point, written once (wave 1): Step 6's whole-run checks and
|
||||
# the review (Phase 7.2) read it. Phase 8's clear removes it with the orders.
|
||||
[ -f "$ORDERS_DIR/run-base.sha" ] || git rev-parse HEAD > "$ORDERS_DIR/run-base.sha"
|
||||
```
|
||||
|
||||
**2a. Create worktrees for this wave's sessions:**
|
||||
|
|
@ -842,8 +909,13 @@ in this order:
|
|||
```
|
||||
Do NOT merge. Do NOT prepare later waves. Jump to Step 5 (cleanup; it keeps
|
||||
every unmerged branch and every worktree git refuses to remove), then Phase 8.
|
||||
- **`all_completed: true`:** if `gates_mode` is true, pause for operator
|
||||
confirmation before merge-back. Then continue with the merge.
|
||||
- **`all_completed: true`:** continue with the merge. The executor asks
|
||||
nothing here, `gates_mode` or not. Each session's push and merge below run
|
||||
in one Bash call, and the guard asks the operator before it (a push always,
|
||||
a merge when the branch is main), so that answer is the one question for the
|
||||
merge (operator decision 2026-10-08; it was two when `--gates` added a pause
|
||||
of its own). A declined guard question means the call did not run: stop and
|
||||
report it like `MERGE_REFUSED`, keeping `wave.json`.
|
||||
|
||||
**Merge session branches back (SEQUENTIAL, one at a time):**
|
||||
|
||||
|
|
@ -864,7 +936,9 @@ If it exits 1, stop and report its message: no merge, no cleanup, and keep
|
|||
For each session N in the wave (in order), push BEFORE merge so the wave
|
||||
branch survives even if cleanup races ahead (research/02 R3 — converts an
|
||||
unrecoverable failure to a recoverable one). Push failure is non-fatal
|
||||
(no remote, offline, etc.):
|
||||
(no remote, offline, etc.). Run the whole block as one Bash call, never split
|
||||
into a push call and a merge call: the guard reads the call and asks once for
|
||||
both.
|
||||
```bash
|
||||
GIT_OPTIONAL_LOCKS=0 git push origin "trek/{slug}/session-{N}" 2>/dev/null || true
|
||||
if GIT_OPTIONAL_LOCKS=0 git merge --no-ff "trek/{slug}/session-{N}" \
|
||||
|
|
@ -940,7 +1014,12 @@ back to Step 1 — this run prepares that wave's orders and stops again. `none`
|
|||
### Step 4 — Run master verification
|
||||
|
||||
After all waves complete and merge successfully, run the plan's `## Verification`
|
||||
section commands to verify the integrated result.
|
||||
section commands to verify the integrated result, through the same criteria
|
||||
runner block as Phase 7 (`### trekplans — run the plan's ## Verification`),
|
||||
from `$REPO_ROOT`. Record `plan_verification` and `end_to_end` exactly as
|
||||
Phase 7 does (the same exit table, the same `exitCode` → `exit_code`
|
||||
translation): the review (Phase 7.2) and the completion gate read them. Step 4
|
||||
passed = the runner exited 0.
|
||||
|
||||
### Step 5 — Cleanup (ALWAYS runs when the run ends, even on failure)
|
||||
|
||||
|
|
@ -1000,7 +1079,13 @@ Keep the orders directory here, whatever the outcome. Only Phase 8's
|
|||
main-merge gate clears it, on confirm, when `next-wave` said `none` (every wave
|
||||
merged) and Step 4 passed. Until then the next run resumes at the lowest wave
|
||||
not yet merged (Step 0), or re-runs Step 4 and returns to the gate when every
|
||||
wave is merged. Jump to Phase 8.
|
||||
wave is merged.
|
||||
|
||||
When every wave is merged and Step 4 passed, the integrated result is checked
|
||||
and reviewed like a single-session run, from the run's start point
|
||||
(`session_start_sha` = the contents of `$ORDERS_DIR/run-base.sha`, Step 1):
|
||||
Phase 7's architecture rules and plan-file sweep (K5, K6), then Phase 7.2
|
||||
(Step 4 is its "run the thing"). Otherwise skip both. Then jump to Phase 8.
|
||||
|
||||
### --session N mode
|
||||
|
||||
|
|
@ -1052,13 +1137,18 @@ progress file.
|
|||
"session_start_sha": null,
|
||||
"status": "{in_progress | completed | failed | stopped | partial}",
|
||||
"steps": {
|
||||
"1": { "status": "pending", "attempts": 0, "fix_rounds": 0, "start_sha": null, "report_path": null, "red_proof": null, "test_lock": null, "error": null, "completed_at": null, "commit": null }
|
||||
"1": { "status": "pending", "attempts": 0, "fix_rounds": 0, "start_sha": null, "report_path": null, "red_proof": null, "test_lock": null, "step_check": null, "regression": null, "error": null, "completed_at": null, "commit": null }
|
||||
},
|
||||
"iterations_remaining": 25,
|
||||
"plan_approval": null,
|
||||
"entry_condition_checked": false,
|
||||
"exit_condition_checked": false,
|
||||
"plan_verification": null,
|
||||
"end_to_end": null,
|
||||
"arch_rules": null,
|
||||
"scope_sweep": null,
|
||||
"simplify": null,
|
||||
"review": null,
|
||||
"decisions": [],
|
||||
"summary": null
|
||||
}
|
||||
|
|
@ -1082,6 +1172,24 @@ a progress file without them still validates and resumes:
|
|||
`progress-validator.mjs` rejects `fix_rounds` outside 0–5 (there is no sixth
|
||||
round) and a `decisions` entry without its `cost_if_wrong`.
|
||||
|
||||
**The coupling fields (Handover 5, wave 2, additive-optional).** Each is a
|
||||
contract CLI's exit said in words. A progress file without them still
|
||||
validates and resumes; when present, `progress-validator.mjs` rejects any
|
||||
other value:
|
||||
|
||||
| Field | Written | Values (exit 0 / 1 / 2) |
|
||||
|---|---|---|
|
||||
| `plan_approval` | Phase 1 (K2) | `"approved"`, or `"not-checked"` for a session spec |
|
||||
| `steps.{N}.step_check` | Sub-step D4 (K9) | `"clean"` / `"findings"` / `"not-measured"` |
|
||||
| `steps.{N}.regression` | Sub-step D4 (K10) | `"passed"` or `"absent"` (no field) / `"failed"` or `"unrunnable"` / `"not-run"` |
|
||||
| `arch_rules` | Phase 7 (K5) | `"pass"` / `"violation"` / `"not-measured"` |
|
||||
| `scope_sweep` | Phase 7 (K6) | `{status: "pass" \| "deviation" \| "not-measured", outside: [paths]}` |
|
||||
| `simplify` | Phase 7.2 | `"applied"` \| `"clean"` \| `"reverted"` \| `"not-run"` |
|
||||
| `review` | Phase 7.2 (K7) | `{round, path, gate_exit: 0 \| 1 \| 2, verdict}`; `round` counts reviews from 1 |
|
||||
|
||||
`"not-measured"` and `"not-run"` are never green: the final report says them
|
||||
in those words, never "pass".
|
||||
|
||||
`plan_version` is the value Phase 2 parsed from the plan header (required by
|
||||
`progress-validator.mjs`). The status words here are the validator's:
|
||||
`PROGRESS_VALID_STATUSES` for the run, `PROGRESS_STEP_STATUSES` for a step.
|
||||
|
|
@ -1242,7 +1350,10 @@ Every stop names its class in the final report (Phase 8 `### Stops`).
|
|||
guesswork), a step went up the fix ladder (Sub-step F), the global
|
||||
iteration budget reached 0 — and the plan's own preconditions failing: an
|
||||
unmet entry condition (Phase 4), or the sandbox pre-flight exiting 77
|
||||
(Sub-step D1: the plan cannot be carried out where it runs).
|
||||
(Sub-step D1: the plan cannot be carried out where it runs). The review
|
||||
loop's stops are this class too (Phase 7.2): a BLOCK on the wave path,
|
||||
remediation steps that are not executable, the plan not approved again,
|
||||
and a BLOCK at the review cap.
|
||||
|
||||
Every stop the executor can make maps to one of these four; `stop_class` in
|
||||
the summary is that number.
|
||||
|
|
@ -1329,6 +1440,11 @@ The step's test is written first, and a command proves it red — never the
|
|||
model's word that "it failed first". This sub-step blocks: a step whose test
|
||||
is not proven red at its start point and green at its result does not pass.
|
||||
|
||||
**How to write the test first is the `test-first` skill** (S2, Voyage plugin:
|
||||
`voyage:test-first`): the build helper is told it by name (Sub-step C). The
|
||||
skill is the advice; the gate below, `red-proof.mjs`, is the guarantee, and it
|
||||
stays a command whatever the skill says.
|
||||
|
||||
**The step's test files** are the `File:` paths under its `Test first:`. The
|
||||
plan template lists them apart from `Files:`, so everywhere below **the step's
|
||||
scope = its `Files:` + its test files**: both are built, both are committed at
|
||||
|
|
@ -1391,10 +1507,12 @@ Launch `subagent_type: "voyage:step-implementer"` with a short `description`
|
|||
summary: the plan path, `plan_version`, the step number, the step's text
|
||||
verbatim (every field, including `Interface in:`, `Interface out:` and
|
||||
`Review focus:` when present), `start_sha`, `report_path`, `mode: build`, the
|
||||
round label (`build`, or `fix round R`), and `voyage_root: ${CLAUDE_PLUGIN_ROOT}`.
|
||||
round label (`build`, or `fix round R`), `voyage_root: ${CLAUDE_PLUGIN_ROOT}`,
|
||||
and the skill to work by: `skill: test-first` for a build round,
|
||||
`skill: debugging` for a fix round (S2; Sub-steps B and F).
|
||||
|
||||
**A step at `fix_rounds` 5 is never built again** (Sub-step F): skip the
|
||||
launch and run only the checks (B, D1, D2, D3, E) on whatever the operator
|
||||
launch and run only the checks (B, D1, D2, D3, D4, E) on whatever the operator
|
||||
changed. A step at `fix_rounds` 4 on `--resume` was re-planned: launch a fresh
|
||||
`build` helper for the revised step.
|
||||
|
||||
|
|
@ -1561,10 +1679,62 @@ It reports a removed assert, a deleted or skipped test, a muted check
|
|||
A finding is never dropped silently: the step report and the final report list
|
||||
every one with its ruling.
|
||||
|
||||
#### Sub-step D4 — Step check and regression
|
||||
|
||||
Two more commands at step end, before the controller and before the
|
||||
checkpoint (Sub-step G). Run them in one Bash call. The plugin root is resolved
|
||||
first, as in Phase 7: an unresolved root would make `node` exit 1, which reads
|
||||
as a finding when it is in fact a check that never ran.
|
||||
|
||||
```bash
|
||||
# zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash.
|
||||
[ -n "${ZSH_VERSION:-}" ] && setopt nonomatch
|
||||
VOYAGE_ROOT="${CLAUDE_PLUGIN_ROOT}"
|
||||
case "$VOYAGE_ROOT" in
|
||||
/*) ;;
|
||||
*) VOYAGE_ROOT="$(ls -d "$HOME"/.claude/plugins/cache/*/voyage/[0-9]*/ 2>/dev/null \
|
||||
| awk -F/ '{ split($(NF-1), v, "."); printf "%09d%09d%09d %s\n", v[1], v[2], v[3], $(0) }' \
|
||||
| sort | tail -1 | cut -d' ' -f2-)"
|
||||
VOYAGE_ROOT="${VOYAGE_ROOT%/}" ;;
|
||||
esac
|
||||
CHECK_CWD="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
if [ ! -f "$VOYAGE_ROOT/lib/verification/step-check.mjs" ]; then
|
||||
echo "[voyage] step check not run - plugin root unresolved (exit 2)"
|
||||
else
|
||||
node "$VOYAGE_ROOT/lib/verification/step-check.mjs" --cwd "$CHECK_CWD"; echo "step-check exit $?"
|
||||
fi
|
||||
if [ ! -f "$VOYAGE_ROOT/lib/verification/regression-check.mjs" ]; then
|
||||
echo "[voyage] regression not run - plugin root unresolved (exit 2)"
|
||||
else
|
||||
node "$VOYAGE_ROOT/lib/verification/regression-check.mjs" --plan "{plan_path}" --step {N} --cwd "$CHECK_CWD" --json; echo "regression exit $?"
|
||||
fi
|
||||
```
|
||||
|
||||
**The step check (K9, P7)** runs the target repo's OWN formatters, linters and
|
||||
type checkers over the step's changes (the diff against `HEAD` plus untracked
|
||||
files: nothing of the step is committed yet) and reads the post-edit guard's
|
||||
log. It installs nothing.
|
||||
|
||||
| Exit | `steps.{N}.step_check` | Executor |
|
||||
|------|------------------------|----------|
|
||||
| 0 | `"clean"` | continue |
|
||||
| 1 | `"findings"` | every finding goes to the controller (Sub-step E) as a quality finding; the round does not pass |
|
||||
| 2 | `"not-measured"` | the repo has no tool for these files, or one could not run. Write "step check: not measured — {reason}" in the step report. Not a finding, and never green: the final report counts the step among those whose step check was not measured |
|
||||
|
||||
**The regression (K10)** runs the step's `Regression:` command through the
|
||||
criteria runner's two screens (the allowlist of test runners, then the
|
||||
executor denylist), before the checkpoint:
|
||||
|
||||
| Exit | `steps.{N}.regression` | Executor |
|
||||
|------|------------------------|----------|
|
||||
| 0 | `"passed"`, or `"absent"` when the step has no `Regression:` (nothing new happens) | continue |
|
||||
| 1 | `"failed"` (the suite went red) or `"unrunnable"` (not one allowlisted command; `"blocked"` by the denylist counts here too) | the step is not done: a finding for Sub-step F, exactly like a red Verify — it goes into the fix rounds, never to the checkpoint |
|
||||
| 2 | `"not-run"` | the CLI could not run (plan unreadable, no such step): a finding for Sub-step F, never green |
|
||||
|
||||
#### Sub-step E — Control
|
||||
|
||||
**One controller, two verdicts.** After the mechanical checks (B, D1, D2, D3)
|
||||
— whatever they said — launch one controller with the Agent tool:
|
||||
**One controller, two verdicts.** After the mechanical checks (B, D1, D2, D3,
|
||||
D4) — whatever they said — launch one controller with the Agent tool:
|
||||
`subagent_type: "general-purpose"`, `model: "opus"`, no `name` (Sub-step C),
|
||||
no `isolation`. It is a general helper with the prompt below, not a second
|
||||
Voyage agent. The Agent tool sets no reasoning effort, so the controller runs
|
||||
|
|
@ -1589,7 +1759,10 @@ The step, verbatim:
|
|||
Review focus from the plan (look hardest here): {Review focus, or "none"}
|
||||
|
||||
Mechanical results: proof of red {red-proof JSON}; Verify exit {code};
|
||||
manifest {pass | fail + drift}; floor guard {test-integrity JSON}.
|
||||
manifest {pass | fail + drift}; floor guard {test-integrity JSON};
|
||||
step check {step-check JSON}; regression {regression-check JSON}.
|
||||
Every step-check finding is a quality finding unless you show it is not in
|
||||
this step's diff.
|
||||
|
||||
Give two verdicts:
|
||||
- "spec": does the diff do what the step's Changes, Files and Interface say -
|
||||
|
|
@ -1613,7 +1786,8 @@ Append both verdicts, the findings and `not_judged` to the step report.
|
|||
**The round passes** only when ALL hold: `red_proof` is `"proven"` (or, below
|
||||
plan_version 1.8, the tolerated `"not-run"` of Sub-step B), Verify PASS,
|
||||
manifest pass, every floor-guard finding ruled `justified` (and the guard
|
||||
ran), `"spec": "pass"` and `"quality": "pass"`. Then go to Sub-step G.
|
||||
ran), `step_check` is `"clean"` or `"not-measured"` (Sub-step D4), `regression` is `"passed"` or `"absent"`,
|
||||
`"spec": "pass"` and `"quality": "pass"`. Then go to Sub-step G.
|
||||
Otherwise go to Sub-step F with every finding.
|
||||
|
||||
**Re-control after a fix** checks only what was fixed. A fresh controller gets
|
||||
|
|
@ -1635,6 +1809,11 @@ It does not re-judge the rest of the step.
|
|||
|
||||
#### Sub-step F — Fix rounds
|
||||
|
||||
**How a fix helper finds the cause is the `debugging` skill** (S2, Voyage
|
||||
plugin: `voyage:debugging`): it is told it by name (Sub-step C,
|
||||
`skill: debugging`). The skill is the advice; the ladder below and the checks
|
||||
that re-run after every round are the guarantee.
|
||||
|
||||
A failed round never loops sideways. **The fix rounds are capped at maximum 5 fix rounds per step, and they escalate upward** — to a fresh helper, then to
|
||||
the plan, then to the operator. The executor does not improvise a new design,
|
||||
and there is no ladder of cheaper models: every helper and every controller
|
||||
|
|
@ -1643,9 +1822,9 @@ session's effort, Sub-step E).
|
|||
|
||||
| `fix_rounds` | Goes to | What happens |
|
||||
|---|---|---|
|
||||
| 1, 2, 3 | a fresh helper | First `node ${CLAUDE_PLUGIN_ROOT}/lib/util/test-lock.mjs pending` (the step is no longer seen green; D1 marks it again). Launch `voyage:step-implementer` again (no `name`, no `isolation`), `mode: fix`, with the findings (file, line, problem, fix) and the mechanical failures. It fixes only those; the tests stay locked (except in a `not-red-at-base` round, Sub-step B). Then B (both ways), D1, D2, D3 run again and a fresh controller re-controls only what was fixed (Sub-step E). `attempts` +1; `iterations_remaining` −1 |
|
||||
| 1, 2, 3 | a fresh helper | First `node ${CLAUDE_PLUGIN_ROOT}/lib/util/test-lock.mjs pending` (the step is no longer seen green; D1 marks it again). Launch `voyage:step-implementer` again (no `name`, no `isolation`), `mode: fix`, with the findings (file, line, problem, fix) and the mechanical failures. It fixes only those; the tests stay locked (except in a `not-red-at-base` round, Sub-step B). Then B (both ways), D1, D2, D3, D4 run again and a fresh controller re-controls only what was fixed (Sub-step E). `attempts` +1; `iterations_remaining` −1 |
|
||||
| 4 | the plan | **After three failed fix rounds the question is the architecture, not a fourth fix.** The step goes back to planning (below). Stop class 4 |
|
||||
| 5 | the operator | A step already sent to the plan (`fix_rounds` 4) that fails its control again after re-planning goes to the operator. The executor never builds this step again: on a later `--resume` it only runs the checks (B, D1, D2, D3, E) on whatever the operator changed. If they fail again the step stays at 5 — there is no sixth round, `fix_rounds` never exceeds 5 — and the run stops again. Stop class 4 |
|
||||
| 5 | the operator | A step already sent to the plan (`fix_rounds` 4) that fails its control again after re-planning goes to the operator. The executor never builds this step again: on a later `--resume` it only runs the checks (B, D1, D2, D3, D4, E) on whatever the operator changed. If they fail again the step stays at 5 — there is no sixth round, `fix_rounds` never exceeds 5 — and the run stops again. Stop class 4 |
|
||||
|
||||
**Back to planning** (`fix_rounds = 4`): save `git diff {start_sha}` to
|
||||
`{report dir}/step-{N}-upward.diff` and write `{report dir}/step-{N}-upward.md`:
|
||||
|
|
@ -1801,6 +1980,8 @@ carrying a second copy of the denylist.)
|
|||
# indistinguishable from a criterion that failed.
|
||||
# Only the literal form is substituted (a `:-` default is never replaced); the
|
||||
# fallback takes the highest installed version under cache/<market>/voyage/.
|
||||
# zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash.
|
||||
[ -n "${ZSH_VERSION:-}" ] && setopt nonomatch
|
||||
VOYAGE_ROOT="${CLAUDE_PLUGIN_ROOT}"
|
||||
case "$VOYAGE_ROOT" in
|
||||
/*) ;;
|
||||
|
|
@ -1854,7 +2035,12 @@ Record in the progress file (additive-optional; unknown keys are tolerated by
|
|||
|
||||
- `plan_verification.status` — `"passed" | "failed" | "not-run"`
|
||||
- `plan_verification.summary` — `{total, passed, failed, blocked, unrunnable}`
|
||||
- `plan_verification.failed_criteria` — `[{label, command, exit_code}]`
|
||||
- `plan_verification.failed_criteria` — `[{label, command, exit_code}]`, one
|
||||
per runner result whose `status` is not `passed`. The runner's JSON says
|
||||
`exitCode`; progress says `exit_code`, so translate the key explicitly,
|
||||
`exitCode` → `exit_code`, and copy `null` as `null` (a blocked or
|
||||
unrunnable criterion never ran). Never write the runner's key into progress:
|
||||
`progress-validator.mjs` rejects it (`PROGRESS_FAILED_CRITERIA_KEY`, C15).
|
||||
|
||||
**A failing criterion FELLS the run.** It is not "recorded and included in the
|
||||
final report": the executor MUST NOT emit `result: completed` while
|
||||
|
|
@ -1883,6 +2069,188 @@ built thing — not in the run. (A ≥ 1.8 plan cannot reach here with its
|
|||
`End-to-end:` command missing from `## Verification`: the plan validator
|
||||
rejects that as `PLAN_END_TO_END_NOT_VERIFIED`.)
|
||||
|
||||
### Architecture rules (K5) and the plan-file sweep (K6)
|
||||
|
||||
Two whole-run checks, for a trekplan and a session spec alike, in one Bash
|
||||
call. Shell state does not survive between Bash calls, so the block resolves
|
||||
the plugin root itself; an unresolved root prints exit 2, never a `node`
|
||||
exit 1 that would read as a violation.
|
||||
|
||||
```bash
|
||||
# zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash.
|
||||
[ -n "${ZSH_VERSION:-}" ] && setopt nonomatch
|
||||
VOYAGE_ROOT="${CLAUDE_PLUGIN_ROOT}"
|
||||
case "$VOYAGE_ROOT" in
|
||||
/*) ;;
|
||||
*) VOYAGE_ROOT="$(ls -d "$HOME"/.claude/plugins/cache/*/voyage/[0-9]*/ 2>/dev/null \
|
||||
| awk -F/ '{ split($(NF-1), v, "."); printf "%09d%09d%09d %s\n", v[1], v[2], v[3], $(0) }' \
|
||||
| sort | tail -1 | cut -d' ' -f2-)"
|
||||
VOYAGE_ROOT="${VOYAGE_ROOT%/}" ;;
|
||||
esac
|
||||
CHECK_CWD="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
if [ ! -f "$VOYAGE_ROOT/lib/verification/arch-rules.mjs" ]; then
|
||||
echo "[voyage] architecture rules not run - plugin root unresolved"; echo "arch-rules exit 2"
|
||||
else
|
||||
node "$VOYAGE_ROOT/lib/verification/arch-rules.mjs" --repo "$CHECK_CWD"; echo "arch-rules exit $?"
|
||||
fi
|
||||
if [ ! -f "$VOYAGE_ROOT/lib/verification/plan-scope-sweep.mjs" ]; then
|
||||
echo "[voyage] plan-file sweep not run - plugin root unresolved"; echo "scope-sweep exit 2"
|
||||
else
|
||||
node "$VOYAGE_ROOT/lib/verification/plan-scope-sweep.mjs" --plan "{plan_path}" --base "{session_start_sha}" --repo "$CHECK_CWD" \
|
||||
--process .claude/voyage/approvals.jsonl --process .claude/rules/voyage-review-lessons.md {--process CLAUDE.md, see below}; echo "scope-sweep exit $?"
|
||||
fi
|
||||
```
|
||||
|
||||
`{--process CLAUDE.md, see below}` is filled in, or left out, before the call:
|
||||
it is passed only when every commit since the start point that touches
|
||||
`CLAUDE.md` is a review-lessons commit (Phase 7.2), which
|
||||
`git log --format=%s {session_start_sha}..HEAD -- CLAUDE.md` shows. A plan
|
||||
step's own change to `CLAUDE.md` is never hidden by it.
|
||||
|
||||
**K5, the architecture rules (P3/P7)**, runs the target repo's OWN
|
||||
architecture tool (dependency-cruiser, import-linter) and installs nothing:
|
||||
|
||||
| Exit | `arch_rules` | Executor |
|
||||
|------|--------------|----------|
|
||||
| 0 | `"pass"` | continue |
|
||||
| 1 | `"violation"` | a finding in the final report with every violation; the completion gate turns `completed` into `partial` (clause 5) |
|
||||
| 2 | `"not-measured"` | the repo has no such tool, or it could not run. The report says "architecture rules: not measured — {reason}", never "pass". Not a finding |
|
||||
|
||||
**K6, the plan-file sweep (P3)**, from the run's start point
|
||||
(`session_start_sha`, Phase 3): every file the run changed must be named by
|
||||
the plan (a step's `Files:`, its `Test first:` file, its manifest) or be a
|
||||
process file. Voyage's own approval log and the review's project-memory file
|
||||
(and its one-line `CLAUDE.md` pointer, as above) are passed as process files:
|
||||
the run writes them, the plan does not name them.
|
||||
|
||||
| Exit | `scope_sweep` | Executor |
|
||||
|------|---------------|----------|
|
||||
| 0 | `{status: "pass", outside: []}` | continue |
|
||||
| 1 | `{status: "deviation", outside: [...]}` | a finding in the final report with the file list (`outside`), never a silent warning; the completion gate turns `completed` into `partial` (clause 5). A deviation is cleared only by the plan naming the file in the same commit |
|
||||
| 2 | `{status: "not-measured", outside: []}` | it could not run (not a git repo, an unknown start point). "plan-file sweep: not measured" in the report, never "pass" |
|
||||
|
||||
## Phase 7.2 — Simplify, review, verdict (P11)
|
||||
|
||||
**Runs only in the closing run, and only when Phase 7 ran** (every step
|
||||
passed). The closing run is the one that ends a trekplan: a single-session
|
||||
execute or resume of the whole plan, or the wave parent once every wave is
|
||||
merged (Phase 2.6 Step 6). It is never a `--step N` run, a `--session N`
|
||||
child or a session spec: those deliver a part, the review is of the whole
|
||||
delivery, and a wave session's work is reviewed once the wave is merged
|
||||
(Phase 8 leaves their result alone). It starts by itself after the last step,
|
||||
never typed by the operator, in this order (Cherny's):
|
||||
|
||||
1. **run the thing** — Phase 7's `## Verification` and its `End-to-end:`
|
||||
criterion already ran (on the wave path: Step 4, which runs the same
|
||||
criteria runner). `end_to_end` must be `"verified"`; otherwise skip the rest
|
||||
of this phase (`review = {round: 1, path: null, gate_exit: 2, verdict: null}`:
|
||||
there is nothing proven to review) and go to Phase 7.5.
|
||||
2. **`/simplify`** — Claude Code's bundled cleanup skill: four agents look at
|
||||
reuse, simplification, efficiency and altitude, and it applies the fixes.
|
||||
Measured 2026-10-08: the Skill tool starts it from the session. It reviews
|
||||
"the changed code": its upstream range, else `main...HEAD` or `HEAD~1`, plus
|
||||
uncommitted changes, or the PR, branch or path given as its argument. Give
|
||||
it the run as its argument:
|
||||
`Skill("simplify", args: "the changes of this run: git diff {session_start_sha}..HEAD — files: {git diff --name-only {session_start_sha} HEAD}")`.
|
||||
Wait for it to finish. Then, from the working tree:
|
||||
- **Outside the run's changed files:** every path it changed that is not
|
||||
in `git diff --name-only {session_start_sha} HEAD` is restored
|
||||
(`git checkout -- {path}`; a new file it made is removed from the index
|
||||
with `git rm --cached -q -- {path}` and left on disk), and listed in the
|
||||
final report. Nothing it did is dropped silently.
|
||||
- **Nothing changed:** `simplify = "clean"`.
|
||||
- **Changes:** run Phase 7's criteria runner again (same block, same exit
|
||||
table). Exit 0 → stage exactly the changed paths and commit them as
|
||||
`refactor: simplify pass (trekexecute)`; `simplify = "applied"`. Exit 1 or
|
||||
2 → restore every path it changed as above, `simplify = "reverted"`, and
|
||||
say why in the report: the cleanup broke the verification.
|
||||
- **It could not start** (the Skill tool refused, the skill is not there):
|
||||
`simplify = "not-run"`, with the reason. Not a stop: `/simplify` is a
|
||||
cleanup pass, not a gate. Continue.
|
||||
3. **`/trekreview`** — Voyage's review gate: Claude Code's `/code-review` and
|
||||
`/security-review` as the engine, then Voyage's control against the brief,
|
||||
which is what stops. Start it through the Skill tool, with the run's range:
|
||||
`Skill("voyage:trekreview", args: "--project {project_dir} --since {session_start_sha}")`.
|
||||
A plugin command is a skill the model may start unless it carries
|
||||
`disable-model-invocation`, which `/trekreview` does not (the Claude Code
|
||||
skills documentation, read 2026-10-08). Follow its phases to the end in
|
||||
this session; its own questions (project-memory lines, one by one) are its
|
||||
own. **Without `--project`** there is no brief to review against:
|
||||
`review = {round: 1, path: null, gate_exit: 2, verdict: null}`, "review not
|
||||
run — no project directory", and the merge stays closed (Phase 8).
|
||||
**Its writes are committed.** `/trekreview` may have written approved
|
||||
project-memory lines (`.claude/rules/voyage-review-lessons.md`) and a
|
||||
one-line pointer in `CLAUDE.md`. Commit exactly those paths, when they
|
||||
changed, as `docs(voyage): review lessons (round {R})`: a process commit
|
||||
(Phase 7.5 step 3). Nothing the review wrote is left uncommitted for the
|
||||
next round's checks or for the merge.
|
||||
4. **`verdict-gate.mjs`** — the review's verdict decides, as a command:
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/review/verdict-gate.mjs --json "{project_dir}/review.md"
|
||||
```
|
||||
Record `review = {round, path, gate_exit, verdict}`. Exit 0 (ALLOW/WARN) →
|
||||
Phase 7.5. Exit 2 (could not decide, or a reviewer or engine that did not
|
||||
run) → Phase 7.5 with the merge closed: never green. Exit 1 (BLOCK) → the
|
||||
loop below.
|
||||
|
||||
**A BLOCK is fixed by new plan steps, in a bounded loop.** The review writes
|
||||
every BLOCKER as a numbered `### Step R<n>` under `## Remediation Steps`
|
||||
(Handover 6: Files, Source finding, Test first, Verify, On failure). The fix is
|
||||
those steps, not improvised. Every stop below is stop class 4 (Phase 5.5):
|
||||
the run is `stopped`, and the merge stays closed.
|
||||
|
||||
0. **On the wave path there is no loop.** The plan's sha256 binds the
|
||||
merged-waves record (Phase 2.6), so new steps would strand the waves. Stop,
|
||||
and tell the operator to re-plan from the review: `/trekplan --brief
|
||||
{project_dir}/review.md` (Handover 6's remediation plan).
|
||||
1. Copy the approved plan aside: `{report dir}/plan-approved-round-{R}.md`. It
|
||||
is the text the log approved, and it is how every stop below leaves the plan
|
||||
matching its approval.
|
||||
2. Append each `### Step R<n>` to the plan's `## Implementation Plan` as the
|
||||
next numbered step (`### Step {total + 1}: …`, then +2, …), its fields
|
||||
copied as they are, never reworded. Add a `Checkpoint:`
|
||||
(`git commit -m "fix(review): {title} [{finding id, first 8}]"`), and on a
|
||||
plan_version ≥ 1.7 plan the manifest the legacy rule synthesizes
|
||||
(Phase 2: `expected_paths` ← its Files, …): a rule already in this file, not
|
||||
a new design. Earlier steps are never touched.
|
||||
3. Check that the new steps can run, before anyone is asked:
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs --strict --json "{plan_path}"
|
||||
```
|
||||
Any exit but 0 (a `Verify:` that is not one test-runner call, a
|
||||
`Test first:` without its `File:`, …): restore the plan from the copy,
|
||||
save the appended text as `{report dir}/review-round-{R}-steps.md`, and
|
||||
stop. Say that the review's remediation steps are not executable as plan
|
||||
steps ({first error code}) and that the way on is a re-plan from the review,
|
||||
`/trekplan --brief {project_dir}/review.md`.
|
||||
4. The plan changed, so its approval is stale (Phase 1). Ask for it again in
|
||||
one question, `AskUserQuestion`: **"The review blocked: {N} BLOCKER(s),
|
||||
added as steps {list}. Approve the plan as it is now?"** Options:
|
||||
**Approve** / **Not yet**. Check the answer with
|
||||
`approval-log.mjs answer` (stdin) exactly as `/trekplan` Phase 11 does;
|
||||
exit 0 → `approval-log.mjs append --kind plan --subject "{plan_path}" --sha256-of "{plan_path}"`
|
||||
and `artifact-commit.mjs --store {artifact_store} --artifact "{plan_path}" --message "docs(voyage): approve plan {slug}"`.
|
||||
This is an approval of new plan content, not an execution decision
|
||||
(Hard rule 1), and not a merge: no guard asks it.
|
||||
5. **Approve** → add the new steps to progress (`steps.{M}` pending,
|
||||
`total_steps` raised), run Phase 6 for them (the same loop: helper, checks,
|
||||
controller, fix rounds), then Phase 7, then this phase again with
|
||||
`review.round` + 1. **Not yet**, or an answer that is not an approval →
|
||||
restore the plan from the copy, save the appended text as
|
||||
`{report dir}/review-round-{R}-steps.md`, and stop. A later `--resume`
|
||||
finds the plan as approved and comes back to this review.
|
||||
|
||||
**The cap:** `TREKEXECUTE_MAX_REVIEW_ROUNDS` reviews per run, default **3**
|
||||
(the first review and two fix loops; env-overridable).
|
||||
A BLOCK at the cap goes to the operator: stop, and the report names every
|
||||
BLOCKER still open. Each loop also takes 1 from `iterations_remaining`, which
|
||||
bounds it a second time (the completion gate's clause 3 counts it).
|
||||
|
||||
Borrowed: the order (run the thing, simplify, review) is Cherny's; a verdict in
|
||||
a file that a command gates on, and the bounded fix-and-review loop, are
|
||||
Kiro's `feature-pipeline` gate and Spec Kit's `/converge` loop (MIT); that no
|
||||
finding is dropped silently is Superpowers' (MIT). Ideas only.
|
||||
|
||||
## Phase 7.5 — Manifest audit (independent)
|
||||
|
||||
**Runs for all modes except dry-run.** This is the last-line-of-defense
|
||||
|
|
@ -1902,11 +2270,19 @@ produces an independent truth based on objective state, not self-narrative.
|
|||
|
||||
3. **Commit-count check:** count commits since session-start
|
||||
(`git rev-list --count {session_start_sha}..HEAD`). Expected count =
|
||||
number of steps with `status=completed` (excludes Step 0 if sandbox_preflight).
|
||||
number of steps with `status=completed` (excludes Step 0 if sandbox_preflight)
|
||||
+ the run's own process commits, and only these: the simplify commit
|
||||
(`refactor: simplify pass (trekexecute)`, when `simplify = "applied"`), each
|
||||
review-lessons commit (`docs(voyage): review lessons (round R)`) and each
|
||||
plan approval the review loop committed (`docs(voyage): approve plan …`),
|
||||
all from Phase 7.2. Each is named in the audit, never folded in silently.
|
||||
(A remediation step's commit is a completed step like any other; the
|
||||
Phase 8 merge comes after the audit.)
|
||||
|
||||
4. **Commit-message pattern sweep:** walk `git log {session_start_sha}..HEAD` and
|
||||
confirm each commit matches one of the declared
|
||||
`commit_message_patterns` (in any order — order is advisory).
|
||||
`commit_message_patterns` (in any order — order is advisory). The process
|
||||
commits of step 3 match their own fixed messages above.
|
||||
|
||||
5. **Bash syntax sweep:** for every `.sh` file in
|
||||
`git diff --name-only {session_start_sha} HEAD`, run `bash -n`. Collect failures.
|
||||
|
|
@ -1960,7 +2336,8 @@ Record in progress file:
|
|||
> 3. **Counter liveness (deterministic, defeats "never decremented")** — the gate
|
||||
> reconciles the budget counter against the ground-truth counters the Phase 7.5
|
||||
> audit already derives:
|
||||
> `iterations_remaining == TREKEXECUTE_MAX_RECOVERY_ITERATIONS − (recovery_depth + Σ steps.*.attempts-beyond-first)`.
|
||||
> `iterations_remaining == TREKEXECUTE_MAX_RECOVERY_ITERATIONS − (recovery_depth + Σ steps.*.attempts-beyond-first + (review.round − 1))`
|
||||
> (the last term is 0 without a review; each review loop of Phase 7.2 takes 1).
|
||||
> A field left un-decremented (or tampered) fails this reconciliation and the
|
||||
> result is OVERRIDDEN `completed → partial`, exactly as the audit-override
|
||||
> already does (`:1221`). So the counter's liveness is auditable at the gate,
|
||||
|
|
@ -1968,6 +2345,15 @@ Record in progress file:
|
|||
> 4. **End to end** — for a trekplan, Phase 7's `end_to_end == "verified"`.
|
||||
> `"not-verified"` OVERRIDES `completed → partial` with the reason
|
||||
> "not verified end to end": tests that pass are not the thing running.
|
||||
> 5. **Architecture and plan scope** — Phase 7's `arch_rules` is not
|
||||
> `"violation"` and `scope_sweep.status` is not `"deviation"`. Either one
|
||||
> OVERRIDES `completed → partial`, and the report lists the violations or
|
||||
> the files outside the plan. `"not-measured"` does not override, and it is
|
||||
> never reported as a pass.
|
||||
>
|
||||
> The review verdict (Phase 7.2) is decided after this gate, by the lock in
|
||||
> Phase 8: in the closing run, a closed lock or a declined merge turns
|
||||
> `completed` into `partial`, as a declined gate always has.
|
||||
>
|
||||
> Termination authority therefore rests on the deterministic audit + an
|
||||
> objective stop-signal, never on the model's narrative. Native `/goal` (when
|
||||
|
|
@ -2085,22 +2471,122 @@ Run each event call as its own command, never chained with `&&`. The writer
|
|||
prints `{"written": true, ...}`. On `"written": false` (exit 1), print its
|
||||
`reason` in one line and continue — an event failure never blocks the gate.
|
||||
|
||||
Pause for operator confirmation via `AskUserQuestion`:
|
||||
**The gate is a lock, not a question** (operator decision 2026-10-08). The
|
||||
guard (`pre-bash-executor`) asks the operator before every merge into main and
|
||||
every push, in auto mode too. When the executor first asked "ready to merge?"
|
||||
itself, one merge cost two questions. Now the executor decides whether the
|
||||
merge may run at all, and the guard's question is the one question.
|
||||
|
||||
**Question:** "All waves merged. Ready to proceed with the main-merge step
|
||||
and finalize the run?"
|
||||
**Only the closing run passes the lock** (Phase 7.2: a single-session run of
|
||||
the whole plan, or the wave parent after the last wave). A `--session N`
|
||||
child, a `--step N` run and a session spec deliver a part: they emit no gate
|
||||
event, merge nothing, and their result is left as Phase 7.5 decided it
|
||||
(`main_merge: "n/a"`). A wave session's `completed` must reach its parent as
|
||||
`completed`, or Step 3 would read the wave as failed.
|
||||
|
||||
| Option | Action |
|
||||
|--------|--------|
|
||||
| **Confirm — main-merge** | Emit `main-merge-approved`, proceed to write progress + state files. |
|
||||
| **Decline — hold for review** | Emit `main-merge-declined`. Nothing is finalized: the run is recorded as `partial`. The commits stay where they are for review — on the multi-session wave path the waves are already merged on the current branch, and the merged-waves record is kept. |
|
||||
**1. The verdict.** Decide with the review's verdict as a command, never from
|
||||
a summary:
|
||||
|
||||
This pause is **always on**, regardless of `gates_mode`. The `--gates` flag
|
||||
re-enables earlier per-wave pauses; this gate is the one boundary that
|
||||
ALWAYS pauses on every run — it is the safety stop between completed waves
|
||||
and the merge that publishes the integrated result.
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/review/verdict-gate.mjs --json "{review_path}"
|
||||
```
|
||||
|
||||
`{review_path}` is `review.path` from Phase 7.2. No review (Phase 7.2 did not
|
||||
run, ran without a project directory, or the run stopped before it) counts as
|
||||
exit 2 without the call.
|
||||
|
||||
| Exit | Verdict | Merge |
|
||||
|------|---------|-------|
|
||||
| 0 | ALLOW / WARN | open, when the run's result is `completed` (Phase 7.5) |
|
||||
| 1 | BLOCK | closed |
|
||||
| 2 | not measured | closed: a review that could not decide is never green |
|
||||
|
||||
The merge runs only on exit 0 and a `completed` result. **Closed:** emit
|
||||
`main-merge-declined` with the reason `review-block`, `review-not-measured` or
|
||||
`run-not-completed`. Nothing is finalized: a `completed` result becomes
|
||||
`partial` (`failed` and `stopped` stay), and the commits stay where they are
|
||||
for review. Say in one line why it is closed and what opens it: the
|
||||
BLOCKERs fixed through Phase 7.2's loop, a review that runs in full, or a run
|
||||
that completes.
|
||||
|
||||
**2. Open: the merge, and the one question.** Find the run's branch and where
|
||||
main is checked out, in a Bash call of its own:
|
||||
|
||||
```bash
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
for b in main master; do git rev-parse -q --verify "refs/heads/$b" >/dev/null && echo "MAIN=$b" && break; done
|
||||
git worktree list --porcelain
|
||||
```
|
||||
|
||||
- **No `MAIN=` line** (the repo has neither branch): there is nothing to merge
|
||||
into. Emit `main-merge-declined` with the reason `no-main-branch`.
|
||||
- **The run's branch IS main (or master):** there is nothing to merge; the
|
||||
commits are already on main. Emit `main-merge-approved` and say in the
|
||||
report that the review ran after the commits landed on main and could not
|
||||
hold them back: run on a branch when the review should gate the merge.
|
||||
- **Another branch:** merge it into main in ONE Bash call, with the values
|
||||
written out as literals (the guard reads the command; it cannot follow a
|
||||
shell variable). When `git worktree list` shows main checked out in a
|
||||
worktree, merge there:
|
||||
```bash
|
||||
if git -C "{main worktree path}" merge --no-ff "{run branch}" -m "merge: trekexecute {slug}"; then
|
||||
echo "MERGED"
|
||||
elif git -C "{main worktree path}" rev-parse -q --verify MERGE_HEAD >/dev/null; then
|
||||
git -C "{main worktree path}" diff --name-only --diff-filter=U
|
||||
git -C "{main worktree path}" merge --abort
|
||||
echo "MERGE_CONFLICT"
|
||||
else
|
||||
echo "MERGE_REFUSED"
|
||||
fi
|
||||
```
|
||||
Otherwise switch this tree to main, and back to the run's branch on any
|
||||
failure:
|
||||
```bash
|
||||
git switch "{main}" || { echo "MERGE_REFUSED"; exit 1; }
|
||||
if git merge --no-ff "{run branch}" -m "merge: trekexecute {slug}"; then
|
||||
echo "MERGED"
|
||||
elif git rev-parse -q --verify MERGE_HEAD >/dev/null; then
|
||||
git diff --name-only --diff-filter=U
|
||||
git merge --abort
|
||||
git switch "{run branch}"
|
||||
echo "MERGE_CONFLICT"
|
||||
else
|
||||
git switch "{run branch}"
|
||||
echo "MERGE_REFUSED"
|
||||
fi
|
||||
```
|
||||
The guard answers "ask" before it runs (measured 2026-10-08/09 against
|
||||
both blocks: they ask, and a merge into another branch does not). The
|
||||
operator's answer there is the one question; the post-approval hook logs it
|
||||
as a `merge` approval (K2). The executor asks nothing before or after it. In
|
||||
the permission mode where Claude Code asks nothing, the guard refuses the
|
||||
call instead, because there is no one to ask (operator decision 2026-10-09).
|
||||
- `MERGED` → emit `main-merge-approved`.
|
||||
- The call did not run (the operator declined at the guard's question, or
|
||||
the guard refused it) → emit `main-merge-declined` with the reason
|
||||
`hold-for-review`; the run is `partial`, and the commits stay on the run's
|
||||
branch. When the guard refused, say so, and that the operator merges by
|
||||
hand.
|
||||
- `MERGE_CONFLICT` → list the conflicting files the block printed (the
|
||||
merge is already aborted), emit `main-merge-declined` with the reason
|
||||
`merge-conflict`; the run is `partial`. Never resolve a conflict with
|
||||
`-X ours/theirs`.
|
||||
- `MERGE_REFUSED` → git refused to start (local changes in the main tree, or
|
||||
untracked files in the way: its message says which). Emit
|
||||
`main-merge-declined` with the reason `merge-refused` and report git's
|
||||
message; the run is `partial`.
|
||||
|
||||
This gate is **always on**, regardless of `gates_mode`: a lock that every run
|
||||
passes through, the one boundary that ALWAYS pauses on every run, either at
|
||||
its lock or at the guard's question. It is the safety stop between completed
|
||||
work and the merge that publishes the integrated result. On the multi-session
|
||||
wave path the waves are already merged on the current branch, the branch the
|
||||
run started on (Phase 2.6 Step 3, one guard question per session); when that
|
||||
branch is not main, this gate merges it into main like a single-session run,
|
||||
and the merged-waves record is kept until the confirm below.
|
||||
|
||||
On confirm:
|
||||
(the merge ran, or there was nothing to merge)
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs --event main-merge-approved --payload - <<'JSON'
|
||||
{"plan":"{plan-path}"}
|
||||
|
|
@ -2117,21 +2603,25 @@ cleared:
|
|||
```
|
||||
|
||||
On decline:
|
||||
(a closed lock, or the guard's question declined, or a conflict)
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs --event main-merge-declined --payload - <<'JSON'
|
||||
{"plan":"{plan-path}","reason":"hold-for-review"}
|
||||
{"plan":"{plan-path}","reason":"{reason}"}
|
||||
JSON
|
||||
```
|
||||
|
||||
The reason is the literal label of the one decline option, never the
|
||||
operator's free text (free text breaks the JSON).
|
||||
The reason is one of the literal words above (`review-block`,
|
||||
`review-not-measured`, `run-not-completed`, `no-main-branch`, `hold-for-review`,
|
||||
`merge-conflict`, `merge-refused`), never the operator's free text (free text breaks the JSON).
|
||||
|
||||
On decline, nothing is cleared: the merged-waves record is kept.
|
||||
|
||||
**Recovery surface:** on the single-session path, if declined,
|
||||
`--resume re-enters at the gate` — re-running
|
||||
`/trekexecute --resume --project <dir>` skips the completed steps and returns
|
||||
to the main-merge gate AskUserQuestion. On the multi-session wave path,
|
||||
to the main-merge gate: the lock reads `review.md` again (a review fixed or
|
||||
re-run in between counts), and when it opens, the guard asks again. On the
|
||||
multi-session wave path,
|
||||
re-run the same `/trekexecute` command instead, not `--resume` (Phase 2.5 runs
|
||||
a `--resume` as a single session): the waves are already merged on the current
|
||||
branch, the kept merged-waves record makes Step 0's `next-wave` say `none`,
|
||||
|
|
@ -2256,6 +2746,17 @@ copy · 4 plan so broken that everything after it is guesswork, and the file or
|
|||
- **Recovery depth:** {N}
|
||||
- **Legacy plan:** {true | false}
|
||||
|
||||
### Checks and review (wave 2)
|
||||
|
||||
- **Plan approval (K2):** {approved | not checked (session spec)}
|
||||
- **Step checks (K9):** {N clean · N with findings (fixed in rounds) · N not measured: steps {list}}
|
||||
- **Regressions (K10):** {N passed · N absent · failed: steps {list}}
|
||||
- **Architecture rules (K5):** {pass | violation: {each one} | not measured — {reason}}
|
||||
- **Plan-file sweep (K6):** {pass | deviation — files outside the plan: {outside, one per line} | not measured — {reason}}
|
||||
- **Simplify:** {applied (commit {hash}) | clean | reverted — {why} | not run — {reason}}{; edits outside the run restored: {paths}}
|
||||
- **Review (K7):** round {R} of {cap} · {review_path} · verdict {ALLOW | WARN | BLOCK | none} · gate exit {0 | 1 | 2}{; open BLOCKERs: {ids}}
|
||||
- **Merge into main:** {merged ({commit}) after the guard's question | nothing to merge (the run was on main) | closed — {reason} | declined at the guard's question | n/a — a part of the plan (`--session N`, `--step N`, a session spec)}
|
||||
|
||||
### Summary
|
||||
|
||||
- Passed: {N}/{total}
|
||||
|
|
@ -2292,7 +2793,9 @@ To continue (multi-session waves): re-run the same /trekexecute command (not --r
|
|||
(for trekplans) Phase 7 plan verification passed
|
||||
- `partial` — steps passed per executor but Phase 7.5 found drift, OR
|
||||
Phase 7.6 recovery incomplete, OR the run is not verified end to end
|
||||
(Phase 7, gate clause 4)
|
||||
(Phase 7, gate clause 4), OR an architecture violation or a file outside the
|
||||
plan (gate clause 5), OR — in the closing run — the main-merge lock stayed
|
||||
closed or the merge was declined (Phase 8)
|
||||
- `blocked` — Step 0 sandbox pre-flight exited 77; no real work attempted
|
||||
- `failed` — a step failed and went up the fix ladder (On failure revert/retry: round 4 to the plan, or 5 to the operator)
|
||||
- `stopped` — On failure: escalate triggered, or another stop on the closed stop list (Phase 5.5)
|
||||
|
|
@ -2325,7 +2828,15 @@ To continue (multi-session waves): re-run the same /trekexecute command (not --r
|
|||
"end_to_end": "{verified | not-verified | n/a}",
|
||||
"steps_without_red_proof": 0,
|
||||
"stop_class": "{none | 1 | 2 | 3 | 4}",
|
||||
"decisions": 0
|
||||
"decisions": 0,
|
||||
"plan_approval": "{approved | not-checked}",
|
||||
"steps_check_not_measured": 0,
|
||||
"arch_rules": "{pass | violation | not-measured | n/a}",
|
||||
"scope_sweep": "{pass | deviation | not-measured | n/a}",
|
||||
"simplify": "{applied | clean | reverted | not-run | n/a}",
|
||||
"review_verdict": "{ALLOW | WARN | BLOCK | none}",
|
||||
"review_gate_exit": "{0 | 1 | 2}",
|
||||
"main_merge": "{merged | nothing-to-merge | closed | declined | n/a}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -2450,7 +2961,7 @@ When `phase_signal_result.effort == 'high'` for the `execute` phase,
|
|||
set `gates_mode = 'closed'` automatically (equivalent to passing
|
||||
`--gates closed` on the command line). All autonomy boundaries become
|
||||
operator-stopping points: manifest-audit FAIL halts immediately, the
|
||||
main-merge gate pauses for operator confirm, and Phase 7.5
|
||||
main-merge gate stays a lock whose one question is the guard's (Phase 8), and Phase 7.5
|
||||
manifest-audit runs in its strictest form (fails on ANY mismatch, not
|
||||
just deltas above threshold). Operator explicit `--gates` flag still
|
||||
wins over the brief signal — if the operator passes `--gates open` on
|
||||
|
|
@ -2479,7 +2990,11 @@ code-path.
|
|||
from the plan's On failure clauses. If the plan says escalate, stop and
|
||||
report — never ask. What the plan leaves open, the executor decides and
|
||||
writes into the decision log with its `cost_if_wrong`; it stops only on the
|
||||
closed stop list (Phase 5.5).
|
||||
closed stop list (Phase 5.5). The one question the executor asks itself is
|
||||
an approval, not an execution decision: the plan's approval after the
|
||||
review loop added remediation steps (Phase 7.2). A merge or a push is asked
|
||||
by the guard, and the executor never asks again (operator decision
|
||||
2026-10-08).
|
||||
|
||||
2. **No scope creep.** Only touch files listed in the step's `Files:` field.
|
||||
If a file outside the list seems to need changing, record it as a finding
|
||||
|
|
@ -2586,6 +3101,8 @@ code-path.
|
|||
cleanup proceeds regardless. Rationale: this converts an unrecoverable
|
||||
failure (worktree removed, branch deleted, work lost) into a recoverable
|
||||
one (push succeeded, branch preserved on remote). Source: research/02 R3.
|
||||
The push and the merge go in one Bash call, so the guard asks once for
|
||||
both (Phase 2.6 Step 3).
|
||||
|
||||
20. **Three distinct iteration axes.** Per-step `fix_rounds` (≤5, upward),
|
||||
per-session `recovery_depth` (<2), and the global
|
||||
|
|
@ -2605,6 +3122,17 @@ code-path.
|
|||
`done-line.mjs` — never the helper's or the executor's statement. Exit 2
|
||||
from any of them ("could not run") is never green.
|
||||
|
||||
23. **The review is a gate, and the guard asks the one question.** A run
|
||||
starts only on a plan approved as it is now (K2, Phase 1). It ends with
|
||||
`/simplify`, `/trekreview` and the verdict gate, started by the executor
|
||||
itself (Phase 7.2). Exit 1 (BLOCK) or 2 (not measured) from the verdict
|
||||
gate keeps the merge into main closed (Phase 8). A BLOCKER is fixed by new
|
||||
numbered plan steps the operator approves, in a loop capped at
|
||||
`TREKEXECUTE_MAX_REVIEW_ROUNDS`, never by improvising. When the lock
|
||||
opens, the guard asks before the merge, and the executor never asks again.
|
||||
Exit 2 from the step check (K9), the architecture rules (K5) or the
|
||||
plan-file sweep (K6) is "not measured", never green.
|
||||
|
||||
## Credits
|
||||
|
||||
Every borrowing in this executor is named here, never silent. All sources are
|
||||
|
|
@ -2632,4 +3160,17 @@ pins and quotes in the local practice analysis.
|
|||
- **Cherny** (public primary sources): "run the thing" — a plan is verified
|
||||
end to end only by a criterion that runs what was built (P9 (6)).
|
||||
- **Claude Code** itself: subagents for the helper and the controller; the
|
||||
`PreToolUse` guard behind the test lock (K8).
|
||||
`PreToolUse` guard behind the test lock (K8); `/simplify`, `/code-review` and
|
||||
`/security-review` as the review's engine, started through the Skill tool
|
||||
(P11).
|
||||
- **The review at the end (P11, wave 2)** — Cherny: the order — run the
|
||||
thing, simplify, then review.
|
||||
Kiro (`feature-pipeline`, documentation only): the gate — a verdict in a file.
|
||||
Spec Kit (MIT, `/converge`): the bounded fix-and-review loop, its
|
||||
remediation steps appended and never reworded.
|
||||
Superpowers (MIT): nothing is dropped silently — what the controller did not
|
||||
judge, and every edit restored, is listed.
|
||||
- **The checks wired in (P7, P3, P12)** — agent-skills (MIT): the step check's
|
||||
tiers and "not measured"; ai-native-sdlc (MIT): the sweep for files outside
|
||||
the plan and the guard + log; Superpowers (MIT): the four classes the guard
|
||||
asks about. Ideas only.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue