--- name: trekexecute description: Disciplined plan executor — single-session, or multi-session through session orders the user starts, with failure recovery argument-hint: "[--project ] [--fg | --resume | --dry-run | --validate | --step N | --session N] [plan.md]" allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion disallowed-tools: 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. 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). ## Phase 1 — Parse mode and validate input Parse `$ARGUMENTS` for mode flags: 1. If arguments contain `--project `: extract the directory path. - Resolve `{dir}` (trim trailing slash). - Set **project_dir = {dir}**. - Derive implicit file path: `{dir}/plan.md`. - If `{dir}` does not exist or `{dir}/plan.md` is missing: ``` Error: project directory missing plan.md: {dir} Run /trekplan --project {dir} to produce it. ``` - If no explicit `` argument is provided after flag parsing, use the derived `{dir}/plan.md`. If a `` argument IS provided, use that path but keep `project_dir` set (user may be pointing at a recovery session spec inside the project). - Continue parsing remaining flags (they combine with `--project`). 2. If arguments contain `--fg`: extract the file path. Set **mode = foreground**. 3. If arguments contain `--resume`: extract the file path. Set **mode = resume**. 4. If arguments contain `--dry-run`: extract the file path. Set **mode = dry-run**. 5. If arguments contain `--validate`: extract the file path. Set **mode = validate**. 6. If arguments contain `--step N` (N is a positive integer): extract N and the file path. Set **mode = step**, **target-step = N**. 7. If arguments contain `--session N` (N is a positive integer): extract N and the file path. Set **mode = session**, **target-session = N**. 8. If arguments contain `--gates`: set `gates_mode = true`. Pause for operator confirmation after each wave's worktrees finish but before merge-back, AND before the main-merge gate. (The MAIN_MERGE_GATE in Phase 8 ALWAYS pauses regardless of `gates_mode` — `--gates` re-enables the per-wave pauses that auto mode otherwise skips.) Default `gates_mode = false`. The flag is consumed by the autonomy-gate state machine via the CLI shim: `node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`. 9. Otherwise: the entire argument string is the file path. Set **mode = execute**. If no path is provided (and `--project` was not used to derive one), output usage and stop: ``` Usage: /trekexecute /trekexecute --project /trekexecute --fg /trekexecute --resume /trekexecute --dry-run /trekexecute --validate /trekexecute --step N /trekexecute --session N /trekexecute --project --resume /trekexecute --project --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 `## Dependencies` with `Entry condition:` AND `## Scope Fence` AND `## Exit Condition` sections. - **Voyage**: contains `## Implementation Plan` with numbered `### Step N:` headings but no `## Scope Fence`. If neither structure is detected, report and stop: ``` Error: unrecognized file format. Expected an trekplan or session spec. ``` ### Parse steps Extract every `### Step N: {description}` heading (in order). For each step, extract: - **Files** — file paths to create or modify - **Changes** — what to modify - **Reuses** — existing code to leverage (informational) - **Test first** — test to run before implementation (optional) - **Verify** — command to run after implementation - **On failure** — recovery action (revert/retry/skip/escalate) - **Checkpoint** — git commit command after success - **Manifest** — YAML block following Checkpoint (v1.7+) 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. ### Parse the Manifest block (v1.7+) Read the plan header for a `plan_version` marker. Treat ≥ `1.7` as strict mode; absence or <1.7 as legacy mode. **Strict mode (plan_version ≥ 1.7):** - Every step MUST have a `Manifest:` block with a YAML fenced payload. - Parse the YAML. Required keys: `expected_paths` (list), `min_file_count` (integer), `commit_message_pattern` (string), `bash_syntax_check` (list), `forbidden_paths` (list), `must_contain` (list of `{path, pattern}` dicts). - Validate each `commit_message_pattern` compiles as a regex (use a tolerant parser — do not execute untrusted input against shell). - If any step is missing its Manifest, or YAML is malformed, STOP with: ``` Error: plan_version=1.7 but step {N} has invalid/missing Manifest. Re-run planning-orchestrator — plan is not executable. ``` **Legacy mode (no plan_version, or < 1.7):** - Synthesize a minimal manifest per step: - `expected_paths` ← step's `Files:` list - `min_file_count` ← `len(expected_paths)` - `commit_message_pattern` ← regex-escape first 3 words of Checkpoint msg - `bash_syntax_check` ← auto-detect `.sh` in Files - `forbidden_paths` ← [] - `must_contain` ← [] - Record `legacy_plan: true` in the progress file. - Emit warning: `Legacy plan (v1.6 or earlier) — manifests synthesized with reduced audit precision.` ### Parse Session Manifest (session specs only) If the file is a session spec (v1.7+), parse the `## Session Manifest` block into a YAML dict. Preserve `session_manifest.*` fields for Phase 7.5 audit. If missing and `plan_version ≥ 1.7`, record a parse warning but continue — the per-step manifests are still available for audit. ### Parse session spec fields (if applicable) - **Entry condition** from `## Dependencies` - **Touch list** and **Never-touch list** from `## Scope Fence` - **Exit condition** checklist from `## Exit Condition` ### Parse Execution Strategy (if present) If the plan contains an `## Execution Strategy` section, extract: - Each `### Session N: {title}` with its Steps, Wave, Depends on, and Scope fence - The `### Execution Order` with wave definitions Set **has_execution_strategy = true**. Report: ``` Type: {plan | session-spec} Steps: {N} {if has_execution_strategy}: Execution Strategy: {S} sessions across {W} waves {if session spec}: Entry condition: {text} {if session spec}: Scope fence: {N} touch, {N} never-touch {if warnings}: Warnings: {list} ``` ## Phase 2.3 — Validate-only mode exit (if mode = validate) **If mode = validate, stop after Phase 2 parsing** and emit a schema-only report. Do NOT run security scan, do NOT touch progress files, do NOT execute any steps. This gives the user a fast sanity-check of plan schema compliance without side effects. **Preferred path (since v3.1.0):** invoke the plan validator directly. It returns the same diagnostic info Phase 2 derives in prose, with stable error codes for downstream tooling: ```bash node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs --strict --json "{path}" # When --project is in scope and progress.json exists, also validate it: [ -f "{project_dir}/progress.json" ] && \ node ${CLAUDE_PLUGIN_ROOT}/lib/validators/progress-validator.mjs --json "{project_dir}/progress.json" ``` Map the validator's `code` field to the error templates below (e.g. `PLAN_FORBIDDEN_HEADING` → "Detected heading format" branch). When both calls exit 0, render the READY report. Otherwise render FAIL with the validator's first error code + message. If Phase 2 parsing succeeded (no fatal errors, every step has a valid Manifest block in strict mode, or synthesized manifests in legacy mode): ``` === Schema Validation: READY === File: {path} Type: {plan | session-spec} plan_version: {1.7 | legacy} Steps: {N} Manifests: {N valid | N synthesized (legacy)} Warnings: {count} {if warnings}: - {each warning on own line} Plan is schema-compliant. Safe to run: /trekexecute {path} ``` If Phase 2 parsing failed (unrecognized format, missing Manifest in strict mode, malformed YAML, invalid regex): ``` === Schema Validation: FAIL === File: {path} Reason: {specific error from Phase 2} {if format not recognized}: Detected heading format: {e.g. "### Fase 1:", "## Phase 1"} Expected: "### Step N: " 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: - `--fg` flag is set - `--step N` mode - `--resume` mode - `--session N` mode (runs only that session's steps, foreground) - Plan has no `## Execution Strategy` section - Plan has Execution Strategy with only 1 session **Run as multi-session (parallel orchestration)** when ALL of these are true: - mode = `execute` (default, no --fg) - Plan has `## Execution Strategy` with 2+ sessions - At least one wave has 2+ sessions (parallelism possible) **Run as multi-session (sequential orchestration)** when: - mode = `execute` (default, no --fg) - Plan has `## Execution Strategy` with 2+ sessions - All sessions are in different waves (no parallelism, but still separate sessions) For single-session (including every `--session N` run inside a session's worktree): continue to Phase 3. For multi-session, first locate the main checkout and the wave marker: ```bash GIT_DIR_ABS="$(cd "$(git rev-parse --git-dir)" && pwd -P)" GIT_COMMON_ABS="$(cd "$(git rev-parse --git-common-dir)" && pwd -P)" MAIN_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')" if [ "$GIT_DIR_ABS" != "$GIT_COMMON_ABS" ]; then echo "Error: this is a linked worktree. Run this from the main checkout $MAIN_ROOT." >&2 exit 1 fi if [ -f "$MAIN_ROOT/.claude/trekplan-sessions/{slug}/orders/wave.json" ]; then echo "WAVE_IN_FLIGHT"; else echo "NO_WAVE"; fi ``` Exit 1 means this is a session's (or another) linked worktree: stop and report the message — the multi-session path runs only from the main checkout, whose root the first entry of `git worktree list` names (`--show-toplevel` would name the linked worktree and miss the wave). `WAVE_IN_FLIGHT`: a wave is awaiting its sessions — skip Phase 2.55 (its clean-tree and stale-worktree checks would touch the live worktrees) and jump to Phase 2.6, which collects the wave. `NO_WAVE`: run Phase 2.55, then Phase 2.6. Report: ``` Strategy: {single session | N sessions (M parallel, K sequential)} ``` ## Phase 2.55 — Pre-flight safety checks **Only runs for multi-session parallel execution.** These checks prevent the catastrophic data loss that occurs when parallel sessions share a working directory. ### Check 1 — Clean working tree Run `git status --porcelain -- ':(top,exclude).claude/trekplan-sessions/'` (the whole repo, from any subdirectory, minus Voyage's own session state: orders, archived progress and kept session worktrees are never merged, and counting them would stop every re-run after a failed wave). If there are ANY uncommitted or untracked changes: ``` Error: working tree is not clean. Parallel execution requires a clean git state. Uncommitted changes are invisible to worktrees and will be lost during merge. Untracked/modified files: {output of git status --porcelain} Commit or stash your changes, then re-run. To run sequentially instead: /trekexecute --fg {plan-path} ``` Stop execution. Update progress with `status: "stopped"`. **Also write `.session-state.local.json`** (Handover 7) — this surfaces the stopped state to `/trekcontinue` so the next session can prompt the user to clean the working tree before resuming. Write atomically alongside `progress.json`: ```json { "schema_version": 1, "project": "{project_dir}", "next_session_brief_path": "{project_dir}/brief.md", "next_session_label": "{current session label, or 'Continue'}", "status": "stopped", "updated_at": "{ISO-8601 now}" } ``` Verify with `node 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: `, `status: stopped`) so the next-session producer-mismatch check has both candidates available. Use the same combined ESM block pattern as Phase 8. ### Check 2 — Plan file reaches every worktree Worktrees are created from HEAD, so tracking the plan file is the cheapest way to make it visible in each one. But the project directory may be **gitignored** — `.claude/projects/` is tool-managed and local-only, and a repo that ignores it is normal, not exotic. `git add -f` is **not** the answer there: it would push operator-local artifacts into history, and into whatever remote the repo publishes to. When the plan file is ignored, Phase 2.6 Step 2a' (which copies brief/plan/research into each worktree) is the delivery path, and this check must step aside instead of failing. ```bash PLAN_PATH="{plan-path}" if git ls-files --error-unmatch "$PLAN_PATH" >/dev/null 2>&1; then PLAN_TRACKING="tracked" else git check-ignore -q "$PLAN_PATH" case "$?" in 0) PLAN_TRACKING="ignored" ;; 1) PLAN_TRACKING="untracked" ;; *) echo "Error: git check-ignore failed on $PLAN_PATH - a fatal probe is not an answer about ignore status." >&2 exit 1 ;; esac fi if [ "$PLAN_TRACKING" = "untracked" ]; then git add "$PLAN_PATH" git commit -m "chore: track plan file for parallel execution" fi ``` Report by outcome: | `PLAN_TRACKING` | Report | |---|---| | `tracked` | `Plan file already tracked.` | | `untracked` | `Plan file committed for worktree visibility.` | | `ignored` | `Plan file is gitignored — not forced into history. Step 2a' copies it into each worktree.` | Any other `git check-ignore` exit code is fatal and stops execution: a probe that failed is not a probe that answered "not ignored". ### Check 3 — Scope fence overlap validation For each wave that has 2+ sessions, validate that no file appears in the Touch list of two different sessions in the same wave: 1. For each session in the wave, extract the "Touch" list from the Execution Strategy. 2. For each pair of sessions (A, B) in the same wave, compute the intersection of their Touch lists. 3. If any intersection is non-empty: ``` Error: scope fence overlap detected in Wave {W}. Sessions {A} and {B} both touch: {overlapping files} These sessions cannot safely run in parallel. Fix the Execution Strategy in the plan, or use --fg for sequential execution. ``` Stop execution. This is a defense-in-depth check — 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. ```bash # zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash. [ -n "${ZSH_VERSION:-}" ] && setopt nonomatch GIT_DIR_ABS="$(cd "$(git rev-parse --git-dir)" && pwd -P)" GIT_COMMON_ABS="$(cd "$(git rev-parse --git-common-dir)" && pwd -P)" MAIN_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')" if [ "$GIT_DIR_ABS" != "$GIT_COMMON_ABS" ]; then echo "Error: this is a linked worktree. Run this from the main checkout $MAIN_ROOT." >&2 exit 1 fi SESSIONS="$MAIN_ROOT/.claude/trekplan-sessions/{slug}" if [ -f "$SESSIONS/orders/wave.json" ]; then echo "Error: $SESSIONS/orders/wave.json exists - a wave is awaiting its sessions. Stale-worktree cleanup refused." >&2 exit 1 fi cd "$MAIN_ROOT" || exit 1 for wt in "$SESSIONS/worktrees"/session-*; do [ -d "$wt" ] || continue GIT_OPTIONAL_LOCKS=0 git worktree remove "$wt" 2>/dev/null || echo "KEPT worktree $wt" done GIT_OPTIONAL_LOCKS=0 git worktree prune git for-each-ref --format='%(refname:short)' "refs/heads/trek/{slug}/" | while read -r b; do GIT_OPTIONAL_LOCKS=0 git branch -d "$b" >/dev/null 2>&1 || echo "KEPT branch $b" done KEPT="$(ls -d "$SESSIONS/worktrees"/session-* 2>/dev/null; git for-each-ref --format='%(refname:short)' "refs/heads/trek/{slug}/")" if [ -n "$KEPT" ]; then echo "Earlier session work kept (changes or unmerged commits):" >&2 echo "$KEPT" >&2 exit 1 fi ``` Exit 0: report `Cleaned stale worktrees and merged branches from a previous run.` Exit 1: stop and report its message. For every kept item say what the user can do: a worktree — inspect it (`git -C status --short`), commit or move what matters, then `git worktree remove `; a branch — inspect it (`git log HEAD..`), merge it by hand, or delete it themselves once its commits are not needed. Then re-run. After all 4 checks pass: ``` Pre-flight: PASS (clean tree, plan reaches worktrees, no overlaps, no stale worktrees) ``` ## Phase 2.6 — Multi-session orchestration (worktree-isolated, session orders) **Only runs for multi-session execution.** Voyage never starts a Claude Code session. This phase prepares ONE wave at a time: it creates an isolated git worktree for each session in the wave, writes a **session order** for each (the worktree, the steps, the done-condition) plus a one-line **starter**, and stops. The user opens each session themselves, in the directory the order names, and gives it the starter. Running the same `/trekexecute` command again in the main checkout collects the wave: it reads every session's progress file, merges the session branches back sequentially, cleans up, and prepares the next wave. After the last wave, jump to Phase 8 (final report). **CRITICAL SAFETY RULE:** Every session MUST run in its own git worktree. Never run two sessions in the same working directory. This rule exists because parallel git operations in a shared worktree cause index corruption, race conditions, and repository destruction. Each order names its own worktree. ### Step 0 — Resolve paths and detect a wave in flight ```bash # The MAIN checkout, never a linked worktree (Phase 2.5): in a session's # worktree --show-toplevel names that worktree. GIT_DIR_ABS="$(cd "$(git rev-parse --git-dir)" && pwd -P)" GIT_COMMON_ABS="$(cd "$(git rev-parse --git-common-dir)" && pwd -P)" REPO_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')" if [ "$GIT_DIR_ABS" != "$GIT_COMMON_ABS" ]; then echo "Error: this is a linked worktree. Run this from the main checkout $REPO_ROOT." >&2 exit 1 fi # The plan path relative to the main checkout: every session-orders.mjs call # runs from $REPO_ROOT with it, and each session finds the plan there in its # own worktree. PLAN_REL="$(python3 -c 'import os.path,sys; print(os.path.relpath(os.path.realpath(sys.argv[1]), os.path.realpath(sys.argv[2])))' "{plan-path}" "$REPO_ROOT")" case "$PLAN_REL" in ""|..*) echo "Error: the plan {plan-path} is not inside the main checkout $REPO_ROOT." >&2 exit 1 ;; esac SESSION_DIR="$REPO_ROOT/.claude/trekplan-sessions/{slug}" WORKTREE_DIR="$SESSION_DIR/worktrees" ORDERS_DIR="$SESSION_DIR/orders" ORIGINAL_BRANCH="$(git rev-parse --abbrev-ref HEAD)" # Resolve the plugin root ONCE (same reason as Phase 7: the variable is # substituted in this command's text but EMPTY in the Bash tool's env). # Only the literal form is substituted (a `:-` default is never replaced); the # fallback takes the highest installed version under cache//voyage/. VOYAGE_ROOT="${CLAUDE_PLUGIN_ROOT}" case "$VOYAGE_ROOT" in /*) ;; *) VOYAGE_ROOT="$(ls -d "$HOME"/.claude/plugins/cache/*/voyage/[0-9]*/ 2>/dev/null \ | awk -F/ '{ split($(NF-1), v, "."); printf "%09d%09d%09d %s\n", v[1], v[2], v[3], $(0) }' \ | sort | tail -1 | cut -d' ' -f2-)" VOYAGE_ROOT="${VOYAGE_ROOT%/}" ;; esac if [ ! -f "$VOYAGE_ROOT/lib/util/session-orders.mjs" ]; then echo "[voyage] session orders cannot be written - plugin root unresolved." >&2 exit 2 fi if [ -f "$ORDERS_DIR/wave.json" ]; then echo "WAVE_IN_FLIGHT"; else echo "NO_WAVE"; fi ``` Record `REPO_ROOT`, `PLAN_REL`, `SESSION_DIR`, `WORKTREE_DIR`, `ORDERS_DIR`, `VOYAGE_ROOT` and `ORIGINAL_BRANCH` for the later steps. All paths except `PLAN_REL` must be absolute. - `WAVE_IN_FLIGHT` → read the wave number `W` from `$ORDERS_DIR/wave.json` and jump to **Step 3** (collect). Phase 2.55 was skipped for this run (Phase 2.5). - `NO_WAVE` → prepare the lowest wave not yet merged: ```bash (cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" next-wave --plan "$PLAN_REL" --session-dir "$SESSION_DIR") ``` A number is `W`: continue to Step 1. `none` means earlier runs merged every wave: go to Step 4. The record it reads, `$ORDERS_DIR/merged-waves.json` (plan path + sha256 of the plan's content), is written by Step 3 after each merged wave, survives Step 5, and is cleared only on confirm at Phase 8's main-merge gate, when every wave is merged and Step 4 passed, so a re-run after a later wave failed (or after a declined gate) never prepares an already-merged wave again. If `next-wave` exits 1, the record belongs to another plan or another version of this one (`{slug}` is the plan's file name, so plans can share `$SESSION_DIR`). Stop, touch nothing, and tell the user exactly what its message says: an unfinished multi-session run of that plan holds `$ORDERS_DIR`. Finish that run (re-run it with its own plan), or clear it and re-run this one: `(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" clear --session-dir "$SESSION_DIR")`. ### Step 1 — Create worktrees for wave W ```bash mkdir -p "$WORKTREE_DIR" "$ORDERS_DIR" ``` **2a. Create worktrees for this wave's sessions:** First make sure no session of this wave left a branch or a worktree behind in an earlier attempt — it may hold that session's work, so it is neither reused nor deleted: ```bash EXISTING="" for N in {wave W session numbers}; do if git rev-parse -q --verify "refs/heads/trek/{slug}/session-$N" >/dev/null || [ -e "$WORKTREE_DIR/session-$N" ]; then EXISTING="$EXISTING trek/{slug}/session-$N" fi done if [ -n "$EXISTING" ]; then echo "Error: left by an earlier attempt:$EXISTING (branch or $WORKTREE_DIR/session-N)." >&2 exit 1 fi ``` If it exits 1, stop and report (no Step 5, no Phase 8; nothing was created or deleted): name each branch and worktree, and what the user can do — inspect it (`git log HEAD..`, `git -C status --short`), merge it by hand, or delete it themselves once its work is not needed — then re-run. Then, for each session N in this wave: ```bash BRANCH_NAME="trek/{slug}/session-{N}" WORKTREE_PATH="$WORKTREE_DIR/session-{N}" GIT_OPTIONAL_LOCKS=0 git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" HEAD ``` `GIT_OPTIONAL_LOCKS=0` disables `.git/index.lock` background polling races during parallel worktree operations (research/02 R2; GH #47721). If `git worktree add` fails, report its error and stop the wave: never delete a branch to make room. Jump to Step 5 (it removes the worktrees this wave just created, without force), then Phase 8. Report: ``` Worktree created: session-{N} → {WORKTREE_PATH} (branch: {BRANCH_NAME}) ``` **2a'. Copy gitignored project artifacts into each worktree (addresses plugin-in-monorepo + gitignored-state topology):** `git worktree add` clones HEAD only. The plugin's `.gitignore` excludes `.claude/projects/`, so the project directory (brief.md, plan.md, research/) is missing in the freshly-created worktree. Each session needs to read `brief.md`, `plan.md`, and `research/*` from `${PROJECT_DIR}` to do real work. Insert this block AFTER the worktree-creation loop and BEFORE the orders are written (Step 2): ```bash # zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash. [ -n "${ZSH_VERSION:-}" ] && setopt nonomatch PROJECT_SOURCE="$(realpath "${PROJECT_DIR}")" REPO_ROOT_REAL="$(realpath "${REPO_ROOT}")" # Compute destination relpath: PROJECT_DIR relative to REPO_ROOT. # This makes $wt/$PROJECT_REL valid regardless of whether the operator # passed --project as relative (.claude/projects/...) or absolute. # python3 + os.path.relpath is stdlib and portable - see the note below the # block for why no realpath flag may be used here. PROJECT_REL="$(python3 -c 'import os.path,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$PROJECT_SOURCE" "$REPO_ROOT_REAL")" case "$PROJECT_REL" in ""|..*) echo "Error: cannot derive a project relpath inside the repo ($PROJECT_SOURCE vs $REPO_ROOT_REAL)." >&2 exit 1 ;; esac for wt in "$WORKTREE_DIR"/session-*; do [ -d "$wt" ] || continue mkdir -p "$wt/$PROJECT_REL" cp "$PROJECT_SOURCE"/brief.md "$wt/$PROJECT_REL/" cp "$PROJECT_SOURCE"/plan.md "$wt/$PROJECT_REL/" if [ -d "$PROJECT_SOURCE/research" ]; then cp -r "$PROJECT_SOURCE/research" "$wt/$PROJECT_REL/" fi done ``` Do not "improve" the relpath line into `realpath --relative-to=...`. That flag is GNU coreutils only; BSD `realpath` (the macOS default) rejects it, and the failure is **silent** — the command substitution leaves `PROJECT_REL` empty, so `mkdir -p "$wt/"` and `cp ... "$wt//"` both succeed and drop `brief.md`/`plan.md` at the worktree root, where no session looks for them. Measured on an Intel Mac 2026-08-31: `realpath --relative-to=... ` → `realpath: illegal option -- -`, while bare `realpath ` works; a whole wave ran with zero steps executed. Both `realpath` calls above are bare path resolution, which BSD and GNU handle identically; resolving both operands before the relpath is what keeps it correct when one side goes through a symlink (macOS `/var` → `/private/var`). Failure modes: an underivable relpath (empty, or outside the repo) aborts before anything is copied — better a loud stop than files delivered where no session reads them; any `cp` failure exits the wave non-zero, reported via Step 5 cleanup. A project without `research/` is not a failure. Source: brief Constraint 2. ### Step 2 — Write the session orders for wave W, then stop ```bash (cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" write --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR" --branch "$ORIGINAL_BRANCH") ``` `$PLAN_REL` is relative to the main checkout (Step 0; the tool refuses an absolute path): each session resolves it inside its own worktree, which has the same repo content from HEAD. The tool writes `$ORDERS_DIR/wave-{W}-session-{N}.md` per session (naming its worktree, directory and progress file) and `$ORDERS_DIR/wave.json` (`status: awaiting-sessions`, the plan path, the sha256 of its content and the branch the wave is prepared on), and prints, per session, the directory to open and the starter line (`/trekexecute --session {N} $PLAN_REL`). A session's per-session `cwd:` field in the Execution Strategy moves the directory it opens in to that subdirectory of its worktree (plugin-in-monorepo layouts, brief Constraint 1, Option B); default `.` is the worktree root. Show the tool's output to the user verbatim, then report and **stop**: ``` Wave {W}: {N} session order(s) written in {ORDERS_DIR}. Awaiting sessions. Open each session yourself in the directory its order names and give it the starter line. When every session is done, run this same /trekexecute command again here to merge the wave. ``` This run ends here. It writes no Phase 8 report: the plan is not finished, and the orders are the hand-over. ### Step 3 — Collect wave W (a later run) ```bash (cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" status --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR" --branch "$ORIGINAL_BRANCH") ``` If it exits 1, stop and report its message, touching nothing (`wave.json` stays). Either `wave.json` was written for another plan or another version of this one — re-run with the plan (version) that wrote the wave, or abandon the wave (the way out below) — or the main checkout is on another branch than the wave was prepared on (Step 0's `ORIGINAL_BRANCH` of that run): the user switches back to that branch and re-runs, so the wave never merges elsewhere. The JSON names each session's progress file (the file `--session N` writes, Phase 3), its raw `status`, and its `state`, exactly one of: `completed`; `running` — no progress file yet, `pending`, `in-progress`, `in_progress`, or any status not named here (an unknown status is never treated as ended); `ended-bad` — `failed`, `stopped`, `partial`, `blocked` or `unreadable`. `running` and `ended_bad` list the sessions in each state. Check in this order: - **Any `running` (checked FIRST):** a session is still open. Report which sessions are still running, with their raw status, point at their order files (`$ORDERS_DIR/wave-{W}-session-{N}.md`), and **stop** — even when another session in the wave already ended badly. Touch no worktree, no branch and not `wave.json`: no merge, no archive, no cleanup. **Way out of a stuck wave** (a session the user decided not to run): the user deletes `$ORDERS_DIR/wave.json` and re-runs. That run takes the fresh path, and Phase 2.55 Check 4 removes the wave's clean worktrees and merged branches without force and stops on everything that holds work, listing it to merge by hand or delete. - No session is running. Before any worktree is removed (the failure branch's Step 5 and the merge below both remove them), archive every session's progress file: ```bash (cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" archive --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR") ``` It copies each one to `$ORDERS_DIR/wave-{W}-session-{N}.progress.json`. Step 5 never deletes these copies; Step 6 and Phase 8 read them. - **Any `ended-bad`:** ``` Wave {W} FAILED: Session {N} ended {status}. Stopping — later waves depend on this wave. See its order: {ORDERS_DIR}/wave-{W}-session-{N}.md ``` Do NOT merge. Do NOT prepare later waves. Jump to Step 5 (cleanup; it keeps every unmerged branch and every worktree git refuses to remove), then Phase 8. - **`all_completed: true`:** if `gates_mode` is 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: ```bash cd "$REPO_ROOT" || exit 1 if ! git diff --quiet || ! git diff --cached --quiet; then echo "Error: uncommitted changes in $REPO_ROOT. Commit or stash your changes in $REPO_ROOT, then re-run." >&2 exit 1 fi ``` If it exits 1, stop and report its message: no merge, no cleanup, and keep `wave.json`, so the re-run collects this wave again. For each session N in the wave (in order), push BEFORE merge so the wave branch survives even if cleanup races ahead (research/02 R3 — converts an unrecoverable failure to a recoverable one). Push failure is non-fatal (no remote, offline, etc.): ```bash GIT_OPTIONAL_LOCKS=0 git push origin "trek/{slug}/session-{N}" 2>/dev/null || true if GIT_OPTIONAL_LOCKS=0 git merge --no-ff "trek/{slug}/session-{N}" \ -m "merge: trekplan session {N} — {session-title}"; then echo "MERGED session {N}" elif git rev-parse -q --verify MERGE_HEAD >/dev/null; then git diff --name-only --diff-filter=U git merge --abort echo "MERGE_CONFLICT session {N}" exit 1 else echo "MERGE_REFUSED session {N}" exit 1 fi ``` - `MERGED`: continue to the next session. - `MERGE_REFUSED`: git refused to start the merge (no `MERGE_HEAD`; its message names the local changes or untracked files in the way). Stop and report `commit, stash or move your changes in {REPO_ROOT}, then re-run`: no Step 5, and keep `wave.json`. Sessions merged before it in this wave stay merged; the re-run merges them again as no-ops. - `MERGE_CONFLICT`: a real conflict, already aborted. Report: ``` Wave {W} MERGE CONFLICT: Session {N} branch conflicts with merged state. Conflicting files: {the files the block printed} Session {N} order: {ORDERS_DIR}/wave-{W}-session-{N}.md Aborting further merges. Sessions already merged in this wave are preserved. ``` Mark remaining sessions as "merge-failed". Jump to Step 5 (it keeps every unmerged branch), then Phase 8. **Record the merged wave, then remove its worktrees:** After successful merge of all sessions in the wave, record it (Step 0 reads the record, so a later run never prepares this wave again): ```bash (cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" record-merged --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR") ``` Then drop the wave marker and remove each session's worktree and merged branch, never with force. A progress file still in a worktree is deleted only when it is untracked and byte-identical to its archived copy (the archive is the record); anything else in a worktree makes git refuse, and that worktree — and the branch checked out in it — is kept and reported: ```bash cd "$REPO_ROOT" || exit 1 rm -f "$ORDERS_DIR/wave.json" for N in {wave W session numbers}; do WT="$WORKTREE_DIR/session-$N" ARCHIVED="$ORDERS_DIR/wave-{W}-session-$N.progress.json" P="$(sed -n 's/^- progress file: //p' "$ORDERS_DIR/wave-{W}-session-$N.md")" if [ -n "$P" ] && [ -f "$P" ] && cmp -s "$ARCHIVED" "$P" \ && ! git -C "$WT" ls-files --error-unmatch "$P" >/dev/null 2>&1; then rm -f "$P" fi GIT_OPTIONAL_LOCKS=0 git worktree remove "$WT" 2>/dev/null || echo "KEPT worktree $WT" GIT_OPTIONAL_LOCKS=0 git branch -d "trek/{slug}/session-$N" >/dev/null 2>&1 || echo "KEPT branch trek/{slug}/session-$N" done GIT_OPTIONAL_LOCKS=0 git worktree prune ``` Report: `Wave {W}: {N} sessions merged.` plus every `KEPT` line, each with what the user can do (inspect it with `git -C status --short`, commit or move what matters, then `git worktree remove ` and `git branch -d `). **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. ```bash # zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash. [ -n "${ZSH_VERSION:-}" ] && setopt nonomatch cd "$REPO_ROOT" || exit 1 # Remove worktrees git agrees are clean (GIT_OPTIONAL_LOCKS=0 to avoid lock-file races) for wt in "$WORKTREE_DIR"/session-*; do [ -d "$wt" ] || continue GIT_OPTIONAL_LOCKS=0 git worktree remove "$wt" 2>/dev/null || echo "KEPT worktree $wt" done GIT_OPTIONAL_LOCKS=0 git worktree prune # Delete merged session branches; every unmerged one is kept git for-each-ref --format='%(refname:short)' "refs/heads/trek/{slug}/" | while read -r b; do GIT_OPTIONAL_LOCKS=0 git branch -d "$b" >/dev/null 2>&1 || echo "KEPT branch $b" done # Clean up the wave marker and an empty worktree directory rm -f "$ORDERS_DIR/wave.json" rmdir "$WORKTREE_DIR" 2>/dev/null || true ``` Report: ``` Cleanup: worktrees removed and merged branches deleted. Kept (they hold work): {every KEPT line} ``` For every kept item, the report says what the user can do: a worktree — inspect it (`git -C status --short`), commit or move what matters, then `git worktree remove `; a branch — inspect it (`git log HEAD..`), merge it by hand (`git merge --no-ff `), 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`: 1. Find session N in the Execution Strategy 2. Extract its step numbers (e.g., Steps: 4, 5, 6) 3. Extract its scope fence (Touch / Never touch lists) 4. Execute ONLY those steps, in order, using the single-session protocol (Phase 3→7) 5. Enforce the session's scope fence as if it were a session spec's scope fence 6. Report results for those steps only This mode is what each session order's starter runs, in a session the user opened in that order's worktree. It can also be used manually to re-run a specific session. When `--session N` runs inside a git worktree (as each session order sets up), all git operations (add, commit) apply to the worktree's branch. The session does not need to know it is in a worktree — git handles this transparently. ## Phase 3 — Progress file setup The progress file location depends on whether `--project` is in use: - **With `--project `:** progress lives at `{project_dir}/progress.json`. - **Without `--project`:** progress lives at `{plan-dir}/.trekexecute-progress-{slug}.json`, where `{slug}` is the plan filename without extension. **Session-scoped naming:** When `mode = session N`: - With `--project`: `{project_dir}/progress-session-{N}.json`. - Without `--project`: `{plan-dir}/.trekexecute-progress-{slug}-session-{N}.json`. This prevents merge conflicts when parallel sessions each write their own progress file. ### Progress file schema ```json { "schema_version": "1", "plan": "{path}", "plan_type": "{plan | session-spec}", "plan_version": "{1.7 | 1.6 | legacy}", "started_at": "{ISO-8601}", "updated_at": "{ISO-8601}", "mode": "{execute | resume | step}", "total_steps": 0, "current_step": 0, "status": "{in_progress | completed | failed | stopped | partial}", "steps": { "1": { "status": "pending", "attempts": 0, "error": null, "completed_at": null, "commit": null } }, "iterations_remaining": 25, "entry_condition_checked": false, "exit_condition_checked": false, "plan_verification": null, "summary": null } ``` `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 step attempt beyond the first, 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`, `stopped` or `partial`: warn that `--resume` is available, then wait 3 seconds (`sleep 3`) and start fresh. This allows headless runs to proceed without blocking. - Otherwise: create the progress file with all steps in `pending` status. **mode = resume:** - If no progress file exists: start from step 1 (same as fresh execute). - If progress file exists: find the first step with status != `completed`. A legacy progress file may say `passed` for a finished step; treat it as `completed`. ``` Resuming from step {N}. {M}/{total} steps already completed. ``` **mode = dry-run:** - Do NOT create or modify the progress file. **mode = step N:** - Create the progress file if it does not exist. - Only step N will be executed. ## Phase 4 — Entry condition check (session specs only) **Skip for trekplans.** Skip in dry-run mode (report what would be checked instead). Read the entry condition. Evaluate it: - `"none"` or similar → pass immediately - References git state (e.g., "git status clean") → run `git status --porcelain` - References passing tests → run the specified command - References a previous session → check `git log --oneline` for commit pattern If the entry condition **fails**: ``` Entry condition FAILED: {condition text} Reason: {what was checked, what was found} Complete the prerequisite first, then re-run. ``` Update progress file with `status: "stopped"`. Stop execution. **Also write `.session-state.local.json`** (Handover 7) with the same `status: "stopped"` and `next_session_brief_path` pointing at the brief that the failed entry-condition session was supposed to consume. This lets `/trekcontinue` surface the stop in the next session. Use the same atomic write pattern + validator check as Phase 2.55. On validator failure, warn but do not block. **Also write sibling `NEXT-SESSION-PROMPT.local.md`** with frontmatter (`produced_by: trekexecute`, `produced_at: `, `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 6 — Step execution loop The core execution phase. Runs for modes: `execute`, `resume`, `step`. ### Determine starting step - **execute**: step 1 - **resume**: first step where status != `completed`. A legacy progress file may say `passed` for a finished step; treat it as `completed`. - **step N**: step N only ### For each step Update progress: `steps.{N}.status = "in_progress"`, `current_step = N`, `updated_at = now`. ``` --- Step {N}/{total}: {description} --- ``` #### 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 (if present) If the step has a `Test first:` field: 1. If test file is marked `(new)`: note it will be created during implementation. 2. If test file exists: run it. Expect failure (RED state). 3. If test unexpectedly passes: warn but continue — step may already be done. Do not block on test-first failures — they are expected. #### Sub-step C — Implement changes Read the step's `Files:` and `Changes:` fields. Implement exactly as described. **Rules:** - Follow `Changes:` exactly — do not improvise, add scope, or optimize - Use Edit for modifications, Write for new files - If `Reuses:` references existing code, read that code first for context - Only touch files listed in `Files:` — nothing else #### Sub-step D1 — Command verification **Security check (mandatory):** Before running the Verify command, check it against the executor security denylist. If the command matches ANY of these patterns, **refuse to execute** — treat as `On failure: escalate` regardless of the plan's On failure setting: - `rm -rf` or `rm -fr` with any path - `chmod 777` or `chmod -R 777` - Pipe-to-shell: `curl ... | bash`, `wget ... | sh`, `base64 ... | bash` - `eval` with variable expansion: `eval $VAR`, `eval $(cmd)`, `` eval `cmd` `` - `mkfs`, `dd` writing to block devices (`/dev/sd*`, `/dev/nvme*`) - `shutdown`, `reboot`, `halt`, `poweroff` - Fork bomb patterns - `crontab` writes, `/etc/cron*` modifications - `kill -9 -1` or `pkill -9 -1` (kill all processes) - `history -c` or truncating `~/.bash_history` If matched: 1. Do NOT execute the command 2. Set step status = "failed" 3. Log: `SECURITY: Verify command blocked — matches executor denylist: {pattern name}` 4. Apply `On failure: escalate` regardless of the plan's On failure setting 5. Include in final report under a "Security blocks" section If the command passes the security check, run it: Run the `Verify:` command exactly as written, via Bash. **Rules:** - Always a fresh run — never trust prior results - Exit code is the authoritative truth: - Exit 0 + expected output (if specified) = **PASS** - Exit non-zero = **FAIL** regardless of output text - Exit 0 but wrong output = **FAIL** ``` Verify: {command} Result: {PASS | FAIL} (exit code {N}) {if FAIL}: Output (first 10 lines): {output} ``` **Step 0 sentinel:** if the Verify command exits `77` AND the step's manifest has `sandbox_preflight: true`, do NOT treat as a normal failure. Set step status = `blocked` and apply `On failure: escalate`. Skip D2 manifest verification (nothing to verify for a read-only sandbox test). Commit none — jump straight to Phase 7 with a structured "sandbox-blocked" reason. If **PASS**: proceed to Sub-step D2 (manifest verification). #### Sub-step D2 — Manifest verification After the Verify command passes, verify the step's Manifest block. This is the objective completion predicate: a step is not passed until its manifest holds, regardless of Verify exit code. **Checks to run (in order):** 1. **Expected paths exist:** for each `expected_paths` entry, verify the file exists in the repo. Count how many exist. 2. **min_file_count satisfied:** count from (1) must be ≥ `min_file_count`. 3. **Forbidden paths untouched:** for each `forbidden_paths` entry, run `git diff --name-only HEAD~{attempts} HEAD -- {path}` (since this step began). Any modified forbidden path = fail. 4. **Bash syntax check:** for each entry in `bash_syntax_check` AND any `.sh` file that appears in `git diff --name-only HEAD~1 HEAD` (safety net for unlisted scripts), run `bash -n {path}`. Non-zero exit = fail. 5. **must_contain patterns:** for each `{path, pattern}` pair, run `grep -E "{pattern}" {path}`. No match = fail. **On manifest failure:** ``` Manifest verification FAIL for step {N}: - {which check failed with detail} ``` Apply the step's `On failure:` clause (same as Sub-step D1 failure). Increment attempts. Manifest failures count against the retry cap equally with command failures. If all manifest checks pass: proceed to Sub-step F (checkpoint). 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 E — On failure handling If **FAIL**, read the `On failure:` clause. Apply the retry cap: **maximum 2 retries** (3 total attempts). Track attempts in `steps.{N}.attempts`. **`On failure: revert`** - If attempts < 3: analyze the failure, re-implement with adjustments, re-verify. ``` Attempt {A}/3 failed. Retrying... ``` - If attempts == 3: revert this step's changes: ```bash git checkout -- {files from Files: field} ``` Record failure. **Do NOT proceed to next step.** Jump to Phase 7. **`On failure: retry`** - If attempts < 3: use the alternative approach described in the On failure clause. - If attempts == 3: revert and stop. Jump to Phase 7. **`On failure: skip`** - Mark step as skipped regardless of attempt count. Continue to next step. ``` Step {N}: SKIPPED (non-critical per plan) ``` Update `steps.{N}.status = "skipped"`. **`On failure: escalate`** - Stop immediately regardless of attempt count. ``` Step {N}: ESCALATED — requires human judgment ``` Commit all completed work before stopping. Stage ONLY files from steps with `status: "completed"` in the progress file — collect their `Files:` fields. Never use `git add -A` (risks staging secrets, binaries, or unrelated work). ```bash git add {files from completed steps' Files: fields} && git commit -m "wip: trekexecute stopped at step {N} — escalation needed" ``` Jump to Phase 7. #### Sub-step F — Checkpoint Run the `Checkpoint:` 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.) ```bash # Resolve the plugin root ONCE. ${CLAUDE_PLUGIN_ROOT} is substituted in this # command's text but is EMPTY in the Bash tool's process env, and a bare # `node ${CLAUDE_PLUGIN_ROOT}/lib/…` then runs `node /lib/…`, which exits 1 — # indistinguishable from a criterion that failed. # Only the literal form is substituted (a `:-` default is never replaced); the # fallback takes the highest installed version under cache//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/