/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>
2554 lines
116 KiB
Markdown
2554 lines
116 KiB
Markdown
---
|
||
name: trekexecute
|
||
description: Disciplined plan executor — single-session, or multi-session through session orders the user starts, with failure recovery
|
||
argument-hint: "[--project <dir>] [--fg | --resume | --dry-run | --validate | --step N | --session N] [plan.md]"
|
||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent
|
||
disallowed-tools: TeamCreate
|
||
---
|
||
|
||
# Ultraexecute Local
|
||
|
||
Disciplined executor for trekplan plans. Reads a plan file, detects if it has
|
||
an Execution Strategy (multi-session), and either executes directly or
|
||
prepares one session order per session, wave by wave — all to realize one plan.
|
||
|
||
Each plan step is built by **one fresh helper** (`voyage:step-implementer`)
|
||
and judged by **one controller** that reads the step's diff. The executor
|
||
itself writes no product code: it records the start point, launches the
|
||
helper, runs the mechanical checks (proof of red, the floor guard, the
|
||
manifest, the 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:
|
||
|
||
1. If arguments contain `--project <dir>`: extract the directory path.
|
||
- Resolve `{dir}` (trim trailing slash).
|
||
- Set **project_dir = {dir}**.
|
||
- Derive implicit file path: `{dir}/plan.md`.
|
||
- If `{dir}` does not exist or `{dir}/plan.md` is missing:
|
||
```
|
||
Error: project directory missing plan.md: {dir}
|
||
Run /trekplan --project {dir} to produce it.
|
||
```
|
||
- If no explicit `<plan.md>` argument is provided after flag parsing,
|
||
use the derived `{dir}/plan.md`. If a `<plan.md>` argument IS provided,
|
||
use that path but keep `project_dir` set (user may be pointing at a
|
||
recovery session spec inside the project).
|
||
- Continue parsing remaining flags (they combine with `--project`).
|
||
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 <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 `## Dependencies` with `Entry condition:` AND `## Scope Fence`
|
||
AND `## Exit Condition` sections.
|
||
- **Voyage**: contains `## Implementation Plan` with numbered `### Step N:` headings
|
||
but no `## Scope Fence`.
|
||
|
||
If neither structure is detected, report and stop:
|
||
```
|
||
Error: unrecognized file format. Expected an trekplan or session spec.
|
||
```
|
||
|
||
### Parse steps
|
||
|
||
Extract every `### Step N: {description}` heading (in order). For each step, extract:
|
||
- **Files** — file paths to create or modify
|
||
- **Changes** — what to modify
|
||
- **Reuses** — existing code to leverage (informational)
|
||
- **Test first** — the test the step writes before it builds: its `File:`
|
||
(existing | new), what it `Verifies:`, an optional `Pattern:` (required from
|
||
plan_version ≥ 1.8, see below)
|
||
- **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_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: <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:
|
||
- `--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: <ISO-8601>`,
|
||
`status: stopped`) so the next-session producer-mismatch check has both
|
||
candidates available. Use the same combined ESM block pattern as Phase 8.
|
||
|
||
### Check 2 — Plan file reaches every worktree
|
||
|
||
Worktrees are created from HEAD, so tracking the plan file is the cheapest way
|
||
to make it visible in each one. But the project directory may be **gitignored**
|
||
— `.claude/projects/` is tool-managed and local-only, and a repo that ignores it
|
||
is normal, not exotic. `git add -f` is **not** the answer there: it would push
|
||
operator-local artifacts into history, and into whatever remote the repo
|
||
publishes to. When the plan file is ignored, Phase 2.6 Step 2a' (which copies
|
||
brief/plan/research into each worktree) is the delivery path, and this check
|
||
must step aside instead of failing.
|
||
|
||
```bash
|
||
PLAN_PATH="{plan-path}"
|
||
if git ls-files --error-unmatch "$PLAN_PATH" >/dev/null 2>&1; then
|
||
PLAN_TRACKING="tracked"
|
||
else
|
||
git check-ignore -q "$PLAN_PATH"
|
||
case "$?" in
|
||
0) PLAN_TRACKING="ignored" ;;
|
||
1) PLAN_TRACKING="untracked" ;;
|
||
*) echo "Error: git check-ignore failed on $PLAN_PATH - a fatal probe is not an answer about ignore status." >&2
|
||
exit 1 ;;
|
||
esac
|
||
fi
|
||
if [ "$PLAN_TRACKING" = "untracked" ]; then
|
||
git add "$PLAN_PATH"
|
||
git commit -m "chore: track plan file for parallel execution"
|
||
fi
|
||
```
|
||
|
||
Report by outcome:
|
||
|
||
| `PLAN_TRACKING` | Report |
|
||
|---|---|
|
||
| `tracked` | `Plan file already tracked.` |
|
||
| `untracked` | `Plan file committed for worktree visibility.` |
|
||
| `ignored` | `Plan file is gitignored — not forced into history. Step 2a' copies it into each worktree.` |
|
||
|
||
Any other `git check-ignore` exit code is fatal and stops execution: a probe
|
||
that failed is not a probe that answered "not ignored".
|
||
|
||
### Check 3 — Scope fence overlap validation
|
||
|
||
For each wave that has 2+ sessions, validate that no file appears in the Touch
|
||
list of two different sessions in the same wave:
|
||
|
||
1. For each session in the wave, extract the "Touch" list from the Execution Strategy.
|
||
2. For each pair of sessions (A, B) in the same wave, compute the intersection
|
||
of their Touch lists.
|
||
3. If any intersection is non-empty:
|
||
|
||
```
|
||
Error: scope fence overlap detected in Wave {W}.
|
||
Sessions {A} and {B} both touch: {overlapping files}
|
||
These sessions cannot safely run in parallel.
|
||
|
||
Fix the Execution Strategy in the plan, or use --fg for sequential execution.
|
||
```
|
||
|
||
Stop execution. This is a defense-in-depth check — 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 <path> status --short`), commit or move
|
||
what matters, then `git worktree remove <path>`; a branch — inspect it
|
||
(`git log HEAD..<branch>`), merge it by hand, or delete it themselves once its
|
||
commits are not needed. Then re-run.
|
||
|
||
After all 4 checks pass:
|
||
```
|
||
Pre-flight: PASS (clean tree, plan reaches worktrees, no overlaps, no stale worktrees)
|
||
```
|
||
|
||
## Phase 2.6 — Multi-session orchestration (worktree-isolated, session orders)
|
||
|
||
**Only runs for multi-session execution.** Voyage never starts a Claude Code
|
||
session. This phase prepares ONE wave at a time: it creates an isolated git
|
||
worktree for each session in the wave, writes a **session order** for each
|
||
(the worktree, the steps, the done-condition) plus a one-line **starter**, and
|
||
stops. The user opens each session themselves, in the directory the order
|
||
names, and gives it the starter. Running the same `/trekexecute` command again
|
||
in the main checkout collects the wave: it reads every session's progress file,
|
||
merges the session branches back sequentially, cleans up, and prepares the next
|
||
wave. After the last wave, jump to Phase 8 (final report).
|
||
|
||
**CRITICAL SAFETY RULE:** Every session MUST run in its own git worktree. Never
|
||
run two sessions in the same working directory. This rule exists because
|
||
parallel git operations in a shared worktree cause index corruption, race
|
||
conditions, and repository destruction. Each order names its own worktree.
|
||
|
||
### Step 0 — Resolve paths and detect a wave in flight
|
||
|
||
```bash
|
||
# The MAIN checkout, never a linked worktree (Phase 2.5): in a session's
|
||
# worktree --show-toplevel names that worktree.
|
||
GIT_DIR_ABS="$(cd "$(git rev-parse --git-dir)" && pwd -P)"
|
||
GIT_COMMON_ABS="$(cd "$(git rev-parse --git-common-dir)" && pwd -P)"
|
||
REPO_ROOT="$(git worktree list --porcelain | sed -n '1s/^worktree //p')"
|
||
if [ "$GIT_DIR_ABS" != "$GIT_COMMON_ABS" ]; then
|
||
echo "Error: this is a linked worktree. Run this from the main checkout $REPO_ROOT." >&2
|
||
exit 1
|
||
fi
|
||
# The plan path relative to the main checkout: every session-orders.mjs call
|
||
# runs from $REPO_ROOT with it, and each session finds the plan there in its
|
||
# own worktree.
|
||
PLAN_REL="$(python3 -c 'import os.path,sys; print(os.path.relpath(os.path.realpath(sys.argv[1]), os.path.realpath(sys.argv[2])))' "{plan-path}" "$REPO_ROOT")"
|
||
case "$PLAN_REL" in
|
||
""|..*)
|
||
echo "Error: the plan {plan-path} is not inside the main checkout $REPO_ROOT." >&2
|
||
exit 1 ;;
|
||
esac
|
||
SESSION_DIR="$REPO_ROOT/.claude/trekplan-sessions/{slug}"
|
||
WORKTREE_DIR="$SESSION_DIR/worktrees"
|
||
ORDERS_DIR="$SESSION_DIR/orders"
|
||
ORIGINAL_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||
# Resolve the plugin root ONCE (same reason as Phase 7: the variable is
|
||
# substituted in this command's text but EMPTY in the Bash tool's env).
|
||
# Only the literal form is substituted (a `:-` default is never replaced); the
|
||
# fallback takes the highest installed version under cache/<market>/voyage/.
|
||
VOYAGE_ROOT="${CLAUDE_PLUGIN_ROOT}"
|
||
case "$VOYAGE_ROOT" in
|
||
/*) ;;
|
||
*) VOYAGE_ROOT="$(ls -d "$HOME"/.claude/plugins/cache/*/voyage/[0-9]*/ 2>/dev/null \
|
||
| awk -F/ '{ split($(NF-1), v, "."); printf "%09d%09d%09d %s\n", v[1], v[2], v[3], $(0) }' \
|
||
| sort | tail -1 | cut -d' ' -f2-)"
|
||
VOYAGE_ROOT="${VOYAGE_ROOT%/}" ;;
|
||
esac
|
||
if [ ! -f "$VOYAGE_ROOT/lib/util/session-orders.mjs" ]; then
|
||
echo "[voyage] session orders cannot be written - plugin root unresolved." >&2
|
||
exit 2
|
||
fi
|
||
if [ -f "$ORDERS_DIR/wave.json" ]; then echo "WAVE_IN_FLIGHT"; else echo "NO_WAVE"; fi
|
||
```
|
||
|
||
Record `REPO_ROOT`, `PLAN_REL`, `SESSION_DIR`, `WORKTREE_DIR`, `ORDERS_DIR`,
|
||
`VOYAGE_ROOT` and `ORIGINAL_BRANCH` for the later steps. All paths except
|
||
`PLAN_REL` must be absolute.
|
||
|
||
- `WAVE_IN_FLIGHT` → read the wave number `W` from `$ORDERS_DIR/wave.json` and
|
||
jump to **Step 3** (collect). Phase 2.55 was skipped for this run (Phase 2.5).
|
||
- `NO_WAVE` → prepare the lowest wave not yet merged:
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" next-wave --plan "$PLAN_REL" --session-dir "$SESSION_DIR")
|
||
```
|
||
A number is `W`: continue to Step 1. `none` means earlier runs merged every
|
||
wave: go to Step 4. The record it reads, `$ORDERS_DIR/merged-waves.json`
|
||
(plan path + sha256 of the plan's content), is written by Step 3 after each
|
||
merged wave, survives Step 5, and is cleared only on confirm at Phase 8's
|
||
main-merge gate, when every wave is merged and Step 4 passed, so a re-run
|
||
after a later wave failed (or after a declined gate) never prepares an
|
||
already-merged wave again.
|
||
If `next-wave` exits 1, the record belongs to another plan or another version
|
||
of this one (`{slug}` is the plan's file name, so plans can share
|
||
`$SESSION_DIR`). Stop, touch nothing, and tell the user exactly what its
|
||
message says: an unfinished multi-session run of that plan holds
|
||
`$ORDERS_DIR`. Finish that run (re-run it with its own plan), or clear it and
|
||
re-run this one:
|
||
`(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" clear --session-dir "$SESSION_DIR")`.
|
||
|
||
### Step 1 — Create worktrees for wave W
|
||
|
||
```bash
|
||
mkdir -p "$WORKTREE_DIR" "$ORDERS_DIR"
|
||
```
|
||
|
||
**2a. Create worktrees for this wave's sessions:**
|
||
|
||
First make sure no session of this wave left a branch or a worktree behind in
|
||
an earlier attempt — it may hold that session's work, so it is neither reused
|
||
nor deleted:
|
||
```bash
|
||
EXISTING=""
|
||
for N in {wave W session numbers}; do
|
||
if git rev-parse -q --verify "refs/heads/trek/{slug}/session-$N" >/dev/null || [ -e "$WORKTREE_DIR/session-$N" ]; then
|
||
EXISTING="$EXISTING trek/{slug}/session-$N"
|
||
fi
|
||
done
|
||
if [ -n "$EXISTING" ]; then
|
||
echo "Error: left by an earlier attempt:$EXISTING (branch or $WORKTREE_DIR/session-N)." >&2
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
If it exits 1, stop and report (no Step 5, no Phase 8; nothing was created or
|
||
deleted): name each branch and worktree, and what the user can do — inspect it
|
||
(`git log HEAD..<branch>`, `git -C <worktree> status --short`), merge it by
|
||
hand, or delete it themselves once its work is not needed — then re-run.
|
||
|
||
Then, for each session N in this wave:
|
||
```bash
|
||
BRANCH_NAME="trek/{slug}/session-{N}"
|
||
WORKTREE_PATH="$WORKTREE_DIR/session-{N}"
|
||
GIT_OPTIONAL_LOCKS=0 git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" HEAD
|
||
```
|
||
|
||
`GIT_OPTIONAL_LOCKS=0` disables `.git/index.lock` background polling races
|
||
during parallel worktree operations (research/02 R2; GH #47721).
|
||
|
||
If `git worktree add` fails, report its error and stop the wave: never delete a
|
||
branch to make room. Jump to Step 5 (it removes the worktrees this wave just
|
||
created, without force), then Phase 8.
|
||
|
||
Report:
|
||
```
|
||
Worktree created: session-{N} → {WORKTREE_PATH} (branch: {BRANCH_NAME})
|
||
```
|
||
|
||
**2a'. Copy gitignored project artifacts into each worktree (addresses
|
||
plugin-in-monorepo + gitignored-state topology):**
|
||
|
||
`git worktree add` clones HEAD only. The plugin's `.gitignore` excludes
|
||
`.claude/projects/`, so the project directory (brief.md, plan.md, research/)
|
||
is missing in the freshly-created worktree. Each session needs to read
|
||
`brief.md`, `plan.md`, and `research/*` from `${PROJECT_DIR}` to do real work.
|
||
|
||
Insert this block AFTER the worktree-creation loop and BEFORE the orders are
|
||
written (Step 2):
|
||
|
||
```bash
|
||
# zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash.
|
||
[ -n "${ZSH_VERSION:-}" ] && setopt nonomatch
|
||
PROJECT_SOURCE="$(realpath "${PROJECT_DIR}")"
|
||
REPO_ROOT_REAL="$(realpath "${REPO_ROOT}")"
|
||
# Compute destination relpath: PROJECT_DIR relative to REPO_ROOT.
|
||
# This makes $wt/$PROJECT_REL valid regardless of whether the operator
|
||
# passed --project as relative (.claude/projects/...) or absolute.
|
||
# python3 + os.path.relpath is stdlib and portable - see the note below the
|
||
# block for why no realpath flag may be used here.
|
||
PROJECT_REL="$(python3 -c 'import os.path,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$PROJECT_SOURCE" "$REPO_ROOT_REAL")"
|
||
case "$PROJECT_REL" in
|
||
""|..*)
|
||
echo "Error: cannot derive a project relpath inside the repo ($PROJECT_SOURCE vs $REPO_ROOT_REAL)." >&2
|
||
exit 1 ;;
|
||
esac
|
||
for wt in "$WORKTREE_DIR"/session-*; do
|
||
[ -d "$wt" ] || continue
|
||
mkdir -p "$wt/$PROJECT_REL"
|
||
cp "$PROJECT_SOURCE"/brief.md "$wt/$PROJECT_REL/"
|
||
cp "$PROJECT_SOURCE"/plan.md "$wt/$PROJECT_REL/"
|
||
if [ -d "$PROJECT_SOURCE/research" ]; then
|
||
cp -r "$PROJECT_SOURCE/research" "$wt/$PROJECT_REL/"
|
||
fi
|
||
done
|
||
```
|
||
|
||
Do not "improve" the relpath line into `realpath --relative-to=...`. That flag
|
||
is GNU coreutils only; BSD `realpath` (the macOS default) rejects it, and the
|
||
failure is **silent** — the command substitution leaves `PROJECT_REL` empty, so
|
||
`mkdir -p "$wt/"` and `cp ... "$wt//"` both succeed and drop `brief.md`/`plan.md`
|
||
at the worktree root, where no session looks for them. Measured on an
|
||
Intel Mac 2026-08-31: `realpath --relative-to=... ` → `realpath: illegal option
|
||
-- -`, while bare `realpath <path>` works; a whole wave ran with zero steps
|
||
executed. Both `realpath` calls above are bare path resolution, which BSD and
|
||
GNU handle identically; resolving both operands before the relpath is what keeps
|
||
it correct when one side goes through a symlink (macOS `/var` → `/private/var`).
|
||
|
||
Failure modes: an underivable relpath (empty, or outside the repo) aborts
|
||
before anything is copied — better a loud stop than files delivered where no
|
||
session reads them; any `cp` failure exits the wave non-zero, reported via
|
||
Step 5 cleanup. A project without `research/` is not a failure.
|
||
Source: brief Constraint 2.
|
||
|
||
### Step 2 — Write the session orders for wave W, then stop
|
||
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" write --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR" --branch "$ORIGINAL_BRANCH")
|
||
```
|
||
|
||
`$PLAN_REL` is relative to the main checkout (Step 0; the tool refuses an
|
||
absolute path): each session resolves it inside its own worktree, which has the
|
||
same repo content from HEAD. The tool writes `$ORDERS_DIR/wave-{W}-session-{N}.md`
|
||
per session (naming its worktree, directory and progress file) and
|
||
`$ORDERS_DIR/wave.json` (`status: awaiting-sessions`, the plan path, the
|
||
sha256 of its content and the branch the wave is prepared on), and prints, per
|
||
session, the directory to open and the starter line
|
||
(`/trekexecute --session {N} $PLAN_REL`). A session's per-session `cwd:`
|
||
field in the Execution Strategy moves the directory it opens in to that
|
||
subdirectory of its worktree (plugin-in-monorepo layouts, brief Constraint 1,
|
||
Option B); default `.` is the worktree root.
|
||
|
||
Show the tool's output to the user verbatim, then report and **stop**:
|
||
|
||
```
|
||
Wave {W}: {N} session order(s) written in {ORDERS_DIR}. Awaiting sessions.
|
||
Open each session yourself in the directory its order names and give it the
|
||
starter line. When every session is done, run this same /trekexecute command
|
||
again here to merge the wave.
|
||
```
|
||
|
||
This run ends here. It writes no Phase 8 report: the plan is not finished, and
|
||
the orders are the hand-over.
|
||
|
||
### Step 3 — Collect wave W (a later run)
|
||
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" status --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR" --branch "$ORIGINAL_BRANCH")
|
||
```
|
||
|
||
If it exits 1, stop and report its message, touching nothing (`wave.json`
|
||
stays). Either `wave.json` was written for another plan or another version of
|
||
this one — re-run with the plan (version) that wrote the wave, or abandon the
|
||
wave (the way out below) — or the main checkout is on another branch than the
|
||
wave was prepared on (Step 0's `ORIGINAL_BRANCH` of that run): the user
|
||
switches back to that branch and re-runs, so the wave never merges elsewhere.
|
||
|
||
The JSON names each session's progress file (the file `--session N` writes,
|
||
Phase 3), its raw `status`, and its `state`, exactly one of:
|
||
`completed`; `running` — no progress file yet, `pending`, `in-progress`,
|
||
`in_progress`, or any status not named here (an unknown status is never treated
|
||
as ended); `ended-bad` — `failed`, `stopped`, `partial`, `blocked` or
|
||
`unreadable`. `running` and `ended_bad` list the sessions in each state. Check
|
||
in this order:
|
||
|
||
- **Any `running` (checked FIRST):** a session is still open. Report which
|
||
sessions are still running, with their raw status, point at their order files
|
||
(`$ORDERS_DIR/wave-{W}-session-{N}.md`), and **stop** — even when another
|
||
session in the wave already ended badly. Touch no worktree, no branch and not
|
||
`wave.json`: no merge, no archive, no cleanup.
|
||
**Way out of a stuck wave** (a session the user decided not to run): the user
|
||
deletes `$ORDERS_DIR/wave.json` and re-runs. That run takes the fresh path,
|
||
and Phase 2.55 Check 4 removes the wave's clean worktrees and merged branches
|
||
without force and stops on everything that holds work, listing it to merge by
|
||
hand or delete.
|
||
- No session is running. Before any worktree is removed (the failure branch's
|
||
Step 5 and the merge below both remove them), archive every session's
|
||
progress file:
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" archive --plan "$PLAN_REL" --wave "{W}" --session-dir "$SESSION_DIR")
|
||
```
|
||
It copies each one to `$ORDERS_DIR/wave-{W}-session-{N}.progress.json`. Step 5
|
||
never deletes these copies; Step 6 and Phase 8 read them.
|
||
- **Any `ended-bad`:**
|
||
```
|
||
Wave {W} FAILED: Session {N} ended {status}.
|
||
Stopping — later waves depend on this wave.
|
||
See its order: {ORDERS_DIR}/wave-{W}-session-{N}.md
|
||
```
|
||
Do NOT merge. Do NOT prepare later waves. Jump to Step 5 (cleanup; it keeps
|
||
every unmerged branch and every worktree git refuses to remove), then Phase 8.
|
||
- **`all_completed: true`:** 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 <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.
|
||
|
||
```bash
|
||
# zsh (the Bash tool runs the user's shell): an unmatched glob stays literal, as in bash.
|
||
[ -n "${ZSH_VERSION:-}" ] && setopt nonomatch
|
||
cd "$REPO_ROOT" || exit 1
|
||
|
||
# Remove worktrees git agrees are clean (GIT_OPTIONAL_LOCKS=0 to avoid lock-file races)
|
||
for wt in "$WORKTREE_DIR"/session-*; do
|
||
[ -d "$wt" ] || continue
|
||
GIT_OPTIONAL_LOCKS=0 git worktree remove "$wt" 2>/dev/null || echo "KEPT worktree $wt"
|
||
done
|
||
GIT_OPTIONAL_LOCKS=0 git worktree prune
|
||
|
||
# Delete merged session branches; every unmerged one is kept
|
||
git for-each-ref --format='%(refname:short)' "refs/heads/trek/{slug}/" | while read -r b; do
|
||
GIT_OPTIONAL_LOCKS=0 git branch -d "$b" >/dev/null 2>&1 || echo "KEPT branch $b"
|
||
done
|
||
|
||
# Clean up the wave marker and an empty worktree directory
|
||
rm -f "$ORDERS_DIR/wave.json"
|
||
rmdir "$WORKTREE_DIR" 2>/dev/null || true
|
||
```
|
||
|
||
Report:
|
||
```
|
||
Cleanup: worktrees removed and merged branches deleted.
|
||
Kept (they hold work): {every KEPT line}
|
||
```
|
||
|
||
For every kept item, the report says what the user can do: a worktree —
|
||
inspect it (`git -C <path> status --short`), commit or move what matters, then
|
||
`git worktree remove <path>`; a branch — inspect it (`git log HEAD..<branch>`),
|
||
merge it by hand (`git merge --no-ff <branch>`), or delete it themselves once
|
||
its commits are not needed. A re-run stops at Phase 2.55 Check 4 until they are
|
||
dealt with.
|
||
|
||
### Step 6 — Aggregate results
|
||
|
||
Read every archived copy, `$ORDERS_DIR/wave-*-session-*.progress.json`, into an
|
||
aggregated report — never the worktree copies, which Steps 3 and 5 remove with
|
||
their worktrees (earlier waves' in earlier runs).
|
||
|
||
Keep the orders directory here, whatever the outcome. Only Phase 8's
|
||
main-merge gate clears it, on confirm, when `next-wave` said `none` (every wave
|
||
merged) and Step 4 passed. Until then the next run resumes at the lowest wave
|
||
not yet merged (Step 0), or re-runs Step 4 and returns to the gate when every
|
||
wave is merged. Jump to Phase 8.
|
||
|
||
### --session N mode
|
||
|
||
When mode = `session N`:
|
||
1. Find session N in the Execution Strategy
|
||
2. Extract its step numbers (e.g., Steps: 4, 5, 6)
|
||
3. Extract its scope fence (Touch / Never touch lists)
|
||
4. Execute ONLY those steps, in order, using the single-session protocol (Phase 3→7)
|
||
5. Enforce the session's scope fence as if it were a session spec's scope fence
|
||
6. Report results for those steps only
|
||
|
||
This mode is what each session order's starter runs, in a session the user
|
||
opened in that order's worktree. It can also be used manually to re-run a
|
||
specific session.
|
||
|
||
When `--session N` runs inside a git worktree (as each session order sets up),
|
||
all git operations (add, commit) apply to the worktree's branch. The session does
|
||
not need to know it is in a worktree — git handles this transparently.
|
||
|
||
## Phase 3 — Progress file setup
|
||
|
||
The progress file location depends on whether `--project` is in use:
|
||
|
||
- **With `--project <dir>`:** progress lives at `{project_dir}/progress.json`.
|
||
- **Without `--project`:** progress lives at
|
||
`{plan-dir}/.trekexecute-progress-{slug}.json`, where `{slug}` is the plan
|
||
filename without extension.
|
||
|
||
**Session-scoped naming:** When `mode = session N`:
|
||
- With `--project`: `{project_dir}/progress-session-{N}.json`.
|
||
- Without `--project`: `{plan-dir}/.trekexecute-progress-{slug}-session-{N}.json`.
|
||
|
||
This prevents merge conflicts when parallel sessions each write their own
|
||
progress file.
|
||
|
||
### Progress file schema
|
||
|
||
```json
|
||
{
|
||
"schema_version": "1",
|
||
"plan": "{path}",
|
||
"plan_type": "{plan | session-spec}",
|
||
"plan_version": "{1.7 | 1.6 | legacy}",
|
||
"started_at": "{ISO-8601}",
|
||
"updated_at": "{ISO-8601}",
|
||
"mode": "{execute | resume | step}",
|
||
"total_steps": 0,
|
||
"current_step": 0,
|
||
"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`, `stopped` or `partial`: warn that
|
||
`--resume` is available, then wait 3 seconds (`sleep 3`) and start fresh.
|
||
This allows headless runs to proceed without blocking.
|
||
- Otherwise: create the progress file with all steps in `pending` status.
|
||
|
||
**mode = resume:**
|
||
- If no progress file exists: start from step 1 (same as fresh execute).
|
||
- If progress file exists: find the first step with status != `completed`.
|
||
A legacy progress file may say `passed` for a finished step; treat it as `completed`.
|
||
```
|
||
Resuming from step {N}. {M}/{total} steps already completed.
|
||
```
|
||
|
||
**mode = dry-run:**
|
||
- Do NOT create or modify the progress file.
|
||
|
||
**mode = step N:**
|
||
- Create the progress file if it does not exist.
|
||
- Only step N will be executed.
|
||
|
||
## Phase 4 — Entry condition check (session specs only)
|
||
|
||
**Skip for trekplans.** Skip in dry-run mode (report what would be checked instead).
|
||
|
||
Read the entry condition. Evaluate it:
|
||
|
||
- `"none"` or similar → pass immediately
|
||
- References git state (e.g., "git status clean") → run `git status --porcelain`
|
||
- References passing tests → run the specified command
|
||
- References a previous session → check `git log --oneline` for commit pattern
|
||
|
||
If the entry condition **fails**:
|
||
```
|
||
Entry condition FAILED: {condition text}
|
||
Reason: {what was checked, what was found}
|
||
Complete the prerequisite first, then re-run.
|
||
```
|
||
Update progress file with `status: "stopped"`. Stop execution.
|
||
|
||
**Also write `.session-state.local.json`** (Handover 7) with the same
|
||
`status: "stopped"` and `next_session_brief_path` pointing at the brief that
|
||
the failed entry-condition session was supposed to consume. This lets
|
||
`/trekcontinue` surface the stop in the next session. Use the same atomic
|
||
write pattern + validator check as Phase 2.55. On validator failure, warn
|
||
but do not block.
|
||
|
||
**Also write sibling `NEXT-SESSION-PROMPT.local.md`** with frontmatter
|
||
(`produced_by: trekexecute`, `produced_at: <ISO-8601>`,
|
||
`status: stopped`) — same combined ESM pattern as Phase 8 — so Phase 1.5
|
||
of `/trekcontinue` can compare project-dir and plugin-root candidates.
|
||
|
||
If the entry condition **passes**:
|
||
```
|
||
Entry condition: PASS
|
||
```
|
||
Update `entry_condition_checked: true` in the progress file.
|
||
|
||
## Phase 5 — Dry-run report (dry-run mode only)
|
||
|
||
**Only runs when mode = dry-run.** Produces a validation report, then stops.
|
||
|
||
```
|
||
## Dry Run Report: {filename}
|
||
|
||
**Type:** {plan | session-spec}
|
||
**Steps:** {N}
|
||
|
||
### Step Validation
|
||
|
||
| Step | Description | Verify | On failure | Checkpoint | Issues |
|
||
|------|-------------|--------|------------|------------|--------|
|
||
| 1 | {desc} | {cmd} | {action} | {msg} | {none / missing X} |
|
||
|
||
### File References
|
||
|
||
{For each file in Files: fields, check existence with Glob}
|
||
- {path}: EXISTS | NOT FOUND {(marked as new file) | (unexpected — may be missing)}
|
||
|
||
### Entry / Exit Conditions (session specs)
|
||
|
||
{What would be checked}
|
||
|
||
### Execution Preview (only when plan has Execution Strategy)
|
||
|
||
If `has_execution_strategy = true`, show a preview of multi-session orchestration:
|
||
|
||
```
|
||
**Sessions:** {S} across {W} waves
|
||
|
||
| Wave | Session | Steps | Depends on | Starter (the user runs it in a session they open) |
|
||
|------|---------|-------|------------|---------|
|
||
| 1 | Session 1: {title} | {nums} | none | `/trekexecute --session 1 {path}` |
|
||
| 1 | Session 2: {title} | {nums} | none | `/trekexecute --session 2 {path}` |
|
||
| 2 | Session 3: {title} | {nums} | S1, S2 | `/trekexecute --session 3 {path}` |
|
||
```
|
||
|
||
### Verdict
|
||
|
||
{READY | NEEDS ATTENTION — N issues found}
|
||
```
|
||
|
||
Stop after the dry-run report. Do not execute anything.
|
||
|
||
## Phase 5.5 — Autonomy: what stops a run, and the decision log
|
||
|
||
A run is long and mostly unattended. It does not stop to ask: what it decides
|
||
on its own it writes down, and it stops only for the four reasons below.
|
||
|
||
### Closed stop list
|
||
|
||
The list is **closed**: a run stops on these four classes and on nothing else.
|
||
Every stop names its class in the final report (Phase 8 `### Stops`).
|
||
|
||
1. **Something that cannot be undone.** A step or command whose effect cannot
|
||
be reverted from git: deleting data outside the repo's tracked files, a
|
||
migration against a live store, anything the plan marks irreversible.
|
||
2. **Something security-sensitive.** A command the executor denylist blocks
|
||
(Phase 2.4, Sub-step D1), a write to a security-sensitive path (Hard
|
||
rule 16), credentials or secrets in a diff.
|
||
3. **Something outside the working copy.** Merge to the main branch, push,
|
||
tag, publish, a write outside the repository (Hard rule 15). The main-merge
|
||
gate (Phase 8) is this class: it always pauses.
|
||
4. **A plan so broken that everything after it is guesswork.** The plan fails
|
||
to parse, a ≥ 1.8 step has no `Test first:`, a step's own `On failure:
|
||
escalate` fired (the plan's author declared that past this point is
|
||
guesswork), 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 say `passed` for a finished step; treat it as `completed`.
|
||
- **step N**: step N only
|
||
|
||
### For each step
|
||
|
||
Update progress: `steps.{N}.status = "in_progress"`, `current_step = N`, `updated_at = now`.
|
||
|
||
```
|
||
--- Step {N}/{total}: {description} ---
|
||
```
|
||
|
||
One step at a time. Inside one session the steps run in order in one working
|
||
tree; parallel steps are the multi-session wave path (Phase 2.6), never two
|
||
helpers in one tree.
|
||
|
||
#### Sub-step 0 — Record the start point
|
||
|
||
Before anything touches the tree, record where the step begins:
|
||
|
||
```bash
|
||
git rev-parse HEAD
|
||
git status --porcelain
|
||
```
|
||
|
||
- `steps.{N}.start_sha` = the `HEAD` sha. Every later check of this step reads
|
||
`git diff {start_sha}`: the review package is the diff from this recorded
|
||
start point, not anything the helper says. On `--resume` of a step that
|
||
already has a `start_sha`, keep it.
|
||
- `steps.{N}.report_path` = `{progress-dir}/steps/step-{N}.report.md` (create
|
||
the directory). Every helper round of the step appends to it, and the done
|
||
line lands in it (Sub-step G). Hand-over between the executor, the helper
|
||
and the controller is in files: the plan, this report, the diff.
|
||
- A non-empty `git status --porcelain` (beyond Voyage's own progress and report
|
||
files) means work the plan does not know about would mix into this step's
|
||
diff. Not a stop (Phase 5.5): log it as a decision
|
||
(`cost_if_wrong`: "the controller judges unrelated changes as part of step N")
|
||
and name the paths in the report.
|
||
|
||
#### Sub-step A — Scope fence check (session specs only)
|
||
|
||
Before touching any file, verify that every file in the step's `Files:` field is
|
||
in the session spec's Touch list (or is a new file to create). If ANY file is in
|
||
the Never-touch list:
|
||
|
||
```
|
||
SCOPE VIOLATION: Step {N} requires {file} which is in the never-touch list.
|
||
Escalating — this step cannot be executed within this session's scope.
|
||
```
|
||
|
||
Treat this as an automatic `escalate`. Jump to the stop-and-report logic.
|
||
|
||
#### Sub-step B — Test first and proof of red
|
||
|
||
The step's test is written first, and a command proves it red — never the
|
||
model's word that "it failed first". This sub-step blocks: a step whose test
|
||
is not proven red at its start point and green at its result does not pass.
|
||
(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.
|
||
|
||
1. **The helper proves red before it builds** (Sub-step C): it writes the test
|
||
from `Test first:`, runs the gate
|
||
`red-proof.mjs --red-only --base {start_sha} --cmd "{Verify}" --tests {files}`
|
||
(the test must FAIL at the start point), locks the test files, and only then
|
||
implements.
|
||
2. **The executor proves it both ways after the helper returns:**
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/verification/red-proof.mjs --base {start_sha} --cmd "{Verify command}" --tests {Test first File paths, comma-separated} --json
|
||
```
|
||
The test must FAIL against the start point (`start_sha` unpacked with
|
||
`git 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 `name` parameter.** A named spawn becomes an
|
||
`in_process_teammate`: it does the work, but its result never reaches this
|
||
orchestrator (measured: named 0/5 returned, unnamed 3/3 —
|
||
`docs/agent-return-channel-defect.md`). The same holds for the controller
|
||
(Sub-step E).
|
||
- **Never pass `isolation`.** Measured 2026-10-06 (Claude Code 2.1.291): a
|
||
worktree-isolated helper branched from the repository's default branch
|
||
(`02f320f`), not from the executor's `HEAD` (`2c89a6b`), so it did not see
|
||
the earlier steps. Only the operator's `worktree.baseRef: "head"` setting
|
||
changes that, and a plugin cannot set it. The helper works in this tree,
|
||
from the recorded `start_sha`.
|
||
- **Wait for the helper to finish** before anything else; when it runs in the
|
||
background, its completion notification is the signal. Never start the next
|
||
sub-step or step while a helper runs.
|
||
- `steps.{N}.attempts` = the number of helpers launched for the step (1 here).
|
||
|
||
The helper's one-line answer is not evidence. After it returns:
|
||
1. The report must exist at `report_path` with its `### Ran`, `### Saw` and
|
||
`### Did NOT check` sections (a missing report is a finding for Sub-step F).
|
||
2. Register the step's new files so the diff sees them: for each path in
|
||
`Files:` that `git status --porcelain` shows as `??`, run
|
||
`git add --intent-to-add {path}` (stages nothing).
|
||
3. 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 -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). If **FAIL**: the
|
||
failure output is a finding for Sub-step F; still run D2 and D3, so the
|
||
controller and the next fix round see every failure at once.
|
||
|
||
#### Sub-step D2 — Manifest verification
|
||
|
||
After the Verify command passes, verify the step's Manifest block. This is
|
||
the objective completion predicate: a step is not passed until its manifest
|
||
holds, regardless of Verify exit code.
|
||
|
||
**Checks to run (in order):**
|
||
|
||
1. **Expected paths exist:** for each `expected_paths` entry, verify the
|
||
file exists in the repo. Count how many exist.
|
||
2. **min_file_count satisfied:** count from (1) must be ≥ `min_file_count`.
|
||
3. **Forbidden paths untouched:** for each `forbidden_paths` entry, run
|
||
`git diff --name-only {start_sha} -- {path}` (since this step
|
||
began). Any modified forbidden path = fail.
|
||
4. **Bash syntax check:** for each entry in `bash_syntax_check` AND any
|
||
`.sh` file that appears in `git diff --name-only {start_sha}` (safety
|
||
net for unlisted scripts), run `bash -n {path}`. Non-zero exit = fail.
|
||
5. **must_contain patterns:** for each `{path, pattern}` pair, run
|
||
`grep -E "{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:
|
||
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/verification/test-integrity.mjs --base {start_sha} --json
|
||
```
|
||
|
||
It reports a removed assert, a deleted or skipped test, a muted check
|
||
(`|| true`, continue-on-error, an inline lint/type suppression), a stub body
|
||
("not implemented") and a threshold loosened in the permissive direction.
|
||
|
||
| Exit | Meaning | Executor |
|
||
|------|---------|----------|
|
||
| 0 | clean | continue |
|
||
| 1 | findings | each finding goes to the controller (Sub-step E), which rules it `justified` only by quoting the plan line that asks for the change; every other finding is a `violation` |
|
||
| 2 | could not run | exit 2 is never green: "floor guard not run: {reason}" is a finding for Sub-step F, and the step cannot pass this round |
|
||
|
||
A finding is never dropped silently: the step report and the final report list
|
||
every one with its ruling.
|
||
|
||
#### Sub-step 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):
|
||
|
||
```text
|
||
You are the controller for step {N} of the plan {plan path}. You judge; you
|
||
change nothing: no Write, no Edit, no git command that writes.
|
||
|
||
Read the step's diff yourself: git diff {start_sha} --stat, then
|
||
git diff {start_sha}. The diff is the evidence. You are not given the
|
||
helper's report and must not look for it.
|
||
|
||
The step, verbatim:
|
||
{every field of the step}
|
||
|
||
Review focus from the plan (look hardest here): {Review focus, or "none"}
|
||
|
||
Mechanical results: proof of red {red-proof JSON}; Verify exit {code};
|
||
manifest {pass | fail + drift}; floor guard {test-integrity JSON}.
|
||
|
||
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):
|
||
```bash
|
||
git checkout -- {files from Files: field}
|
||
```
|
||
Record failure. **Do NOT proceed to next step.** Jump to Phase 7.
|
||
|
||
**`On failure: skip`**
|
||
- Mark step as skipped at its first failed round, without fix rounds. Continue
|
||
to next step.
|
||
```
|
||
Step {N}: SKIPPED (non-critical per plan)
|
||
```
|
||
Update `steps.{N}.status = "skipped"`.
|
||
|
||
**`On failure: escalate`**
|
||
- Stop at the first failed round, without fix rounds (stop class 4: the plan's
|
||
author declared that past this point is guesswork).
|
||
```
|
||
Step {N}: ESCALATED — requires human judgment
|
||
```
|
||
Commit all completed work before stopping. Stage ONLY files from steps with
|
||
`status: "completed"` in the progress file — collect their `Files:` fields. Never
|
||
use `git add -A` (risks staging secrets, binaries, or unrelated work).
|
||
```bash
|
||
git add {files from completed steps' Files: fields} && git commit -m "wip: trekexecute stopped at step {N} — escalation needed"
|
||
```
|
||
Jump to Phase 7.
|
||
|
||
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
|
||
|
||
1. **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:
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/verification/done-line.mjs --step {N} --cmd "{Verify command}" --report {report_path}
|
||
```
|
||
Exit 0 = `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's `Verify:` (up to the plan,
|
||
as in Sub-step B); below 1.8, a step whose `Verify:` is not a test-runner
|
||
command records "no done line (Verify is not a test-runner command)" in its
|
||
report and rests on Sub-step D1's exit code. Confirm with
|
||
`node ${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.
|
||
2. **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).
|
||
3. **Checkpoint.** Stage the step's `Files:` — and only those, never
|
||
`git add -A` — then 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/<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 contains `end to end`, `end-to-end`
|
||
or `run 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:**
|
||
|
||
1. **Enumerate expected paths:** aggregate `expected_paths` across every
|
||
step manifest (and the session_manifest, if present). Deduplicate.
|
||
|
||
2. **Filesystem check:** for each expected_path, confirm the file exists.
|
||
|
||
3. **Commit-count check:** count commits since session-start
|
||
(`git rev-list --count {start_sha}..HEAD`). Expected count =
|
||
number of steps with `status=completed` (excludes Step 0 if sandbox_preflight).
|
||
|
||
4. **Commit-message pattern sweep:** walk `git log {start_sha}..HEAD` and
|
||
confirm each commit matches one of the declared
|
||
`commit_message_patterns` (in any order — order is advisory).
|
||
|
||
5. **Bash syntax sweep:** for every `.sh` file in
|
||
`git diff --name-only {start_sha} HEAD`, run `bash -n`. Collect failures.
|
||
|
||
6. **Forbidden-path sweep:** for each `scope_forbidden` entry, check
|
||
`git diff --name-only {start_sha} HEAD -- {path}` is empty.
|
||
|
||
7. **Done-line sweep:** for every step with `status=completed` that carries a
|
||
`report_path`, run
|
||
`node ${CLAUDE_PLUGIN_ROOT}/lib/verification/done-line.mjs --check --step {N} --report {report_path}`.
|
||
Exit 1 on a plan ≥ 1.8 = drift (`{check: "done-line", expected: "DONE step N", actual: "absent"}`);
|
||
below 1.8 it is drift only when the step's report does not say why it has
|
||
none (Sub-step G).
|
||
|
||
Compare the audit result against the executor's `progress.status`:
|
||
|
||
- **Audit pass + progress=completed:** status stays `completed`.
|
||
- **Audit fail + progress=completed:** OVERRIDE to `partial`. The executor
|
||
believed it was done; the filesystem says otherwise. This is the Wave 1
|
||
hallucination case — the audit is the defense.
|
||
- **Audit fail + progress=failed/stopped:** status stays as-is; drift is
|
||
informational.
|
||
|
||
Record in progress file:
|
||
- `manifest_audit.status = "pass" | "drift"`
|
||
- `manifest_audit.drift_details = [{check, expected, actual}, ...]`
|
||
|
||
### Machine-verifiable completion gate (stop-signal contract)
|
||
|
||
> **Sealed inline so the termination rule survives even when the executor
|
||
> model is the very thing it constrains.**
|
||
>
|
||
> This is the `machine-verifiable completion gate` (the `stop-signal
|
||
> contract`): the executor may emit `result: completed` ONLY when BOTH hold —
|
||
>
|
||
> 1. **Objective predicate** — the **Phase 7.5** manifest audit (above) PASSED,
|
||
> re-verified directly from the filesystem + git log, ignoring the executor's
|
||
> own per-step bookkeeping. "The audit wins"; a self-reported `completed` over
|
||
> a drifting audit is OVERRIDDEN to `partial` (Phase 7.5, `:1221`).
|
||
> 2. **Explicit stop-signal** — a signal *distinct from the executor's
|
||
> self-assessment* is satisfied: the plan's Verify command(s) **passed (exit
|
||
> 0)** / lint clean / an explicit **DONE token emitted AFTER the Phase 7.5
|
||
> audit ran** (never before, never in place of it). A transcript that merely
|
||
> "feels done" is NOT a stop-signal — victory-declaration bias is the exact
|
||
> failure mode this gate exists to defeat. For a trekplan this clause is
|
||
> **Phase 7's** `plan_verification.status == "passed"` — a real exit code
|
||
> from `lib/verification/criteria-runner.mjs`, on the single-session path as
|
||
> well as the multi-session one. `"failed"` or `"not-run"` OVERRIDES
|
||
> `completed` to `failed`, exactly as the audit-override does.
|
||
> 3. **Counter liveness (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 OVERRIDDEN `completed → partial`, exactly as the audit-override
|
||
> already does (`:1221`). So the counter's liveness is auditable at the gate,
|
||
> not merely asserted in prose.
|
||
> 4. **End to end** — for a trekplan, Phase 7's `end_to_end == "verified"`.
|
||
> `"not-verified"` OVERRIDES `completed → partial` with the reason
|
||
> "not verified end to end": tests that pass are not the thing running.
|
||
>
|
||
> 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:**
|
||
|
||
1. Determine the missing step numbers from `manifest_audit.drift_details`
|
||
(steps whose expected_paths are absent or whose commits are missing).
|
||
2. Read the original session spec. Copy only the missing steps (preserving
|
||
their Manifest blocks) into a new file:
|
||
`{output_dir}/session-{N}-recovery-{depth}.md`
|
||
3. Populate `## Recovery Metadata`:
|
||
- `recovery_of: {original session spec path}`
|
||
- `recovery_depth: {current depth + 1}`
|
||
- `missing_steps: [N, M, ...]`
|
||
- `entry_condition_override: "previous partial session committed at {sha}"`
|
||
- `parent_progress_file: {path}`
|
||
4. Prepend the synthetic Step 0 pre-flight (same as normal session specs).
|
||
5. Set `recovery_dispatched = true` in parent progress file.
|
||
|
||
**Hand the recovery session over as an order (Voyage does not start it):**
|
||
|
||
Report the recovery spec as a session order and stop the run as `partial`:
|
||
|
||
```
|
||
Recovery order: session {N}, depth {depth}
|
||
Open a Claude Code session yourself in {REPO_ROOT} (the main checkout, which
|
||
holds the session's merged work) and give it:
|
||
/trekexecute --session {N} {recovery spec path}
|
||
That session runs the missing steps and its own Phase 7.5 audit.
|
||
```
|
||
|
||
Nothing routes a later run of this command back to the recovery: the recovery
|
||
session's own audit is its check. It runs as a child, so if it still drifts it
|
||
stops `partial` and escalates to the user (the preconditions above) instead of
|
||
dispatching again. If Phase 7.5 drifts here while `recovery_depth` is already 2,
|
||
report instead:
|
||
|
||
```
|
||
RECOVERY EXHAUSTED: session {N} drifted after 2 recovery attempts.
|
||
Missing: {steps and details}
|
||
Status: partial (recovery_depth=2, escalated to user)
|
||
```
|
||
|
||
Do NOT dispatch a third recovery. Report to the user.
|
||
|
||
### Iteration caps + global recovery/retry budget
|
||
|
||
Every recovery/retry loop in this executor has a documented numeric bound.
|
||
These are **three distinct axes** — not one nested counter:
|
||
|
||
- **Per-step fix-round cap** — `fix_rounds ≤ 5`: maximum 5 fix rounds per step, escalating upward
|
||
(rounds 1–3 fix with a fresh helper, 4 goes to the plan, 5 to the operator;
|
||
Hard Rule 5 / Sub-step F). `attempts` counts the helpers a step launched.
|
||
Bounds the rounds of a *single* step.
|
||
- **Per-session recovery cap** — `recovery_depth < 2` (Phase 7.6 hard cap).
|
||
Bounds how deep recovery *dispatch* nests.
|
||
- **Global recovery/retry budget** — a new env-overridable constant
|
||
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS` (default **25**) — the aggregate ceiling
|
||
on the **total count of recovery + retry iterations across the whole
|
||
execution**. This is the case the two caps above do NOT bound: many steps each
|
||
using up to 3 fix rounds can sum unbounded. It primarily backstops the **fix-round
|
||
aggregate** (recovery dispatch is already hard-capped at 2). Reaching 0 stops
|
||
the run (stop class 4: a plan that needs that many fixes is guesswork). Surfaced and
|
||
enforced via the `iterations_remaining` counter (Phase 3 schema; decremented
|
||
on every recovery/retry iteration; reconciled at the Phase 7.5 completion
|
||
gate).
|
||
|
||
Default **25** is an independent value from the cap research (practitioner range
|
||
10–50; OWASP Agentic ASI08 financial-DoS), tuned down from the research ~50
|
||
starting point for a tighter backstop. Operators may override:
|
||
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS=10 /trekexecute --project ...`. The
|
||
`effort=='low'` single-foreground-loop path and the wave path are both subject to
|
||
this aggregate ceiling; it is an execution-spec ceiling (enforced via the
|
||
`iterations_remaining` counter), not a code-loop edit.
|
||
|
||
## Phase 8 — Final report
|
||
|
||
Always produce a final report.
|
||
|
||
### Main-merge gate (MAIN_MERGE_GATE — always pauses)
|
||
|
||
Before writing the final progress + state files (and before any "merge to
|
||
main" prose), emit the `main-merge-gate` lifecycle event so observability
|
||
and operator tooling can see that the pipeline reached its terminal
|
||
boundary:
|
||
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs --event main-merge-gate --payload - <<'JSON'
|
||
{"plan":"{plan-path}","wave_count":{wave-count}}
|
||
JSON
|
||
```
|
||
|
||
Fill in the placeholders yourself. The payload goes on stdin through a quoted
|
||
heredoc, so the shell expands nothing in it: not a variable (shell state from an
|
||
earlier Bash call does not survive, and an expanded variable is how this event
|
||
used to print `payload-not-json`), and not a quote character or `$(...)` in the
|
||
plan path. `{plan-path}` is the repo-relative plan path written as a JSON
|
||
string (escape a `"` or `\` in it). `{wave-count}` is the number of merged
|
||
waves, `1` on the single-session path.
|
||
|
||
Run each event call as its own command, never chained with `&&`. The writer
|
||
prints `{"written": true, ...}`. On `"written": false` (exit 1), print its
|
||
`reason` in one line and continue — an event failure never blocks the gate.
|
||
|
||
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:
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs --event main-merge-approved --payload - <<'JSON'
|
||
{"plan":"{plan-path}"}
|
||
JSON
|
||
```
|
||
|
||
On the multi-session wave path (Phase 2.6), and only when `next-wave` said
|
||
`none` (every wave merged) and Step 4 passed, also clear the plan's orders
|
||
directory — orders, archived progress copies and the merged-waves record — so a
|
||
later plan with the same `{slug}` starts clean. This is the one place it is
|
||
cleared:
|
||
```bash
|
||
(cd "$REPO_ROOT" && node "$VOYAGE_ROOT/lib/util/session-orders.mjs" clear --session-dir "$SESSION_DIR")
|
||
```
|
||
|
||
On decline:
|
||
```bash
|
||
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:
|
||
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"project": "{project_dir}",
|
||
"next_session_brief_path": "{determined from Execution Strategy: next session's brief path, or current brief path if last session}",
|
||
"next_session_label": "{label of next session from Execution Strategy, or 'Complete' if last}",
|
||
"status": "{same as progress.json status — completed | failed | stopped | partial}",
|
||
"updated_at": "{ISO-8601 now}"
|
||
}
|
||
```
|
||
|
||
Use `lib/util/atomic-write.mjs` (`atomicWriteJson`) — same crash-safety as
|
||
`progress.json`. Then verify: `node lib/validators/session-state-validator.mjs
|
||
--json {project_dir}/.session-state.local.json`. On validator failure, warn
|
||
to stderr but do NOT block — Phase 8 must always reach the final report.
|
||
|
||
**Also write sibling `NEXT-SESSION-PROMPT.local.md`** (Bug 3 frontmatter
|
||
contract — see `docs/HANDOVER-CONTRACTS.md` § Handover 7 Lifecycle) in the
|
||
project directory. The frontmatter MUST contain `produced_by: trekexecute`
|
||
and `produced_at: <ISO-8601>` so `/trekcontinue` Phase 1.5 can detect
|
||
producer drift between project-dir and plugin-root candidates. Use a single
|
||
ESM inline block so state-file + prompt-file writes succeed or fail together:
|
||
|
||
```bash
|
||
node --input-type=module -e "
|
||
import path from 'node:path';
|
||
import { writeFileSync } from 'node:fs';
|
||
import { atomicWriteJson } from './lib/util/atomic-write.mjs';
|
||
const [, dir, briefPath, label, status] = process.argv;
|
||
const now = new Date().toISOString();
|
||
const stateObj = { schema_version: 1, project: dir, next_session_brief_path: briefPath, next_session_label: label, status, updated_at: now };
|
||
atomicWriteJson(path.join(dir, '.session-state.local.json'), stateObj);
|
||
const promptBody = '---\\nproduced_by: trekexecute\\nproduced_at: ' + now + '\\nproject: ' + dir + '\\nstatus: ' + status + '\\n---\\n\\n# ' + label + '\\n\\nResume via /trekcontinue.\\n';
|
||
writeFileSync(path.join(dir, 'NEXT-SESSION-PROMPT.local.md'), promptBody);
|
||
" '{project_dir}' '{next_session_brief_path}' '{next_session_label}' '{status}'
|
||
```
|
||
|
||
> Note: this block is a runtime template for Phase 8 orchestration; the
|
||
> `{project_dir}`/`{next_session_brief_path}`/`{next_session_label}`/`{status}`
|
||
> placeholders are filled in by the orchestrator at execution time. The
|
||
> leading `!` prefix that previously wrapped this block was removed because
|
||
> Claude Code's slash-command parser auto-executes `!`...` patterns at
|
||
> skill-load time, even inside bash code fences — see
|
||
> `docs/HANDOVER-CONTRACTS.md` § Handover 7 Lifecycle for context.
|
||
|
||
This single insertion covers every multi-session execution path that
|
||
converges here (Path A: successful single session, Path B: `--session N`
|
||
explicit, Path C: compaction-survival recovery, Path D: standard plan
|
||
completion). Phase 2.55 and Phase 4 cover the early-stop paths E1/E2.
|
||
Phase 2.3 (validate exit) and Phase 5 (dry-run) intentionally do not write
|
||
— neither path is resumable.
|
||
|
||
```
|
||
## Ultraexecute Local Complete
|
||
|
||
**Plan:** {path}
|
||
**Type:** {plan | session-spec}
|
||
**Mode:** {execute | resume | step N}
|
||
**Result:** {COMPLETED | FAILED at step N | STOPPED (escalation) | PARTIAL (N/total passed)}
|
||
**End to end:** {verified | not verified end to end}
|
||
|
||
### Step Results
|
||
|
||
| Step | Description | Result | Red proof | Fix rounds | Test lock | Done line | Attempts | Commit | Manifest |
|
||
|------|-------------|--------|-----------|------------|-----------|-----------|----------|--------|----------|
|
||
| 0 | Sandbox pre-flight | PASS | n/a | 0 | n/a | n/a | 1 | — | n/a |
|
||
| 1 | {desc} | PASS | proven | 0 | released | yes | 1 | abc1234 | pass |
|
||
| 2 | {desc} | FAIL | rejected | 4 (sent to the plan) | released | no | 4 | — | — |
|
||
| 3 | {desc} | — | — | 0 | — | — | 0 | — | — |
|
||
|
||
Steps without proof of red: {N} ({step numbers}; below plan_version 1.8 only)
|
||
|
||
### Controller verdicts
|
||
|
||
Per step and round: spec {pass | fail}, quality {pass | fail}, findings (file:line — problem),
|
||
floor-guard rulings, and what the controller did not judge. Report paths: {report_path per step}.
|
||
|
||
### Stops
|
||
|
||
{none — the run ended without a stop | the stop that ended the run, with its class from the
|
||
closed stop list (Phase 5.5): 1 cannot be undone · 2 security-sensitive · 3 outside the working
|
||
copy · 4 plan so broken that everything after it is guesswork, and the file or command that shows it}
|
||
|
||
### Decisions taken
|
||
|
||
| Step | Decision | Cost if wrong |
|
||
|------|----------|---------------|
|
||
| {N} | {what the executor or a helper decided that the plan did not dictate} | {what must be redone, by whom, how big} |
|
||
|
||
{none, when the plan dictated every call}
|
||
|
||
### Plan Verification (Phase 7, trekplans only)
|
||
|
||
- **Status:** {passed | failed | not-run | n/a}
|
||
- **Criteria:** {passed}/{total} passed, {failed} failed, {blocked} blocked, {unrunnable} not run
|
||
- **Failed criteria:** {label + command + exit code, one per line; empty when passed}
|
||
|
||
### Manifest Audit (Phase 7.5)
|
||
|
||
- **Status:** {pass | drift}
|
||
- **Drift details:** {enumerated; empty on pass}
|
||
- **Recovery dispatched:** {true | false}
|
||
- **Recovery depth:** {N}
|
||
- **Legacy plan:** {true | false}
|
||
|
||
### Summary
|
||
|
||
- Passed: {N}/{total}
|
||
- Skipped: {N}
|
||
- Failed: {N}
|
||
- Not reached: {N}
|
||
- Blocked (sandbox): {N}
|
||
|
||
{if all passed + exit condition passed + plan verification passed + end to end verified}:
|
||
All steps completed. Exit condition: PASS. Plan verification: PASS. End to end: verified.
|
||
|
||
{if all passed but end_to_end = not-verified}:
|
||
All steps passed their checks, but the plan has no passing run-the-thing criterion:
|
||
not verified end to end. Result: PARTIAL.
|
||
|
||
{if failed/stopped}:
|
||
### Failure Details
|
||
|
||
Step {N}: {description}
|
||
On failure: {action}
|
||
Error: {error output, first 20 lines}
|
||
Attempts: {N}
|
||
|
||
### What Remains
|
||
|
||
{Numbered list of unexecuted steps}
|
||
|
||
To resume (single session): /trekexecute --resume {path}
|
||
To continue (multi-session waves): re-run the same /trekexecute command (not --resume)
|
||
```
|
||
|
||
**Result vocabulary (v1.7, strict):**
|
||
- `completed` — all steps passed AND Phase 7.5 manifest audit passed AND
|
||
(for trekplans) Phase 7 plan verification passed
|
||
- `partial` — steps passed per executor but Phase 7.5 found drift, OR
|
||
Phase 7.6 recovery incomplete, OR the run is not verified end to end
|
||
(Phase 7, gate clause 4)
|
||
- `blocked` — Step 0 sandbox pre-flight exited 77; no real work attempted
|
||
- `failed` — 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):
|
||
|
||
```json
|
||
{
|
||
"trekexecute_summary": {
|
||
"plan": "{path}",
|
||
"plan_type": "{plan | session-spec}",
|
||
"plan_version": "{1.7 | 1.6 | legacy}",
|
||
"result": "{completed | partial | blocked | failed | stopped}",
|
||
"steps_total": 0,
|
||
"steps_passed": 0,
|
||
"steps_failed": 0,
|
||
"steps_skipped": 0,
|
||
"steps_not_reached": 0,
|
||
"steps_blocked": 0,
|
||
"failed_at_step": null,
|
||
"exit_condition": "{pass | fail | skipped | n/a}",
|
||
"plan_verification": "{passed | failed | not-run | n/a}",
|
||
"manifest_audit": "{pass | drift | n/a}",
|
||
"drift_details": [],
|
||
"recovery_dispatched": false,
|
||
"recovery_depth": 0,
|
||
"iterations_remaining": 25,
|
||
"legacy_plan": false,
|
||
"progress_file": "{path}",
|
||
"end_to_end": "{verified | not-verified | n/a}",
|
||
"steps_without_red_proof": 0,
|
||
"stop_class": "{none | 1 | 2 | 3 | 4}",
|
||
"decisions": 0
|
||
}
|
||
}
|
||
```
|
||
|
||
The `trekexecute_summary` key makes it grep-able in a session's output.
|
||
|
||
**Multi-session path (Phase 2.6).** Per-session results come from Step 6's
|
||
aggregate of the archived copies `$ORDERS_DIR/wave-*-session-*.progress.json`.
|
||
The report lists every worktree and `trek/{slug}/*` branch Steps 3 and 5 kept,
|
||
with what the user can do (Step 5). If the orders directory was kept (not every
|
||
wave merged, Step 4 did not pass, or the gate was declined), say so: the next
|
||
run resumes at the lowest wave not yet merged, or re-runs Step 4 and returns to
|
||
the gate, and the `clear` command in Step 0 starts the plan over from its first
|
||
wave instead.
|
||
|
||
## Phase 9 — Stats tracking
|
||
|
||
Append one record to `trekexecute-stats.jsonl` in the plugin data directory.
|
||
Run it exactly like this. The record goes on stdin, and the writer resolves
|
||
the directory: `CLAUDE_PLUGIN_DATA`, which the Bash tool env does not carry,
|
||
else the directory `scripts/yardstick.mjs` reads.
|
||
|
||
```bash
|
||
node ${CLAUDE_PLUGIN_ROOT}/lib/stats/stats-append.mjs trekexecute <<'JSON'
|
||
{
|
||
"ts": "{ISO-8601}",
|
||
"plan": "{filename only}",
|
||
"plan_type": "{plan | session-spec}",
|
||
"mode": "{execute | resume | dry-run | step}",
|
||
"result": "{completed | failed | stopped | partial}",
|
||
"steps_total": 0,
|
||
"steps_passed": 0,
|
||
"steps_failed": 0,
|
||
"steps_skipped": 0,
|
||
"failed_at_step": null
|
||
}
|
||
JSON
|
||
```
|
||
|
||
`steps_passed` counts the steps whose status is `completed`.
|
||
|
||
The writer prints `{"written": true, ...}`. On `"written": false` (exit 1),
|
||
print its `reason` in one line and continue. A stats failure never blocks the
|
||
workflow, but it is reported, not skipped silently.
|
||
|
||
## Profile (v4.1)
|
||
|
||
Accepts `--profile <name>` where `<name>` is `economy`, `balanced`, `premium`,
|
||
`fable`, or a custom profile under `voyage-profiles/`. Default: `premium`.
|
||
|
||
Resolution order (per `lib/profiles/resolver.mjs`):
|
||
1. `--profile` flag (source: `flag`)
|
||
2. `VOYAGE_PROFILE` env-var (source: `env`)
|
||
3. Inherited from `plan.md` frontmatter `profile:` field (source: `inheritance`)
|
||
4. `premium` default for v4.0-style plans without `profile:` (source: `default`)
|
||
|
||
The selected profile 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:
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
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
|
||
|
||
1. **No AskUserQuestion for execution decisions.** All execution decisions come
|
||
from the plan's On failure clauses. If the plan says escalate, stop and
|
||
report — never ask. What the plan leaves open, the executor decides and
|
||
writes into the decision log with its `cost_if_wrong`; it stops only on the
|
||
closed stop list (Phase 5.5).
|
||
|
||
2. **No scope creep.** Only touch files listed in the step's `Files:` field.
|
||
If a file outside the list seems to need changing, record it as a finding
|
||
in the final report — do not touch it.
|
||
|
||
3. **Exit code is truth.** The Verify command's exit code is authoritative.
|
||
Non-zero = FAIL regardless of output. Zero with wrong output = FAIL.
|
||
|
||
4. **Fresh verification.** Re-run the Verify command from scratch every time.
|
||
Never trust cached or prior results.
|
||
|
||
5. **Fix rounds go up — maximum 5 fix rounds per step, escalating upward.**
|
||
Rounds 1–3 are fixes by a fresh helper, round 4 sends the step back to
|
||
planning, round 5 to the operator (Sub-step F). Never a fourth guess at the
|
||
same level, never a cheaper model, never a loop.
|
||
|
||
6. **Never corrupt completed work.** Only revert files from the failing step.
|
||
Never touch files from earlier passed steps.
|
||
|
||
7. **Checkpoint discipline.** Run the Checkpoint commit exactly as written.
|
||
Do not combine, reorder, or skip checkpoints on passed steps.
|
||
|
||
8. **Scope fence enforcement.** For session specs: never modify files in the
|
||
Never-touch list, regardless of what the Changes field says.
|
||
|
||
9. **Progress file is ground truth.** Resume uses the progress file, not git log.
|
||
|
||
10. **One fresh helper per plan step, one controller, nothing more.** The
|
||
executor writes no product code: each step is built by a fresh
|
||
`voyage:step-implementer` (Sub-step C) and judged by one controller, a
|
||
general-purpose helper that reads the diff (Sub-step E).
|
||
Helpers never start helpers: the step agent has no Agent tool, and the
|
||
controller is told it changes nothing. Never the Agent tool's `name` (a named spawn's
|
||
result never returns), never `isolation` (measured: wrong base). No
|
||
TeamCreate — kept out via `disallowed-tools: TeamCreate`, since omission
|
||
from `allowed-tools` alone leaves a tool callable (CC 2.1.152).
|
||
|
||
11. **Worktree isolation is mandatory for multi-session execution.** Every
|
||
session order names its own git worktree. Never run two or more sessions
|
||
in the same working directory. This rule has no exceptions.
|
||
Sequential (single-session) execution does not require worktrees.
|
||
|
||
12. **Worktree cleanup always runs, and never with force.** Before producing
|
||
the final report (Phase 8), remove the worktrees and merged session
|
||
branches created during this execution, even if the run failed or was
|
||
stopped — but only with `git worktree remove` without force and
|
||
`git branch -d`. A worktree git refuses to remove or a branch it refuses to
|
||
delete holds work: keep it, never retry with force, and list it in the
|
||
final report with what the user can do (Phase 2.6 Step 5). Every unmerged
|
||
`trek/{slug}/*` branch survives every failure path. The one exception to
|
||
running cleanup: never while a wave awaits its sessions — a run that stops
|
||
at Phase 2.6 Step 2, or at a Step 3 that finds a session `running` or a
|
||
dirty or refused merge, writes no Phase 8 report and leaves every worktree
|
||
and branch in place.
|
||
|
||
13. **Merge sequentially, abort on conflict.** After a parallel wave completes,
|
||
merge each session's branch into the main branch one at a time with
|
||
`--no-ff`. If any merge produces a conflict, run `git merge --abort`,
|
||
report the conflicting files, and do not attempt further merges. Never use
|
||
`--force` or `--strategy-option theirs/ours` to silently resolve conflicts.
|
||
|
||
14. **Verify command security check.** Before executing any `Verify:` or
|
||
`Checkpoint:` command, check it against the executor security denylist
|
||
(Sub-step D). If the command matches a blocked pattern, escalate
|
||
immediately — do not execute, do not retry.
|
||
|
||
15. **No writing outside the repository.** During step execution, never write
|
||
files outside the git repository root (`git rev-parse --show-toplevel`).
|
||
Exception: `.claude/` paths for plans, progress files, and stats,
|
||
AND `~/.claude/projects/*/memory/` paths for memory files when the
|
||
step's manifest declares `memory_write: true`. The `memory_write: true`
|
||
opt-in is a second gate: unauthorized memory writes still fail. This
|
||
prevents escape-from-repo attacks where a plan step modifies home
|
||
directory or system files, while permitting the narrow case of
|
||
deliberate memory updates.
|
||
|
||
16. **No writing to security-sensitive paths.** Never write to `.git/hooks/`
|
||
(git hook injection), `~/.ssh/`, `~/.aws/`, `~/.gnupg/`, `.env` files,
|
||
shell configs (`~/.zshrc`, `~/.bashrc`, `~/.profile`), or
|
||
`.claude/settings.json` / `.claude/hooks/` (infrastructure self-modification).
|
||
|
||
17. **Manifest is the completion predicate.** A step may not be marked passed
|
||
if its manifest does not verify, regardless of the Verify command's exit
|
||
code. The manifest is the objective contract — Verify is necessary but
|
||
not sufficient. For v1.7+ plans: a Verify pass with a manifest fail sets
|
||
step result to `failed` and triggers the On-failure clause. For legacy
|
||
v1.6 plans: synthesized manifests apply with the same force, but
|
||
`legacy_plan: true` is logged in progress.
|
||
|
||
18. **Last-activity rule + stop-signal.** The executor's final tool call before
|
||
writing Phase 8 must be a manifest check (Phase 7.5 audit), never an arbitrary
|
||
file review. This prevents the "hallucinated completion" failure mode
|
||
where a transcript ends on an unrelated Read and the agent self-reports
|
||
`completed` without verifying. If Phase 7.5 has not run, the executor
|
||
may not emit `result: completed` under any circumstances. Beyond that:
|
||
`result: completed` requires the **machine-verifiable completion gate**
|
||
(the **stop-signal contract**, Phase 7.5) satisfied in full — Phase 7.5
|
||
audit PASS **and** an objective stop-signal (Verify exit 0 / DONE token
|
||
emitted AFTER the audit), never executor self-assessment alone.
|
||
|
||
19. **push-before-cleanup.** After successful `git merge --no-ff` of a wave
|
||
branch, run `git push origin <branch>` BEFORE `git worktree remove` and
|
||
`git branch -d`. Push failure is non-fatal (no remote, offline, etc.) —
|
||
cleanup proceeds regardless. Rationale: this converts an unrecoverable
|
||
failure (worktree removed, branch deleted, work lost) into a recoverable
|
||
one (push succeeded, branch preserved on remote). Source: research/02 R3.
|
||
|
||
20. **Three distinct iteration axes.** Per-step `fix_rounds` (≤5, upward),
|
||
per-session `recovery_depth` (<2), and the global
|
||
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS` budget (default 25 — aggregate
|
||
fix-round + recovery backstop) are independent bounds, not one nested
|
||
counter. Every recovery/retry loop is bounded by at least one of them. See
|
||
"Iteration caps + global recovery/retry budget".
|
||
|
||
21. **Never start Claude Code.** No `claude` CLI invocation, no Agent SDK, no
|
||
background process that runs a session — not for waves, not for recovery,
|
||
not for a test. Work that needs another session becomes a session order
|
||
plus a starter line the user gives a session they open themselves
|
||
(Phase 2.6, Phase 7.6).
|
||
|
||
22. **Proof, not word.** Red before green, an unweakened test and "done" are
|
||
each a command's exit code — `red-proof.mjs`, `test-integrity.mjs`,
|
||
`done-line.mjs` — never the helper's or the executor's statement. Exit 2
|
||
from any of them ("could not run") is never green.
|
||
|
||
## Credits
|
||
|
||
Every borrowing in this executor is named here, never silent. All sources are
|
||
MIT-licensed open source except Kiro (a closed product: only its published
|
||
documentation was read) and Cherny (public talks and posts). Read 2026-10-06;
|
||
pins and quotes in the local practice analysis.
|
||
|
||
- **Superpowers** — adapted from obra/superpowers@8ca22db (MIT, © 2025 Jesse
|
||
Vincent): `subagent-driven-development` — a fresh helper per task, hand-over
|
||
in files, a review package from a recorded start point, a reviewer between
|
||
tasks, the stop classes and the decision log of an unattended run (P8, P4);
|
||
`executing-plans/scripts/task-done` — the completion line appended only when
|
||
the test command exits 0, here `done-line.mjs` (P9 (4));
|
||
`systematic-debugging` — after three failed fixes, question the
|
||
architecture (P9 (7)). Not borrowed: its ladder of cheaper models.
|
||
- **ai-native-sdlc** — bashebr/ai-native-sdlc@11fb8d7 (MIT): the helper's
|
||
report contract, ran / saw / did NOT check (P8).
|
||
- **Kiro** (documentation only): the task runner takes the diff, not the
|
||
agent's account of it, as the reviewer's input (P8). Its background daemon is
|
||
not borrowed; only the step loop.
|
||
- **GitHub Spec Kit** (MIT), github/spec-kit@2e941fe: test-first as a rule,
|
||
mechanized here as `red-proof.mjs` (P9 (1)).
|
||
- **agent-skills** — addyosmani/agent-skills@9d0c60d (MIT): the floor guard of
|
||
constraint-driven development, as `test-integrity.mjs` (P9 (2)).
|
||
- **Cherny** (public primary sources): "run the thing" — a plan is verified
|
||
end to end only by a criterion that runs what was built (P9 (6)).
|
||
- **Claude Code** itself: subagents for the helper and the controller; the
|
||
`PreToolUse` guard behind the test lock (K8).
|