3255 lines
165 KiB
Markdown
3255 lines
165 KiB
Markdown
---
|
||
name: trekexecute
|
||
description: Disciplined plan executor — single-session, or multi-session through session orders the user starts, with failure recovery
|
||
argument-hint: "[--project <dir>] [--profile <name>] [--gates [true|false|closed|open]] [--fg | --resume | --dry-run | --validate | --step N | --session N] [plan.md]"
|
||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent, Skill
|
||
disallowed-tools: TeamCreate
|
||
---
|
||
|
||
# Ultraexecute Local
|
||
|
||
Disciplined executor for trekplan plans. Reads a plan file, detects if it has
|
||
an Execution Strategy (multi-session), and either executes directly or
|
||
prepares one session order per session, wave by wave — all to realize one plan.
|
||
|
||
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 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
|
||
themselves (Phase 2.6). Helpers inside this session are subagents, and helpers
|
||
never start helpers.
|
||
|
||
## Phase 1 — Parse mode and validate input
|
||
|
||
Parse `$ARGUMENTS` for mode flags:
|
||
|
||
1. If arguments contain `--project <dir>`: extract the directory path.
|
||
- Resolve `{dir}` (trim trailing slash).
|
||
- Set **project_dir = {dir}**.
|
||
- Derive implicit file path: `{dir}/plan.md`.
|
||
- If `{dir}` does not exist or `{dir}/plan.md` is missing:
|
||
```
|
||
Error: project directory missing plan.md: {dir}
|
||
Run /trekplan --project {dir} to produce it.
|
||
```
|
||
- If no explicit `<plan.md>` argument is provided after flag parsing,
|
||
use the derived `{dir}/plan.md`. If a `<plan.md>` argument IS provided,
|
||
use that path but keep `project_dir` set (user may be pointing at a
|
||
recovery session spec inside the project).
|
||
- Continue parsing remaining flags (they combine with `--project`).
|
||
- Brief check, once every flag below is parsed: when `{dir}/brief.md`
|
||
exists, run the two lines `/trekplan` runs on it (its Phase 1), for the
|
||
execute phase; append `--profile {profile}` when the operator passed
|
||
`--profile`:
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase execute --brief-path "{dir}/brief.md" [--profile {profile}] --json
|
||
```
|
||
The validator exits 1 when its `errors` are not empty; read its JSON
|
||
either way. Halt only when the validator's `errors` hold
|
||
`BRIEF_V51_MISSING_SIGNALS`, with the message from "Sequencing gate
|
||
surface" below. Any other error is printed in one line and the run
|
||
continues. A validator or resolver that cannot run (no JSON) is "not
|
||
measured" in one line, and the run continues. Capture the resolver's
|
||
JSON as `phase_signal_result` (Composition rule below). No
|
||
`{dir}/brief.md`: skip both lines.
|
||
2. If arguments contain `--fg`: extract the file path. Set **mode = foreground**.
|
||
3. If arguments contain `--resume`: extract the file path. Set **mode = resume**.
|
||
4. If arguments contain `--dry-run`: extract the file path. Set **mode = dry-run**.
|
||
5. If arguments contain `--validate`: extract the file path. Set **mode = validate**.
|
||
6. If arguments contain `--step N` (N is a positive integer): extract N and the file path.
|
||
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`: `--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. 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
|
||
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. If arguments contain `--profile <name>`: set `profile = {name}`; strip both tokens.
|
||
10. Otherwise: the arguments left after every flag above is stripped are the file path.
|
||
Set **mode = execute**.
|
||
|
||
If no path is provided (and `--project` was not used to derive one), output
|
||
usage and stop:
|
||
|
||
```
|
||
Usage: /trekexecute <plan.md>
|
||
/trekexecute --project <dir>
|
||
/trekexecute --fg <plan.md>
|
||
/trekexecute --resume <plan.md>
|
||
/trekexecute --dry-run <plan.md>
|
||
/trekexecute --validate <plan.md>
|
||
/trekexecute --step N <plan.md>
|
||
/trekexecute --session N <plan.md>
|
||
/trekexecute --profile <name> <plan.md>
|
||
/trekexecute --project <dir> --resume
|
||
/trekexecute --project <dir> --session N
|
||
|
||
Modes:
|
||
(default) Auto — multi-session if plan has Execution Strategy, else foreground
|
||
--project Resolve plan.md inside an trekbrief project folder; progress.json
|
||
is written there too
|
||
--fg Force foreground — all steps sequentially, ignore Execution Strategy
|
||
--resume Resume from last progress checkpoint
|
||
--dry-run Validate plan and show execution strategy without running
|
||
--validate Schema-only check — parse steps + manifests, no security scan, no execution
|
||
--step N Execute only step N (foreground)
|
||
--session N Execute only session N from the plan's Execution Strategy
|
||
--profile Profile for this run's resolver; the step helper and the controller stay on opus
|
||
--gates Record gates_mode: bare, true or closed = on; false or open = off
|
||
|
||
Examples:
|
||
/trekexecute .claude/plans/trekplan-2026-04-06-auth-refactor.md
|
||
/trekexecute --project .claude/projects/2026-04-18-jwt-auth
|
||
/trekexecute --project .claude/projects/2026-04-18-jwt-auth --resume
|
||
/trekexecute --fg .claude/plans/trekplan-2026-04-06-auth-refactor.md
|
||
/trekexecute --session 2 .claude/plans/trekplan-2026-04-06-auth-refactor.md
|
||
/trekexecute --dry-run .claude/plans/trekplan-2026-04-06-auth-refactor.md
|
||
/trekexecute --validate .claude/plans/trekplan-2026-04-06-auth-refactor.md
|
||
```
|
||
|
||
If the file does not exist, report and stop:
|
||
```
|
||
Error: file not found: {path}
|
||
```
|
||
|
||
Report detected mode:
|
||
```
|
||
Mode: {execute | resume | dry-run | step N | session N}
|
||
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**:
|
||
|
||
- **Session spec**: contains `## Dependencies` with `Entry condition:` AND `## Scope Fence`
|
||
AND `## Exit Condition` sections.
|
||
- **Voyage**: contains `## Implementation Plan` with numbered `### Step N:` headings
|
||
but no `## Scope Fence`.
|
||
|
||
If neither structure is detected, report and stop:
|
||
```
|
||
Error: unrecognized file format. Expected an trekplan or session spec.
|
||
```
|
||
|
||
### Parse steps
|
||
|
||
Extract every `### Step N: {description}` heading (in order). For each step, extract:
|
||
- **Files** — file paths to create or modify
|
||
- **Changes** — what to modify
|
||
- **Reuses** — existing code to leverage (informational)
|
||
- **Test first** — the test the step writes before it builds: its `File:`
|
||
(existing | new), what it `Verifies:`, an optional `Pattern:` (required from
|
||
plan_version ≥ 1.8, see below). Its files are the step's test files, as Sub-step B defines them.
|
||
- **Verify** — command to run after implementation: the first backticked
|
||
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: 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):
|
||
what the step consumes and produces, and what its controller must look at
|
||
hardest. Read when present and passed to the helper and the controller;
|
||
never required, and the executor tolerates their absence in every plan
|
||
version.
|
||
|
||
If a step is missing `On failure`, default to `escalate` and record a parse warning.
|
||
If a step is missing `Verify`, record a parse warning.
|
||
|
||
**`Test first:` is required from plan_version ≥ 1.8.** A ≥ 1.8 step without a
|
||
`Test first:` field (or whose `File:` is missing) is not executable: STOP with
|
||
```
|
||
Error: plan_version={v} but step {N} has no Test first: field.
|
||
Re-run /trekplan - from plan_version 1.8 every step proves red before it builds.
|
||
```
|
||
For plans below 1.8 the field stays optional: a step without it runs, its
|
||
`red_proof` is recorded as `"not-run"`, and the final report counts it among
|
||
the steps without proof of red (never hidden, never called proven).
|
||
|
||
### Parse the Manifest block (v1.7+)
|
||
|
||
Read the plan header for a `plan_version` marker. Treat ≥ `1.7` as strict
|
||
mode; absence or <1.7 as legacy mode.
|
||
|
||
**Strict mode (plan_version ≥ 1.7):**
|
||
- Every step MUST have a `Manifest:` block with a YAML fenced payload.
|
||
- Parse the YAML. Required keys: `expected_paths` (list), `min_file_count`
|
||
(integer), `commit_message_pattern` (string), `bash_syntax_check` (list),
|
||
`forbidden_paths` (list), `must_contain` (list of `{path, pattern}` dicts).
|
||
- Validate each `commit_message_pattern` compiles as a regex (use a tolerant
|
||
parser — do not execute untrusted input against shell).
|
||
- If any step is missing its Manifest, or YAML is malformed, STOP with:
|
||
```
|
||
Error: plan_version=1.7 but step {N} has invalid/missing Manifest.
|
||
Re-run /trekplan — the plan is not executable.
|
||
```
|
||
|
||
**Legacy mode (no plan_version, or < 1.7):**
|
||
- Synthesize a minimal manifest per step:
|
||
- `expected_paths` ← step's `Files:` list
|
||
- `min_file_count` ← `len(expected_paths)`
|
||
- `commit_message_pattern` ← regex-escape first 3 words of Checkpoint msg
|
||
- `bash_syntax_check` ← auto-detect `.sh` in Files
|
||
- `forbidden_paths` ← []
|
||
- `must_contain` ← []
|
||
- Record `legacy_plan: true` in the progress file.
|
||
- Emit warning: `Legacy plan (v1.6 or earlier) — manifests synthesized with
|
||
reduced audit precision.`
|
||
|
||
### Parse Session Manifest (session specs only)
|
||
|
||
If the file is a session spec (v1.7+), parse the `## Session Manifest` block
|
||
into a YAML dict. Preserve `session_manifest.*` fields for Phase 7.5 audit.
|
||
If missing and `plan_version ≥ 1.7`, record a parse warning but continue —
|
||
the per-step manifests are still available for audit.
|
||
|
||
### Parse session spec fields (if applicable)
|
||
|
||
- **Entry condition** from `## Dependencies`
|
||
- **Touch list** and **Never-touch list** from `## Scope Fence`
|
||
- **Exit condition** checklist from `## Exit Condition`
|
||
|
||
### Parse Execution Strategy (if present)
|
||
|
||
If the plan contains an `## Execution Strategy` section, extract:
|
||
- Each `### Session N: {title}` with its Steps, Wave, Depends on, and Scope fence
|
||
- The `### Execution Order` with wave definitions
|
||
|
||
Set **has_execution_strategy = true**.
|
||
|
||
Report:
|
||
```
|
||
Type: {plan | session-spec}
|
||
Steps: {N}
|
||
{if has_execution_strategy}: Execution Strategy: {S} sessions across {W} waves
|
||
{if session spec}: Entry condition: {text}
|
||
{if session spec}: Scope fence: {N} touch, {N} never-touch
|
||
{if warnings}: Warnings: {list}
|
||
```
|
||
|
||
## Phase 2.3 — Validate-only mode exit (if mode = validate)
|
||
|
||
**If mode = validate, stop after Phase 2 parsing** and emit a schema-only
|
||
report. Do NOT run security scan, do NOT touch progress files, do NOT
|
||
execute any steps. This gives the user a fast sanity-check of plan
|
||
schema compliance without side effects.
|
||
|
||
**Preferred path (since v3.1.0):** invoke the plan validator directly. It
|
||
returns the same diagnostic info Phase 2 derives in prose, with stable
|
||
error codes for downstream tooling:
|
||
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs --strict --json "{path}"
|
||
|
||
# When --project is in scope and progress.json exists, also validate it:
|
||
[ -f "{project_dir}/progress.json" ] && \
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/progress-validator.mjs --json "{project_dir}/progress.json"
|
||
```
|
||
|
||
Map the validator's `code` field to the error templates below (e.g.
|
||
`PLAN_FORBIDDEN_HEADING` → "Detected heading format" branch). When both
|
||
calls exit 0, render the READY report. Otherwise render FAIL with the
|
||
validator's first error code + message.
|
||
|
||
If Phase 2 parsing succeeded (no fatal errors, every step has a valid
|
||
Manifest block in strict mode, or synthesized manifests in legacy mode):
|
||
|
||
```
|
||
=== Schema Validation: READY ===
|
||
File: {path}
|
||
Type: {plan | session-spec}
|
||
plan_version: {1.7 | legacy}
|
||
Steps: {N}
|
||
Manifests: {N valid | N synthesized (legacy)}
|
||
Warnings: {count}
|
||
{if warnings}: - {each warning on own line}
|
||
|
||
Plan is schema-compliant. Safe to run:
|
||
/trekexecute {path}
|
||
```
|
||
|
||
If Phase 2 parsing failed (unrecognized format, missing Manifest in strict
|
||
mode, malformed YAML, invalid regex):
|
||
|
||
```
|
||
=== Schema Validation: FAIL ===
|
||
File: {path}
|
||
Reason: {specific error from Phase 2}
|
||
|
||
{if format not recognized}:
|
||
Detected heading format: {e.g. "### Fase 1:", "## Phase 1"}
|
||
Expected: "### Step N: <description>"
|
||
Fix: re-run /trekplan — the plan must use v1.7 format
|
||
|
||
{if missing manifest}:
|
||
Step {N} has no Manifest block (plan_version=1.7 requires one per step)
|
||
Fix: re-run /trekplan — every step needs a Manifest YAML block
|
||
|
||
{if malformed YAML or invalid regex}:
|
||
Step {N}: {specific YAML/regex error}
|
||
Fix: edit the plan manually or re-run /trekplan
|
||
```
|
||
|
||
Exit after emitting the report. Do not continue to Phase 2.4 or later.
|
||
|
||
## Phase 2.4 — Pre-execution security scan
|
||
|
||
**Runs for all modes except dry-run and validate** (those modes exit earlier or have their own report format).
|
||
|
||
Scan every `Verify:` and `Checkpoint:` command in the parsed plan against the
|
||
executor security denylist. This catches dangerous commands before execution begins.
|
||
|
||
### Extract commands
|
||
|
||
For each step in the plan, extract the command string from:
|
||
- `Verify:` field (the shell command after the backtick-quoted command)
|
||
- `Checkpoint:` field (the git commit command)
|
||
|
||
Also extract Exit Condition commands if present.
|
||
|
||
### Check against denylist
|
||
|
||
For each extracted command, check against these patterns:
|
||
|
||
**BLOCK patterns (stop execution immediately):**
|
||
|
||
| Pattern | Threat |
|
||
|---------|--------|
|
||
| `rm` with both `-r` and `-f` flags (any order) | Recursive force delete |
|
||
| `chmod 777` or `chmod -R 777` | World-writable permissions |
|
||
| `curl`/`wget` piped to `bash`/`sh`/`zsh` | Remote code execution |
|
||
| `eval` with `$`, backtick, or `$(` | Code injection via eval |
|
||
| `mkfs` or `dd` writing to `/dev/sd*`, `/dev/nvme*`, `/dev/hd*` | Disk destruction |
|
||
| `shutdown`, `reboot`, `halt`, `poweroff` | System shutdown |
|
||
| `:(){ :\|:& };:` pattern | Fork bomb |
|
||
| `base64` piped to `bash`/`sh` | Obfuscated code execution |
|
||
| `crontab -e` or writing to `/etc/cron*` | Persistence via cron |
|
||
| `kill -9 -1` or `pkill -9 -1` | Kill all user processes |
|
||
| `history -c` or truncating `~/.bash_history` | Evidence destruction |
|
||
|
||
**WARN patterns (report but continue):**
|
||
|
||
| Pattern | Concern |
|
||
|---------|---------|
|
||
| `npm install --save`, `pip install`, `cargo add` | Dependency changes during execution |
|
||
| `git push --force` | History rewrite |
|
||
| `git reset --hard` | Discard uncommitted changes |
|
||
|
||
### Scan output
|
||
|
||
For each match:
|
||
```
|
||
Security scan: Step {N} — {description}
|
||
Command: {command}
|
||
{BLOCKED | WARNING}: {pattern name}
|
||
```
|
||
|
||
**If ANY BLOCK pattern is found:**
|
||
|
||
```
|
||
SECURITY SCAN FAILED: {count} dangerous command(s) found in plan.
|
||
|
||
Blocked commands:
|
||
Step {N}: {command} → {reason}
|
||
|
||
This plan contains commands blocked by the executor security policy.
|
||
The plan may have been tampered with or contain hallucinated dangerous commands.
|
||
|
||
Options:
|
||
1. Review and fix the plan file: {path}
|
||
2. Use --dry-run to inspect all commands without executing
|
||
3. Use --fg for interactive execution (hooks provide additional protection)
|
||
```
|
||
|
||
Stop execution. Do NOT continue to Phase 2.5.
|
||
|
||
**If only WARN patterns found:** Continue execution but include warnings in the
|
||
pre-execution summary. Report them in the final output under "Security advisories."
|
||
|
||
**If clean:** Report `Security scan: PASS ({N} commands checked)` and continue.
|
||
|
||
## Phase 2.5 — Execution strategy decision
|
||
|
||
Determine how to execute this plan:
|
||
|
||
**Run as single session (foreground)** when ANY of these are true:
|
||
- `--fg` flag is set
|
||
- `--step N` mode
|
||
- `--resume` mode
|
||
- `--session N` mode (runs only that session's steps, foreground)
|
||
- `phase_signal_result.effort == 'low'` → one session, steps in order — never while a wave is in flight (`WAVE_IN_FLIGHT`)
|
||
- Plan has no `## Execution Strategy` section
|
||
- Plan has Execution Strategy with only 1 session
|
||
|
||
**Run as multi-session (parallel orchestration)** when ALL of these are true:
|
||
- mode = `execute` (default, no --fg)
|
||
- Plan has `## Execution Strategy` with 2+ sessions
|
||
- At least one wave has 2+ sessions (parallelism possible)
|
||
|
||
**Run as multi-session (sequential orchestration)** when:
|
||
- mode = `execute` (default, no --fg)
|
||
- Plan has `## Execution Strategy` with 2+ sessions
|
||
- All sessions are in different waves (no parallelism, but still separate sessions)
|
||
|
||
For single-session (including every `--session N` run inside a session's
|
||
worktree): continue to Phase 3.
|
||
For multi-session, first locate the main checkout and the wave marker:
|
||
|
||
```bash
|
||
GIT_DIR_ABS="$(cd "$(git rev-parse --git-dir)" && pwd -P)"
|
||
GIT_COMMON_ABS="$(cd "$(git rev-parse --git-common-dir)" && pwd -P)"
|
||
MAIN_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')"
|
||
if [ "$GIT_DIR_ABS" != "$GIT_COMMON_ABS" ]; then
|
||
echo "Error: this is a linked worktree. Run this from the main checkout $MAIN_ROOT." >&2
|
||
exit 1
|
||
fi
|
||
if [ -f "$MAIN_ROOT/.claude/trekplan-sessions/{slug}/orders/wave.json" ]; then echo "WAVE_IN_FLIGHT"; else echo "NO_WAVE"; fi
|
||
```
|
||
|
||
Exit 1 means this is a session's (or another) linked worktree: stop and report
|
||
the message — the multi-session path runs only from the main checkout, whose
|
||
root the first entry of `git worktree list` names (`--show-toplevel` would name
|
||
the linked worktree and miss the wave). `WAVE_IN_FLIGHT`: a wave is awaiting
|
||
its sessions — skip Phase 2.55 (its clean-tree and stale-worktree checks would
|
||
touch the live worktrees) and jump to Phase 2.6, which collects the wave.
|
||
`NO_WAVE`: run Phase 2.55, then Phase 2.6.
|
||
|
||
Report:
|
||
```
|
||
Strategy: {single session | N sessions (M parallel, K sequential)}
|
||
```
|
||
|
||
## Phase 2.55 — Pre-flight safety checks
|
||
|
||
**Only runs for multi-session parallel execution.** These checks prevent the
|
||
catastrophic data loss that occurs when parallel sessions share a working directory.
|
||
|
||
### Check 1 — Clean working tree
|
||
|
||
Run `git status --porcelain -- ':(top,exclude).claude/trekplan-sessions/'`
|
||
(the whole repo, from any subdirectory, minus Voyage's own session state: orders,
|
||
archived progress and kept session worktrees are never merged, and counting them
|
||
would stop every re-run after a failed wave). If there are ANY uncommitted or
|
||
untracked changes:
|
||
|
||
```
|
||
Error: working tree is not clean. Parallel execution requires a clean git state.
|
||
Uncommitted changes are invisible to worktrees and will be lost during merge.
|
||
|
||
Untracked/modified files:
|
||
{output of git status --porcelain}
|
||
|
||
Commit or stash your changes, then re-run.
|
||
To run sequentially instead: /trekexecute --fg {plan-path}
|
||
```
|
||
|
||
Stop execution. Update progress with `status: "stopped"`.
|
||
|
||
**Also write `.session-state.local.json`** (Handover 7) — this surfaces the
|
||
stopped state to `/trekcontinue` so the next session can prompt the user to
|
||
clean the working tree before resuming. Write atomically alongside `progress.json`:
|
||
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"project": "{project_dir}",
|
||
"next_session_brief_path": "{project_dir}/brief.md",
|
||
"next_session_label": "{current session label, or 'Continue'}",
|
||
"status": "stopped",
|
||
"updated_at": "{ISO-8601 now}"
|
||
}
|
||
```
|
||
|
||
Verify with `node ${CLAUDE_PLUGIN_ROOT}/lib/validators/session-state-validator.mjs --json
|
||
{project_dir}/.session-state.local.json`. On validator failure, emit a warning
|
||
to stderr but do NOT block the stop; `progress.json` is still authoritative.
|
||
|
||
**Also write sibling `NEXT-SESSION-PROMPT.local.md`** with frontmatter
|
||
(`produced_by: trekexecute`, `produced_at: <ISO-8601>`,
|
||
`status: stopped`) so the next-session producer-mismatch check has both
|
||
candidates available. Use the same combined ESM block pattern as Phase 8.
|
||
|
||
### Check 2 — Plan file reaches every worktree
|
||
|
||
Worktrees are created from HEAD, so tracking the plan file is the cheapest way
|
||
to make it visible in each one. But the project directory may be **gitignored**
|
||
— `.claude/projects/` is tool-managed and local-only, and a repo that ignores it
|
||
is normal, not exotic. `git add -f` is **not** the answer there: it would push
|
||
operator-local artifacts into history, and into whatever remote the repo
|
||
publishes to. When the plan file is ignored, Phase 2.6 Step 2a' (which copies
|
||
brief/plan/research into each worktree) is the delivery path, and this check
|
||
must step aside instead of failing.
|
||
|
||
```bash
|
||
PLAN_PATH="{plan-path}"
|
||
if git ls-files --error-unmatch "$PLAN_PATH" >/dev/null 2>&1; then
|
||
PLAN_TRACKING="tracked"
|
||
else
|
||
git check-ignore -q "$PLAN_PATH"
|
||
case "$?" in
|
||
0) PLAN_TRACKING="ignored" ;;
|
||
1) PLAN_TRACKING="untracked" ;;
|
||
*) echo "Error: git check-ignore failed on $PLAN_PATH - a fatal probe is not an answer about ignore status." >&2
|
||
exit 1 ;;
|
||
esac
|
||
fi
|
||
if [ "$PLAN_TRACKING" = "untracked" ]; then
|
||
git add "$PLAN_PATH"
|
||
git commit -m "chore: track plan file for parallel execution"
|
||
fi
|
||
```
|
||
|
||
Report by outcome:
|
||
|
||
| `PLAN_TRACKING` | Report |
|
||
|---|---|
|
||
| `tracked` | `Plan file already tracked.` |
|
||
| `untracked` | `Plan file committed for worktree visibility.` |
|
||
| `ignored` | `Plan file is gitignored — not forced into history. Step 2a' copies it into each worktree.` |
|
||
|
||
Any other `git check-ignore` exit code is fatal and stops execution: a probe
|
||
that failed is not a probe that answered "not ignored".
|
||
|
||
### Check 3 — Scope fence overlap validation
|
||
|
||
For each wave that has 2+ sessions, validate that no file appears in the Touch
|
||
list of two different sessions in the same wave:
|
||
|
||
1. For each session in the wave, extract the "Touch" list from the Execution Strategy.
|
||
2. For each pair of sessions (A, B) in the same wave, compute the intersection
|
||
of their Touch lists.
|
||
3. If any intersection is non-empty:
|
||
|
||
```
|
||
Error: scope fence overlap detected in Wave {W}.
|
||
Sessions {A} and {B} both touch: {overlapping files}
|
||
These sessions cannot safely run in parallel.
|
||
|
||
Fix the Execution Strategy in the plan, or use --fg for sequential execution.
|
||
```
|
||
|
||
Stop execution. This is a defense-in-depth check — /trekplan's validator
|
||
should have prevented this, but verifying at execution time catches plans
|
||
that were manually edited or have bugs.
|
||
|
||
### Check 4 — Stale worktree cleanup
|
||
|
||
Clean up what an earlier run of this plan left under
|
||
`trekplan-sessions/{slug}/` — never with force. It refuses from a linked
|
||
worktree and while a wave is awaiting its sessions (those worktrees and branches
|
||
are live or finished session work, not stale). `git worktree remove` without
|
||
force refuses a worktree with modified or untracked files, and `git branch -d`
|
||
refuses an unmerged branch: both are kept, reported, and never retried with
|
||
force.
|
||
|
||
```bash
|
||
# zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash.
|
||
[ -n "${ZSH_VERSION:-}" ] && setopt nonomatch
|
||
GIT_DIR_ABS="$(cd "$(git rev-parse --git-dir)" && pwd -P)"
|
||
GIT_COMMON_ABS="$(cd "$(git rev-parse --git-common-dir)" && pwd -P)"
|
||
MAIN_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')"
|
||
if [ "$GIT_DIR_ABS" != "$GIT_COMMON_ABS" ]; then
|
||
echo "Error: this is a linked worktree. Run this from the main checkout $MAIN_ROOT." >&2
|
||
exit 1
|
||
fi
|
||
SESSIONS="$MAIN_ROOT/.claude/trekplan-sessions/{slug}"
|
||
if [ -f "$SESSIONS/orders/wave.json" ]; then
|
||
echo "Error: $SESSIONS/orders/wave.json exists - a wave is awaiting its sessions. Stale-worktree cleanup refused." >&2
|
||
exit 1
|
||
fi
|
||
cd "$MAIN_ROOT" || exit 1
|
||
for wt in "$SESSIONS/worktrees"/session-*; do
|
||
[ -d "$wt" ] || continue
|
||
GIT_OPTIONAL_LOCKS=0 git worktree remove "$wt" 2>/dev/null || echo "KEPT worktree $wt"
|
||
done
|
||
GIT_OPTIONAL_LOCKS=0 git worktree prune
|
||
git for-each-ref --format='%(refname:short)' "refs/heads/trek/{slug}/" | while read -r b; do
|
||
GIT_OPTIONAL_LOCKS=0 git branch -d "$b" >/dev/null 2>&1 || echo "KEPT branch $b"
|
||
done
|
||
KEPT="$(ls -d "$SESSIONS/worktrees"/session-* 2>/dev/null; git for-each-ref --format='%(refname:short)' "refs/heads/trek/{slug}/")"
|
||
if [ -n "$KEPT" ]; then
|
||
echo "Earlier session work kept (changes or unmerged commits):" >&2
|
||
echo "$KEPT" >&2
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
Exit 0: report `Cleaned stale worktrees and merged branches from a previous run.`
|
||
Exit 1: stop and report its message. For every kept item say what the user can
|
||
do: a worktree — inspect it (`git -C <path> status --short`), commit or move
|
||
what matters, then `git worktree remove <path>`; a branch — inspect it
|
||
(`git log HEAD..<branch>`), merge it by hand, or delete it themselves once its
|
||
commits are not needed. Then re-run.
|
||
|
||
After all 4 checks pass:
|
||
```
|
||
Pre-flight: PASS (clean tree, plan reaches worktrees, no overlaps, no stale worktrees)
|
||
```
|
||
|
||
## Phase 2.6 — Multi-session orchestration (worktree-isolated, session orders)
|
||
|
||
**Only runs for multi-session execution.** Voyage never starts a Claude Code
|
||
session. This phase prepares ONE wave at a time: it creates an isolated git
|
||
worktree for each session in the wave, writes a **session order** for each
|
||
(the worktree, the steps, the done-condition) plus a one-line **starter**, and
|
||
stops. The user opens each session themselves, in the directory the order
|
||
names, and gives it the starter. Running the same `/trekexecute` command again
|
||
in the main checkout collects the wave: it reads every session's progress file,
|
||
merges the session branches back sequentially, cleans up, and prepares the next
|
||
wave. After the last wave, jump to Phase 8 (final report).
|
||
|
||
**CRITICAL SAFETY RULE:** Every session MUST run in its own git worktree. Never
|
||
run two sessions in the same working directory. This rule exists because
|
||
parallel git operations in a shared worktree cause index corruption, race
|
||
conditions, and repository destruction. Each order names its own worktree.
|
||
|
||
### Step 0 — Resolve paths and detect a wave in flight
|
||
|
||
```bash
|
||
# The MAIN checkout, never a linked worktree (Phase 2.5): in a session's
|
||
# worktree --show-toplevel names that worktree.
|
||
GIT_DIR_ABS="$(cd "$(git rev-parse --git-dir)" && pwd -P)"
|
||
GIT_COMMON_ABS="$(cd "$(git rev-parse --git-common-dir)" && pwd -P)"
|
||
REPO_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')"
|
||
if [ "$GIT_DIR_ABS" != "$GIT_COMMON_ABS" ]; then
|
||
echo "Error: this is a linked worktree. Run this from the main checkout $REPO_ROOT." >&2
|
||
exit 1
|
||
fi
|
||
# The plan path relative to the main checkout: every session-orders.mjs call
|
||
# runs from $REPO_ROOT with it, and each session finds the plan there in its
|
||
# own worktree.
|
||
PLAN_REL="$(python3 -c 'import os.path,sys; print(os.path.relpath(os.path.realpath(sys.argv[1]), os.path.realpath(sys.argv[2])))' "{plan-path}" "$REPO_ROOT")"
|
||
case "$PLAN_REL" in
|
||
""|..*)
|
||
echo "Error: the plan {plan-path} is not inside the main checkout $REPO_ROOT." >&2
|
||
exit 1 ;;
|
||
esac
|
||
SESSION_DIR="$REPO_ROOT/.claude/trekplan-sessions/{slug}"
|
||
WORKTREE_DIR="$SESSION_DIR/worktrees"
|
||
ORDERS_DIR="$SESSION_DIR/orders"
|
||
ORIGINAL_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||
# Resolve the plugin root ONCE (same reason as Phase 7: the variable is
|
||
# substituted in this command's text but EMPTY in the Bash tool's env).
|
||
# Only the literal form is substituted (a `:-` default is never replaced); the
|
||
# fallback takes the highest installed version under cache/<market>/voyage/.
|
||
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
|
||
if [ ! -f "$VOYAGE_ROOT/lib/util/session-orders.mjs" ]; then
|
||
echo "[voyage] session orders cannot be written - plugin root unresolved." >&2
|
||
exit 2
|
||
fi
|
||
if [ -f "$ORDERS_DIR/wave.json" ]; then echo "WAVE_IN_FLIGHT"; else echo "NO_WAVE"; fi
|
||
```
|
||
|
||
Record `REPO_ROOT`, `PLAN_REL`, `SESSION_DIR`, `WORKTREE_DIR`, `ORDERS_DIR`,
|
||
`VOYAGE_ROOT` and `ORIGINAL_BRANCH` for the later steps. All paths except
|
||
`PLAN_REL` must be absolute.
|
||
|
||
- `WAVE_IN_FLIGHT` → read the wave number `W` from `$ORDERS_DIR/wave.json` and
|
||
jump to **Step 3** (collect). Phase 2.55 was skipped for this run (Phase 2.5).
|
||
- `NO_WAVE` → prepare the lowest wave not yet merged:
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" next-wave --plan "$PLAN_REL" --session-dir "$SESSION_DIR")
|
||
```
|
||
A number is `W`: continue to Step 1. `none` means earlier runs merged every
|
||
wave: go to Step 4. The record it reads, `$ORDERS_DIR/merged-waves.json`
|
||
(plan path + sha256 of the plan's content), is written by Step 3 after each
|
||
merged wave, survives Step 5, and is cleared only on confirm at Phase 8's
|
||
main-merge gate, when every wave is merged and Step 4 passed, so a re-run
|
||
after a later wave failed (or after a declined gate) never prepares an
|
||
already-merged wave again.
|
||
If `next-wave` exits 1, the record belongs to another plan or another version
|
||
of this one (`{slug}` is the plan's file name, so plans can share
|
||
`$SESSION_DIR`). Stop, touch nothing, and tell the user exactly what its
|
||
message says: an unfinished multi-session run of that plan holds
|
||
`$ORDERS_DIR`. Finish that run (re-run it with its own plan), or clear it and
|
||
re-run this one:
|
||
`(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" clear --session-dir "$SESSION_DIR")`.
|
||
|
||
### Step 1 — Create worktrees for wave W
|
||
|
||
```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:**
|
||
|
||
First make sure no session of this wave left a branch or a worktree behind in
|
||
an earlier attempt — it may hold that session's work, so it is neither reused
|
||
nor deleted:
|
||
```bash
|
||
EXISTING=""
|
||
for N in {wave W session numbers}; do
|
||
if git rev-parse -q --verify "refs/heads/trek/{slug}/session-$N" >/dev/null || [ -e "$WORKTREE_DIR/session-$N" ]; then
|
||
EXISTING="$EXISTING trek/{slug}/session-$N"
|
||
fi
|
||
done
|
||
if [ -n "$EXISTING" ]; then
|
||
echo "Error: left by an earlier attempt:$EXISTING (branch or $WORKTREE_DIR/session-N)." >&2
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
If it exits 1, stop and report (no Step 5, no Phase 8; nothing was created or
|
||
deleted): name each branch and worktree, and what the user can do — inspect it
|
||
(`git log HEAD..<branch>`, `git -C <worktree> status --short`), merge it by
|
||
hand, or delete it themselves once its work is not needed — then re-run.
|
||
|
||
Then, for each session N in this wave:
|
||
```bash
|
||
BRANCH_NAME="trek/{slug}/session-{N}"
|
||
WORKTREE_PATH="$WORKTREE_DIR/session-{N}"
|
||
GIT_OPTIONAL_LOCKS=0 git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" HEAD
|
||
```
|
||
|
||
`GIT_OPTIONAL_LOCKS=0` disables `.git/index.lock` background polling races
|
||
during parallel worktree operations (research/02 R2; GH #47721).
|
||
|
||
If `git worktree add` fails, report its error and stop the wave: never delete a
|
||
branch to make room. Jump to Step 5 (it removes the worktrees this wave just
|
||
created, without force), then Phase 8.
|
||
|
||
Report:
|
||
```
|
||
Worktree created: session-{N} → {WORKTREE_PATH} (branch: {BRANCH_NAME})
|
||
```
|
||
|
||
**2a'. Copy gitignored project artifacts into each worktree (addresses
|
||
plugin-in-monorepo + gitignored-state topology):**
|
||
|
||
`git worktree add` clones HEAD only. The plugin's `.gitignore` excludes
|
||
`.claude/projects/`, so the project directory (brief.md, plan.md, research/)
|
||
is missing in the freshly-created worktree. Each session needs to read
|
||
`brief.md`, `plan.md`, and `research/*` from `${PROJECT_DIR}` to do real work.
|
||
|
||
Insert this block AFTER the worktree-creation loop and BEFORE the orders are
|
||
written (Step 2):
|
||
|
||
```bash
|
||
# zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash.
|
||
[ -n "${ZSH_VERSION:-}" ] && setopt nonomatch
|
||
PROJECT_SOURCE="$(realpath "${PROJECT_DIR}")"
|
||
REPO_ROOT_REAL="$(realpath "${REPO_ROOT}")"
|
||
# Compute destination relpath: PROJECT_DIR relative to REPO_ROOT.
|
||
# This makes $wt/$PROJECT_REL valid regardless of whether the operator
|
||
# passed --project as relative (.claude/projects/...) or absolute.
|
||
# python3 + os.path.relpath is stdlib and portable - see the note below the
|
||
# block for why no realpath flag may be used here.
|
||
PROJECT_REL="$(python3 -c 'import os.path,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$PROJECT_SOURCE" "$REPO_ROOT_REAL")"
|
||
case "$PROJECT_REL" in
|
||
""|..*)
|
||
echo "Error: cannot derive a project relpath inside the repo ($PROJECT_SOURCE vs $REPO_ROOT_REAL)." >&2
|
||
exit 1 ;;
|
||
esac
|
||
for wt in "$WORKTREE_DIR"/session-*; do
|
||
[ -d "$wt" ] || continue
|
||
mkdir -p "$wt/$PROJECT_REL"
|
||
cp "$PROJECT_SOURCE"/brief.md "$wt/$PROJECT_REL/"
|
||
cp "$PROJECT_SOURCE"/plan.md "$wt/$PROJECT_REL/"
|
||
if [ -d "$PROJECT_SOURCE/research" ]; then
|
||
cp -r "$PROJECT_SOURCE/research" "$wt/$PROJECT_REL/"
|
||
fi
|
||
done
|
||
```
|
||
|
||
Do not "improve" the relpath line into `realpath --relative-to=...`. That flag
|
||
is GNU coreutils only; BSD `realpath` (the macOS default) rejects it, and the
|
||
failure is **silent** — the command substitution leaves `PROJECT_REL` empty, so
|
||
`mkdir -p "$wt/"` and `cp ... "$wt//"` both succeed and drop `brief.md`/`plan.md`
|
||
at the worktree root, where no session looks for them. Measured on an
|
||
Intel Mac 2026-08-31: `realpath --relative-to=... ` → `realpath: illegal option
|
||
-- -`, while bare `realpath <path>` works; a whole wave ran with zero steps
|
||
executed. Both `realpath` calls above are bare path resolution, which BSD and
|
||
GNU handle identically; resolving both operands before the relpath is what keeps
|
||
it correct when one side goes through a symlink (macOS `/var` → `/private/var`).
|
||
|
||
Failure modes: an underivable relpath (empty, or outside the repo) aborts
|
||
before anything is copied — better a loud stop than files delivered where no
|
||
session reads them; any `cp` failure exits the wave non-zero, reported via
|
||
Step 5 cleanup. A project without `research/` is not a failure.
|
||
Source: brief Constraint 2.
|
||
|
||
### Step 2 — Write the session orders for wave W, then stop
|
||
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" write --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR" --branch "$ORIGINAL_BRANCH")
|
||
```
|
||
|
||
`$PLAN_REL` is relative to the main checkout (Step 0; the tool refuses an
|
||
absolute path): each session resolves it inside its own worktree, which has the
|
||
same repo content from HEAD. The tool writes `$ORDERS_DIR/wave-{W}-session-{N}.md`
|
||
per session (naming its worktree, directory and progress file) and
|
||
`$ORDERS_DIR/wave.json` (`status: awaiting-sessions`, the plan path, the
|
||
sha256 of its content and the branch the wave is prepared on), and prints, per
|
||
session, the directory to open and the starter line
|
||
(`/trekexecute --session {N} $PLAN_REL`). A session's per-session `cwd:`
|
||
field in the Execution Strategy moves the directory it opens in to that
|
||
subdirectory of its worktree (plugin-in-monorepo layouts, brief Constraint 1,
|
||
Option B); default `.` is the worktree root.
|
||
|
||
Show the tool's output to the user verbatim, then report and **stop**:
|
||
|
||
```
|
||
Wave {W}: {N} session order(s) written in {ORDERS_DIR}. Awaiting sessions.
|
||
Open each session yourself in the directory its order names and give it the
|
||
starter line. When every session is done, run this same /trekexecute command
|
||
again here to merge the wave.
|
||
```
|
||
|
||
This run ends here. It writes no Phase 8 report: the plan is not finished, and
|
||
the orders are the hand-over.
|
||
|
||
### Step 3 — Collect wave W (a later run)
|
||
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" status --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR" --branch "$ORIGINAL_BRANCH")
|
||
```
|
||
|
||
If it exits 1, stop and report its message, touching nothing (`wave.json`
|
||
stays). Either `wave.json` was written for another plan or another version of
|
||
this one — re-run with the plan (version) that wrote the wave, or abandon the
|
||
wave (the way out below) — or the main checkout is on another branch than the
|
||
wave was prepared on (Step 0's `ORIGINAL_BRANCH` of that run): the user
|
||
switches back to that branch and re-runs, so the wave never merges elsewhere.
|
||
|
||
The JSON names each session's progress file (the file `--session N` writes,
|
||
Phase 3), its raw `status`, and its `state`, exactly one of:
|
||
`completed`; `running` — no progress file yet, `pending`, `in-progress`,
|
||
`in_progress`, or any status not named here (an unknown status is never treated
|
||
as ended); `ended-bad` — `failed`, `stopped`, `partial`, `blocked` or
|
||
`unreadable`. `running` and `ended_bad` list the sessions in each state. Check
|
||
in this order:
|
||
|
||
- **Any `running` (checked FIRST):** a session is still open. Report which
|
||
sessions are still running, with their raw status, point at their order files
|
||
(`$ORDERS_DIR/wave-{W}-session-{N}.md`), and **stop** — even when another
|
||
session in the wave already ended badly. Touch no worktree, no branch and not
|
||
`wave.json`: no merge, no archive, no cleanup.
|
||
**Way out of a stuck wave** (a session the user decided not to run): the user
|
||
deletes `$ORDERS_DIR/wave.json` and re-runs. That run takes the fresh path,
|
||
and Phase 2.55 Check 4 removes the wave's clean worktrees and merged branches
|
||
without force and stops on everything that holds work, listing it to merge by
|
||
hand or delete.
|
||
- No session is running. Before any worktree is removed (the failure branch's
|
||
Step 5 and the merge below both remove them), archive every session's
|
||
progress file:
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" archive --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR")
|
||
```
|
||
It copies each one to `$ORDERS_DIR/wave-{W}-session-{N}.progress.json`. Step 5
|
||
never deletes these copies; Step 6 and Phase 8 read them.
|
||
- **Any `ended-bad`:**
|
||
```
|
||
Wave {W} FAILED: Session {N} ended {status}.
|
||
Stopping — later waves depend on this wave.
|
||
See its order: {ORDERS_DIR}/wave-{W}-session-{N}.md
|
||
```
|
||
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`:** 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`. In the permission mode
|
||
that shows no prompts the guard does not ask: it refuses the push and the
|
||
merge (exit 2, "no one to ask"). Then stop, keep `wave.json`, and report that
|
||
the guard refused because no one can be asked, not that local changes are in
|
||
the way: the operator runs this session's push-and-merge block by hand, or
|
||
switches to a mode that asks and re-runs. A re-run in the same mode is
|
||
refused again.
|
||
|
||
**Merge session branches back (SEQUENTIAL, one at a time):**
|
||
|
||
Return to the main worktree and make sure it is clean. While the sessions ran
|
||
the user may have kept working here, and Phase 2.55 (its Check 1) did not run
|
||
for this collect:
|
||
```bash
|
||
cd "$REPO_ROOT" || exit 1
|
||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||
echo "Error: uncommitted changes in $REPO_ROOT. Commit or stash your changes in $REPO_ROOT, then re-run." >&2
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
If it exits 1, stop and report its message: no merge, no cleanup, and keep
|
||
`wave.json`, so the re-run collects this wave again.
|
||
|
||
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.). 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}" \
|
||
-m "merge: trekplan session {N} — {session-title}"; then
|
||
echo "MERGED session {N}"
|
||
elif git rev-parse -q --verify MERGE_HEAD >/dev/null; then
|
||
git diff --name-only --diff-filter=U
|
||
git merge --abort
|
||
echo "MERGE_CONFLICT session {N}"
|
||
exit 1
|
||
else
|
||
echo "MERGE_REFUSED session {N}"
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
- `MERGED`: continue to the next session.
|
||
- `MERGE_REFUSED`: git refused to start the merge (no `MERGE_HEAD`; its message
|
||
names the local changes or untracked files in the way). Stop and report
|
||
`commit, stash or move your changes in {REPO_ROOT}, then re-run`: no Step 5, and
|
||
keep `wave.json`. Sessions merged before it in this wave stay merged; the
|
||
re-run merges them again as no-ops.
|
||
- `MERGE_CONFLICT`: a real conflict, already aborted. Report:
|
||
```
|
||
Wave {W} MERGE CONFLICT: Session {N} branch conflicts with merged state.
|
||
Conflicting files:
|
||
{the files the block printed}
|
||
|
||
Session {N} order: {ORDERS_DIR}/wave-{W}-session-{N}.md
|
||
Aborting further merges. Sessions already merged in this wave are preserved.
|
||
```
|
||
Mark remaining sessions as "merge-failed". Jump to Step 5 (it keeps every
|
||
unmerged branch), then Phase 8.
|
||
|
||
**Record the merged wave, then remove its worktrees:**
|
||
|
||
After successful merge of all sessions in the wave, record it (Step 0 reads the
|
||
record, so a later run never prepares this wave again):
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" record-merged --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR")
|
||
```
|
||
|
||
Then drop the wave marker and remove each session's worktree and merged branch,
|
||
never with force. A progress file still in a worktree is deleted only when it is
|
||
untracked and byte-identical to its archived copy (the archive is the record);
|
||
anything else in a worktree makes git refuse, and that worktree — and the branch
|
||
checked out in it — is kept and reported:
|
||
```bash
|
||
cd "$REPO_ROOT" || exit 1
|
||
rm -f "$ORDERS_DIR/wave.json"
|
||
for N in {wave W session numbers}; do
|
||
WT="$WORKTREE_DIR/session-$N"
|
||
ARCHIVED="$ORDERS_DIR/wave-{W}-session-$N.progress.json"
|
||
P="$(sed -n 's/^- progress file: //p' "$ORDERS_DIR/wave-{W}-session-$N.md")"
|
||
if [ -n "$P" ] && [ -f "$P" ] && cmp -s "$ARCHIVED" "$P" \
|
||
&& ! git -C "$WT" ls-files --error-unmatch "$P" >/dev/null 2>&1; then
|
||
rm -f "$P"
|
||
fi
|
||
GIT_OPTIONAL_LOCKS=0 git worktree remove "$WT" 2>/dev/null || echo "KEPT worktree $WT"
|
||
GIT_OPTIONAL_LOCKS=0 git branch -d "trek/{slug}/session-$N" >/dev/null 2>&1 || echo "KEPT branch trek/{slug}/session-$N"
|
||
done
|
||
GIT_OPTIONAL_LOCKS=0 git worktree prune
|
||
```
|
||
|
||
Report: `Wave {W}: {N} sessions merged.` plus every `KEPT` line, each with what
|
||
the user can do (inspect it with `git -C <path> status --short`, commit or move
|
||
what matters, then `git worktree remove <path>` and `git branch -d <branch>`).
|
||
|
||
**Next wave:** run Step 0's `next-wave` command. A number is the next `W`: go
|
||
back to Step 1 — this run prepares that wave's orders and stops again. `none`
|
||
(every wave merged): continue to Step 4.
|
||
|
||
### Step 4 — Run master verification
|
||
|
||
After all waves complete and merge successfully, run the plan's `## Verification`
|
||
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)
|
||
|
||
This step MUST execute whenever this phase ends the run with a failure, a merge
|
||
conflict, or after the last wave — never while a wave is awaiting its sessions
|
||
(Step 2, and a Step 3 that finds a session `running` or a dirty or refused
|
||
merge, stop without it). It is the worktree equivalent of a `finally` block, and
|
||
it never forces: `git worktree remove` without force refuses a worktree with
|
||
modified or untracked files, `git branch -d` refuses an unmerged branch, and
|
||
whatever git refuses is kept and reported, never retried with force. From
|
||
`$ORDERS_DIR` it deletes only `wave.json`: the orders, the archived progress
|
||
copies and the merged-waves record stay until the main-merge gate's confirm
|
||
(Phase 8) clears them.
|
||
|
||
```bash
|
||
# zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash.
|
||
[ -n "${ZSH_VERSION:-}" ] && setopt nonomatch
|
||
cd "$REPO_ROOT" || exit 1
|
||
|
||
# Remove worktrees git agrees are clean (GIT_OPTIONAL_LOCKS=0 to avoid lock-file races)
|
||
for wt in "$WORKTREE_DIR"/session-*; do
|
||
[ -d "$wt" ] || continue
|
||
GIT_OPTIONAL_LOCKS=0 git worktree remove "$wt" 2>/dev/null || echo "KEPT worktree $wt"
|
||
done
|
||
GIT_OPTIONAL_LOCKS=0 git worktree prune
|
||
|
||
# Delete merged session branches; every unmerged one is kept
|
||
git for-each-ref --format='%(refname:short)' "refs/heads/trek/{slug}/" | while read -r b; do
|
||
GIT_OPTIONAL_LOCKS=0 git branch -d "$b" >/dev/null 2>&1 || echo "KEPT branch $b"
|
||
done
|
||
|
||
# Clean up the wave marker and an empty worktree directory
|
||
rm -f "$ORDERS_DIR/wave.json"
|
||
rmdir "$WORKTREE_DIR" 2>/dev/null || true
|
||
```
|
||
|
||
Report:
|
||
```
|
||
Cleanup: worktrees removed and merged branches deleted.
|
||
Kept (they hold work): {every KEPT line}
|
||
```
|
||
|
||
For every kept item, the report says what the user can do: a worktree —
|
||
inspect it (`git -C <path> status --short`), commit or move what matters, then
|
||
`git worktree remove <path>`; a branch — inspect it (`git log HEAD..<branch>`),
|
||
merge it by hand (`git merge --no-ff <branch>`), or delete it themselves once
|
||
its commits are not needed. A re-run stops at Phase 2.55 Check 4 until they are
|
||
dealt with.
|
||
|
||
### Step 6 — Aggregate results
|
||
|
||
Read every archived copy, `$ORDERS_DIR/wave-*-session-*.progress.json`, into an
|
||
aggregated report — never the worktree copies, which Steps 3 and 5 remove with
|
||
their worktrees (earlier waves' in earlier runs).
|
||
|
||
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.
|
||
|
||
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
|
||
|
||
When mode = `session N`:
|
||
1. Find session N in the Execution Strategy
|
||
2. Extract its step numbers (e.g., Steps: 4, 5, 6)
|
||
3. Extract its scope fence (Touch / Never touch lists)
|
||
4. Execute ONLY those steps, in order, using the single-session protocol (Phase 3→7)
|
||
5. Enforce the session's scope fence as if it were a session spec's scope fence
|
||
6. Report results for those steps only
|
||
|
||
This mode is what each session order's starter runs, in a session the user
|
||
opened in that order's worktree. It can also be used manually to re-run a
|
||
specific session.
|
||
|
||
When `--session N` runs inside a git worktree (as each session order sets up),
|
||
all git operations (add, commit) apply to the worktree's branch. The session does
|
||
not need to know it is in a worktree — git handles this transparently.
|
||
|
||
## Phase 3 — Progress file setup
|
||
|
||
The progress file location depends on whether `--project` is in use:
|
||
|
||
- **With `--project <dir>`:** progress lives at `{project_dir}/progress.json`.
|
||
- **Without `--project`:** progress lives at
|
||
`{plan-dir}/.trekexecute-progress-{slug}.json`, where `{slug}` is the plan
|
||
filename without extension.
|
||
|
||
**Session-scoped naming:** When `mode = session N`:
|
||
- With `--project`: `{project_dir}/progress-session-{N}.json`.
|
||
- Without `--project`: `{plan-dir}/.trekexecute-progress-{slug}-session-{N}.json`.
|
||
|
||
This prevents merge conflicts when parallel sessions each write their own
|
||
progress file.
|
||
|
||
### Progress file schema
|
||
|
||
```json
|
||
{
|
||
"schema_version": "1",
|
||
"plan": "{path}",
|
||
"plan_type": "{plan | session-spec}",
|
||
"plan_version": "{1.7 | 1.6 | legacy}",
|
||
"started_at": "{ISO-8601}",
|
||
"updated_at": "{ISO-8601}",
|
||
"mode": "{execute | resume | step}",
|
||
"total_steps": 0,
|
||
"current_step": 0,
|
||
"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, "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
|
||
}
|
||
```
|
||
|
||
**The step-loop fields (Handover 5, additive-optional).** Written by Phase 6;
|
||
a progress file without them still validates and resumes:
|
||
|
||
| Field | Written | Meaning |
|
||
|---|---|---|
|
||
| `session_start_sha` | Phase 3 | the commit the run started from; Phase 7.5 audits the whole run from it |
|
||
| `steps.{N}.start_sha` | Sub-step 0 | the commit the step started from; every check of the step reads `git diff {start_sha}` |
|
||
| `steps.{N}.report_path` | Sub-step 0 | the step's report file (helper rounds + the done line) |
|
||
| `steps.{N}.red_proof` | Sub-step B | `"proven" \| "rejected" \| "not-run"` — the exit of `red-proof.mjs` |
|
||
| `steps.{N}.test_lock` | Sub-steps B, G | `"locked" \| "released" \| "not-locked"` (K8) |
|
||
| `steps.{N}.fix_rounds` | Sub-step F | 0–5: 0 = passed its first control, 1–3 = fix rounds, 4 = sent up to the plan, 5 = sent up to the operator |
|
||
| `steps.{N}.attempts` | Sub-steps C, F | helpers launched for the step: 1 + the fix rounds that launched one |
|
||
| `end_to_end` | Phase 7 | `"verified" \| "not-verified"` (P9 (6)) |
|
||
| `decisions` | any phase | `[{step, decision, cost_if_wrong}]` — every call the executor made on its own (Phase 5.5) |
|
||
|
||
`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.
|
||
|
||
**`plan_verification` (additive-optional):** written by Phase 7 when the input
|
||
is a trekplan — `{status, summary, failed_criteria}`, where `status` is
|
||
`"passed" | "failed" | "not-run"`. `null` for session specs (they have an exit
|
||
condition instead) and on early stop. Unknown keys are tolerated by
|
||
`progress-validator.mjs`, so a legacy progress file without it still validates.
|
||
|
||
**`iterations_remaining` (v1.7+, additive-optional):** the remaining
|
||
recovery/retry budget against the global hard cap (unit = **iterations**, per the
|
||
brief — token/cost budgeting is out of scope). Seeded to
|
||
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS` (default 25) at start. **Single writer:** the
|
||
executor decrements it by 1 on **every recovery/retry iteration** — each fix
|
||
round of a step (Sub-step F), and each Phase 7.6 recovery dispatch. **Backfill-on-absent:**
|
||
when resuming a legacy `progress.json` that lacks the field, seed it to the cap
|
||
rather than hard-failing (forward/backward-compatible; shape-validated by
|
||
`progress-validator.mjs` — non-negative integer, never required-top). The field is
|
||
present in the spec at minimum — there is **no `(if wired)` conditional**.
|
||
|
||
### Mode-specific behavior
|
||
|
||
**Run start point (every mode that executes):** a fresh progress file records
|
||
`session_start_sha` = `git rev-parse HEAD` before step 1; a resumed one keeps
|
||
the value it has (a legacy file without it is backfilled with the parent of
|
||
the first completed step's `commit`, or `HEAD` when none is completed). Phase
|
||
7.5 audits the whole run from it — never from a step's own `start_sha`.
|
||
|
||
**mode = execute (fresh):**
|
||
- If a progress file exists with status `in_progress`, `failed`, `stopped` or `partial`: warn that
|
||
`--resume` is available, then wait 3 seconds (`sleep 3`) and start fresh.
|
||
This allows headless runs to proceed without blocking.
|
||
- Otherwise: create the progress file with all steps in `pending` status.
|
||
|
||
**mode = resume:**
|
||
- If no progress file exists: start from step 1 (same as fresh execute).
|
||
- If progress file exists: find the first step with status != `completed`.
|
||
A legacy progress file may say `passed` for a finished step; treat it as `completed`.
|
||
```
|
||
Resuming from step {N}. {M}/{total} steps already completed.
|
||
```
|
||
|
||
**mode = dry-run:**
|
||
- Do NOT create or modify the progress file.
|
||
|
||
**mode = step N:**
|
||
- Create the progress file if it does not exist.
|
||
- Only step N will be executed.
|
||
|
||
## Phase 4 — Entry condition check (session specs only)
|
||
|
||
**Skip for trekplans.** Skip in dry-run mode (report what would be checked instead).
|
||
|
||
Read the entry condition. Evaluate it:
|
||
|
||
- `"none"` or similar → pass immediately
|
||
- References git state (e.g., "git status clean") → run `git status --porcelain`
|
||
- References passing tests → run the specified command
|
||
- References a previous session → check `git log --oneline` for commit pattern
|
||
|
||
If the entry condition **fails**:
|
||
```
|
||
Entry condition FAILED: {condition text}
|
||
Reason: {what was checked, what was found}
|
||
Complete the prerequisite first, then re-run.
|
||
```
|
||
Update progress file with `status: "stopped"`. Stop execution.
|
||
|
||
**Also write `.session-state.local.json`** (Handover 7) with the same
|
||
`status: "stopped"` and `next_session_brief_path` pointing at the brief that
|
||
the failed entry-condition session was supposed to consume. This lets
|
||
`/trekcontinue` surface the stop in the next session. Use the same atomic
|
||
write pattern + validator check as Phase 2.55. On validator failure, warn
|
||
but do not block.
|
||
|
||
**Also write sibling `NEXT-SESSION-PROMPT.local.md`** with frontmatter
|
||
(`produced_by: trekexecute`, `produced_at: <ISO-8601>`,
|
||
`status: stopped`) — same combined ESM pattern as Phase 8 — so Phase 1.5
|
||
of `/trekcontinue` can compare project-dir and plugin-root candidates.
|
||
|
||
If the entry condition **passes**:
|
||
```
|
||
Entry condition: PASS
|
||
```
|
||
Update `entry_condition_checked: true` in the progress file.
|
||
|
||
## Phase 5 — Dry-run report (dry-run mode only)
|
||
|
||
**Only runs when mode = dry-run.** Produces a validation report, then stops.
|
||
|
||
```
|
||
## Dry Run Report: {filename}
|
||
|
||
**Type:** {plan | session-spec}
|
||
**Steps:** {N}
|
||
|
||
### Step Validation
|
||
|
||
| Step | Description | Verify | On failure | Checkpoint | Issues |
|
||
|------|-------------|--------|------------|------------|--------|
|
||
| 1 | {desc} | {cmd} | {action} | {msg} | {none / missing X} |
|
||
|
||
### File References
|
||
|
||
{For each file in Files: fields, check existence with Glob}
|
||
- {path}: EXISTS | NOT FOUND {(marked as new file) | (unexpected — may be missing)}
|
||
|
||
### Entry / Exit Conditions (session specs)
|
||
|
||
{What would be checked}
|
||
|
||
### Execution Preview (only when plan has Execution Strategy)
|
||
|
||
If `has_execution_strategy = true`, show a preview of multi-session orchestration:
|
||
|
||
```
|
||
**Sessions:** {S} across {W} waves
|
||
|
||
| Wave | Session | Steps | Depends on | Starter (the user runs it in a session they open) |
|
||
|------|---------|-------|------------|---------|
|
||
| 1 | Session 1: {title} | {nums} | none | `/trekexecute --session 1 {path}` |
|
||
| 1 | Session 2: {title} | {nums} | none | `/trekexecute --session 2 {path}` |
|
||
| 2 | Session 3: {title} | {nums} | S1, S2 | `/trekexecute --session 3 {path}` |
|
||
```
|
||
|
||
### Verdict
|
||
|
||
{READY | NEEDS ATTENTION — N issues found}
|
||
```
|
||
|
||
Stop after the dry-run report. Do not execute anything.
|
||
|
||
## Phase 5.5 — Autonomy: what stops a run, and the decision log
|
||
|
||
A run is long and mostly unattended. It does not stop to ask: what it decides
|
||
on its own it writes down, and it stops only for the four reasons below.
|
||
|
||
### Closed stop list
|
||
|
||
The list is **closed**: a run stops on these four classes and on nothing else.
|
||
Every stop names its class in the final report (Phase 8 `### Stops`).
|
||
|
||
1. **Something that cannot be undone.** A step or command whose effect cannot
|
||
be reverted from git: deleting data outside the repo's tracked files, a
|
||
migration against a live store, anything the plan marks irreversible — and
|
||
a dirty main checkout before a multi-session wave (Phase 2.55 Check 1:
|
||
merging would lose the uncommitted work).
|
||
2. **Something security-sensitive.** A command the executor denylist blocks
|
||
(Phase 2.4, Sub-step D1), a write to a security-sensitive path (Hard
|
||
rule 16), credentials or secrets in a diff.
|
||
3. **Something outside the working copy.** Merge to the main branch, push,
|
||
tag, publish, a write outside the repository (Hard rule 15). The main-merge
|
||
gate (Phase 8) is this class: it always pauses.
|
||
4. **A plan so broken that everything after it is guesswork.** The plan fails
|
||
to parse, a ≥ 1.8 step has no `Test first:`, a step's own `On failure:
|
||
escalate` fired (the plan's author declared that past this point is
|
||
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). 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.
|
||
|
||
Not on the list, and therefore never a stop: a failing test (that is a fix
|
||
round), an ambiguity the plan leaves open (decide, and log it), a style
|
||
choice, a missing optional field.
|
||
|
||
### Decision log
|
||
|
||
Every decision the executor (or a helper, through its report) takes that the
|
||
plan did not dictate goes into progress `decisions` as one entry:
|
||
`{"step": N, "decision": "...", "cost_if_wrong": "..."}`. `cost_if_wrong` is
|
||
concrete: what has to be redone, by whom, how big — "one rename commit",
|
||
"step 4's API shape; steps 5–7 build on it". The final report prints the log
|
||
(Phase 8 `### Decisions taken`), so the operator reviews the calls instead of
|
||
being asked them one by one.
|
||
|
||
Borrowed: Superpowers subagent-driven-development (MIT) — the stop classes and
|
||
the log of choices made while unattended.
|
||
|
||
## Phase 6 — Step execution loop
|
||
|
||
The core execution phase. Runs for modes: `execute`, `resume`, `step`.
|
||
|
||
### Determine starting step
|
||
|
||
- **execute**: step 1
|
||
- **resume**: first step where status != `completed`.
|
||
A legacy progress file may say `passed` for a finished step; treat it as `completed`.
|
||
- **step N**: step N only
|
||
|
||
### For each step
|
||
|
||
Update progress: `steps.{N}.status = "in_progress"`, `current_step = N`, `updated_at = now`.
|
||
|
||
```
|
||
--- Step {N}/{total}: {description} ---
|
||
```
|
||
|
||
One step at a time. Inside one session the steps run in order in one working
|
||
tree; parallel steps are the multi-session wave path (Phase 2.6), never two
|
||
helpers in one tree.
|
||
|
||
#### Sub-step 0 — Record the start point
|
||
|
||
Before anything touches the tree, record where the step begins:
|
||
|
||
```bash
|
||
git rev-parse HEAD
|
||
git status --porcelain
|
||
```
|
||
|
||
- `steps.{N}.start_sha` = the `HEAD` sha. Every later check of this step reads
|
||
`git diff {start_sha}`: the review package is the diff from this recorded
|
||
start point, not anything the helper says. On `--resume` of a step that
|
||
already has a `start_sha`, keep it.
|
||
- `steps.{N}.report_path` = `{progress-dir}/steps/step-{N}.report.md` (create
|
||
the directory). Every helper round of the step appends to it, and the done
|
||
line lands in it (Sub-step G). Hand-over between the executor, the helper
|
||
and the controller is in files: the plan, this report, the diff.
|
||
- A non-empty `git status --porcelain` (beyond Voyage's own progress and report
|
||
files) means work the plan does not know about would mix into this step's
|
||
diff. Not a stop (Phase 5.5): log it as a decision
|
||
(`cost_if_wrong`: "the controller judges unrelated changes as part of step N")
|
||
and name the paths in the report.
|
||
|
||
#### Sub-step A — Scope fence check (session specs only)
|
||
|
||
Before touching any file, verify that every file in the step's `Files:` field is
|
||
in the session spec's Touch list (or is a new file to create). If ANY file is in
|
||
the Never-touch list:
|
||
|
||
```
|
||
SCOPE VIOLATION: Step {N} requires {file} which is in the never-touch list.
|
||
Escalating — this step cannot be executed within this session's scope.
|
||
```
|
||
|
||
Treat this as an automatic `escalate`. Jump to the stop-and-report logic.
|
||
|
||
#### Sub-step B — Test first and proof of red
|
||
|
||
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 every file on the `Test first:` `File:` line: the backticked paths that stand right after `File:`, separated only by commas or spaces, and below plan_version 1.9 also each further path after a note in brackets such as `(new)` or after `and`, `plus` or `+`, as long as it is clearly a test (the first path that is not ends the list; operator decision 31); a path in the description after them is not a test file (every plan_version; operator decision 20, 2026-10-12). `red-proof.mjs --plan --step` reads that list from the plan, so neither the helper nor the executor passes `--tests` (operator decision 30, 2026-10-14), except in the one case below; it prints the list it ran (`tests`), refuses a `--tests` that differs (exit 2, `tests-mismatch`, the plan's list in `plan_tests`) and a plan or step it cannot read (exit 2, `plan-unreadable`). Below plan_version 1.8 only (a `File:` line there may name its test without backticks), a plan that names no test file gets `no test files given` from red proof; then the helper runs it again with `--tests` naming the test files it wrote, and the executor runs its own check again with `--tests` naming the files on the `tests:` line the helper copied into its report (operator decision 41, 2026-10-14). The lock's `--files` is the list red proof printed as `tests`. 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
|
||
the checkpoint, and neither is a scope finding.
|
||
|
||
**The test command** is the step's `Verify:` command: the FIRST backticked
|
||
span of the field. Anything after it (expected output, notes) is description
|
||
and is not run. It must be ONE test-runner invocation without shell operators
|
||
(the allowlist of `lib/verification/criteria-runner.mjs`); the tools below
|
||
refuse anything else with exit 2, and from plan_version 1.8 the plan validator
|
||
already rejects such a step (`PLAN_STEP_VERIFY_NOT_RUNNABLE`).
|
||
|
||
1. **The helper proves red before it builds** (Sub-step C): it writes the test
|
||
from `Test first:`, runs the gate
|
||
`red-proof.mjs --red-only --base {start_sha} --cmd "{Verify}" --plan "{plan_path}" --step {N}`
|
||
(the test must FAIL at the start point), locks the test files, and only then
|
||
implements. A `tests_only: true` or `plan-unreadable` answer stops the helper before it builds
|
||
(it does not rewrite the test); the executor's own run below then sends the
|
||
step up to the plan, or (`plan-unreadable`) acts as the exit-2 row says. The plan's `Files:` alone decides `tests_only`. Red proof reads the step's test files, as Sub-step B defines them, from the plan.
|
||
2. **The executor proves it both ways after the helper returns:**
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/verification/red-proof.mjs --base {start_sha} --cmd "{Verify command}" --plan "{plan_path}" --step {N} --json
|
||
```
|
||
Red proof reads the step's test files, as Sub-step B defines them, from the plan. The test must FAIL against the start point and PASS against the result
|
||
(the working tree), and a **control run** must PASS: the start-point copy
|
||
with the step's whole change laid on top. The start point is a temp
|
||
`git clone --shared` checked out at `start_sha` (a real git checkout), with
|
||
the step's test files copied in and every gitignored top-level entry of the
|
||
working tree (`node_modules`, `.venv`, …) linked in; `--link a,b` adds
|
||
deeper dependency paths. A start point that fails for an environment reason
|
||
fails the control too, so it is never read as red.
|
||
|
||
| Exit | `steps.{N}.red_proof` | Executor |
|
||
|------|-----------------------|----------|
|
||
| 0 | `"proven"` | continue |
|
||
| 1, `not-red-at-base`, `tests_only: true` | `"rejected"` | every file in the step's `Files:` is a test (the plan validator's `PLAN_STEP_TESTS_ONLY`, the same reading). The plan's `Files:` alone decides: the step's diff is not read, and a path changed outside `Files:` is the controller's scope finding (Sub-step C). The behaviour its tests cover already exists at the start point (an earlier step built it, or it was there before the plan), so the step can never be red and no rewrite of the test can fix that. The plan is what failed: the step goes straight up to the plan (Sub-step F, `fix_rounds = 4`, stop class 4), with no fix round — the planner moves the test into the step that builds the behaviour, or makes a test meant to pass from the start the `Regression:` of the step that changes the code |
|
||
| 1, `not-red-at-base`, otherwise | `"rejected"` | the test passes without the change, so the TEST is wrong: a finding for Sub-step F, and that fix round is the one exception to the lock — the executor releases the step's test lock for it, the fix helper rewrites the test, passes the `--red-only` gate and locks again |
|
||
| 1, `not-green-at-result` | `"rejected"` | the change does not make the test pass: a finding for Sub-step F |
|
||
| 2 | `"not-run"` | never a pass and never proven. **`plan-unreadable` first:** a reason that starts `plan-unreadable` means the executor's own `--plan` or `--step` is wrong (a path, a step number), which no helper can fix: the executor corrects `{plan_path}` to the absolute path of the plan file it loaded in Phase 1 and `{N}` to the number in the step's own `### Step N:` heading. When the helper's report shows it built (proven and locked), the executor reruns only its own red proof with the corrected call. When the helper stopped on the same answer before it built, the executor records the restart (progress `red_proof: "not-run"`, the reason in the step's `error`, and a decision-log entry with `cost_if_wrong` "the step's first helper run"), and starts the step again with a fresh build helper (Sub-step C, round label `build (restart)`; `attempts` +1 and `iterations_remaining` −1 as for any launch; the fresh helper starts from the tree as the first helper left it, nothing is reverted, and it keeps one copy of each test case it writes), once, outside the fix rounds (`fix_rounds` does not change); a step whose `error` already holds a `plan-unreadable` restart, on a `--resume` too, does not restart again; a second `plan-unreadable` goes straight up to the plan (Sub-step F, `fix_rounds = 4`, stop class 4), and the upward file's question is red proof's reason (the plan path or step heading it could not read), not a step assumption. A `tests-mismatch` (a `--tests` that differs from the plan's list, in `plan_tests`) cannot come from the documented call, which passes no `--tests`; when it does, it is a finding for Sub-step F. **plan_version ≥ 1.8:** when the reason is the command (the `Verify:` is not one test-runner invocation), the plan is what failed: the step goes straight up to the plan (Sub-step F, `fix_rounds = 4`, stop class 4); any other reason (a missing test file, an unresolvable start point, a control run that stays red) is a finding for Sub-step F. **Below 1.8:** a step with no `Test first:`, or whose `Verify:` is not a test-runner command, is tolerated as `"not-run"` — counted in the final report among the steps without proof of red, never called proven; `no test files given` on a step that has `Test first:` is not a finding: when the helper's report has a `tests:` line, the executor reruns its check with `--tests` naming the files on the `tests:` line in the helper's report, as Sub-step B says (operator decision 41); without such a line, or on a second `no test files given`, the step is tolerated as `"not-run"` like a step with no `Test first:`; any other reason is a finding |
|
||
|
||
The exit code is the verdict, the output text is not. `base.tap` (when the
|
||
runner prints TAP) says what the red was: `fail 1 of 1` on a node test file is
|
||
usually a load error, not a named failing test — say so in the step report.
|
||
|
||
**The test lock (K8).** After its red gate the helper runs
|
||
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/test-lock.mjs lock --files {test files} --step {N} --cmd "{Verify command}"`.
|
||
`--files` names the step's test files, as Sub-step B defines them (the list red proof printed as `tests`). From then on the guard refuses Write/Edit/NotebookEdit on those files, so the
|
||
code moves to meet the test and never the reverse. The executor reads
|
||
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/test-lock.mjs status` and records
|
||
`steps.{N}.test_lock = "locked"` when it exits 0 and names the step's files.
|
||
If `lib/util/test-lock.mjs` is not in this plugin, record `"not-locked"` and
|
||
write "not locked: test-lock.mjs missing" in the step report — never silent.
|
||
The floor guard (Sub-step D3) still reads every test change in the diff. The
|
||
lock is released in Sub-step G, or when the run stops.
|
||
|
||
#### Sub-step C — Implement through one fresh helper
|
||
|
||
The executor does not write the step's code. **Launch** one fresh helper for
|
||
the step with the Agent tool:
|
||
|
||
Launch `subagent_type: "voyage:step-implementer"` with a short `description`
|
||
("step {N}: {title}") and a prompt that carries, as files and values, not as a
|
||
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`, `build (restart)`, 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, D4, E) on whatever the operator
|
||
changed. A step at `fix_rounds` 4 on `--resume` was re-planned: first park
|
||
the earlier attempt's new files, which the revert left on disk untracked, so a
|
||
stale file can never pass the revised step's checks (voyage-lekerepo trial 5,
|
||
finding 4):
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/util/park-attempt-files.mjs --diff {report dir}/step-{N}-upward.diff --step {N} --attempt {attempts}
|
||
```
|
||
It moves every file the attempt created that is still untracked to the git
|
||
dir (`voyage/parked/step-{N}-attempt-{attempts}/`, outside the work tree, so
|
||
Sub-step C.2 never registers it again): moved, never deleted, never over a file
|
||
already parked. Name the `parked` paths and every `kept` path with its reason
|
||
in the step report. Exit 1 means a file the attempt made is still in the tree
|
||
(a header it could not read, a move that failed): stop and name it; exit 2 is
|
||
a stop too (the tree was not checked). Then launch a fresh `build` helper for
|
||
the revised step.
|
||
|
||
- **Never pass the Agent tool's `name` parameter.** A named spawn becomes an
|
||
`in_process_teammate`: it does the work, but its result never reaches this
|
||
orchestrator (measured: named 0/5 returned, unnamed 3/3 —
|
||
`docs/agent-return-channel-defect.md`). The same holds for the controller
|
||
(Sub-step E).
|
||
- **Never pass `isolation`.** Measured 2026-10-06 (Claude Code 2.1.291): a
|
||
worktree-isolated helper branched from the repository's default branch
|
||
(`02f320f`), not from the executor's `HEAD` (`2c89a6b`), so it did not see
|
||
the earlier steps. Only the operator's `worktree.baseRef: "head"` setting
|
||
changes that, and a plugin cannot set it. The helper works in this tree,
|
||
from the recorded `start_sha`.
|
||
- **Wait for the helper to finish** before anything else; when it runs in the
|
||
background, its completion notification is the signal. Never start the next
|
||
sub-step or step while a helper runs.
|
||
- `steps.{N}.attempts` += 1 for every helper launched for the step — this
|
||
one, each fix round's, and the `build` helper after a re-plan. It is never
|
||
reset, and every launch after a step's first also takes 1 from
|
||
`iterations_remaining`, so the completion gate's reconciliation
|
||
(`Σ (attempts − 1)`) holds across re-plans.
|
||
|
||
The helper's one-line answer is not evidence. After it returns:
|
||
1. The report at `report_path` must carry a NEW section for this round
|
||
(`## Step {N} - {round label}`) with its `### Ran`, `### Saw` and
|
||
`### Did NOT check`; earlier rounds' sections and verdicts stay. A missing
|
||
section is a finding for Sub-step F (stale sections do not count).
|
||
2. Register EVERY new file so the diff and every check see it — not only the
|
||
listed ones: for each path `git ls-files --others --exclude-standard`
|
||
prints, run `git add --intent-to-add -- {path}` (stages nothing). A new file
|
||
the step did not list is otherwise invisible to `git diff`, to the floor
|
||
guard and to the controller. Voyage's own progress file and step reports
|
||
are the one exception: never registered.
|
||
3. A changed or new path outside the step's scope (`Files:` + test files) is a
|
||
scope finding (Hard rule 2) for the controller. The executor does not
|
||
revert it on its own.
|
||
|
||
**Rules the helper is held to** (its agent file carries them; the controller
|
||
checks them in the diff): follow `Changes:` exactly — no improvising, no added
|
||
scope; touch only the step's scope (`Files:` + test files); read `Reuses:`
|
||
first; never weaken a test; never write a stub; never commit.
|
||
|
||
#### Sub-step D1 — Command verification
|
||
|
||
**Security check (mandatory):** Before running the Verify command, check it against
|
||
the executor security denylist. If the command matches ANY of these patterns,
|
||
**refuse to execute** — treat as `On failure: escalate` regardless of the plan's
|
||
On failure setting:
|
||
|
||
- `rm -rf` or `rm -fr` with any path
|
||
- `chmod 777` or `chmod -R 777`
|
||
- Pipe-to-shell: `curl ... | bash`, `wget ... | sh`, `base64 ... | bash`
|
||
- `eval` with variable expansion: `eval $VAR`, `eval $(cmd)`, `` eval `cmd` ``
|
||
- `mkfs`, `dd` writing to block devices (`/dev/sd*`, `/dev/nvme*`)
|
||
- `shutdown`, `reboot`, `halt`, `poweroff`
|
||
- Fork bomb patterns
|
||
- `crontab` writes, `/etc/cron*` modifications
|
||
- `kill -9 -1` or `pkill -9 -1` (kill all processes)
|
||
- `history -c` or truncating `~/.bash_history`
|
||
|
||
If matched:
|
||
1. Do NOT execute the command
|
||
2. Set step status = "failed"
|
||
3. Log: `SECURITY: Verify command blocked — matches executor denylist: {pattern name}`
|
||
4. Apply `On failure: escalate` regardless of the plan's On failure setting
|
||
5. Include in final report under a "Security blocks" section
|
||
|
||
If the command passes the security check, run it:
|
||
|
||
Run the `Verify:` command exactly as written, via Bash.
|
||
|
||
**Rules:**
|
||
- Always a fresh run — never trust prior results
|
||
- Exit code is the authoritative truth:
|
||
- Exit 0 + expected output (if specified) = **PASS**
|
||
- Exit non-zero = **FAIL** regardless of output text
|
||
- Exit 0 but wrong output = **FAIL**
|
||
|
||
```
|
||
Verify: {command}
|
||
Result: {PASS | FAIL} (exit code {N})
|
||
{if FAIL}: Output (first 10 lines): {output}
|
||
```
|
||
|
||
**Step 0 sentinel:** if the Verify command exits `77` AND the step's manifest
|
||
has `sandbox_preflight: true`, do NOT treat as a normal failure. Set step
|
||
status = `blocked` and apply `On failure: escalate`. Skip D2 manifest
|
||
verification (nothing to verify for a read-only sandbox test). Commit none —
|
||
jump straight to Phase 7 with a structured "sandbox-blocked" reason.
|
||
|
||
If **PASS**: mark the step's test lock green, then proceed to Sub-step D2
|
||
(manifest verification). The mark is what lets the session wait for its
|
||
controller (Sub-step E) without the stop guard refusing every turn — the
|
||
test files stay locked against edits (operator decision 2026-10-08):
|
||
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/util/test-lock.mjs green
|
||
```
|
||
|
||
It runs the test command the helper stored in the lock (`lock --cmd`), never
|
||
one given now, and marks only on exit 0; a lock without a stored command is
|
||
never marked (exit 2), so the stop guard keeps holding as before; exit 1 (the test
|
||
did not pass this time) is a finding for Sub-step F like a FAIL, exit 2 (could
|
||
not run) is never green. A step that is `"not-locked"` skips it.
|
||
If **FAIL**: the failure output is a finding for Sub-step F; still run D2 and
|
||
D3, so the controller and the next fix round see every failure at once.
|
||
|
||
#### Sub-step D2 — Manifest verification
|
||
|
||
After the Verify command passes, verify the step's Manifest block. This is
|
||
the objective completion predicate: a step is not passed until its manifest
|
||
holds, regardless of Verify exit code.
|
||
|
||
**Checks to run (in order):**
|
||
|
||
1. **Expected paths exist:** for each `expected_paths` entry, verify the
|
||
file exists in the repo. Count how many exist.
|
||
2. **min_file_count satisfied:** count from (1) must be ≥ `min_file_count`.
|
||
3. **Forbidden paths untouched:** for each `forbidden_paths` entry, run
|
||
`git diff --name-only {start_sha} -- {path}` (since this step
|
||
began). Any modified forbidden path = fail.
|
||
4. **Bash syntax check:** for each entry in `bash_syntax_check` AND any
|
||
`.sh` file that appears in `git diff --name-only {start_sha}` (safety
|
||
net for unlisted scripts), run `bash -n {path}`. Non-zero exit = fail.
|
||
5. **must_contain patterns:** for each `{path, pattern}` pair, run
|
||
`grep -E -e "{pattern}" -- {path}` (`-e` and `--`: a pattern such as `--sep`
|
||
is never read as an option). No match = fail.
|
||
|
||
**On manifest failure:**
|
||
|
||
```
|
||
Manifest verification FAIL for step {N}:
|
||
- {which check failed with detail}
|
||
```
|
||
|
||
A manifest failure is a finding for Sub-step F, exactly like a command
|
||
failure: it counts against the same fix rounds.
|
||
|
||
Either way, proceed to Sub-step D3 (floor guard).
|
||
|
||
Record per-step manifest audit result in progress file:
|
||
`steps.{N}.manifest_audit = "pass" | "fail"`,
|
||
`steps.{N}.manifest_drift = [{check: reason, ...}]` on fail.
|
||
|
||
#### Sub-step D3 — Floor guard
|
||
|
||
Read the step's diff for weakened tests — by a command:
|
||
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/verification/test-integrity.mjs --base {start_sha} --json
|
||
```
|
||
|
||
It reports a removed assert, a deleted or skipped test, a muted check
|
||
(`|| true`, continue-on-error, an inline lint/type suppression), a stub body
|
||
("not implemented") and a threshold loosened in the permissive direction.
|
||
|
||
| Exit | Meaning | Executor |
|
||
|------|---------|----------|
|
||
| 0 | clean | continue |
|
||
| 1 | findings | each finding goes to the controller (Sub-step E), which rules it `justified` only by quoting the plan line that asks for the change; every other finding is a `violation` |
|
||
| 2 | could not run | exit 2 is never green: "floor guard not run: {reason}" is a finding for Sub-step F, and the step cannot pass this round |
|
||
|
||
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:
|
||
|
||
`steps.{N}.regression` is the JSON's `progress_status`, copied as it is: the
|
||
CLI already speaks progress.json's words (`"blocked"` → `"unrunnable"`,
|
||
`"error"` → `"not-run"`).
|
||
|
||
| 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), or the runner could not start (not installed): a finding for Sub-step F, never green |
|
||
|
||
#### Sub-step E — Control
|
||
|
||
**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
|
||
at the session's effort: run `/trekexecute` at effort `high` or above (the
|
||
standard). A session below `high` is a decision-log entry ("controller ran at
|
||
effort {level}"), never silent. It reads the diff, not the helper's summary: it is never given
|
||
the step report, and it judges what changed, not what the helper says changed.
|
||
|
||
The prompt (fill in every `{…}`; pass the mechanical results verbatim as JSON):
|
||
|
||
```text
|
||
You are the controller for step {N} of the plan {plan path}. You judge; you
|
||
change nothing: no Write, no Edit, no git command that writes.
|
||
|
||
Read the step's diff yourself: git diff {start_sha} --stat, then
|
||
git diff {start_sha}. The diff is the evidence. You are not given the
|
||
helper's report and must not look for it.
|
||
|
||
The step, verbatim:
|
||
{every field of the step}
|
||
|
||
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};
|
||
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 -
|
||
no more, no less? Name any changed path outside Files.
|
||
- "quality": does the code hold - correctness, error handling, edge cases the
|
||
step names, no stub, no weakened test, the repo's own patterns?
|
||
Rule every floor-guard finding: "justified: <the plan line, quoted>" or
|
||
"violation". List what you did not judge.
|
||
|
||
Answer with exactly one JSON block:
|
||
{"spec": "pass|fail", "quality": "pass|fail",
|
||
"findings": [{"verdict": "spec|quality", "file": "...", "line": 0, "problem": "...", "fix": "..."}],
|
||
"floor_guard": [{"kind": "...", "file": "...", "ruling": "justified: ... | violation"}],
|
||
"not_judged": ["..."]}
|
||
```
|
||
|
||
Wait for the controller to finish. Parse its JSON block; an answer without a
|
||
parseable block is a failed control (a finding for Sub-step F), never a pass.
|
||
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), `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
|
||
the previous round's verdicts, findings and floor-guard rulings, and the
|
||
round's own change — before the fix round the executor saves
|
||
`git diff {start_sha}` to `{report dir}/step-{N}-round-{R}.diff`, and afterwards
|
||
hands over `diff -u` of that file against the new `git diff {start_sha}`. It
|
||
answers in the SAME JSON block as above:
|
||
- per previous finding, fixed or not — a finding not fixed stays in
|
||
`findings`; whether the round changed anything the findings did not name
|
||
(that is a new finding);
|
||
- `"spec"` and `"quality"` for the step as a whole: the previous verdict
|
||
carries over for every part the round did not touch, re-judged only where
|
||
the round changed something;
|
||
- `floor_guard`: a finding identical (same kind, file and text) to one the
|
||
previous round already ruled keeps that ruling; only new floor-guard
|
||
findings are ruled.
|
||
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
|
||
runs `opus` (the step helper pins effort `high`; the controller follows the
|
||
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, 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). Sub-step B sends three answers here directly, with no fix round: a `"not-run"` caused by the `Verify:` command, a second `plan-unreadable` (the executor's own call still wrong after one correction), and a `not-red-at-base` with `tests_only: true` (a step of only tests). 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 --no-ext-diff --no-renames --src-prefix=a/ --dst-prefix=b/ {start_sha}` (one form whatever the user's git config) to
|
||
`{report dir}/step-{N}-upward.diff` and write `{report dir}/step-{N}-upward.md`:
|
||
the step verbatim, every round's findings and verdicts, and the question for
|
||
the planner — which assumption in the step (its Changes, its Interface, its
|
||
test) makes it fail. Then stop: step `failed`, run `failed`, and tell the
|
||
operator to re-plan the step (`/trekplan` revises it) and run
|
||
`/trekexecute --resume`, which launches a fresh `build` helper for the revised
|
||
step (`attempts` +1, `iterations_remaining` −1) and keeps `fix_rounds` at 4. A step whose proof of red cannot run on a
|
||
≥ 1.8 plan because its `Verify:` is not one test-runner invocation goes here
|
||
directly (Sub-step B), and so does a second `plan-unreadable` after one corrected restart, with red proof's reason as the upward file's question.
|
||
|
||
**The plan's `On failure:` clause** decides what happens around the ladder:
|
||
|
||
**`On failure: revert`** and **`On failure: retry`**
|
||
- Fix rounds 1–3 run as above; for `retry`, the clause's alternative approach
|
||
is handed to the round-1 fix helper.
|
||
- When the step goes up (round 4 or 5), first save the upward files, then
|
||
revert this step's changes (nothing of the step is committed yet):
|
||
```bash
|
||
git checkout -- {the step's tracked files changed since start_sha}
|
||
git rm --cached -q -- {the step's new files: registered with --intent-to-add since start_sha}
|
||
```
|
||
The new files stay on disk, untracked, and `step-{N}-upward.md` names them —
|
||
nothing the step made is deleted, and none of it is left registered. A
|
||
`--resume` after re-planning parks them in the git dir before the fresh
|
||
build (Sub-step C).
|
||
When the guard refuses the restore (it is destructive git: in the permission
|
||
mode that shows no prompts the guard refuses it, exit 2, "no one to ask"), or
|
||
the operator declines it, nothing was restored: write in `step-{N}-upward.md`
|
||
and the final report that the step's changes are still in the working tree,
|
||
with the paths, and that the operator restores them by hand. Never call the
|
||
step reverted.
|
||
Record failure. **Do NOT proceed to next step.** Jump to Phase 7.5 (Phase 7
|
||
runs only when every step passed).
|
||
|
||
**`On failure: skip`**
|
||
- Mark step as skipped at its first failed round, without fix rounds. Continue
|
||
to next step.
|
||
```
|
||
Step {N}: SKIPPED (non-critical per plan)
|
||
```
|
||
Update `steps.{N}.status = "skipped"`.
|
||
|
||
**`On failure: escalate`**
|
||
- Stop at the first failed round, without fix rounds (stop class 4: the plan's
|
||
author declared that past this point is guesswork).
|
||
```
|
||
Step {N}: ESCALATED — requires human judgment
|
||
```
|
||
Commit all completed work before stopping. Stage ONLY files from steps with
|
||
`status: "completed"` in the progress file — collect their `Files:` fields. Never
|
||
use `git add -A` (risks staging secrets, binaries, or unrelated work).
|
||
```bash
|
||
git add {files from completed steps' Files: fields} && git commit -m "wip: trekexecute stopped at step {N} — escalation needed"
|
||
```
|
||
Jump to Phase 7.5 (Phase 7 runs only when every step passed).
|
||
|
||
The same wip commit of completed work runs whenever a step goes up (round 4
|
||
or 5). Release the step's test lock before stopping
|
||
(`node ${CLAUDE_PLUGIN_ROOT}/lib/util/test-lock.mjs unlock`, `test_lock` =
|
||
`"released"`), so the operator and the planner can edit the test.
|
||
|
||
#### Sub-step G — Checkpoint, done line, unlock
|
||
|
||
1. **Checkpoint.** Stage the step's scope — its `Files:` and its test files,
|
||
and only those, never `git add -A` — then run the `Checkpoint:` git commit
|
||
command exactly as written in the plan. (The test files are listed apart
|
||
from `Files:` in the plan; staging only `Files:` would leave the step's
|
||
test uncommitted and charge it to the next step's diff.)
|
||
If the commit fails (nothing to commit, etc.): warn but do NOT fail the
|
||
step. The step's verification already passed — the commit is bookkeeping.
|
||
2. **The done line**, at the step's own commit. A step is done only when a
|
||
script says so, and the script writes its line only when the test command
|
||
exits 0:
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/verification/done-line.mjs --step {N} --cmd "{Verify command}" --report {report_path}
|
||
```
|
||
Exit 0 = `DONE step {N}: … exit 0 at {the checkpoint sha} …` appended to the
|
||
step report. Confirm with
|
||
`node ${CLAUDE_PLUGIN_ROOT}/lib/verification/done-line.mjs --check --step {N} --report {report_path} --commit {hash}`
|
||
(exit 0 = the line names this step's commit). Neither the helper nor the
|
||
executor ever types a DONE line.
|
||
- Exit 1 = the test went red after the checkpoint (it passed in Sub-step E
|
||
moments before: a flaky test or an order dependency). A finding for
|
||
Sub-step F; the fix lands as a second commit with the same message, and
|
||
Phase 7.5's commit count reports that extra commit as drift — said, not
|
||
hidden.
|
||
- Exit 2 = it could not run: never green — on a ≥ 1.8 plan that is the
|
||
plan's `Verify:` (up to the plan, as in Sub-step B); below 1.8, a step
|
||
whose `Verify:` is not a test-runner command records "no done line
|
||
(Verify is not a test-runner command)" in its report and rests on
|
||
Sub-step D1's exit code.
|
||
3. **Release the test lock**:
|
||
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/test-lock.mjs unlock` → `test_lock` =
|
||
`"released"` (a step that was `"not-locked"` stays so).
|
||
|
||
```
|
||
Step {N}: PASS (committed: {hash})
|
||
```
|
||
|
||
**Commit-message-pattern check:** after the commit succeeds, read the HEAD
|
||
commit message (`git log -1 --pretty=%s`) and match it against the step's
|
||
`commit_message_pattern`. Mismatch does NOT fail the step (verification
|
||
already passed) but is recorded as `checkpoint_drift: {expected_pattern,
|
||
actual_message}` in the progress file. Phase 7.5 audit reports drift as
|
||
an advisory.
|
||
|
||
Update progress: `steps.{N}.status = "completed"`, `steps.{N}.commit = {hash}`,
|
||
`steps.{N}.completed_at = now`, `steps.{N}.manifest_audit = "pass"`.
|
||
|
||
### Step mode exit
|
||
|
||
If mode = `step N`: after completing step N (pass or fail), skip remaining steps
|
||
and jump to Phase 8 (final report).
|
||
|
||
## Phase 7 — Exit / verification check
|
||
|
||
**Runs only when all steps passed** (not on early stop). Which check runs
|
||
depends on what was executed:
|
||
|
||
| Input | Check |
|
||
|-------|-------|
|
||
| session spec | the `## Exit Condition` checklist |
|
||
| trekplan | the plan's `## Verification` criteria |
|
||
|
||
Phase 4's entry-condition skip for trekplans stands — a plan carries no entry
|
||
condition. The exit side is not symmetrical: a trekplan's `## Verification`
|
||
section is where the brief's success criteria land, and it used to run only on
|
||
the multi-session wave path (Phase 2.6 Step 4). A single-session run therefore
|
||
reported `completed` without ever executing the criteria it was measured
|
||
against. It no longer does.
|
||
|
||
### Session specs — exit condition checklist
|
||
|
||
Run each exit condition command from the `## Exit Condition` checklist:
|
||
|
||
```
|
||
Exit condition check:
|
||
- [ ] {command} → {PASS | FAIL}
|
||
- [ ] {command} → {PASS | FAIL}
|
||
```
|
||
|
||
If all pass: `exit_condition_checked: true` in progress file.
|
||
If any fail: record which failed. Include in final report.
|
||
|
||
### trekplans — run the plan's `## Verification`
|
||
|
||
Run the criteria runner over the plan file parsed in Phase 1
|
||
(`{plan_path}`). It parses the `## Verification` section, screens every command
|
||
through the plugin's own PreToolUse denylist before it runs (with no shell:
|
||
its words reach the runner as written), and runs what survives — in the
|
||
FOREGROUND, never backgrounded. (A command spawned
|
||
from node does not pass through the Bash tool, so that hook cannot fire by
|
||
itself; the runner invokes it over its documented stdin protocol instead of
|
||
carrying a second copy of the denylist.)
|
||
|
||
```bash
|
||
# Resolve the plugin root ONCE. ${CLAUDE_PLUGIN_ROOT} is substituted in this
|
||
# command's text but is EMPTY in the Bash tool's process env, and a bare
|
||
# `node ${CLAUDE_PLUGIN_ROOT}/lib/…` then runs `node /lib/…`, which exits 1 —
|
||
# 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
|
||
/*) ;;
|
||
*) 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
|
||
if [ ! -f "$VOYAGE_ROOT/lib/verification/criteria-runner.mjs" ]; then
|
||
echo "[voyage] plan verification could not run - plugin root unresolved (exit 2)."
|
||
echo " NOT a pass: report it and never emit result: completed."
|
||
exit 2
|
||
fi
|
||
|
||
# The working tree the criteria run in. It is where every allowlisted command
|
||
# is executed and where a `bash tests/<script>.sh` criterion resolves, so it is
|
||
# resolved, never left to the process cwd - and a repo-less checkout falls back
|
||
# rather than passing "".
|
||
CRITERIA_CWD="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||
|
||
node "$VOYAGE_ROOT/lib/verification/criteria-runner.mjs" --plan "{plan_path}" \
|
||
--json --cwd "$CRITERIA_CWD"
|
||
```
|
||
|
||
The exit code is the verdict, and it is not advisory:
|
||
|
||
| Exit | Meaning | Executor |
|
||
|------|---------|----------|
|
||
| 0 | every criterion passed | `plan_verification.status = "passed"`; continue |
|
||
| 1 | a criterion failed, was blocked, or could not run (no command, or a command outside the allowlist of test runners) | `plan_verification.status = "failed"`; progress `status: "failed"` |
|
||
| 2 | the runner itself could not run | `plan_verification.status = "not-run"`; treated exactly like exit 1 |
|
||
|
||
A plan with no `## Verification` section exits 1 with
|
||
`error.code = NO_VERIFICATION_SECTION`. That is deliberate: a plan that
|
||
promises no end-to-end check cannot be reported as verified.
|
||
|
||
A plan whose `## Verification` section IS there but declares nothing the runner
|
||
can read exits 1 with `error.code = NO_CRITERIA` and a **source line**
|
||
(`{plan_path}:NN`). Say that in the report in plain words — "the plan's
|
||
`## Verification` at {plan_path}:NN declares no criterion the runner reads; the
|
||
plan is what failed here, not the run" — and name the two forms it does read
|
||
(a bullet whose first backticked span is the command; a shell-tagged fence).
|
||
Measured 2026-09-18: nine ordinary shapes of that section — an untagged fence,
|
||
a ```text fence, a table, `## Verification (acceptance)`, `### Verification` —
|
||
all parsed to zero criteria, and Phase 7 then forbade `result: completed`
|
||
without anyone being told that the format, not the code, was the problem.
|
||
Silence is what made an unreadable section indistinguishable from a broken run.
|
||
|
||
Record in the progress file (additive-optional; unknown keys are tolerated by
|
||
`progress-validator.mjs`, so a legacy progress file still validates):
|
||
|
||
- `plan_verification.status` — `"passed" | "failed" | "not-run"`
|
||
- `plan_verification.summary` — `{total, passed, failed, blocked, unrunnable}`
|
||
- `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
|
||
`plan_verification.status != "passed"`. This is clause 2 of the stop-signal
|
||
contract below (Phase 7.5) — "the plan's Verify command(s) passed (exit 0)" —
|
||
now enforced on the single-session path and not only on the multi-session one.
|
||
Continue to Phase 7.5 either way: the manifest audit is independent, and a
|
||
verification failure must not hide a manifest drift.
|
||
|
||
### End to end — "run the thing" (P9 (6))
|
||
|
||
Tests passing is not the product working. To see and drive the app, use
|
||
Claude Code's built-in `run` skill; the verdict is still the `End-to-end:`
|
||
command, because `run` gives no exit code. Copy `end_to_end.status` from the
|
||
runner's JSON into `end_to_end` — the runner decides it, not this prose:
|
||
|
||
- `"verified"` — the verification passed and at least one passed criterion
|
||
runs a step's `End-to-end:` command (`by: "end-to-end-field"`), or, the
|
||
older form, says in its text that it runs the thing end to end (`end to
|
||
end`, `end-to-end` or `run the thing`, any case; `by: "criterion-text"`);
|
||
- `"not-verified"` — otherwise, including a session spec and a plan whose
|
||
verification did not pass.
|
||
|
||
`"not-verified"` is never green: the report says **not verified end to end**
|
||
in plain words, and the completion gate (Phase 7.5, clause 4) turns
|
||
`completed` into `partial`. The fix is in the plan — a criterion that runs the
|
||
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.
|
||
- **A restore did not run** (the guard refused it — `git checkout --` is
|
||
destructive git, refused in the permission mode that shows no prompts —
|
||
or the operator declined it): the paths are still changed.
|
||
`simplify = "revert-refused"`, never `"reverted"`. When the cleanup broke
|
||
the verification, stop here: the run is `partial`, no review runs over a
|
||
tree that failed it, and the report lists the paths for the operator to
|
||
restore by hand. Paths outside the run's files that were not restored are
|
||
listed too.
|
||
- **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" --repo .
|
||
```
|
||
`--repo .` checks that an ALLOW/WARN review is of the commit being merged:
|
||
its `scope_sha_end` is HEAD, or an ancestor of HEAD whose later commits
|
||
change only the review lessons and the `CLAUDE.md` pointer (step 3's
|
||
commit). Record `review = {round, path, gate_exit, verdict}`. Exit 0
|
||
(ALLOW/WARN) → Phase 7.5. Exit 2 (could not decide, a reviewer or engine
|
||
that did not run, or a reason that starts `review-stale`: a review of
|
||
another commit) → 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. **Today the loop runs only for steps written as
|
||
runnable plan steps:** `Test first:` with its `File:` and `Verifies:`, and a
|
||
`Verify:` that is one allowlisted test command. A step in the review
|
||
template's own form (a prose `Test first:`, `Verify:` "re-run `/trekreview`")
|
||
does not pass step 3, and the run stops with "re-plan from the review" — the
|
||
usual outcome until a later order makes the template's steps runnable as
|
||
written. 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. `--steps-from`
|
||
holds the appended steps to the step rules (`Test first:` with its `File:`,
|
||
one runnable `Verify:`) whatever the plan's version; below 1.8 nothing else
|
||
would:
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs --strict --json --steps-from {total + 1} "{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}" --trailer "Co-Authored-By: Claude {model} <noreply@anthropic.com>"`,
|
||
`{model}` being this session's model name (e.g. `Opus 5.5`). On exit 1 or 2, print the `reason` in one line and continue: the approval is logged but not committed.
|
||
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
|
||
check: it ignores the executor's own per-step bookkeeping and re-verifies
|
||
session-wide state directly from the filesystem and git log. If the audit
|
||
disagrees with the executor's self-report, the audit wins.
|
||
|
||
This phase exists because agents can hallucinate completion. Phase 7.5
|
||
produces an independent truth based on objective state, not self-narrative.
|
||
|
||
**Steps:**
|
||
|
||
1. **Enumerate expected paths:** aggregate `expected_paths` across every
|
||
step manifest (and the session_manifest, if present). Deduplicate.
|
||
|
||
2. **Filesystem check:** for each expected_path, confirm the file exists.
|
||
|
||
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)
|
||
+ 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). 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.
|
||
|
||
6. **Forbidden-path sweep:** for each `scope_forbidden` entry, check
|
||
`git diff --name-only {session_start_sha} HEAD -- {path}` is empty.
|
||
|
||
7. **Done-line sweep:** for every step with `status=completed` that carries a
|
||
`report_path`, run
|
||
`node ${CLAUDE_PLUGIN_ROOT}/lib/verification/done-line.mjs --check --step {N} --report {report_path} --commit {steps.N.commit}`
|
||
(the line must name the step's own checkpoint commit).
|
||
Exit 1 on a plan ≥ 1.8 = drift (`{check: "done-line", expected: "DONE step N", actual: "absent"}`);
|
||
below 1.8 it is drift only when the step's report does not say why it has
|
||
none (Sub-step G).
|
||
|
||
Compare the audit result against the executor's `progress.status`:
|
||
|
||
- **Audit pass + progress=completed:** status stays `completed`.
|
||
- **Audit fail + progress=completed:** OVERRIDE to `partial`. The executor
|
||
believed it was done; the filesystem says otherwise. This is the Wave 1
|
||
hallucination case — the audit is the defense.
|
||
- **Audit fail + progress=failed/stopped:** status stays as-is; drift is
|
||
informational.
|
||
|
||
Record in progress file:
|
||
- `manifest_audit.status = "pass" | "drift"`
|
||
- `manifest_audit.drift_details = [{check, expected, actual}, ...]`
|
||
|
||
### Machine-verifiable completion gate (stop-signal contract)
|
||
|
||
> **Sealed inline so the termination rule survives even when the executor
|
||
> model is the very thing it constrains.**
|
||
>
|
||
> This is the `machine-verifiable completion gate` (the `stop-signal
|
||
> contract`): the executor may emit `result: completed` ONLY when BOTH hold —
|
||
>
|
||
> 1. **Objective predicate** — the **Phase 7.5** manifest audit (above) PASSED,
|
||
> re-verified directly from the filesystem + git log, ignoring the executor's
|
||
> own per-step bookkeeping. "The audit wins"; a self-reported `completed` over
|
||
> a drifting audit is OVERRIDDEN to `partial` (Phase 7.5).
|
||
> 2. **Explicit stop-signal** — a signal *distinct from the executor's
|
||
> self-assessment* is satisfied: the plan's Verify command(s) **passed (exit
|
||
> 0)** / lint clean / an explicit **DONE token emitted AFTER the Phase 7.5
|
||
> audit ran** (never before, never in place of it). A transcript that merely
|
||
> "feels done" is NOT a stop-signal — victory-declaration bias is the exact
|
||
> failure mode this gate exists to defeat. For a trekplan this clause is
|
||
> **Phase 7's** `plan_verification.status == "passed"` — a real exit code
|
||
> from `lib/verification/criteria-runner.mjs`, on the single-session path as
|
||
> well as the multi-session one. `"failed"` or `"not-run"` OVERRIDES
|
||
> `completed` to `failed`, exactly as the audit-override does.
|
||
> 3. **Counter liveness (checked by the executor against the audit's counts,
|
||
> 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 + (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 (Phase 7.5). So the counter's liveness is auditable at the gate,
|
||
> not merely asserted in prose.
|
||
> 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.
|
||
>
|
||
> **Commands:** Phase 7.5 step 3's `git rev-list --count`, step 5's `bash -n`,
|
||
> step 6's `git diff --name-only` and step 7's `done-line.mjs --check`; the
|
||
> criteria runner behind clauses 2 and 4; clause 5's `arch-rules.mjs` and
|
||
> `plan-scope-sweep.mjs`. **Executor judgement:** steps 1, 2 and 4, the expected
|
||
> count in step 3, and clause 3's formula. A script for the judgement parts is
|
||
> part c of the hardening.
|
||
>
|
||
> 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 audit and an objective
|
||
> stop-signal, never on the model's narrative. Native `/goal` (when
|
||
> capability-probed and wired) is an OPTIONAL accelerator only: transcript-only,
|
||
> it cannot run the real check, so it can never be the sole gate.
|
||
|
||
## Phase 7.6 — Recovery dispatch (multi-session parent context only)
|
||
|
||
**Preconditions:**
|
||
- This is the parent trekexecute invocation (not a child `--session N`)
|
||
- Phase 7.5 reported `drift`
|
||
- `recovery_depth < 2` (hard cap to prevent infinite loops)
|
||
|
||
**Skip otherwise.** Recovery in child context or at depth 2+ escalates to
|
||
the user for manual resolution.
|
||
|
||
**Synthesize a recovery session spec:**
|
||
|
||
1. Determine the missing step numbers from `manifest_audit.drift_details`
|
||
(steps whose expected_paths are absent or whose commits are missing).
|
||
2. Read the original session spec. Copy only the missing steps (preserving
|
||
their Manifest blocks) into a new file:
|
||
`{output_dir}/session-{N}-recovery-{depth}.md`
|
||
3. Populate `## Recovery Metadata`:
|
||
- `recovery_of: {original session spec path}`
|
||
- `recovery_depth: {current depth + 1}`
|
||
- `missing_steps: [N, M, ...]`
|
||
- `entry_condition_override: "previous partial session committed at {sha}"`
|
||
- `parent_progress_file: {path}`
|
||
4. Prepend the synthetic Step 0 pre-flight (same as normal session specs).
|
||
5. Set `recovery_dispatched = true` in parent progress file.
|
||
|
||
**Hand the recovery session over as an order (Voyage does not start it):**
|
||
|
||
Report the recovery spec as a session order and stop the run as `partial`:
|
||
|
||
```
|
||
Recovery order: session {N}, depth {depth}
|
||
Open a Claude Code session yourself in {REPO_ROOT} (the main checkout, which
|
||
holds the session's merged work) and give it:
|
||
/trekexecute --session {N} {recovery spec path}
|
||
That session runs the missing steps and its own Phase 7.5 audit.
|
||
```
|
||
|
||
Nothing routes a later run of this command back to the recovery: the recovery
|
||
session's own audit is its check. It runs as a child, so if it still drifts it
|
||
stops `partial` and escalates to the user (the preconditions above) instead of
|
||
dispatching again. If Phase 7.5 drifts here while `recovery_depth` is already 2,
|
||
report instead:
|
||
|
||
```
|
||
RECOVERY EXHAUSTED: session {N} drifted after 2 recovery attempts.
|
||
Missing: {steps and details}
|
||
Status: partial (recovery_depth=2, escalated to user)
|
||
```
|
||
|
||
Do NOT dispatch a third recovery. Report to the user.
|
||
|
||
### Iteration caps + global recovery/retry budget
|
||
|
||
Every recovery/retry loop in this executor has a documented numeric bound.
|
||
These are **three distinct axes** — not one nested counter:
|
||
|
||
- **Per-step fix-round cap** — `fix_rounds ≤ 5`: maximum 5 fix rounds per step, escalating upward
|
||
(rounds 1–3 fix with a fresh helper, 4 goes to the plan, 5 to the operator;
|
||
Hard Rule 5 / Sub-step F). `attempts` counts the helpers a step launched.
|
||
Bounds the rounds of a *single* step.
|
||
- **Per-session recovery cap** — `recovery_depth < 2` (Phase 7.6 hard cap).
|
||
Bounds how deep recovery *dispatch* nests.
|
||
- **Global recovery/retry budget** — a new env-overridable constant
|
||
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS` (default **25**) — the aggregate ceiling
|
||
on the **total count of recovery + retry iterations across the whole
|
||
execution**. This is the case the two caps above do NOT bound: many steps each
|
||
using up to 3 fix rounds can sum unbounded. It primarily backstops the **fix-round
|
||
aggregate** (recovery dispatch is already hard-capped at 2). Reaching 0 stops
|
||
the run (stop class 4: a plan that needs that many fixes is guesswork). Surfaced and
|
||
enforced via the `iterations_remaining` counter (Phase 3 schema; decremented
|
||
on every recovery/retry iteration; reconciled at the Phase 7.5 completion
|
||
gate).
|
||
|
||
Default **25** is an independent value from the cap research (practitioner range
|
||
10–50; OWASP Agentic ASI08 financial-DoS), tuned down from the research ~50
|
||
starting point for a tighter backstop. Operators may override:
|
||
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS=10 /trekexecute --project ...`. The
|
||
`effort=='low'` single-foreground-loop path and the wave path are both subject to
|
||
this aggregate ceiling; it is an execution-spec ceiling (enforced via the
|
||
`iterations_remaining` counter), not a code-loop edit.
|
||
|
||
## Phase 8 — Final report
|
||
|
||
Always produce a final report.
|
||
|
||
### Main-merge gate (MAIN_MERGE_GATE — always pauses)
|
||
|
||
Before writing the final progress + state files (and before any "merge to
|
||
main" prose), emit the `main-merge-gate` lifecycle event so observability
|
||
and operator tooling can see that the pipeline reached its terminal
|
||
boundary:
|
||
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs --event main-merge-gate --payload - <<'JSON'
|
||
{"plan":"{plan-path}","wave_count":{wave-count}}
|
||
JSON
|
||
```
|
||
|
||
Fill in the placeholders yourself. The payload goes on stdin through a quoted
|
||
heredoc, so the shell expands nothing in it: not a variable (shell state from an
|
||
earlier Bash call does not survive, and an expanded variable is how this event
|
||
used to print `payload-not-json`), and not a quote character or `$(...)` in the
|
||
plan path. `{plan-path}` is the repo-relative plan path written as a JSON
|
||
string (escape a `"` or `\` in it). `{wave-count}` is the number of merged
|
||
waves, `1` on the single-session path.
|
||
|
||
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.
|
||
|
||
**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.
|
||
|
||
**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.
|
||
|
||
**1. The verdict.** Decide with the review's verdict as a command, never from
|
||
a summary:
|
||
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/review/verdict-gate.mjs --json "{review_path}" --repo .
|
||
```
|
||
|
||
`{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 |
|
||
|
||
`--repo .` makes the gate check the review against the commit being merged.
|
||
Exit 2 with a reason that starts `review-stale` is closed with the decline
|
||
reason `review-stale` and the line "re-run /trekreview on this commit": the
|
||
review is of another commit than this one, or something other than the
|
||
review lessons and the `CLAUDE.md` pointer changed after it. Any other exit 2
|
||
is `review-not-measured`.
|
||
|
||
The merge runs only on exit 0 and a `completed` result. **Closed:** emit
|
||
`main-merge-declined` with the reason `review-block`, `review-stale`,
|
||
`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, a review
|
||
of this commit, 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}"}
|
||
JSON
|
||
```
|
||
|
||
On the multi-session wave path (Phase 2.6), and only when `next-wave` said
|
||
`none` (every wave merged) and Step 4 passed, also clear the plan's orders
|
||
directory — orders, archived progress copies and the merged-waves record — so a
|
||
later plan with the same `{slug}` starts clean. This is the one place it is
|
||
cleared:
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" clear --session-dir "$SESSION_DIR")
|
||
```
|
||
|
||
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":"{reason}"}
|
||
JSON
|
||
```
|
||
|
||
The reason is one of the literal words above (`review-block`,
|
||
`review-stale`, `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: the lock reads the review again (a review fixed or re-run
|
||
in between counts) and runs the same gate against this repo, so a review of
|
||
an older commit stays refused; 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`,
|
||
master verification (Step 4) runs again, and the run returns to this gate. The
|
||
session branches (`trek/{slug}/session-N`) are merged and deleted unless
|
||
Step 3 or 5 kept them; Step 3 pushed each one before merging it (Hard Rule 19),
|
||
so where that push succeeded it also survives on the remote. Inspect the
|
||
integrated result with `git log` on the current branch.
|
||
|
||
### Progress + state-file writes
|
||
|
||
Update progress file: `status` to `completed`/`failed`/`stopped`/`partial`, `updated_at`, `summary`.
|
||
A sandbox-blocked run (Step 0 sentinel) writes run status `stopped`; its report
|
||
result is `blocked`.
|
||
|
||
**Also atomically write `.session-state.local.json`** (Handover 7) at this
|
||
convergence point — every successful, failed, stopped, or partial run that
|
||
reaches Phase 8 must produce or refresh the state file. Schema:
|
||
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"project": "{project_dir}",
|
||
"next_session_brief_path": "{determined from Execution Strategy: next session's brief path, or current brief path if last session}",
|
||
"next_session_label": "{label of next session from Execution Strategy, or 'Complete' if last}",
|
||
"status": "{same as progress.json status — completed | failed | stopped | partial}",
|
||
"updated_at": "{ISO-8601 now}"
|
||
}
|
||
```
|
||
|
||
Use `lib/util/atomic-write.mjs` (`atomicWriteJson`) — same crash-safety as
|
||
`progress.json`. Then verify: `node ${CLAUDE_PLUGIN_ROOT}/lib/validators/session-state-validator.mjs
|
||
--json {project_dir}/.session-state.local.json`. On validator failure, warn
|
||
to stderr but do NOT block — Phase 8 must always reach the final report.
|
||
|
||
**Also write sibling `NEXT-SESSION-PROMPT.local.md`** (Bug 3 frontmatter
|
||
contract — see `docs/HANDOVER-CONTRACTS.md` § Handover 7 Lifecycle) in the
|
||
project directory. The frontmatter MUST contain `produced_by: trekexecute`
|
||
and `produced_at: <ISO-8601>` so `/trekcontinue` Phase 1.5 can detect
|
||
producer drift between project-dir and plugin-root candidates. Use a single
|
||
ESM inline block so state-file + prompt-file writes succeed or fail together:
|
||
|
||
```bash
|
||
node --input-type=module -e "
|
||
import path from 'node:path';
|
||
import { writeFileSync } from 'node:fs';
|
||
import { atomicWriteJson } from '${CLAUDE_PLUGIN_ROOT}/lib/util/atomic-write.mjs';
|
||
const [, dir, briefPath, label, status] = process.argv;
|
||
const now = new Date().toISOString();
|
||
const stateObj = { schema_version: 1, project: dir, next_session_brief_path: briefPath, next_session_label: label, status, updated_at: now };
|
||
atomicWriteJson(path.join(dir, '.session-state.local.json'), stateObj);
|
||
const promptBody = '---\\nproduced_by: trekexecute\\nproduced_at: ' + now + '\\nproject: ' + dir + '\\nstatus: ' + status + '\\n---\\n\\n# ' + label + '\\n\\nResume via /trekcontinue.\\n';
|
||
writeFileSync(path.join(dir, 'NEXT-SESSION-PROMPT.local.md'), promptBody);
|
||
" '{project_dir}' '{next_session_brief_path}' '{next_session_label}' '{status}'
|
||
```
|
||
|
||
> Note: this block is a runtime template for Phase 8 orchestration; the
|
||
> `{project_dir}`/`{next_session_brief_path}`/`{next_session_label}`/`{status}`
|
||
> placeholders are filled in by the orchestrator at execution time. The
|
||
> leading `!` prefix that previously wrapped this block was removed because
|
||
> Claude Code's slash-command parser auto-executes `!`...` patterns at
|
||
> skill-load time, even inside bash code fences — see
|
||
> `docs/HANDOVER-CONTRACTS.md` § Handover 7 Lifecycle for context.
|
||
|
||
This single insertion covers every multi-session execution path that
|
||
converges here (Path A: successful single session, Path B: `--session N`
|
||
explicit, Path C: compaction-survival recovery, Path D: standard plan
|
||
completion). Phase 2.55 and Phase 4 cover the early-stop paths E1/E2.
|
||
Phase 2.3 (validate exit) and Phase 5 (dry-run) intentionally do not write
|
||
— neither path is resumable.
|
||
|
||
```
|
||
## Ultraexecute Local Complete
|
||
|
||
**Plan:** {path}
|
||
**Type:** {plan | session-spec}
|
||
**Mode:** {execute | resume | step N}
|
||
**Result:** {COMPLETED | FAILED at step N | STOPPED (escalation) | PARTIAL (N/total passed)}
|
||
**End to end:** {verified | not verified end to end}
|
||
|
||
### Step Results
|
||
|
||
| Step | Description | Result | Red proof | Fix rounds | Test lock | Done line | Attempts | Commit | Manifest |
|
||
|------|-------------|--------|-----------|------------|-----------|-----------|----------|--------|----------|
|
||
| 0 | Sandbox pre-flight | PASS | n/a | 0 | n/a | n/a | 1 | — | n/a |
|
||
| 1 | {desc} | PASS | proven | 0 | released | yes | 1 | abc1234 | pass |
|
||
| 2 | {desc} | FAIL | rejected | 4 (sent to the plan) | released | no | 4 | — | — |
|
||
| 3 | {desc} | — | — | 0 | — | — | 0 | — | — |
|
||
|
||
Steps without proof of red: {N} ({step numbers}; below plan_version 1.8 only)
|
||
|
||
### Controller verdicts
|
||
|
||
Per step and round: spec {pass | fail}, quality {pass | fail}, findings (file:line — problem),
|
||
floor-guard rulings, and what the controller did not judge. Report paths: {report_path per step}.
|
||
|
||
### Stops
|
||
|
||
{none — the run ended without a stop | the stop that ended the run, with its class from the
|
||
closed stop list (Phase 5.5): 1 cannot be undone · 2 security-sensitive · 3 outside the working
|
||
copy · 4 plan so broken that everything after it is guesswork, and the file or command that shows it}
|
||
|
||
### Decisions taken
|
||
|
||
| Step | Decision | Cost if wrong |
|
||
|------|----------|---------------|
|
||
| {N} | {what the executor or a helper decided that the plan did not dictate} | {what must be redone, by whom, how big} |
|
||
|
||
{none, when the plan dictated every call}
|
||
|
||
### Plan Verification (Phase 7, trekplans only)
|
||
|
||
- **Status:** {passed | failed | not-run | n/a}
|
||
- **Criteria:** {passed}/{total} passed, {failed} failed, {blocked} blocked, {unrunnable} not run
|
||
- **Failed criteria:** {label + command + exit code, one per line; empty when passed}
|
||
|
||
### Manifest Audit (Phase 7.5)
|
||
|
||
- **Status:** {pass | drift}
|
||
- **Drift details:** {enumerated; empty on pass}
|
||
- **Recovery dispatched:** {true | false}
|
||
- **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}
|
||
- Skipped: {N}
|
||
- Failed: {N}
|
||
- Not reached: {N}
|
||
- Blocked (sandbox): {N}
|
||
|
||
{if all passed + exit condition passed + plan verification passed + end to end verified}:
|
||
All steps completed. Exit condition: PASS. Plan verification: PASS. End to end: verified.
|
||
|
||
{if all passed but end_to_end = not-verified}:
|
||
All steps passed their checks, but the plan has no passing run-the-thing criterion:
|
||
not verified end to end. Result: PARTIAL.
|
||
|
||
{if failed/stopped}:
|
||
### Failure Details
|
||
|
||
Step {N}: {description}
|
||
On failure: {action}
|
||
Error: {error output, first 20 lines}
|
||
Attempts: {N}
|
||
|
||
### What Remains
|
||
|
||
{Numbered list of unexecuted steps}
|
||
|
||
To resume (single session): /trekexecute --resume {path}
|
||
To continue (multi-session waves): re-run the same /trekexecute command (not --resume)
|
||
```
|
||
|
||
**Result vocabulary (v1.7, strict):**
|
||
- `completed` — all steps passed AND Phase 7.5 manifest audit passed AND
|
||
(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), 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)
|
||
|
||
**JSON summary block** (always at the end, machine-parseable):
|
||
|
||
```json
|
||
{
|
||
"trekexecute_summary": {
|
||
"plan": "{path}",
|
||
"plan_type": "{plan | session-spec}",
|
||
"plan_version": "{1.7 | 1.6 | legacy}",
|
||
"result": "{completed | partial | blocked | failed | stopped}",
|
||
"steps_total": 0,
|
||
"steps_passed": 0,
|
||
"steps_failed": 0,
|
||
"steps_skipped": 0,
|
||
"steps_not_reached": 0,
|
||
"steps_blocked": 0,
|
||
"failed_at_step": null,
|
||
"exit_condition": "{pass | fail | skipped | n/a}",
|
||
"plan_verification": "{passed | failed | not-run | n/a}",
|
||
"manifest_audit": "{pass | drift | n/a}",
|
||
"drift_details": [],
|
||
"recovery_dispatched": false,
|
||
"recovery_depth": 0,
|
||
"iterations_remaining": 25,
|
||
"legacy_plan": false,
|
||
"progress_file": "{path}",
|
||
"end_to_end": "{verified | not-verified | n/a}",
|
||
"steps_without_red_proof": 0,
|
||
"stop_class": "{none | 1 | 2 | 3 | 4}",
|
||
"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 | revert-refused | 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}"
|
||
}
|
||
}
|
||
```
|
||
|
||
The `trekexecute_summary` key makes it grep-able in a session's output.
|
||
|
||
**Multi-session path (Phase 2.6).** Per-session results come from Step 6's
|
||
aggregate of the archived copies `$ORDERS_DIR/wave-*-session-*.progress.json`.
|
||
The report lists every worktree and `trek/{slug}/*` branch Steps 3 and 5 kept,
|
||
with what the user can do (Step 5). If the orders directory was kept (not every
|
||
wave merged, Step 4 did not pass, or the gate was declined), say so: the next
|
||
run resumes at the lowest wave not yet merged, or re-runs Step 4 and returns to
|
||
the gate, and the `clear` command in Step 0 starts the plan over from its first
|
||
wave instead.
|
||
|
||
## Phase 9 — Stats tracking
|
||
|
||
Append one record to `trekexecute-stats.jsonl` in the plugin data directory.
|
||
Run it exactly like this. The record goes on stdin, and the writer resolves
|
||
the directory: `CLAUDE_PLUGIN_DATA`, which the Bash tool env does not carry,
|
||
else the directory `scripts/yardstick.mjs` reads.
|
||
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/stats-append.mjs trekexecute <<'JSON'
|
||
{
|
||
"ts": "{ISO-8601}",
|
||
"plan": "{filename only}",
|
||
"plan_type": "{plan | session-spec}",
|
||
"mode": "{execute | resume | dry-run | step}",
|
||
"result": "{completed | failed | stopped | partial}",
|
||
"steps_total": 0,
|
||
"steps_passed": 0,
|
||
"steps_failed": 0,
|
||
"steps_skipped": 0,
|
||
"failed_at_step": null
|
||
}
|
||
JSON
|
||
```
|
||
|
||
`steps_passed` counts the steps whose status is `completed`.
|
||
|
||
The writer prints `{"written": true, ...}`. On `"written": false` (exit 1),
|
||
print its `reason` in one line and continue. A stats failure never blocks the
|
||
workflow, but it is reported, not skipped silently.
|
||
|
||
## Profile (v4.1)
|
||
|
||
Accepts `--profile <name>` where `<name>` is `economy`, `balanced`, `premium`,
|
||
`fable`, or a custom profile under `voyage-profiles/`. Default: `premium`.
|
||
|
||
Resolution order (per `lib/profiles/resolver.mjs`):
|
||
1. `--profile` flag (source: `flag`)
|
||
2. `VOYAGE_PROFILE` env-var (source: `env`)
|
||
3. Inherited from `plan.md` frontmatter `profile:` field (source: `inheritance`)
|
||
4. `premium` default for v4.0-style plans without `profile:` (source: `default`)
|
||
|
||
The selected profile goes to the resolver. It changes no model here: the step helper and the
|
||
controller run opus (Composition rule below), and nothing in this command reads `parallel_agents`.
|
||
|
||
Examples:
|
||
```
|
||
/trekexecute --profile economy --project .claude/projects/2026-05-09-add-auth
|
||
VOYAGE_PROFILE=balanced /trekexecute --project ...
|
||
```
|
||
|
||
## Composition rule (v5.1)
|
||
|
||
Independent of the profile system. When `brief.md` carries
|
||
`phase_signals` (brief_version ≥ 2.1), each downstream phase resolves
|
||
effort + model as:
|
||
|
||
```
|
||
effort_for_phase = brief.phase_signals[<phase>]?.effort ?? 'standard'
|
||
model_for_phase = brief.phase_signals[<phase>]?.model ?? profile.phase_models[<phase>]
|
||
```
|
||
|
||
The brief signal wins per-phase when present; the profile fills any
|
||
gaps. Both fields are mechanically resolved by the single composed CLI,
|
||
invoked in Phase 1 alongside the sequencing-gate brief-validator call:
|
||
|
||
```bash
|
||
# v5.9 — composed phase-model resolution (brief > profile > default) for the
|
||
# execute phase. ONE call returns {model, source}, plus
|
||
# effort only when the brief's phase signal sets one; without it the phase
|
||
# runs at effort `standard` (no change). Captured as phase_signal_result.
|
||
# Append --profile {profile} when the operator passed --profile.
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase execute --brief-path "{dir}/brief.md" [--profile {profile}] --json
|
||
```
|
||
|
||
`phase_signal_result.effort` (absent = `standard`) is consumed when picking the execution
|
||
strategy (the parallel-wave choice, Phase 2.5). The resolver does NOT control the orchestrator's own
|
||
model — that is fixed at invocation time (command frontmatter omits
|
||
`model:`, so it follows the session model) and cannot be switched mid-turn.
|
||
`/trekexecute` spawns no swarm: one step helper at a time and one controller
|
||
(Hard Rule 10), and both run `opus` — the step agent's pin and the
|
||
controller's `model: "opus"` — so `phase_signal_result.model` still has no
|
||
spawn site here; it is returned for cross-command uniformity; no stats record carries it.
|
||
|
||
For `/trekexecute` specifically: `effort == 'low'` runs the plan as one
|
||
session, step after step (Phase 2.5). `effort == 'standard'` (or absent) →
|
||
no change (default execution strategy applies). `effort == 'high'`: see
|
||
`### High-effort behavior (v5.1.1)` below.
|
||
|
||
### Sequencing gate surface
|
||
|
||
When `/trekexecute --project <dir>` is invoked, ALWAYS run
|
||
`brief-validator.mjs --soft --json` against `{dir}/brief.md`. If
|
||
`BRIEF_V51_MISSING_SIGNALS` appears in `errors` (brief_version ≥ 2.1
|
||
without `phase_signals` or `phase_signals_partial: true`), halt with:
|
||
`Brief is brief_version 2.1 but does not carry phase_signals — re-run
|
||
/trekbrief to commit them (Phase 3.5).` Enforcement is validator-only;
|
||
commands surface, don't re-enforce.
|
||
|
||
### High-effort behavior (v5.1.1)
|
||
|
||
High effort changes nothing in `/trekexecute`: the step helper and the
|
||
controller already run opus, and every gate already runs. No `gates_mode` is
|
||
set from the effort.
|
||
|
||
## Hard rules
|
||
|
||
1. **No AskUserQuestion for execution decisions.** All execution decisions come
|
||
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). 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
|
||
in the final report — do not touch it.
|
||
|
||
3. **Exit code is truth.** The Verify command's exit code is authoritative.
|
||
Non-zero = FAIL regardless of output. Zero with wrong output = FAIL.
|
||
|
||
4. **Fresh verification.** Re-run the Verify command from scratch every time.
|
||
Never trust cached or prior results.
|
||
|
||
5. **Fix rounds go up — maximum 5 fix rounds per step, escalating upward.**
|
||
Rounds 1–3 are fixes by a fresh helper, round 4 sends the step back to
|
||
planning, round 5 to the operator (Sub-step F). Never a fourth guess at the
|
||
same level, never a cheaper model, never a loop.
|
||
|
||
6. **Never corrupt completed work.** Only revert files from the failing step.
|
||
Never touch files from earlier passed steps.
|
||
|
||
7. **Checkpoint discipline.** Run the Checkpoint commit exactly as written.
|
||
Do not combine, reorder, or skip checkpoints on passed steps.
|
||
|
||
8. **Scope fence enforcement.** For session specs: never modify files in the
|
||
Never-touch list, regardless of what the Changes field says.
|
||
|
||
9. **Progress file is ground truth.** Resume uses the progress file, not git log.
|
||
|
||
10. **One fresh helper per plan step, one controller, nothing more.** The
|
||
executor writes no product code: each step is built by a fresh
|
||
`voyage:step-implementer` (Sub-step C) and judged by one controller, a
|
||
general-purpose helper that reads the diff (Sub-step E).
|
||
Helpers never start helpers: the step agent has no Agent tool, and the
|
||
controller is told it changes nothing. Never the Agent tool's `name` (a named spawn's
|
||
result never returns), never `isolation` (measured: wrong base). No
|
||
TeamCreate — kept out via `disallowed-tools: TeamCreate`, since omission
|
||
from `allowed-tools` alone leaves a tool callable (CC 2.1.152).
|
||
|
||
11. **Worktree isolation is mandatory for multi-session execution.** Every
|
||
session order names its own git worktree. Never run two or more sessions
|
||
in the same working directory. This rule has no exceptions.
|
||
Sequential (single-session) execution does not require worktrees.
|
||
|
||
12. **Worktree cleanup always runs, and never with force.** Before producing
|
||
the final report (Phase 8), remove the worktrees and merged session
|
||
branches created during this execution, even if the run failed or was
|
||
stopped — but only with `git worktree remove` without force and
|
||
`git branch -d`. A worktree git refuses to remove or a branch it refuses to
|
||
delete holds work: keep it, never retry with force, and list it in the
|
||
final report with what the user can do (Phase 2.6 Step 5). Every unmerged
|
||
`trek/{slug}/*` branch survives every failure path. The one exception to
|
||
running cleanup: never while a wave awaits its sessions — a run that stops
|
||
at Phase 2.6 Step 2, or at a Step 3 that finds a session `running` or a
|
||
dirty or refused merge, writes no Phase 8 report and leaves every worktree
|
||
and branch in place.
|
||
|
||
13. **Merge sequentially, abort on conflict.** After a parallel wave completes,
|
||
merge each session's branch into the main branch one at a time with
|
||
`--no-ff`. If any merge produces a conflict, run `git merge --abort`,
|
||
report the conflicting files, and do not attempt further merges. Never use
|
||
`--force` or `--strategy-option theirs/ours` to silently resolve conflicts.
|
||
|
||
14. **Verify command security check.** Before executing any `Verify:` or
|
||
`Checkpoint:` command, check it against the executor security denylist
|
||
(Sub-step D). If the command matches a blocked pattern, escalate
|
||
immediately — do not execute, do not retry.
|
||
|
||
15. **No writing outside the repository.** During step execution, never write
|
||
files outside the git repository root (`git rev-parse --show-toplevel`).
|
||
Exception: `.claude/` paths for plans, progress files, and stats,
|
||
AND `~/.claude/projects/*/memory/` paths for memory files when the
|
||
step's manifest declares `memory_write: true`. The `memory_write: true`
|
||
opt-in is a second gate: unauthorized memory writes still fail. This
|
||
prevents escape-from-repo attacks where a plan step modifies home
|
||
directory or system files, while permitting the narrow case of
|
||
deliberate memory updates.
|
||
|
||
16. **No writing to security-sensitive paths.** Never write to `.git/hooks/`
|
||
(git hook injection), `~/.ssh/`, `~/.aws/`, `~/.gnupg/`, `.env` files,
|
||
shell configs (`~/.zshrc`, `~/.bashrc`, `~/.profile`), or
|
||
`.claude/settings.json` / `.claude/hooks/` (infrastructure self-modification).
|
||
|
||
17. **Manifest is the completion predicate.** A step may not be marked passed
|
||
if its manifest does not verify, regardless of the Verify command's exit
|
||
code. The manifest is the objective contract — Verify is necessary but
|
||
not sufficient. For v1.7+ plans: a Verify pass with a manifest fail sets
|
||
step result to `failed` and triggers the On-failure clause. For legacy
|
||
v1.6 plans: synthesized manifests apply with the same force, but
|
||
`legacy_plan: true` is logged in progress.
|
||
|
||
18. **Last-activity rule + stop-signal.** The executor's final tool call before
|
||
writing Phase 8 must be a manifest check (Phase 7.5 audit), never an arbitrary
|
||
file review. This prevents the "hallucinated completion" failure mode
|
||
where a transcript ends on an unrelated Read and the agent self-reports
|
||
`completed` without verifying. If Phase 7.5 has not run, the executor
|
||
may not emit `result: completed` under any circumstances. Beyond that:
|
||
`result: completed` requires the **machine-verifiable completion gate**
|
||
(the **stop-signal contract**, Phase 7.5) satisfied in full — Phase 7.5
|
||
audit PASS **and** an objective stop-signal (Verify exit 0 / DONE token
|
||
emitted AFTER the audit), never executor self-assessment alone.
|
||
|
||
19. **push-before-cleanup.** After successful `git merge --no-ff` of a wave
|
||
branch, run `git push origin <branch>` BEFORE `git worktree remove` and
|
||
`git branch -d`. Push failure is non-fatal (no remote, offline, etc.) —
|
||
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
|
||
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS` budget (default 25 — aggregate
|
||
fix-round + recovery backstop) are independent bounds, not one nested
|
||
counter. Every recovery/retry loop is bounded by at least one of them. See
|
||
"Iteration caps + global recovery/retry budget".
|
||
|
||
21. **Never start Claude Code.** No `claude` CLI invocation, no Agent SDK, no
|
||
background process that runs a session — not for waves, not for recovery,
|
||
not for a test. Work that needs another session becomes a session order
|
||
plus a starter line the user gives a session they open themselves
|
||
(Phase 2.6, Phase 7.6).
|
||
|
||
22. **Proof, not word.** Red before green, an unweakened test and "done" are
|
||
each a command's exit code — `red-proof.mjs`, `test-integrity.mjs`,
|
||
`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
|
||
MIT-licensed open source except Kiro (a closed product: only its published
|
||
documentation was read) and Cherny (public talks and posts). Read 2026-10-06;
|
||
pins and quotes in the local practice analysis.
|
||
|
||
- **Superpowers** — adapted from obra/superpowers@8ca22db (MIT, © 2025 Jesse
|
||
Vincent): `subagent-driven-development` — a fresh helper per task, hand-over
|
||
in files, a review package from a recorded start point, a reviewer between
|
||
tasks, the stop classes and the decision log of an unattended run (P8, P4);
|
||
`executing-plans/scripts/task-done` — the completion line appended only when
|
||
the test command exits 0, here `done-line.mjs` (P9 (4));
|
||
`systematic-debugging` — after three failed fixes, question the
|
||
architecture (P9 (7)). Not borrowed: its ladder of cheaper models.
|
||
- **ai-native-sdlc** — bashebr/ai-native-sdlc@11fb8d7 (MIT): the helper's
|
||
report contract, ran / saw / did NOT check (P8).
|
||
- **Kiro** (documentation only): the task runner takes the diff, not the
|
||
agent's account of it, as the reviewer's input (P8). Its background daemon is
|
||
not borrowed; only the step loop.
|
||
- **GitHub Spec Kit** (MIT), github/spec-kit@2e941fe: test-first as a rule,
|
||
mechanized here as `red-proof.mjs` (P9 (1)).
|
||
- **agent-skills** — addyosmani/agent-skills@9d0c60d (MIT): the floor guard of
|
||
constraint-driven development, as `test-integrity.mjs` (P9 (2)).
|
||
- **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); `/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.
|