/trekexecute no longer builds steps itself. Each plan step is built by one fresh helper (new agent step-implementer: opus, effort high, no Agent tool) and judged by one controller (a general helper on opus) that reads the diff from a recorded start_sha and gives two verdicts, spec and quality. Fix rounds go up, maximum 5: three fixes by a fresh helper, then back to the plan, then to the operator. No cheaper-model ladder. Proof is a command, never the model's word: - lib/verification/red-proof.mjs: the step's test must fail at the start point and pass at the result (exit 0/1/2; 2 is never green). Reproduces the02243c6red-first measurement (6 of 55) through the shim fixture. - lib/verification/test-integrity.mjs: the floor guard on the diff (removed asserts, skipped/deleted tests, muted checks, stubs, loosened thresholds). Over the last 150 commits: 149 clean, 1 real finding. - lib/verification/done-line.mjs: DONE step N only when the test command exits 0. Test first is required from plan_version 1.8. The test lock (K8) is used through its CLI and reported as not-locked while it is missing. A run stops only on a closed list of four classes; every other call goes into a decision log with its cost if wrong. Without a passing run-the-thing criterion the result is not verified end to end, never green. Handover 5 gains optional per-step start_sha, report_path, red_proof, test_lock, fix_rounds (0-5) and run-level decisions and end_to_end; old progress files still validate and resume. No isolation on the helper: measured, a worktree-isolated subagent branches from the default branch (02f320f), not the executor's HEAD (2c89a6b), so it would miss the earlier steps. Borrowings credited in commands/trekexecute.md ## Credits (Superpowers, ai-native-sdlc, Kiro, GitHub Spec Kit, agent-skills, Cherny). Tests: 1411 -> 1457 (1455 pass, 0 fail, 2 skipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 KiB
| name | description | argument-hint | allowed-tools | disallowed-tools |
|---|---|---|---|---|
| trekexecute | Disciplined plan executor — single-session, or multi-session through session orders the user starts, with failure recovery | [--project <dir>] [--fg | --resume | --dry-run | --validate | --step N | --session N] [plan.md] | Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent | 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 done line), and decides from exit codes and the controller's
verdicts (Phase 6).
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:
- 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.mdis 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 keepproject_dirset (user may be pointing at a recovery session spec inside the project). - Continue parsing remaining flags (they combine with
--project).
- Resolve
- If arguments contain
--fg: extract the file path. Set mode = foreground. - If arguments contain
--resume: extract the file path. Set mode = resume. - If arguments contain
--dry-run: extract the file path. Set mode = dry-run. - If arguments contain
--validate: extract the file path. Set mode = validate. - If arguments contain
--step N(N is a positive integer): extract N and the file path. Set mode = step, target-step = N. - If arguments contain
--session N(N is a positive integer): extract N and the file path. Set mode = session, target-session = N. - If arguments contain
--gates: setgates_mode = true. Pause for operator confirmation after each wave's worktrees finish but before merge-back, AND before the main-merge gate. (The MAIN_MERGE_GATE in Phase 8 ALWAYS pauses regardless ofgates_mode—--gatesre-enables the per-wave pauses that auto mode otherwise skips.) Defaultgates_mode = false. The flag is consumed by the autonomy-gate state machine via the CLI shim:node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}. - Otherwise: the entire argument string is the file path. Set mode = execute.
If no path is provided (and --project was not used to derive one), output
usage and stop:
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 --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
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}
Phase 2 — Detect file type and parse structure
Read the file. Determine whether it is an trekplan or a session spec:
- Session spec: contains
## DependencieswithEntry condition:AND## Scope FenceAND## Exit Conditionsections. - Voyage: contains
## Implementation Planwith 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 itVerifies:, an optionalPattern:(required from plan_version ≥ 1.8, see below) - Verify — command to run after implementation. It is also the step's test command: proof of red (Sub-step B) and the done line (Sub-step G) run it, so it must be one test-runner invocation
- On failure — recovery action (revert/retry/skip/escalate)
- 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_patterncompiles 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 planning-orchestrator — plan is not executable.
Legacy mode (no plan_version, or < 1.7):
- Synthesize a minimal manifest per step:
expected_paths← step'sFiles:listmin_file_count←len(expected_paths)commit_message_pattern← regex-escape first 3 words of Checkpoint msgbash_syntax_check← auto-detect.shin Filesforbidden_paths← []must_contain← []
- Record
legacy_plan: truein 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 Orderwith 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:
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 — planning-orchestrator must emit v1.7 format
{if missing manifest}:
Step {N} has no Manifest block (plan_version=1.7 requires one per step)
Fix: re-run /trekplan — planning-orchestrator must include manifest YAML
{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:
--fgflag is set--step Nmode--resumemode--session Nmode (runs only that session's steps, foreground)- Plan has no
## Execution Strategysection - 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 Strategywith 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 Strategywith 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:
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:
{
"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 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.
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:
- For each session in the wave, extract the "Touch" list from the Execution Strategy.
- For each pair of sessions (A, B) in the same wave, compute the intersection of their Touch lists.
- 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 — the planning-orchestrator 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.
# 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
# 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 numberWfrom$ORDERS_DIR/wave.jsonand 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:
A number is(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" next-wave --plan "$PLAN_REL" --session-dir "$SESSION_DIR")W: continue to Step 1.nonemeans 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. Ifnext-waveexits 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
mkdir -p "$WORKTREE_DIR" "$ORDERS_DIR"
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:
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:
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):
# 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
(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)
(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 notwave.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.jsonand 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:
It copies each one to(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" archive --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR")$ORDERS_DIR/wave-{W}-session-{N}.progress.json. Step 5 never deletes these copies; Step 6 and Phase 8 read them. - Any
ended-bad:
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.Wave {W} FAILED: Session {N} ended {status}. Stopping — later waves depend on this wave. See its order: {ORDERS_DIR}/wave-{W}-session-{N}.md all_completed: true: ifgates_modeis true, pause for operator confirmation before merge-back. Then continue with the merge.
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:
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.):
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 (noMERGE_HEAD; its message names the local changes or untracked files in the way). Stop and reportcommit, stash or move your changes in {REPO_ROOT}, then re-run: no Step 5, and keepwave.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:
Mark remaining sessions as "merge-failed". Jump to Step 5 (it keeps every unmerged branch), then Phase 8.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.
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):
(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:
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.
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.
# 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. Jump to Phase 8.
--session N mode
When mode = session N:
- Find session N in the Execution Strategy
- Extract its step numbers (e.g., Steps: 4, 5, 6)
- Extract its scope fence (Touch / Never touch lists)
- Execute ONLY those steps, in order, using the single-session protocol (Phase 3→7)
- Enforce the session's scope fence as if it were a session spec's scope fence
- 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
{
"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,
"status": "{in_progress | completed | failed | stopped | partial}",
"steps": {
"1": { "status": "pending", "attempts": 0, "fix_rounds": 0, "start_sha": null, "report_path": null, "red_proof": null, "test_lock": null, "error": null, "completed_at": null, "commit": null }
},
"iterations_remaining": 25,
"entry_condition_checked": false,
"exit_condition_checked": false,
"plan_verification": null,
"end_to_end": 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 |
|---|---|---|
steps.{N}.start_sha |
Sub-step 0 | the commit the step started from; every check 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.
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
mode = execute (fresh):
- If a progress file exists with status
in_progress,failed,stoppedorpartial: warn that--resumeis 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
pendingstatus.
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 saypassedfor a finished step; treat it ascompleted.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 --onelinefor 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).
- 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.
- 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.
- 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.
- 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 ownOn failure: escalatefired (the plan's author declared that past this point is guesswork), or a step went up the fix ladder to the plan (Sub-step F).
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 saypassedfor a finished step; treat it ascompleted. - 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:
git rev-parse HEAD
git status --porcelain
steps.{N}.start_sha= theHEADsha. Every later check of this step readsgit diff {start_sha}: the review package is the diff from this recorded start point, not anything the helper says. On--resumeof a step that already has astart_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.
(A plan below 1.8 whose step has no Test first: skips it: red_proof =
"not-run", counted in the final report, never called proven.)
The test command is the step's Verify: command. 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.
- The helper proves red before it builds (Sub-step C): it writes the test
from
Test first:, runs the gatered-proof.mjs --red-only --base {start_sha} --cmd "{Verify}" --tests {files}(the test must FAIL at the start point), locks the test files, and only then implements. - The executor proves it both ways after the helper returns:
The test must FAIL against the start point (node ${CLAUDE_PLUGIN_ROOT}/lib/verification/red-proof.mjs --base {start_sha} --cmd "{Verify command}" --tests {Test first File paths, comma-separated} --jsonstart_shaunpacked withgit archive, the step's test files copied in) and must PASS against the result (the working tree). The start point holds tracked files only: add--link node_modules(or the repo's untracked dependency directory) when the test needs it.
| Exit | steps.{N}.red_proof |
Executor |
|---|---|---|
| 0 | "proven" |
continue |
1, not-red-at-base |
"rejected" |
the test passes without the change, so it proves nothing: a finding for Sub-step F |
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. When the reason is the command (the Verify: is not one test-runner invocation) on a plan ≥ 1.8, 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) is a finding for Sub-step F |
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};
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, 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, and
voyage_root: ${CLAUDE_PLUGIN_ROOT}.
- Never pass the Agent tool's
nameparameter. A named spawn becomes anin_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'sHEAD(2c89a6b), so it did not see the earlier steps. Only the operator'sworktree.baseRef: "head"setting changes that, and a plugin cannot set it. The helper works in this tree, from the recordedstart_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= the number of helpers launched for the step (1 here).
The helper's one-line answer is not evidence. After it returns:
- The report must exist at
report_pathwith its### Ran,### Sawand### Did NOT checksections (a missing report is a finding for Sub-step F). - Register the step's new files so the diff sees them: for each path in
Files:thatgit status --porcelainshows as??, rungit add --intent-to-add {path}(stages nothing). - A changed path outside the step's
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 files in 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 -rforrm -frwith any pathchmod 777orchmod -R 777- Pipe-to-shell:
curl ... | bash,wget ... | sh,base64 ... | bash evalwith variable expansion:eval $VAR,eval $(cmd),eval `cmd`mkfs,ddwriting to block devices (/dev/sd*,/dev/nvme*)shutdown,reboot,halt,poweroff- Fork bomb patterns
crontabwrites,/etc/cron*modificationskill -9 -1orpkill -9 -1(kill all processes)history -cor truncating~/.bash_history
If matched:
- Do NOT execute the command
- Set step status = "failed"
- Log:
SECURITY: Verify command blocked — matches executor denylist: {pattern name} - Apply
On failure: escalateregardless of the plan's On failure setting - 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: proceed to Sub-step D2 (manifest verification). 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):
- Expected paths exist: for each
expected_pathsentry, verify the file exists in the repo. Count how many exist. - min_file_count satisfied: count from (1) must be ≥
min_file_count. - Forbidden paths untouched: for each
forbidden_pathsentry, rungit diff --name-only {start_sha} -- {path}(since this step began). Any modified forbidden path = fail. - Bash syntax check: for each entry in
bash_syntax_checkAND any.shfile that appears ingit diff --name-only {start_sha}(safety net for unlisted scripts), runbash -n {path}. Non-zero exit = fail. - must_contain patterns: for each
{path, pattern}pair, rungrep -E "{pattern}" {path}. 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:
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 E — Control
One controller, two verdicts. After the mechanical checks (B, D1, D2, D3)
— 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. 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):
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}.
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, "not-run" because the step has no Test first:), Verify
PASS, manifest pass, every floor-guard finding ruled justified (and the guard
ran), "spec": "pass" and "quality": "pass". Then go to Sub-step G.
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 findings 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} — and answers,
per previous finding, fixed or not, plus whether the round changed anything
the findings did not name (that is a new finding). It does not re-judge the
rest of the step.
Sub-step F — Fix rounds
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 (Voyage runs no helper below Opus 5.5 High).
fix_rounds |
Goes to | What happens |
|---|---|---|
| 1, 2, 3 | a fresh helper | 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. Then B (both ways), D1, D2, D3 run again and a fresh controller re-controls only what was fixed (Sub-step E). attempts +1; iterations_remaining −1 |
| 4 | the plan | After three failed fix rounds the question is the architecture, not a fourth fix. The step goes back to planning (below). Stop class 4 |
| 5 | the operator | A step already sent to the plan (fix_rounds 4) that fails its control again after re-planning goes to the operator. The executor never builds this step again: on a later --resume it only runs the checks (B, D1, D2, D3, E) on whatever the operator changed. Stop class 4 |
Back to planning (fix_rounds = 4): save git diff {start_sha} to
{report dir}/step-{N}-upward.diff and write {report dir}/step-{N}-upward.md:
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 stopped, 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 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).
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):
Record failure. Do NOT proceed to next step. Jump to Phase 7.git checkout -- {files from Files: field}
On failure: skip
- Mark step as skipped at its first failed round, without fix rounds. Continue
to next step.
UpdateStep {N}: SKIPPED (non-critical per plan)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).
Commit all completed work before stopping. Stage ONLY files from steps withStep {N}: ESCALATED — requires human judgmentstatus: "completed"in the progress file — collect theirFiles:fields. Never usegit add -A(risks staging secrets, binaries, or unrelated work).
Jump to Phase 7.git add {files from completed steps' Files: fields} && git commit -m "wip: trekexecute stopped at step {N} — escalation needed"
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 — Done line, unlock, checkpoint
- The done line. A step is done only when a script says so, and the script
writes its line only when the test command exits 0:
Exit 0 =node ${CLAUDE_PLUGIN_ROOT}/lib/verification/done-line.mjs --step {N} --cmd "{Verify command}" --report {report_path}DONE step {N}: …appended to the step report. Exit 1 = the test command failed after all: back to Sub-step F. Exit 2 = it could not run: never green — on a ≥ 1.8 plan that is the plan'sVerify:(up to the plan, as in Sub-step B); below 1.8, a step whoseVerify: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. Confirm withnode ${CLAUDE_PLUGIN_ROOT}/lib/verification/done-line.mjs --check --step {N} --report {report_path}(exit 0 = present). Neither the helper nor the executor ever types a DONE line. - 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). - Checkpoint. Stage the step's
Files:— and only those, nevergit add -A— then run theCheckpoint:git commit command exactly as written in the plan.
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.
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 reaches a shell, 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.)
# 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/.
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}]
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. From the runner's JSON, set
end_to_end:
"verified"— at least one criterion that passed says in its text that it runs the thing end to end (its text containsend to end,end-to-endorrun the thing, any case);"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. (This reads the existing ## Verification
criteria; it adds no plan field.)
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:
-
Enumerate expected paths: aggregate
expected_pathsacross every step manifest (and the session_manifest, if present). Deduplicate. -
Filesystem check: for each expected_path, confirm the file exists.
-
Commit-count check: count commits since session-start (
git rev-list --count {start_sha}..HEAD). Expected count = number of steps withstatus=completed(excludes Step 0 if sandbox_preflight). -
Commit-message pattern sweep: walk
git log {start_sha}..HEADand confirm each commit matches one of the declaredcommit_message_patterns(in any order — order is advisory). -
Bash syntax sweep: for every
.shfile ingit diff --name-only {start_sha} HEAD, runbash -n. Collect failures. -
Forbidden-path sweep: for each
scope_forbiddenentry, checkgit diff --name-only {start_sha} HEAD -- {path}is empty. -
Done-line sweep: for every step with
status=completedthat carries areport_path, runnode ${CLAUDE_PLUGIN_ROOT}/lib/verification/done-line.mjs --check --step {N} --report {report_path}. 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(thestop-signal contract): the executor may emitresult: completedONLY when BOTH hold —
- 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
completedover a drifting audit is OVERRIDDEN topartial(Phase 7.5,:1221).- 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 fromlib/verification/criteria-runner.mjs, on the single-session path as well as the multi-session one."failed"or"not-run"OVERRIDEScompletedtofailed, exactly as the audit-override does.- Counter liveness (deterministic, defeats "never decremented") — the gate reconciles the budget counter against the ground-truth counters the Phase 7.5 audit already derives:
iterations_remaining == TREKEXECUTE_MAX_RECOVERY_ITERATIONS − (recovery_depth + Σ steps.*.attempts-beyond-first). A field left un-decremented (or tampered) fails this reconciliation and the result is OVERRIDDENcompleted → partial, exactly as the audit-override already does (:1221). So the counter's liveness is auditable at the gate, not merely asserted in prose.- End to end — for a trekplan, Phase 7's
end_to_end == "verified"."not-verified"OVERRIDEScompleted → partialwith the reason "not verified end to end": tests that pass are not the thing running.Termination authority therefore rests on the deterministic audit + 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:
- Determine the missing step numbers from
manifest_audit.drift_details(steps whose expected_paths are absent or whose commits are missing). - 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 - 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}
- Prepend the synthetic Step 0 pre-flight (same as normal session specs).
- Set
recovery_dispatched = truein 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).attemptscounts 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 theiterations_remainingcounter (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:
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.
Pause for operator confirmation via AskUserQuestion:
Question: "All waves merged. Ready to proceed with the main-merge step and finalize the run?"
| Option | Action |
|---|---|
| Confirm — main-merge | Emit main-merge-approved, proceed to write progress + state files. |
| Decline — hold for review | Emit main-merge-declined. Nothing is finalized: the run is recorded as partial. The commits stay where they are for review — on the multi-session wave path the waves are already merged on the current branch, and the merged-waves record is kept. |
This pause is always on, regardless of gates_mode. The --gates flag
re-enables earlier per-wave pauses; this gate is the one boundary that
ALWAYS pauses on every run — it is the safety stop between completed waves
and the merge that publishes the integrated result.
On confirm:
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:
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" clear --session-dir "$SESSION_DIR")
On decline:
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs --event main-merge-declined --payload - <<'JSON'
{"plan":"{plan-path}","reason":"hold-for-review"}
JSON
The reason is the literal label of the one decline option, never the operator's free text (free text breaks the JSON).
On decline, nothing is cleared: the merged-waves record is kept.
Recovery surface: on the single-session path, if declined,
--resume re-enters at the gate — re-running
/trekexecute --resume --project <dir> skips the completed steps and returns
to the main-merge gate AskUserQuestion. On the multi-session wave path,
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:
{
"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 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:
node --input-type=module -e "
import path from 'node:path';
import { writeFileSync } from 'node:fs';
import { atomicWriteJson } from './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 — seedocs/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}
### 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 passedpartial— 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)blocked— Step 0 sandbox pre-flight exited 77; no real work attemptedfailed— a step failed and On failure was revert/retry (retry cap hit)stopped— On failure: escalate triggered
JSON summary block (always at the end, machine-parseable):
{
"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
}
}
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.
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):
--profileflag (source:flag)VOYAGE_PROFILEenv-var (source:env)- Inherited from
plan.mdfrontmatterprofile:field (source:inheritance) premiumdefault for v4.0-style plans withoutprofile:(source:default)
The selected profile drives phase_models.execute (model for execution
orchestrator) and parallel_agents for wave-parallel sessions.
Examples:
/trekexecute --profile economy --project .claude/projects/2026-05-09-add-auth
VOYAGE_PROFILE=balanced /trekexecute --project ...
Stats records emit profile, phase_models, and profile_source per
Phase 9 record.
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 2.4 alongside the sequencing-gate brief-validator call:
# v5.9 — composed phase-model resolution (brief > profile > default) for the
# execute phase. ONE call returns {effort, model, source}; 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 is consumed when picking the execution
strategy (gates auto-escalation, parallel-wave choice — see High-effort
behavior below). 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 and stats.
For /trekexecute specifically: effort == 'low' activates --gates open
- sequential-only execution (no worktree-isolated parallel waves — runs
all sessions in a single foreground loop).
effort == 'standard'(or absent) → no change (default execution strategy applies).effort == 'high'activates the high-effort behavior documented under### 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)
When phase_signal_result.effort == 'high' for the execute phase,
set gates_mode = 'closed' automatically (equivalent to passing
--gates closed on the command line). All autonomy boundaries become
operator-stopping points: manifest-audit FAIL halts immediately, the
main-merge gate pauses for operator confirm, and Phase 7.5
manifest-audit runs in its strictest form (fails on ANY mismatch, not
just deltas above threshold). Operator explicit --gates flag still
wins over the brief signal — if the operator passes --gates open on
the command line while phase_signal_result.effort == 'high', the
flag takes precedence and the override is logged:
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs \
--event gates_mode_flag_override --payload '{"flag":"--gates","signal_effort":"high"}'
The record lands in trekexecute-stats.jsonl in the plugin data directory as
{ts, event, known_event, payload:{flag, signal_effort}}. The two payload keys
stay in the jsonl; the OTel export allowlist drops them by omission. On
"written": false (exit 1), print its reason in one line and continue — the
override record never blocks the run.
Standard effort (or absent): use gates_mode = 'adaptive' (the
default). Low effort: gates_mode = 'open' plus sequential-only
execution (no parallel waves) — existing --quick-equivalent
code-path.
Hard rules
-
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). -
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. -
Exit code is truth. The Verify command's exit code is authoritative. Non-zero = FAIL regardless of output. Zero with wrong output = FAIL.
-
Fresh verification. Re-run the Verify command from scratch every time. Never trust cached or prior results.
-
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.
-
Never corrupt completed work. Only revert files from the failing step. Never touch files from earlier passed steps.
-
Checkpoint discipline. Run the Checkpoint commit exactly as written. Do not combine, reorder, or skip checkpoints on passed steps.
-
Scope fence enforcement. For session specs: never modify files in the Never-touch list, regardless of what the Changes field says.
-
Progress file is ground truth. Resume uses the progress file, not git log.
-
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'sname(a named spawn's result never returns), neverisolation(measured: wrong base). No TeamCreate — kept out viadisallowed-tools: TeamCreate, since omission fromallowed-toolsalone leaves a tool callable (CC 2.1.152). -
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.
-
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 removewithout force andgit 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 unmergedtrek/{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 sessionrunningor a dirty or refused merge, writes no Phase 8 report and leaves every worktree and branch in place. -
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, rungit merge --abort, report the conflicting files, and do not attempt further merges. Never use--forceor--strategy-option theirs/oursto silently resolve conflicts. -
Verify command security check. Before executing any
Verify:orCheckpoint: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. -
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 declaresmemory_write: true. Thememory_write: trueopt-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. -
No writing to security-sensitive paths. Never write to
.git/hooks/(git hook injection),~/.ssh/,~/.aws/,~/.gnupg/,.envfiles, shell configs (~/.zshrc,~/.bashrc,~/.profile), or.claude/settings.json/.claude/hooks/(infrastructure self-modification). -
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
failedand triggers the On-failure clause. For legacy v1.6 plans: synthesized manifests apply with the same force, butlegacy_plan: trueis logged in progress. -
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
completedwithout verifying. If Phase 7.5 has not run, the executor may not emitresult: completedunder any circumstances. Beyond that:result: completedrequires 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. -
push-before-cleanup. After successful
git merge --no-ffof a wave branch, rungit push origin <branch>BEFOREgit worktree removeandgit 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. -
Three distinct iteration axes. Per-step
fix_rounds(≤5, upward), per-sessionrecovery_depth(<2), and the globalTREKEXECUTE_MAX_RECOVERY_ITERATIONSbudget (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". -
Never start Claude Code. No
claudeCLI 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). -
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.
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, heredone-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
PreToolUseguard behind the test lock (K8).