feat(execute)!: hand waves over as session orders, never start Claude Code (green)
Operator decision 27.09.2026: Voyage works the PM way, so nothing in it starts Claude Code. Work that needs another session becomes an order plus a starter line the user gives a session they open themselves; Voyage never starts the session. /trekexecute's multi-session path (Phase 2.6) now prepares one wave at a time: worktree per session, then lib/util/session-orders.mjs writes one order per session (worktree, steps, done-condition, starter) plus orders/wave.json, and the run stops. Running the same command again reads each session's progress file (status mode), merges sequentially with push-before-cleanup as before, and prepares the next wave. Phase 2.5 skips the destructive pre-flight while a wave is in flight. Phase 7.6 recovery is handed over the same way. Hard rule 21: never start Claude Code. Removed with it, because they existed only for launched children: the ANTHROPIC_API_KEY billing check (and its dry-run line and Hard rule 1 exception), the hooks-fire version precheck, the safety preamble, the scoped-tool/turn/budget flags, templates/headless-launch-template.md, the decomposer's launch.sh (now orders.md), /trekplan's "launch", the README billing and MCP tuning sections, and scripts/q3-cache-prefix-experiment.mjs (an experiment that spawned the binary; its results stay in docs/). /trekresearch's deep-research pre-gate no longer runs the binary for its version; the post-hoc check was already the authoritative guard. Chose orders + starters over in-session subagents because the order's done criterion asks for exactly that, and because /trekexecute disallows the Agent tool on purpose (Hard rule 10); reopening that is a separate change. Gate: 54 -> 0 hit lines outside EXEMPT. npm test 1250 (1245 pass, 0 fail, 5 skipped: 3 are the retired-reference scan without its local list). verify.sh 7/0. The fixture run (tests/lib/session-orders.test.mjs) writes two orders and wave.json for wave 1 and leaves a trap binary untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bc652ec7d3
commit
219e40cd91
18 changed files with 664 additions and 1108 deletions
|
|
@ -13,7 +13,7 @@ body:
|
||||||
id: claude-version
|
id: claude-version
|
||||||
attributes:
|
attributes:
|
||||||
label: Claude Code version
|
label: Claude Code version
|
||||||
description: Output of `claude --version`
|
description: The Claude Code version you ran
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: steps
|
id: steps
|
||||||
attributes:
|
attributes:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ This is a solo project. Issues are welcome. PRs may be considered but are not ex
|
||||||
|
|
||||||
Open an issue with:
|
Open an issue with:
|
||||||
- Plugin version (from `.claude-plugin/plugin.json`)
|
- Plugin version (from `.claude-plugin/plugin.json`)
|
||||||
- Claude Code version (`claude --version`)
|
- Claude Code version
|
||||||
- What you did, what you expected, what happened instead
|
- What you did, what you expected, what happened instead
|
||||||
- Whether it fails consistently or occasionally
|
- Whether it fails consistently or occasionally
|
||||||
|
|
||||||
|
|
@ -39,15 +39,16 @@ Changes to this plugin must preserve:
|
||||||
| `templates/research-brief-template.md` | Research brief format with triangulation and confidence |
|
| `templates/research-brief-template.md` | Research brief format with triangulation and confidence |
|
||||||
| `templates/spec-template.md` | Spec file format |
|
| `templates/spec-template.md` | Spec file format |
|
||||||
| `templates/session-spec-template.md` | Session spec format for headless execution |
|
| `templates/session-spec-template.md` | Session spec format for headless execution |
|
||||||
| `templates/headless-launch-template.md` | Launch script template |
|
|
||||||
|
|
||||||
The command files are the core. All logic lives in markdown.
|
The command files are the core. All logic lives in markdown.
|
||||||
|
|
||||||
## Testing locally
|
## Testing locally
|
||||||
|
|
||||||
```bash
|
Start a Claude Code session yourself with this checkout loaded as a local
|
||||||
claude --plugin-dir /path/to/trekplan
|
plugin (Claude Code's plugin-directory option; see its plugin docs). Voyage
|
||||||
# Then in the session:
|
never starts a session for you. Then, in that session:
|
||||||
|
|
||||||
|
```
|
||||||
/trekresearch <research question>
|
/trekresearch <research question>
|
||||||
/trekplan <describe a task>
|
/trekplan <describe a task>
|
||||||
/trekexecute <path to plan>
|
/trekexecute <path to plan>
|
||||||
|
|
|
||||||
72
README.md
72
README.md
|
|
@ -116,7 +116,7 @@ An optional architect step can sit between research and plan — `/trekplan` aut
|
||||||
- You're working in an unfamiliar codebase and need a map before you start
|
- You're working in an unfamiliar codebase and need a map before you start
|
||||||
- The implementation has non-obvious dependencies, ordering constraints, or risks
|
- The implementation has non-obvious dependencies, ordering constraints, or risks
|
||||||
- You want a reviewable plan before committing to an approach
|
- You want a reviewable plan before committing to an approach
|
||||||
- You need autonomous headless execution without human intervention
|
- You want execution that asks no questions — every recovery decision comes from the plan
|
||||||
- You need to research a technology, library, or approach before deciding
|
- You need to research a technology, library, or approach before deciding
|
||||||
|
|
||||||
**Don't use it when:**
|
**Don't use it when:**
|
||||||
|
|
@ -305,46 +305,35 @@ Per step: apply Changes exactly as written → run Verify (exit code is truth)
|
||||||
| **Project** | `/trekexecute --project <dir>` | Read `{dir}/plan.md`, write `{dir}/progress.json` |
|
| **Project** | `/trekexecute --project <dir>` | Read `{dir}/plan.md`, write `{dir}/progress.json` |
|
||||||
| **Plan path** | `/trekexecute plan.md` | Execute a specific plan file |
|
| **Plan path** | `/trekexecute plan.md` | Execute a specific plan file |
|
||||||
| **Resume** | `/trekexecute --project <dir> --resume` | Resume from last progress checkpoint |
|
| **Resume** | `/trekexecute --project <dir> --resume` | Resume from last progress checkpoint |
|
||||||
| **Dry run** | `/trekexecute --project <dir> --dry-run` | Validate plan structure + preview sessions and billing |
|
| **Dry run** | `/trekexecute --project <dir> --dry-run` | Validate plan structure + preview sessions and their starters |
|
||||||
| **Validate** | `/trekexecute --project <dir> --validate` | Schema-only check — parse steps + manifests, report `READY \| FAIL`, no execution |
|
| **Validate** | `/trekexecute --project <dir> --validate` | Schema-only check — parse steps + manifests, report `READY \| FAIL`, no execution |
|
||||||
| **Single step** | `/trekexecute --project <dir> --step 3` | Execute only step 3 |
|
| **Single step** | `/trekexecute --project <dir> --step 3` | Execute only step 3 |
|
||||||
| **Foreground** | `/trekexecute --project <dir> --fg` | Force sequential, ignore Execution Strategy |
|
| **Foreground** | `/trekexecute --project <dir> --fg` | Force sequential, ignore Execution Strategy |
|
||||||
| **Single session** | `/trekexecute --project <dir> --session 2` | Execute only session 2 from Execution Strategy |
|
| **Single session** | `/trekexecute --project <dir> --session 2` | Execute only session 2 from Execution Strategy |
|
||||||
| **Profile** | `/trekexecute --profile <name> --project <dir>` | (v4.1.0) Pin model profile for execution. Plan-frontmatter `profile:` is honored when this flag is omitted. See [Profile system](#profile-system-v410). |
|
| **Profile** | `/trekexecute --profile <name> --project <dir>` | (v4.1.0) Pin model profile for execution. Plan-frontmatter `profile:` is honored when this flag is omitted. See [Profile system](#profile-system-v410). |
|
||||||
|
|
||||||
### Session-aware parallel execution (worktree-isolated)
|
### Multi-session execution through session orders (worktree-isolated)
|
||||||
|
|
||||||
When a plan has an `## Execution Strategy` section (auto-generated by `/trekplan` for plans with > 5 steps), `/trekexecute` automatically:
|
Voyage never starts a Claude Code session. When a plan has an `## Execution Strategy` section (auto-generated by `/trekplan` for plans with > 5 steps), `/trekexecute` works one wave at a time:
|
||||||
|
|
||||||
1. **Pre-flight checks** — validates clean working tree, plan file tracked in git, no scope fence overlaps between parallel sessions, no stale worktrees
|
1. **Pre-flight checks** — validates clean working tree, plan file tracked in git, no scope fence overlaps between parallel sessions, no stale worktrees
|
||||||
2. **Creates git worktrees** — each parallel session gets its own isolated worktree and branch (`trek/{slug}/session-{N}`)
|
2. **Creates git worktrees** — each session in the wave gets its own isolated worktree and branch (`trek/{slug}/session-{N}`)
|
||||||
3. Launches `claude -p` per session per wave, each in its own worktree
|
3. **Writes a session order per session** — the worktree to open, the steps, the done-condition, and a one-line starter (`/trekexecute --session N <plan>`) — then stops
|
||||||
4. **Merges branches back** sequentially with `--no-ff` after each wave completes
|
4. **You open each session yourself** in the directory its order names and give it the starter
|
||||||
5. **Cleans up** worktrees and branches unconditionally (even on failure)
|
5. **Run the same `/trekexecute` command again** — it reads every session's progress file, merges the branches back sequentially with `--no-ff`, cleans up, and writes the next wave's orders
|
||||||
6. Runs master verification on the merged result
|
6. After the last wave: master verification on the merged result; worktrees and branches are cleaned up unconditionally (even on failure)
|
||||||
|
|
||||||
```
|
```
|
||||||
Wave 1: Session 1 (worktree-1) + Session 2 (worktree-2) -- parallel
|
Wave 1 orders: Session 1 (worktree-1) + Session 2 (worktree-2) -- you run them, in parallel
|
||||||
↓ both complete → sequential merge to main
|
↓ re-run /trekexecute: both complete → sequential merge to main
|
||||||
Wave 2: Session 3 (worktree-3) -- sequential
|
Wave 2 orders: Session 3 (worktree-3)
|
||||||
↓ complete → merge to main
|
↓ re-run /trekexecute: complete → merge to main
|
||||||
Cleanup worktrees + Master verification
|
Cleanup worktrees + Master verification
|
||||||
```
|
```
|
||||||
|
|
||||||
Each session operates in complete filesystem isolation — no shared git index, no race conditions, no data loss. If a merge produces conflicts, the merge is aborted and conflicting files are reported.
|
Each session operates in complete filesystem isolation — no shared git index, no race conditions, no data loss. If a merge produces conflicts, the merge is aborted and conflicting files are reported.
|
||||||
|
|
||||||
Use `--fg` to force sequential execution even when a plan has an Execution Strategy.
|
Use `--fg` to run every step sequentially in the current session instead, even when a plan has an Execution Strategy.
|
||||||
|
|
||||||
### Billing safety
|
|
||||||
|
|
||||||
Before launching parallel `claude -p` sessions, `/trekexecute` checks whether `ANTHROPIC_API_KEY` is set in your environment. If it is, parallel sessions will bill your **API account** (pay-per-token), not your Claude subscription (Max/Pro). This can be expensive — parallel Opus sessions can cost $50-100+ per run.
|
|
||||||
|
|
||||||
When an API key is detected, you are asked how to proceed:
|
|
||||||
- **Use --fg instead** (recommended) — run sequentially in the current session using your subscription
|
|
||||||
- **Continue with API billing** — launch parallel sessions on your API account
|
|
||||||
- **Stop** — cancel and unset the API key first
|
|
||||||
|
|
||||||
If no API key is set, parallel sessions use your subscription and proceed without asking.
|
|
||||||
|
|
||||||
### Failure recovery
|
### Failure recovery
|
||||||
|
|
||||||
|
|
@ -356,12 +345,11 @@ If no API key is set, parallel sessions use your subscription and proceed withou
|
||||||
|
|
||||||
### Security hardening
|
### Security hardening
|
||||||
|
|
||||||
The executor implements defense-in-depth security across four layers:
|
The executor implements defense-in-depth security across three layers:
|
||||||
|
|
||||||
1. **Plugin hooks** — `pre-bash-executor.mjs` blocks 13 categories of destructive commands (rm -rf /, chmod 777, pipe-to-shell, eval injection, disk wipe, shutdown, fork bombs, cron persistence, process killing, history destruction) with bash evasion normalization. `pre-write-executor.mjs` blocks writes to `.git/hooks/`, `.claude/settings.json`, shell configs, `.ssh/`, `.aws/`, and `.env` files
|
1. **Plugin hooks** — `pre-bash-executor.mjs` blocks 13 categories of destructive commands (rm -rf /, chmod 777, pipe-to-shell, eval injection, disk wipe, shutdown, fork bombs, cron persistence, process killing, history destruction) with bash evasion normalization. `pre-write-executor.mjs` blocks writes to `.git/hooks/`, `.claude/settings.json`, shell configs, `.ssh/`, `.aws/`, and `.env` files
|
||||||
2. **Prompt-level denylist** — Security rules embedded in the executor command and session spec template that work even in headless `claude -p` sessions where hooks don't run
|
2. **Prompt-level denylist** — Security rules embedded in the executor command and session spec template that hold even if a hook does not fire
|
||||||
3. **Pre-execution plan scan** — Phase 2.4 scans all `Verify:` and `Checkpoint:` commands against the denylist before execution begins, catching dangerous commands before they reach the executor
|
3. **Pre-execution plan scan** — Phase 2.4 scans all `Verify:` and `Checkpoint:` commands against the denylist before execution begins, catching dangerous commands before they reach the executor
|
||||||
4. **Scoped tool access** — Headless child sessions use `--allowedTools "Read,Write,Edit,Bash,Glob,Grep"` instead of `--dangerously-skip-permissions`, blocking Agent spawning, MCP tools, and web access in parallel sessions
|
|
||||||
|
|
||||||
#### Recommended: disable Skill shell execution (CC v2.1.91+)
|
#### Recommended: disable Skill shell execution (CC v2.1.91+)
|
||||||
|
|
||||||
|
|
@ -380,35 +368,19 @@ prompt-injection vector that the plugin's own hooks cannot fully mitigate
|
||||||
(Skills can fire before `pre-bash-executor` matches). See
|
(Skills can fire before `pre-bash-executor` matches). See
|
||||||
[SECURITY.md](SECURITY.md) for the full hardening list.
|
[SECURITY.md](SECURITY.md) for the full hardening list.
|
||||||
|
|
||||||
### Headless execution
|
### Unattended execution
|
||||||
|
|
||||||
`/trekexecute` is designed for `claude -p` headless sessions:
|
`/trekexecute` runs without asking questions:
|
||||||
- **No questions asked** — all recovery decisions come from the plan
|
- **No questions asked** — all recovery decisions come from the plan
|
||||||
- **Progress file** — crash recovery via `{project_dir}/progress.json` (or `.trekexecute-progress-{slug}.json` for legacy plans)
|
- **Progress file** — crash recovery via `{project_dir}/progress.json` (or `.trekexecute-progress-{slug}.json` for legacy plans)
|
||||||
- **Scope fence enforcement** — never touches files outside the session's scope
|
- **Scope fence enforcement** — never touches files outside the session's scope
|
||||||
- **JSON summary** — machine-parseable `trekexecute_summary` block for log parsing
|
- **JSON summary** — machine-parseable `trekexecute_summary` block for log parsing
|
||||||
|
|
||||||
#### Headless multi-session tuning (CC v2.1.89+)
|
|
||||||
|
|
||||||
When running multiple parallel `claude -p` sessions (decomposed plans
|
|
||||||
or wave-based execution), set `MCP_CONNECTION_NONBLOCKING=true` in the
|
|
||||||
launching environment so MCP server connection latency does not
|
|
||||||
serialize startup across waves:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export MCP_CONNECTION_NONBLOCKING=true
|
|
||||||
bash .claude/projects/{slug}/sessions/launch.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Without this, each child session can spend 1-3 s blocking on MCP
|
|
||||||
connect, multiplying across waves. Setting it lets MCP connect lazily
|
|
||||||
on first tool call.
|
|
||||||
|
|
||||||
### Session titles for voyage commands (CC v2.1.94+)
|
### Session titles for voyage commands (CC v2.1.94+)
|
||||||
|
|
||||||
A `UserPromptSubmit` hook (`hooks/scripts/session-title.mjs`) sets the
|
A `UserPromptSubmit` hook (`hooks/scripts/session-title.mjs`) sets the
|
||||||
session title to `voyage:<command>:<slug>` whenever you invoke one of
|
session title to `voyage:<command>:<slug>` whenever you invoke one of
|
||||||
the four voyage commands. This makes multi-session headless runs and
|
the four voyage commands. This makes the sessions of a multi-session run and
|
||||||
session-picker output trivially identifiable. Slug derivation:
|
session-picker output trivially identifiable. Slug derivation:
|
||||||
|
|
||||||
| Invocation | Session title |
|
| Invocation | Session title |
|
||||||
|
|
@ -669,7 +641,7 @@ flowchart TB
|
||||||
PLG --> PLS --> PLC
|
PLG --> PLS --> PLC
|
||||||
end
|
end
|
||||||
subgraph EX["/trekexecute · no sub-agents"]
|
subgraph EX["/trekexecute · no sub-agents"]
|
||||||
EXI["inline step-loop + claude -p worktrees<br/>+ Phase 7.5/7.6 manifest audit (deterministic)"]
|
EXI["inline step-loop + session orders per wave (worktrees)<br/>+ Phase 7.5/7.6 manifest audit (deterministic)"]
|
||||||
end
|
end
|
||||||
subgraph RV["/trekreview"]
|
subgraph RV["/trekreview"]
|
||||||
RVR["Phase 5 — parallel · code-correctness-reviewer · brief-conformance-reviewer*"]
|
RVR["Phase 5 — parallel · code-correctness-reviewer · brief-conformance-reviewer*"]
|
||||||
|
|
@ -732,7 +704,7 @@ An opt-in upstream architect plugin (not bundled) can insert a Claude-Code-speci
|
||||||
**Dry run + validate before executing:**
|
**Dry run + validate before executing:**
|
||||||
```bash
|
```bash
|
||||||
/trekexecute --project <dir> --validate # schema check, no execution
|
/trekexecute --project <dir> --validate # schema check, no execution
|
||||||
/trekexecute --project <dir> --dry-run # preview sessions and billing
|
/trekexecute --project <dir> --dry-run # preview sessions and starters
|
||||||
/trekexecute --project <dir> # execute
|
/trekexecute --project <dir> # execute
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -891,7 +863,7 @@ synthesis.
|
||||||
The command orchestrators pin no model: `/trekbrief`, `/trekresearch`,
|
The command orchestrators pin no model: `/trekbrief`, `/trekresearch`,
|
||||||
`/trekplan`, and `/trekexecute` run on whatever model the session runs on.
|
`/trekplan`, and `/trekexecute` run on whatever model the session runs on.
|
||||||
To run them on Sonnet for cost or latency, start the session on Sonnet
|
To run them on Sonnet for cost or latency, start the session on Sonnet
|
||||||
(`claude --model sonnet`) or switch with `/model` inside the session.
|
or switch with `/model` inside the session.
|
||||||
|
|
||||||
The exploration and review sub-agents are chosen separately, at the spawn
|
The exploration and review sub-agents are chosen separately, at the spawn
|
||||||
sites: brief `phase_signals` first, then the profile's `phase_models`, with the
|
sites: brief `phase_signals` first, then the profile's `phase_models`, with the
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ questions, which defeats the purpose.
|
||||||
(not "handle the error" but "revert file X, do not proceed to step N+1")
|
(not "handle the error" but "revert file X, do not proceed to step N+1")
|
||||||
- Is there a circuit breaker? (steps that should halt execution on failure
|
- Is there a circuit breaker? (steps that should halt execution on failure
|
||||||
must say so explicitly — never assume the executor will "figure it out")
|
must say so explicitly — never assume the executor will "figure it out")
|
||||||
- Could a headless `claude -p` session execute each step without asking questions?
|
- Could a fresh session running `/trekexecute` execute each step without asking questions?
|
||||||
|
|
||||||
Steps missing On failure or Checkpoint clauses are **major** findings
|
Steps missing On failure or Checkpoint clauses are **major** findings
|
||||||
(not blockers — the plan is still valid for interactive use, but it
|
(not blockers — the plan is still valid for interactive use, but it
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: session-decomposer
|
||||||
description: |
|
description: |
|
||||||
Use this agent to decompose an trekplan into self-contained headless sessions.
|
Use this agent to decompose an trekplan into self-contained headless sessions.
|
||||||
Reads a plan file, analyzes step dependencies, groups steps into sessions,
|
Reads a plan file, analyzes step dependencies, groups steps into sessions,
|
||||||
identifies parallelism, and generates session specs + dependency graph + launch script.
|
identifies parallelism, and generates session specs + dependency graph + session orders.
|
||||||
model: opus
|
model: opus
|
||||||
color: green
|
color: green
|
||||||
tools: ["Read", "Glob", "Grep", "Write"]
|
tools: ["Read", "Glob", "Grep", "Write"]
|
||||||
|
|
@ -166,8 +166,8 @@ The Step 0 block to prepend verbatim:
|
||||||
```
|
```
|
||||||
→ expected: non-77 exit code
|
→ expected: non-77 exit code
|
||||||
- **On failure:** `escalate` — exit code 77 means this sandbox cannot push.
|
- **On failure:** `escalate` — exit code 77 means this sandbox cannot push.
|
||||||
Abort immediately; do not attempt any work. Main orchestrator will
|
Abort immediately; do not attempt any work. Fix the sandbox's push
|
||||||
re-spawn with correct permissions.
|
permission, then rerun this session.
|
||||||
- **Checkpoint:** none (no file changes)
|
- **Checkpoint:** none (no file changes)
|
||||||
- **Manifest:**
|
- **Manifest:**
|
||||||
```yaml
|
```yaml
|
||||||
|
|
@ -218,28 +218,22 @@ graph LR
|
||||||
| 3 | S4 | sequential | Wave 2 |
|
| 3 | S4 | sequential | Wave 2 |
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 7 — Generate the launch script
|
### Step 7 — Write the session orders
|
||||||
|
|
||||||
Write a bash launch script to `{output_dir}/launch.sh`.
|
Voyage never starts a session, and neither does anything you write: no launch
|
||||||
|
script, no `claude` command line. Write `{output_dir}/orders.md` — the hand-over
|
||||||
|
the user works from. For each wave, in order, list every session with:
|
||||||
|
|
||||||
The script must:
|
1. Its spec file (`{output_dir}/session-{N}-{slug}.md`)
|
||||||
1. Group sessions into waves matching the dependency graph
|
2. Its starter line, which the user gives a Claude Code session they open
|
||||||
2. Launch parallel sessions in each wave using `claude -p "$(cat session-file.md)"`
|
themselves: `/trekexecute {output_dir}/session-{N}-{slug}.md`
|
||||||
3. Wait for all sessions in a wave before starting the next wave
|
3. What it depends on, and its exit condition
|
||||||
4. Log each session to a separate file in `{output_dir}/logs/`
|
|
||||||
5. Run exit-condition verification after each wave
|
|
||||||
6. Stop if any wave's verification fails
|
|
||||||
7. Run the master plan's overall verification at the end
|
|
||||||
|
|
||||||
**Important script conventions:**
|
State the two rules at the top of the file: a wave starts only after every
|
||||||
- Use `#!/usr/bin/env bash` shebang
|
session of the previous wave met its exit condition; and sessions of the same
|
||||||
- Use `set -euo pipefail`
|
wave run in parallel only in separate git worktrees (never two in one working
|
||||||
- Each `claude -p` invocation must use `--allowedTools "Read,Write,Edit,Bash,Glob,Grep"`
|
directory) — run sequentially, one after another in the main checkout, is
|
||||||
and `--permission-mode bypassPermissions`. Prepend `unset ANTHROPIC_API_KEY`
|
always safe. End with the master plan's overall verification commands.
|
||||||
before each invocation to prevent accidental API billing
|
|
||||||
- Background processes use `&` and are collected with `wait`
|
|
||||||
- PID tracking for wait targets
|
|
||||||
- Exit codes propagated correctly
|
|
||||||
|
|
||||||
### Step 8 — Write the summary
|
### Step 8 — Write the summary
|
||||||
|
|
||||||
|
|
@ -271,7 +265,7 @@ Output a structured summary:
|
||||||
|
|
||||||
- Session specs: `{output_dir}/session-*.md`
|
- Session specs: `{output_dir}/session-*.md`
|
||||||
- Dependency graph: `{output_dir}/dependency-graph.md`
|
- Dependency graph: `{output_dir}/dependency-graph.md`
|
||||||
- Launch script: `{output_dir}/launch.sh`
|
- Session orders: `{output_dir}/orders.md`
|
||||||
|
|
||||||
### Final verification
|
### Final verification
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -894,8 +894,6 @@ topic.
|
||||||
> levels deep), so a delegated redesign is under evaluation (see
|
> levels deep), so a delegated redesign is under evaluation (see
|
||||||
> `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26). Until then,
|
> `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26). Until then,
|
||||||
> running each research pass inline in main context keeps the swarm intact.
|
> running each research pass inline in main context keeps the swarm intact.
|
||||||
> For true parallel execution, use `claude -p` invocations in separate
|
|
||||||
> terminal windows.
|
|
||||||
|
|
||||||
### Step 6c — Verify all briefs landed
|
### Step 6c — Verify all briefs landed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: trekexecute
|
name: trekexecute
|
||||||
description: Disciplined plan executor — single-session or multi-session with parallel orchestration, failure recovery, and headless support
|
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]"
|
argument-hint: "[--project <dir>] [--fg | --resume | --dry-run | --validate | --step N | --session N] [plan.md]"
|
||||||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
||||||
disallowed-tools: Agent, TeamCreate
|
disallowed-tools: Agent, TeamCreate
|
||||||
|
|
@ -10,9 +10,11 @@ disallowed-tools: Agent, TeamCreate
|
||||||
|
|
||||||
Disciplined executor for trekplan plans. Reads a plan file, detects if it has
|
Disciplined executor for trekplan plans. Reads a plan file, detects if it has
|
||||||
an Execution Strategy (multi-session), and either executes directly or
|
an Execution Strategy (multi-session), and either executes directly or
|
||||||
orchestrates parallel headless sessions — all to realize one plan.
|
prepares one session order per session, wave by wave — all to realize one plan.
|
||||||
|
|
||||||
Designed to work identically in interactive and headless (`claude -p`) mode.
|
Voyage never starts a Claude Code session. Work that needs another session
|
||||||
|
becomes an order plus a starter line the user gives a session they open
|
||||||
|
themselves (Phase 2.6).
|
||||||
|
|
||||||
## Phase 1 — Parse mode and validate input
|
## Phase 1 — Parse mode and validate input
|
||||||
|
|
||||||
|
|
@ -329,20 +331,6 @@ pre-execution summary. Report them in the final output under "Security advisorie
|
||||||
|
|
||||||
**If clean:** Report `Security scan: PASS ({N} commands checked)` and continue.
|
**If clean:** Report `Security scan: PASS ({N} commands checked)` and continue.
|
||||||
|
|
||||||
### Hooks-fire precheck (advisory)
|
|
||||||
|
|
||||||
Verify the operator's Claude Code version supports cross-model hook firing
|
|
||||||
(v2.1.117+ shipped the cross-model malware-reminder fix per research/04 D4
|
|
||||||
+ R5 + research/06 R1). Run `claude --version` (or read
|
|
||||||
`${CLAUDE_CODE_VERSION}` if available) and compare against `2.1.117`.
|
|
||||||
|
|
||||||
If unavailable or older: WARN (do not block) —
|
|
||||||
`Hooks-fire precheck: CC version {X} predates v2.1.117. Plugin hooks may
|
|
||||||
not fire reliably in headless child sessions; the in-prompt safety
|
|
||||||
preamble (Phase 2.6 Step 2b) is the primary defense for affected runs.`
|
|
||||||
|
|
||||||
If version >= 2.1.117 or unparseable: continue silently.
|
|
||||||
|
|
||||||
## Phase 2.5 — Execution strategy decision
|
## Phase 2.5 — Execution strategy decision
|
||||||
|
|
||||||
Determine how to execute this plan:
|
Determine how to execute this plan:
|
||||||
|
|
@ -366,7 +354,10 @@ Determine how to execute this plan:
|
||||||
- All sessions are in different waves (no parallelism, but still separate sessions)
|
- All sessions are in different waves (no parallelism, but still separate sessions)
|
||||||
|
|
||||||
For single-session: continue to Phase 3.
|
For single-session: continue to Phase 3.
|
||||||
For multi-session: jump to Phase 2.6.
|
For multi-session: if `.claude/trekplan-sessions/{slug}/orders/wave.json`
|
||||||
|
exists, a wave is awaiting its sessions — skip Phase 2.55 (its clean-tree and
|
||||||
|
stale-worktree checks would destroy the live worktrees) and jump to Phase 2.6,
|
||||||
|
which collects the wave. Otherwise run Phase 2.55, then Phase 2.6.
|
||||||
|
|
||||||
Report:
|
Report:
|
||||||
```
|
```
|
||||||
|
|
@ -508,91 +499,59 @@ After all 4 checks pass:
|
||||||
Pre-flight: PASS (clean tree, plan reaches worktrees, no overlaps, no stale worktrees)
|
Pre-flight: PASS (clean tree, plan reaches worktrees, no overlaps, no stale worktrees)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Phase 2.6 — Multi-session orchestration (worktree-isolated)
|
## Phase 2.6 — Multi-session orchestration (worktree-isolated, session orders)
|
||||||
|
|
||||||
**Only runs for multi-session execution.** This phase creates isolated git
|
**Only runs for multi-session execution.** Voyage never starts a Claude Code
|
||||||
worktrees for each parallel session, launches headless child sessions in their
|
session. This phase prepares ONE wave at a time: it creates an isolated git
|
||||||
own worktrees, merges results back sequentially, and cleans up. After this
|
worktree for each session in the wave, writes a **session order** for each
|
||||||
phase, jump directly to Phase 8 (final report).
|
(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 parallel `claude -p` session MUST run in its own
|
**CRITICAL SAFETY RULE:** Every session MUST run in its own git worktree. Never
|
||||||
git worktree. Never launch two sessions in the same working directory. This rule
|
run two sessions in the same working directory. This rule exists because
|
||||||
exists because parallel git operations in a shared worktree cause index corruption,
|
parallel git operations in a shared worktree cause index corruption, race
|
||||||
race conditions, and repository destruction.
|
conditions, and repository destruction. Each order names its own worktree.
|
||||||
|
|
||||||
### Step 0 — Billing safety check (MANDATORY)
|
### Step 0 — Resolve paths and detect a wave in flight
|
||||||
|
|
||||||
Before launching ANY `claude -p` process, check the environment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "${ANTHROPIC_API_KEY:+SET}"
|
|
||||||
```
|
|
||||||
|
|
||||||
If the result is `SET`, **STOP** and warn the user. `claude -p` sessions with
|
|
||||||
`ANTHROPIC_API_KEY` in the environment bill the **API account** (pay-per-token),
|
|
||||||
not the user's Claude subscription (Max/Pro). Parallel Opus sessions can cost
|
|
||||||
$50–100+ per run.
|
|
||||||
|
|
||||||
Use AskUserQuestion with these options:
|
|
||||||
|
|
||||||
**Question:** "ANTHROPIC_API_KEY is set in your environment. Parallel `claude -p`
|
|
||||||
sessions will bill your API account, not your Claude subscription. How do you
|
|
||||||
want to proceed?"
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| **Use --fg instead (Recommended)** | Run all steps sequentially in this session using your subscription. No extra cost. |
|
|
||||||
| **Continue with API billing** | Launch parallel sessions. Each session bills your API account at token rates. |
|
|
||||||
| **Stop** | Cancel execution. Unset ANTHROPIC_API_KEY first, then re-run. |
|
|
||||||
|
|
||||||
If the user chooses `--fg`: restart execution with mode = foreground (jump back
|
|
||||||
to Phase 3, single-session).
|
|
||||||
|
|
||||||
If the user chooses `Continue`: proceed with Phase 2.6 Step 1.
|
|
||||||
|
|
||||||
If the user chooses `Stop`: report "Execution cancelled — billing safety check"
|
|
||||||
and stop.
|
|
||||||
|
|
||||||
If `ANTHROPIC_API_KEY` is NOT set: proceed silently to Step 1.
|
|
||||||
|
|
||||||
### Step 1 — Create session infrastructure
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
SESSION_DIR="$REPO_ROOT/.claude/trekplan-sessions/{slug}"
|
SESSION_DIR="$REPO_ROOT/.claude/trekplan-sessions/{slug}"
|
||||||
WORKTREE_DIR="$SESSION_DIR/worktrees"
|
WORKTREE_DIR="$SESSION_DIR/worktrees"
|
||||||
LOG_DIR="$SESSION_DIR/logs"
|
ORDERS_DIR="$SESSION_DIR/orders"
|
||||||
mkdir -p "$WORKTREE_DIR" "$LOG_DIR"
|
|
||||||
ORIGINAL_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
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).
|
||||||
|
VOYAGE_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
|
||||||
|
case "$VOYAGE_ROOT" in
|
||||||
|
/*) ;;
|
||||||
|
*) VOYAGE_ROOT="$(ls -d "$HOME"/.claude/plugins/cache/*/voyage 2>/dev/null | head -1)" ;;
|
||||||
|
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`, `WORKTREE_DIR`, `LOG_DIR`, and `ORIGINAL_BRANCH` for use
|
Record `REPO_ROOT`, `SESSION_DIR`, `WORKTREE_DIR`, `ORDERS_DIR`, `VOYAGE_ROOT`
|
||||||
in subsequent steps. All paths must be absolute.
|
and `ORIGINAL_BRANCH` for the later steps. All paths must be absolute.
|
||||||
|
|
||||||
### Step 2 — Execute waves with worktree isolation
|
- `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 first wave of the Execution Strategy: `W` = the lowest
|
||||||
|
wave number. Continue to Step 1.
|
||||||
|
|
||||||
For each wave (in order):
|
### Step 1 — Create worktrees for wave W
|
||||||
|
|
||||||
**2a-pre. Build shared context + budget caps (runs once per wave, before
|
|
||||||
worktree creation):**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
WORKTREE_BASE="$(realpath "${WORKTREE_BASE:-${SESSION_DIR:-$REPO_ROOT}/worktrees}")"
|
mkdir -p "$WORKTREE_DIR" "$ORDERS_DIR"
|
||||||
mkdir -p "${WORKTREE_BASE}"
|
|
||||||
SHARED_CONTEXT_FILE="${WORKTREE_BASE}/.shared-context.md"
|
|
||||||
cat "${PROJECT_DIR}/brief.md" > "${SHARED_CONTEXT_FILE}"
|
|
||||||
if [ -f "${PROJECT_DIR}/architecture/overview.md" ]; then
|
|
||||||
printf '\n\n---\n\n' >> "${SHARED_CONTEXT_FILE}"
|
|
||||||
cat "${PROJECT_DIR}/architecture/overview.md" >> "${SHARED_CONTEXT_FILE}"
|
|
||||||
fi
|
|
||||||
MAX_TURNS="${TREKEXECUTE_MAX_TURNS:-50}"
|
|
||||||
MAX_BUDGET_USD="${TREKEXECUTE_MAX_BUDGET_USD:-5}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The budget caps default to safe values for normal-length steps. Operators may
|
|
||||||
override for long runs:
|
|
||||||
`TREKEXECUTE_MAX_TURNS=120 TREKEXECUTE_MAX_BUDGET_USD=20 /trekexecute --project ...`
|
|
||||||
|
|
||||||
**2a. Create worktrees for this wave's sessions:**
|
**2a. Create worktrees for this wave's sessions:**
|
||||||
|
|
||||||
For each session N in this wave:
|
For each session N in this wave:
|
||||||
|
|
@ -623,11 +582,11 @@ plugin-in-monorepo + gitignored-state topology):**
|
||||||
|
|
||||||
`git worktree add` clones HEAD only. The plugin's `.gitignore` excludes
|
`git worktree add` clones HEAD only. The plugin's `.gitignore` excludes
|
||||||
`.claude/projects/`, so the project directory (brief.md, plan.md, research/)
|
`.claude/projects/`, so the project directory (brief.md, plan.md, research/)
|
||||||
is missing in the freshly-created worktree. Each child session needs to read
|
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.
|
`brief.md`, `plan.md`, and `research/*` from `${PROJECT_DIR}` to do real work.
|
||||||
|
|
||||||
Insert this block AFTER the worktree-creation loop and BEFORE wave dispatch
|
Insert this block AFTER the worktree-creation loop and BEFORE the orders are
|
||||||
(Step 2b):
|
written (Step 2):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PROJECT_SOURCE="$(realpath "${PROJECT_DIR}")"
|
PROJECT_SOURCE="$(realpath "${PROJECT_DIR}")"
|
||||||
|
|
@ -658,7 +617,7 @@ 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
|
is GNU coreutils only; BSD `realpath` (the macOS default) rejects it, and the
|
||||||
failure is **silent** — the command substitution leaves `PROJECT_REL` empty, so
|
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`
|
`mkdir -p "$wt/"` and `cp ... "$wt//"` both succeed and drop `brief.md`/`plan.md`
|
||||||
at the worktree root, where no child session looks for them. Measured on an
|
at the worktree root, where no session looks for them. Measured on an
|
||||||
Intel Mac 2026-08-31: `realpath --relative-to=... ` → `realpath: illegal option
|
Intel Mac 2026-08-31: `realpath --relative-to=... ` → `realpath: illegal option
|
||||||
-- -`, while bare `realpath <path>` works; a whole wave ran with zero steps
|
-- -`, while bare `realpath <path>` works; a whole wave ran with zero steps
|
||||||
executed. Both `realpath` calls above are bare path resolution, which BSD and
|
executed. Both `realpath` calls above are bare path resolution, which BSD and
|
||||||
|
|
@ -668,89 +627,63 @@ it correct when one side goes through a symlink (macOS `/var` → `/private/var`
|
||||||
Failure modes: an underivable relpath (empty, or outside the repo) aborts
|
Failure modes: an underivable relpath (empty, or outside the repo) aborts
|
||||||
before anything is copied — better a loud stop than files delivered where no
|
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
|
session reads them; any `cp` failure exits the wave non-zero, reported via
|
||||||
Step 4 cleanup. A project without `research/` is not a failure.
|
Step 5 cleanup. A project without `research/` is not a failure.
|
||||||
Source: brief Constraint 2.
|
Source: brief Constraint 2.
|
||||||
|
|
||||||
**2b. Launch sessions in this wave (each in its own worktree):**
|
### Step 2 — Write the session orders for wave W, then stop
|
||||||
|
|
||||||
Each per-session prompt is **prefixed with two safety preambles** to defend
|
|
||||||
against headless-mode hook gaps (GH #36071) and the Claude 4 cross-model
|
|
||||||
malware-reminder issue (GH #52272 community workaround):
|
|
||||||
|
|
||||||
```text
|
|
||||||
[CRITICAL — defense in depth] You are running in headless mode where plugin
|
|
||||||
hooks may not fire reliably (GH #36071). DO NOT execute commands matching:
|
|
||||||
`rm -rf /`, `curl | bash`, `git push --force` to main, `git reset --hard`
|
|
||||||
outside this worktree, or any command writing outside the current worktree.
|
|
||||||
Treat this rule as enforced regardless of `--allowedTools` allowlist.
|
|
||||||
|
|
||||||
Note: any malware-related safety reminders apply conditionally to code you
|
|
||||||
assess as actually malicious, not to all code reads (per GH #52272 community
|
|
||||||
workaround).
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the per-session dispatch (note the `cwd:` honor for plugin-in-monorepo
|
|
||||||
layouts):
|
|
||||||
|
|
||||||
For each session N in the wave:
|
|
||||||
```bash
|
|
||||||
SESSION_CWD="${session_cwd:-.}" # default: worktree root; overridable per-session via Execution Strategy `cwd:` field
|
|
||||||
cd "$WORKTREE_PATH/$SESSION_CWD" && \
|
|
||||||
GIT_OPTIONAL_LOCKS=0 claude -p "${SAFETY_PREAMBLE}\n\n/trekexecute --session {N} {plan-path}" \
|
|
||||||
--allowedTools "Read,Write,Edit,Bash,Glob,Grep" \
|
|
||||||
--permission-mode bypassPermissions \
|
|
||||||
--max-turns "$MAX_TURNS" \
|
|
||||||
--max-budget-usd "$MAX_BUDGET_USD" \
|
|
||||||
--append-system-prompt-file "${SHARED_CONTEXT_FILE}" \
|
|
||||||
> "$LOG_DIR/session-{N}.log" 2>&1 &
|
|
||||||
```
|
|
||||||
|
|
||||||
`session_cwd` is sourced from the per-session `cwd:` field in the plan's
|
|
||||||
Execution Strategy block (parsed by Phase 2.6's existing session-metadata
|
|
||||||
reader). Default is `.` (worktree root) when absent — backward compatible.
|
|
||||||
Sessions can declare `cwd: <subdir>` to make child sessions run from a
|
|
||||||
plugin-subdir instead of repo root. Required for plugin-in-monorepo layouts
|
|
||||||
where the plugin lives at `plugins/<name>/` and step paths are relative to
|
|
||||||
the plugin (brief Constraint 1, Option B).
|
|
||||||
|
|
||||||
Key rules:
|
|
||||||
- `$WORKTREE_PATH` is the absolute path to the session's worktree
|
|
||||||
- `$LOG_DIR` is an absolute path in the main worktree (NOT inside the session worktree)
|
|
||||||
- `{plan-path}` is the same relative path — it works because the worktree has
|
|
||||||
the same repo content from HEAD
|
|
||||||
- `${SHARED_CONTEXT_FILE}` is absolute (from Step 2a-pre) so child sessions
|
|
||||||
can resolve it after `cd`
|
|
||||||
- If the wave has only 1 session, run without `&` (no background needed)
|
|
||||||
- Track PIDs for parallel sessions
|
|
||||||
- `--max-turns` + `--max-budget-usd` are guardrails (research/06 R3 + R4)
|
|
||||||
- `--append-system-prompt-file` injects shared brief + architecture context
|
|
||||||
into every child without bloating the per-session prompt
|
|
||||||
|
|
||||||
**2c. Wait for wave completion:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wait {PID1} {PID2} ...
|
node "$VOYAGE_ROOT/lib/util/session-orders.mjs" write \
|
||||||
|
--plan "{plan-path}" --wave "{W}" --session-dir "$SESSION_DIR"
|
||||||
```
|
```
|
||||||
|
|
||||||
**2d. Check results after each wave:**
|
`{plan-path}` is repo-relative (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
|
||||||
|
and `$ORDERS_DIR/wave.json` (`status: awaiting-sessions`), and prints, per
|
||||||
|
session, the directory to open and the starter line
|
||||||
|
(`/trekexecute --session {N} {plan-path}`). 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.
|
||||||
|
|
||||||
For each session in the wave, read its log file (in `$LOG_DIR`, always accessible
|
Show the tool's output to the user verbatim, then report and **stop**:
|
||||||
from the main worktree) and grep for `"trekexecute_summary"`. Parse the JSON to
|
|
||||||
determine:
|
|
||||||
- Did the session complete? (`result: "completed"`)
|
|
||||||
- Did it fail? (`result: "failed"` or `"stopped"`)
|
|
||||||
|
|
||||||
If ANY session in the wave failed:
|
|
||||||
```
|
```
|
||||||
Wave {W} FAILED: Session {N} failed at step {S}.
|
Wave {W}: {N} session order(s) written in {ORDERS_DIR}. Awaiting sessions.
|
||||||
Stopping — later waves depend on this wave.
|
Open each session yourself in the directory its order names and give it the
|
||||||
See log: {LOG_DIR}/session-{N}.log
|
starter line. When every session is done, run this same /trekexecute command
|
||||||
|
again here to merge the wave.
|
||||||
```
|
```
|
||||||
Do NOT merge. Do NOT start later waves. Jump to Step 4 (cleanup), then Phase 8.
|
|
||||||
|
|
||||||
If all sessions in the wave passed: continue to Step 2e.
|
This run ends here. It writes no Phase 8 report: the plan is not finished, and
|
||||||
|
the orders are the hand-over.
|
||||||
|
|
||||||
**2e. Merge session branches back (SEQUENTIAL, one at a time):**
|
### Step 3 — Collect wave W (a later run)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node "$VOYAGE_ROOT/lib/util/session-orders.mjs" status \
|
||||||
|
--plan "{plan-path}" --wave "{W}" --session-dir "$SESSION_DIR"
|
||||||
|
```
|
||||||
|
|
||||||
|
The JSON names each session's progress file (the file `--session N` writes,
|
||||||
|
Phase 3) and its `status`: `completed`, `failed`, `stopped`, `pending` (no
|
||||||
|
progress file yet) or `unreadable`.
|
||||||
|
|
||||||
|
- **Any `pending`:** the wave is still running. Report which sessions are
|
||||||
|
pending, point at their order files (`$ORDERS_DIR/wave-{W}-session-{N}.md`),
|
||||||
|
and stop. Do not merge, do not clean up.
|
||||||
|
- **Any `failed`, `stopped` or `unreadable`:**
|
||||||
|
```
|
||||||
|
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), 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:
|
Return to the main worktree:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -781,13 +714,13 @@ Wave {W} MERGE CONFLICT: Session {N} branch conflicts with merged state.
|
||||||
Conflicting files:
|
Conflicting files:
|
||||||
{CONFLICTS}
|
{CONFLICTS}
|
||||||
|
|
||||||
Session {N} log: {LOG_DIR}/session-{N}.log
|
Session {N} order: {ORDERS_DIR}/wave-{W}-session-{N}.md
|
||||||
Aborting further merges. Sessions already merged in this wave are preserved.
|
Aborting further merges. Sessions already merged in this wave are preserved.
|
||||||
```
|
```
|
||||||
|
|
||||||
Mark remaining sessions as "merge-failed". Jump to Step 4 (cleanup), then Phase 8.
|
Mark remaining sessions as "merge-failed". Jump to Step 5 (cleanup), then Phase 8.
|
||||||
|
|
||||||
**2f. Remove worktrees for completed wave:**
|
**Remove worktrees for the merged wave:**
|
||||||
|
|
||||||
After successful merge of all sessions in the wave:
|
After successful merge of all sessions in the wave:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -796,21 +729,26 @@ for each session N in the wave:
|
||||||
GIT_OPTIONAL_LOCKS=0 git branch -d "trek/{slug}/session-{N}"
|
GIT_OPTIONAL_LOCKS=0 git branch -d "trek/{slug}/session-{N}"
|
||||||
done
|
done
|
||||||
GIT_OPTIONAL_LOCKS=0 git worktree prune
|
GIT_OPTIONAL_LOCKS=0 git worktree prune
|
||||||
|
rm -f "$ORDERS_DIR/wave.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
Report: `Wave {W}: {N} sessions merged, worktrees cleaned up.`
|
Report: `Wave {W}: {N} sessions merged, worktrees cleaned up.`
|
||||||
|
|
||||||
Continue to the next wave.
|
**Next wave:** if the Execution Strategy has a later wave, set `W` to it and go
|
||||||
|
back to Step 1 — this run prepares that wave's orders and stops again. If wave
|
||||||
|
W was the last, continue to Step 4.
|
||||||
|
|
||||||
### Step 3 — Run master verification
|
### Step 4 — Run master verification
|
||||||
|
|
||||||
After all waves complete and merge successfully, run the plan's `## Verification`
|
After all waves complete and merge successfully, run the plan's `## Verification`
|
||||||
section commands to verify the integrated result.
|
section commands to verify the integrated result.
|
||||||
|
|
||||||
### Step 4 — Cleanup (ALWAYS runs, even on failure)
|
### Step 5 — Cleanup (ALWAYS runs when the run ends, even on failure)
|
||||||
|
|
||||||
This step MUST execute regardless of how Step 2 exited — success, failure, or
|
This step MUST execute whenever this phase ends the run with a failure, a merge
|
||||||
merge conflict. It is the worktree equivalent of a `finally` block.
|
conflict, or after the last wave — never while a wave is awaiting its sessions
|
||||||
|
(Step 2 and a `pending` Step 3 stop without it). It is the worktree equivalent
|
||||||
|
of a `finally` block.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
|
@ -826,7 +764,8 @@ GIT_OPTIONAL_LOCKS=0 git branch --list "trek/{slug}/*" | while read branch; do
|
||||||
GIT_OPTIONAL_LOCKS=0 git branch -D "$branch" 2>/dev/null
|
GIT_OPTIONAL_LOCKS=0 git branch -D "$branch" 2>/dev/null
|
||||||
done
|
done
|
||||||
|
|
||||||
# Clean up empty directories
|
# Clean up empty directories and the wave marker
|
||||||
|
rm -f "$ORDERS_DIR/wave.json"
|
||||||
rmdir "$WORKTREE_DIR" 2>/dev/null
|
rmdir "$WORKTREE_DIR" 2>/dev/null
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -841,9 +780,9 @@ Warning: failed to remove worktree {path}. Manual cleanup:
|
||||||
git worktree remove {path} --force && git worktree prune
|
git worktree remove {path} --force && git worktree prune
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5 — Aggregate results
|
### Step 6 — Aggregate results
|
||||||
|
|
||||||
Collect all session summaries into an aggregated report. Jump to Phase 8.
|
Collect every session's progress file into an aggregated report. Jump to Phase 8.
|
||||||
|
|
||||||
### --session N mode
|
### --session N mode
|
||||||
|
|
||||||
|
|
@ -855,12 +794,13 @@ When mode = `session N`:
|
||||||
5. Enforce the session's scope fence as if it were a session spec's scope fence
|
5. Enforce the session's scope fence as if it were a session spec's scope fence
|
||||||
6. Report results for those steps only
|
6. Report results for those steps only
|
||||||
|
|
||||||
This mode is used internally by Phase 2.6 when launching child sessions.
|
This mode is what each session order's starter runs, in a session the user
|
||||||
It can also be used manually to re-run a specific session.
|
opened in that order's worktree. It can also be used manually to re-run a
|
||||||
|
specific session.
|
||||||
|
|
||||||
When `--session N` is invoked inside a git worktree (as done by Phase 2.6), all
|
When `--session N` runs inside a git worktree (as each session order sets up),
|
||||||
git operations (add, commit) apply to the worktree's branch. The session does not
|
all git operations (add, commit) apply to the worktree's branch. The session does
|
||||||
need to know it is in a worktree — git handles this transparently.
|
not need to know it is in a worktree — git handles this transparently.
|
||||||
|
|
||||||
## Phase 3 — Progress file setup
|
## Phase 3 — Progress file setup
|
||||||
|
|
||||||
|
|
@ -1010,16 +950,11 @@ If `has_execution_strategy = true`, show a preview of multi-session orchestratio
|
||||||
```
|
```
|
||||||
**Sessions:** {S} across {W} waves
|
**Sessions:** {S} across {W} waves
|
||||||
|
|
||||||
| Wave | Session | Steps | Depends on | Command |
|
| Wave | Session | Steps | Depends on | Starter (the user runs it in a session they open) |
|
||||||
|------|---------|-------|------------|---------|
|
|------|---------|-------|------------|---------|
|
||||||
| 1 | Session 1: {title} | {nums} | none | `claude -p "/trekexecute --session 1 {path}"` |
|
| 1 | Session 1: {title} | {nums} | none | `/trekexecute --session 1 {path}` |
|
||||||
| 1 | Session 2: {title} | {nums} | none | `claude -p "/trekexecute --session 2 {path}"` |
|
| 1 | Session 2: {title} | {nums} | none | `/trekexecute --session 2 {path}` |
|
||||||
| 2 | Session 3: {title} | {nums} | S1, S2 | `claude -p "/trekexecute --session 3 {path}"` |
|
| 2 | Session 3: {title} | {nums} | S1, S2 | `/trekexecute --session 3 {path}` |
|
||||||
```
|
|
||||||
|
|
||||||
Check billing status via `echo "${ANTHROPIC_API_KEY:+SET}"` and report:
|
|
||||||
```
|
|
||||||
Billing: ANTHROPIC_API_KEY is {SET — parallel sessions will bill API account | NOT SET — sessions will use subscription}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Verdict
|
### Verdict
|
||||||
|
|
@ -1245,7 +1180,7 @@ depends on what was executed:
|
||||||
Phase 4's entry-condition skip for trekplans stands — a plan carries no entry
|
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`
|
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
|
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 3). A single-session run therefore
|
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
|
reported `completed` without ever executing the criteria it was measured
|
||||||
against. It no longer does.
|
against. It no longer does.
|
||||||
|
|
||||||
|
|
@ -1443,17 +1378,20 @@ the user for manual resolution.
|
||||||
4. Prepend the synthetic Step 0 pre-flight (same as normal session specs).
|
4. Prepend the synthetic Step 0 pre-flight (same as normal session specs).
|
||||||
5. Set `recovery_dispatched = true` in parent progress file.
|
5. Set `recovery_dispatched = true` in parent progress file.
|
||||||
|
|
||||||
**Invoke the recovery session:**
|
**Hand the recovery session over as an order (Voyage does not start it):**
|
||||||
|
|
||||||
```bash
|
Report the recovery spec as a session order and stop the run as `partial`:
|
||||||
cd "$WORKTREE_PATH" && claude -p "/trekexecute --session {N} {recovery spec path}" \
|
|
||||||
--allowedTools "Read,Write,Edit,Bash,Glob,Grep" \
|
```
|
||||||
--permission-mode bypassPermissions \
|
Recovery order: session {N}, depth {depth}
|
||||||
> "$LOG_DIR/session-{N}-recovery-{depth}.log" 2>&1
|
Open a Claude Code session yourself in {WORKTREE_PATH} and give it:
|
||||||
|
/trekexecute --session {N} {recovery spec path}
|
||||||
|
When it is done, run this same /trekexecute command again: it re-runs the
|
||||||
|
Phase 7.5 audit.
|
||||||
```
|
```
|
||||||
|
|
||||||
Wait for the recovery session to complete. After it returns, re-run Phase
|
On that later run, re-run the Phase 7.5 audit one more time. If it still
|
||||||
7.5 audit one more time. If it still drifts at `recovery_depth=2`:
|
drifts at `recovery_depth=2`:
|
||||||
|
|
||||||
```
|
```
|
||||||
RECOVERY EXHAUSTED: session {N} drifted after 2 recovery attempts.
|
RECOVERY EXHAUSTED: session {N} drifted after 2 recovery attempts.
|
||||||
|
|
@ -1466,8 +1404,7 @@ Do NOT dispatch a third recovery. Report to the user.
|
||||||
### Iteration caps + global recovery/retry budget
|
### Iteration caps + global recovery/retry budget
|
||||||
|
|
||||||
Every recovery/retry loop in this executor has a documented numeric bound.
|
Every recovery/retry loop in this executor has a documented numeric bound.
|
||||||
These are **three distinct axes** — not one nested counter — plus a separate
|
These are **three distinct axes** — not one nested counter:
|
||||||
child turn cap:
|
|
||||||
|
|
||||||
- **Per-step retry cap** — `attempts ≤ 3` (initial + **maximum 2 retries**;
|
- **Per-step retry cap** — `attempts ≤ 3` (initial + **maximum 2 retries**;
|
||||||
Hard Rule 5 / Sub-step E). Bounds the retries of a *single* step.
|
Hard Rule 5 / Sub-step E). Bounds the retries of a *single* step.
|
||||||
|
|
@ -1482,14 +1419,10 @@ child turn cap:
|
||||||
enforced via the `iterations_remaining` counter (Phase 3 schema; decremented
|
enforced via the `iterations_remaining` counter (Phase 3 schema; decremented
|
||||||
on every recovery/retry iteration; reconciled at the Phase 7.5 completion
|
on every recovery/retry iteration; reconciled at the Phase 7.5 completion
|
||||||
gate).
|
gate).
|
||||||
- **Child turn cap** — `--max-turns` (`TREKEXECUTE_MAX_TURNS`, default 50)
|
|
||||||
bounds model turns inside a single `-p` child; a *different* axis from the
|
|
||||||
three above.
|
|
||||||
|
|
||||||
Default **25** is an independent value from the cap research (practitioner range
|
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
|
10–50; OWASP Agentic ASI08 financial-DoS), tuned down from the research ~50
|
||||||
starting point for a tighter backstop. It is **not** a reconciliation with
|
starting point for a tighter backstop. Operators may override:
|
||||||
`--max-turns` (50) — the axes are incomparable. Operators may override:
|
|
||||||
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS=10 /trekexecute --project ...`. The
|
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS=10 /trekexecute --project ...`. The
|
||||||
`effort=='low'` single-foreground-loop path and the wave path are both subject to
|
`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
|
this aggregate ceiling; it is an execution-spec ceiling (enforced via the
|
||||||
|
|
@ -1703,7 +1636,7 @@ To resume: /trekexecute --resume {path}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `trekexecute_summary` key makes it grep-able in log files from headless runs.
|
The `trekexecute_summary` key makes it grep-able in a session's output.
|
||||||
|
|
||||||
## Phase 9 — Stats tracking
|
## Phase 9 — Stats tracking
|
||||||
|
|
||||||
|
|
@ -1823,8 +1756,7 @@ code-path.
|
||||||
|
|
||||||
1. **No AskUserQuestion for execution decisions.** All execution decisions come
|
1. **No AskUserQuestion for execution decisions.** All execution decisions come
|
||||||
from the plan's On failure clauses. If the plan says escalate, stop and
|
from the plan's On failure clauses. If the plan says escalate, stop and
|
||||||
report — never ask. **Exception:** the billing safety check in Phase 2.6
|
report — never ask.
|
||||||
Step 0 MUST ask before spending money on the user's API account.
|
|
||||||
|
|
||||||
2. **No scope creep.** Only touch files listed in the step's `Files:` field.
|
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
|
If a file outside the list seems to need changing, record it as a finding
|
||||||
|
|
@ -1855,9 +1787,9 @@ code-path.
|
||||||
which removes both tools from the pool — `allowed-tools` alone does not,
|
which removes both tools from the pool — `allowed-tools` alone does not,
|
||||||
since omission leaves a tool callable (CC 2.1.152).
|
since omission leaves a tool callable (CC 2.1.152).
|
||||||
|
|
||||||
11. **Worktree isolation is mandatory for parallel execution.** Every parallel
|
11. **Worktree isolation is mandatory for multi-session execution.** Every
|
||||||
`claude -p` session MUST run in its own git worktree. Never launch two or
|
session order names its own git worktree. Never run two or more sessions
|
||||||
more sessions in the same working directory. This rule has no exceptions.
|
in the same working directory. This rule has no exceptions.
|
||||||
Sequential (single-session) execution does not require worktrees.
|
Sequential (single-session) execution does not require worktrees.
|
||||||
|
|
||||||
12. **Worktree cleanup is unconditional.** Before producing the final report
|
12. **Worktree cleanup is unconditional.** Before producing the final report
|
||||||
|
|
@ -1921,6 +1853,11 @@ code-path.
|
||||||
20. **Three distinct iteration axes.** Per-step `attempts` (≤3), per-session
|
20. **Three distinct iteration axes.** Per-step `attempts` (≤3), per-session
|
||||||
`recovery_depth` (<2), and the global `TREKEXECUTE_MAX_RECOVERY_ITERATIONS`
|
`recovery_depth` (<2), and the global `TREKEXECUTE_MAX_RECOVERY_ITERATIONS`
|
||||||
budget (default 25 — aggregate recovery+retry backstop) are independent
|
budget (default 25 — aggregate recovery+retry backstop) are independent
|
||||||
bounds, not one nested counter; child `--max-turns` (50) is a fourth,
|
bounds, not one nested counter. Every recovery/retry loop is bounded by at
|
||||||
separate axis. Every recovery/retry loop is bounded by at least one of them.
|
least one of them. See "Iteration caps + global recovery/retry budget".
|
||||||
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).
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ The session-decomposer will:
|
||||||
4. Identify which sessions can run in parallel (waves)
|
4. Identify which sessions can run in parallel (waves)
|
||||||
5. Generate one session spec file per session
|
5. Generate one session spec file per session
|
||||||
6. Generate a dependency diagram (mermaid)
|
6. Generate a dependency diagram (mermaid)
|
||||||
7. Generate a launch script (`launch.sh`)
|
7. Write the session orders (`orders.md`): one starter line per session, wave by wave
|
||||||
|
|
||||||
When the session-decomposer completes, present the summary to the user:
|
When the session-decomposer completes, present the summary to the user:
|
||||||
|
|
||||||
|
|
@ -318,16 +318,16 @@ When the session-decomposer completes, present the summary to the user:
|
||||||
|
|
||||||
- Session specs: .claude/trekplan-sessions/{slug}/session-*.md
|
- Session specs: .claude/trekplan-sessions/{slug}/session-*.md
|
||||||
- Dependency graph: .claude/trekplan-sessions/{slug}/dependency-graph.md
|
- Dependency graph: .claude/trekplan-sessions/{slug}/dependency-graph.md
|
||||||
- Launch script: .claude/trekplan-sessions/{slug}/launch.sh
|
- Session orders: .claude/trekplan-sessions/{slug}/orders.md
|
||||||
|
|
||||||
You can:
|
You can:
|
||||||
- Review individual session specs before running
|
- Review individual session specs before running
|
||||||
- Run all sessions: `bash .claude/trekplan-sessions/{slug}/launch.sh`
|
- Run the sessions yourself, wave by wave: open a Claude Code session per spec
|
||||||
- Run a single session: `claude -p "$(cat .claude/trekplan-sessions/{slug}/session-1-*.md)"`
|
and give it the starter line from orders.md (`/trekexecute <spec>`). Run the
|
||||||
- Say **"launch"** to start headless execution from here
|
sessions of one wave in parallel only in separate git worktrees.
|
||||||
```
|
```
|
||||||
|
|
||||||
If the user says **"launch"**: run the launch script via Bash.
|
Voyage does not start these sessions.
|
||||||
|
|
||||||
Then **stop**. Do not continue to any subsequent phase.
|
Then **stop**. Do not continue to any subsequent phase.
|
||||||
|
|
||||||
|
|
@ -372,8 +372,7 @@ Then continue to the next phase inline.
|
||||||
> can spawn sub-agents (up to 5 levels deep), so that block no longer holds —
|
> can spawn sub-agents (up to 5 levels deep), so that block no longer holds —
|
||||||
> a delegated redesign is under evaluation (see
|
> a delegated redesign is under evaluation (see
|
||||||
> `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26). Until then, running
|
> `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26). Until then, running
|
||||||
> the phases inline in main context keeps the swarm intact. Use `claude -p`
|
> the phases inline in main context keeps the swarm intact.
|
||||||
> in a separate terminal window for long-running headless work.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,8 +245,7 @@ Then continue to the next phase inline.
|
||||||
> deep), so that block no longer holds — a delegated redesign is under
|
> deep), so that block no longer holds — a delegated redesign is under
|
||||||
> evaluation (see `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26).
|
> evaluation (see `docs/cc-upgrade-2.1.181-decision-matrix.md`, W1/CC-26).
|
||||||
> Until then, running the phases inline in main context keeps the swarm
|
> Until then, running the phases inline in main context keeps the swarm
|
||||||
> intact. Use `claude -p` in a separate terminal window for long-running
|
> intact.
|
||||||
> headless work.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -331,26 +330,21 @@ changes nothing (SC1). Keep the native-swarm anchors intact ("in parallel",
|
||||||
|
|
||||||
**engine = deep-research:**
|
**engine = deep-research:**
|
||||||
|
|
||||||
1. **Coarse pre-gate (best-effort, NOT a trust signal).** `Bash: claude --version`;
|
1. **Coarse pre-gate (best-effort, NOT a trust signal).** The supported window is
|
||||||
parse the leading `X.Y.Z` (e.g. from `2.1.196 (Claude Code)`) and compare
|
`2.1.154 <= version < 2.1.218`: below `2.1.154` there are no dynamic workflows,
|
||||||
numerically against **both ends** of the supported window
|
and from `2.1.218` (the ceiling) `/deep-research` starts only when the operator
|
||||||
`2.1.154 <= version < 2.1.218` — split each on `.` and compare major, then minor,
|
invokes it, and the Skill tool refuses a model invocation with
|
||||||
then patch as integers (do NOT string-compare; lexical comparison mis-orders
|
`disable-model-invocation` — see step 4's reason token. Voyage does not run the
|
||||||
multi-digit patch numbers). If the version is `< 2.1.154` (below the
|
Claude Code binary to read its version (it never calls Claude Code), so the
|
||||||
dynamic-workflows floor), OR `>= 2.1.218` (the ceiling: `/deep-research` starts
|
version is *indeterminate* here. If `disableWorkflows: true` /
|
||||||
only when the operator invokes it, and the Skill tool refuses a model invocation
|
`CLAUDE_CODE_DISABLE_WORKFLOWS=1` is set, skip to the fallback (step 4);
|
||||||
with `disable-model-invocation` — see step 4's reason token), OR if
|
otherwise proceed to step 2 — do NOT hard-fail. There is no positive
|
||||||
`disableWorkflows: true` / `CLAUDE_CODE_DISABLE_WORKFLOWS=1` is set, skip to the
|
|
||||||
fallback (step 4). **If `claude` is not on PATH inside the Bash tool (possible
|
|
||||||
under `claude -p`) or the version cannot be parsed, treat the pre-gate as
|
|
||||||
*indeterminate* and proceed to step 2 — do NOT hard-fail.** There is no positive
|
|
||||||
availability probe (research Dim 4), so a passing pre-gate does not guarantee the
|
availability probe (research Dim 4), so a passing pre-gate does not guarantee the
|
||||||
workflow runs; the post-hoc check (step 3) is the authoritative guard.
|
workflow runs; the post-hoc check (step 3) is the authoritative guard.
|
||||||
|
|
||||||
2. **Run.** Instruct Claude (in prose, this turn) to run
|
2. **Run.** Instruct Claude (in prose, this turn) to run
|
||||||
`/deep-research <research question>` and request per-claim citations. Note:
|
`/deep-research <research question>` and request per-claim citations. Note:
|
||||||
interactive default/acceptEdits triggers a per-run approval prompt; `claude -p` /
|
interactive default/acceptEdits triggers a per-run approval prompt.
|
||||||
SDK / bypass runs immediately.
|
|
||||||
|
|
||||||
3. **Post-hoc presence + provenance check (the real guard).** Verify a real, cited
|
3. **Post-hoc presence + provenance check (the real guard).** Verify a real, cited
|
||||||
`/deep-research` report actually landed in context — substantive findings with
|
`/deep-research` report actually landed in context — substantive findings with
|
||||||
|
|
@ -366,8 +360,8 @@ changes nothing (SC1). Keep the native-swarm anchors intact ("in parallel",
|
||||||
`Engine: deep-research → swarm (fallback: <reason>)` and carry the reason into the
|
`Engine: deep-research → swarm (fallback: <reason>)` and carry the reason into the
|
||||||
Phase-8 Present summary and the brief's `## Executive Summary`. Known reason
|
Phase-8 Present summary and the brief's `## Executive Summary`. Known reason
|
||||||
tokens: `disable-model-invocation` (CC >= 2.1.218 — the expected reason on any
|
tokens: `disable-model-invocation` (CC >= 2.1.218 — the expected reason on any
|
||||||
current Claude Code), `version-below-floor`, `workflows-disabled`,
|
current Claude Code), `workflows-disabled`, `no-cited-report` (also what a
|
||||||
`no-cited-report`. **NEVER fabricate
|
Claude Code below the `2.1.154` floor produces, since no version is read). **NEVER fabricate
|
||||||
or synthesize a substitute report** — a structurally-valid-but-invented brief
|
or synthesize a substitute report** — a structurally-valid-but-invented brief
|
||||||
passes the structure-only validator and silently poisons `/trekplan`; that is the
|
passes the structure-only validator and silently poisons `/trekplan`; that is the
|
||||||
worst outcome of this feature.
|
worst outcome of this feature.
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ Doc-consistency test at `tests/lib/doc-consistency.test.mjs` pins agent-table co
|
||||||
|
|
||||||
**Decompose:** Parse plan → Analyze step dependencies → Group into sessions → Identify parallel waves → Generate session specs + dependency graph + launch script.
|
**Decompose:** Parse plan → Analyze step dependencies → Group into sessions → Identify parallel waves → Generate session specs + dependency graph + launch script.
|
||||||
|
|
||||||
**Execute:** Parse plan → Security scan (Phase 2.4) → Detect Execution Strategy → Single-session (step loop) or multi-session (parallel waves via `claude -p` with scoped `--allowedTools`) → Phase 7.5 manifest audit → Phase 7.6 bounded recovery (if partial) → Phase 8 atomically writes `progress.json` + `.session-state.local.json` (Handover 7) → Report. With `--project`, reads `{dir}/plan.md`. Phase 2.55 (pre-flight stop) and Phase 4 (entry-condition stop) also write `.session-state.local.json` so `/trekcontinue` can surface the stop and prompt for next steps.
|
**Execute:** Parse plan → Security scan (Phase 2.4) → Detect Execution Strategy → Single-session (step loop) or multi-session (one wave at a time: a worktree plus a session order and starter per session; the user opens every session, and a re-run merges the wave — Voyage never starts a session) → Phase 7.5 manifest audit → Phase 7.6 bounded recovery (if partial) → Phase 8 atomically writes `progress.json` + `.session-state.local.json` (Handover 7) → Report. With `--project`, reads `{dir}/plan.md`. Phase 2.55 (pre-flight stop) and Phase 4 (entry-condition stop) also write `.session-state.local.json` so `/trekcontinue` can surface the stop and prompt for next steps.
|
||||||
|
|
||||||
**Continue:** `/trekcontinue` reads `{dir}/.session-state.local.json` (Handover 7), validates schema-v1 via `session-state-validator`, narrates a 3-line summary (project / next-session-label / brief-path), and immediately begins executing the next session. Auto-discovers active project state files under `.claude/projects/*/.session-state.local.json` if no explicit `<project-dir>` argument. Operator-invoked only — never auto-loaded via SessionStart. The `/trekendsession` helper is the informal-flow producer: writes the same state file for ad-hoc multi-session handovers that don't run through `/trekexecute`.
|
**Continue:** `/trekcontinue` reads `{dir}/.session-state.local.json` (Handover 7), validates schema-v1 via `session-state-validator`, narrates a 3-line summary (project / next-session-label / brief-path), and immediately begins executing the next session. Auto-discovers active project state files under `.claude/projects/*/.session-state.local.json` if no explicit `<project-dir>` argument. Operator-invoked only — never auto-loaded via SessionStart. The `/trekendsession` helper is the informal-flow producer: writes the same state file for ad-hoc multi-session handovers that don't run through `/trekexecute`.
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ Doc-consistency test at `tests/lib/doc-consistency.test.mjs` pins agent-table co
|
||||||
|
|
||||||
**Operator-annotation HTML (v5.0.3):** the last step of `/trekbrief`, `/trekplan`, and `/trekreview` runs `scripts/annotate.mjs` against the just-written `.md` and prints the resulting `file://<abs path>` link. The HTML is self-contained (zero npm deps, zero external network, design-system-styled, light + dark + print) and modelled on an existing annotation surface the operator pointed at. The operator opens the file, the document renders as a proper article (headings / paragraphs / lists / tables / code / quotes — every element gets a stable `data-anchor-id`). In annotation mode (default ON, pencil-toggle in topbar), the operator can **select any text or click any element** → a form popover opens at the cursor with: section context auto-detected from nearest h1/h2, the anchored snippet (selection if any, else element text), **three intent buttons (Fiks / Endre / Spørsmål)**, comment textarea, Save/Cancel. The sidebar (Show annotations button) lists every annotation grouped by section with intent badge + snippet + comment + delete; clicking a card scrolls to and flashes the source element. **Copy Prompt** assembles a structured markdown (`### N. [Intent] Section: <…>` + `Quote: «…»` + `Comment: …`) and copies to clipboard. Persistence: `localStorage` keyed on absolute artifact path (`voyage-annotate:v2:<abs path>`). v5.0.0 removed the v4.2/v4.3 bespoke playground SPA + `/trekrevise` + Handover 8; v5.0.1 pointed at `/playground document-critique` (Claude-leads, wrong direction); v5.0.2 was operator-led but too thin (line-click + freeform note, no intents); v5.0.3 matches the annotation reference the operator first pointed at, with pencil-toggle / selection capture / intent categories / popover form / structured export. See [CHANGELOG.md](../CHANGELOG.md) § v5.0.3.
|
**Operator-annotation HTML (v5.0.3):** the last step of `/trekbrief`, `/trekplan`, and `/trekreview` runs `scripts/annotate.mjs` against the just-written `.md` and prints the resulting `file://<abs path>` link. The HTML is self-contained (zero npm deps, zero external network, design-system-styled, light + dark + print) and modelled on an existing annotation surface the operator pointed at. The operator opens the file, the document renders as a proper article (headings / paragraphs / lists / tables / code / quotes — every element gets a stable `data-anchor-id`). In annotation mode (default ON, pencil-toggle in topbar), the operator can **select any text or click any element** → a form popover opens at the cursor with: section context auto-detected from nearest h1/h2, the anchored snippet (selection if any, else element text), **three intent buttons (Fiks / Endre / Spørsmål)**, comment textarea, Save/Cancel. The sidebar (Show annotations button) lists every annotation grouped by section with intent badge + snippet + comment + delete; clicking a card scrolls to and flashes the source element. **Copy Prompt** assembles a structured markdown (`### N. [Intent] Section: <…>` + `Quote: «…»` + `Comment: …`) and copies to clipboard. Persistence: `localStorage` keyed on absolute artifact path (`voyage-annotate:v2:<abs path>`). v5.0.0 removed the v4.2/v4.3 bespoke playground SPA + `/trekrevise` + Handover 8; v5.0.1 pointed at `/playground document-critique` (Claude-leads, wrong direction); v5.0.2 was operator-led but too thin (line-click + freeform note, no intents); v5.0.3 matches the annotation reference the operator first pointed at, with pencil-toggle / selection capture / intent categories / popover form / structured export. See [CHANGELOG.md](../CHANGELOG.md) § v5.0.3.
|
||||||
|
|
||||||
**Security:** 4-layer defense-in-depth: plugin hooks (pre-bash-executor, pre-write-executor), prompt-level denylist (works in headless sessions), pre-execution plan scan (Phase 2.4), scoped `--allowedTools` replacing `--dangerously-skip-permissions`. Hard Rules 14-16 enforce verify command security, repo-boundary writes, and sensitive path protection.
|
**Security:** 3-layer defense-in-depth: plugin hooks (pre-bash-executor, pre-write-executor), prompt-level denylist (holds even if a hook does not fire), pre-execution plan scan (Phase 2.4). Hard Rules 14-16 enforce verify command security, repo-boundary writes, and sensitive path protection.
|
||||||
|
|
||||||
**Pipeline:** `/trekbrief` produces the task brief. `/trekresearch --project <dir>` fills in `{dir}/research/`. `/trekplan --project <dir>` reads brief + research to produce `{dir}/plan.md` (and auto-discovers `{dir}/architecture/overview.md` if an opt-in upstream architect plugin produced one). `/trekexecute --project <dir>` executes and writes `{dir}/progress.json`. `/trekreview --project <dir>` produces `{dir}/review.md`. `/trekbrief`, `/trekplan`, and `/trekreview` each end by running `scripts/annotate.mjs` on the just-written artifact, producing `{dir}/{artifact}.html` — a self-contained operator-annotation surface — and printing the `file://` link. The operator opens it, clicks lines, writes their own notes, copies a structured prompt, pastes back, Claude revises the `.md`. All artifacts live in one project directory.
|
**Pipeline:** `/trekbrief` produces the task brief. `/trekresearch --project <dir>` fills in `{dir}/research/`. `/trekplan --project <dir>` reads brief + research to produce `{dir}/plan.md` (and auto-discovers `{dir}/architecture/overview.md` if an opt-in upstream architect plugin produced one). `/trekexecute --project <dir>` executes and writes `{dir}/progress.json`. `/trekreview --project <dir>` produces `{dir}/review.md`. `/trekbrief`, `/trekplan`, and `/trekreview` each end by running `scripts/annotate.mjs` on the just-written artifact, producing `{dir}/{artifact}.html` — a self-contained operator-annotation surface — and printing the `file://` link. The operator opens it, clicks lines, writes their own notes, copies a structured prompt, pastes back, Claude revises the `.md`. All artifacts live in one project directory.
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ Which native Claude Code primitive each pipeline step runs on today, and the alt
|
||||||
| **brief** | Inline interview loop → `brief.md` (framing gate) | `AskUserQuestion` as the Q&A engine (recommended, not yet wired); Skill/Workflow auto-orchestration (rejected — one-shot, can't span slash-command/handover boundaries) |
|
| **brief** | Inline interview loop → `brief.md` (framing gate) | `AskUserQuestion` as the Q&A engine (recommended, not yet wired); Skill/Workflow auto-orchestration (rejected — one-shot, can't span slash-command/handover boundaries) |
|
||||||
| **research** | Inline spawns a foreground swarm; MCP per agent¹ | Delegated `synthesis-agent` (PoC measured Δ main-context ≈ 0 → ships **dormant**, wired to nothing) |
|
| **research** | Inline spawns a foreground swarm; MCP per agent¹ | Delegated `synthesis-agent` (PoC measured Δ main-context ≈ 0 → ships **dormant**, wired to nothing) |
|
||||||
| **plan** | Inline: parallel explorer swarm → synthesis → adversarial `plan-critic` | Delegated orchestrator via nested sub-agents (CC-26 → lean NO, not wired; `planning-orchestrator.md` is a reference doc, not a spawnable agent) |
|
| **plan** | Inline: parallel explorer swarm → synthesis → adversarial `plan-critic` | Delegated orchestrator via nested sub-agents (CC-26 → lean NO, not wired; `planning-orchestrator.md` is a reference doc, not a spawnable agent) |
|
||||||
| **execute** | Inline step loop; multi-session via `git worktree` + `claude -p` waves; deterministic manifest audit | CC `TaskCreate`/`TodoWrite` for progress/resume (insufficient — carries no step status / attempts / SHA / drift → own typed `progress.json` contract) |
|
| **execute** | Inline step loop; multi-session via `git worktree` + session orders the user runs, wave by wave; deterministic manifest audit | CC `TaskCreate`/`TodoWrite` for progress/resume (insufficient — carries no step status / attempts / SHA / drift → own typed `progress.json` contract) |
|
||||||
| **review** | Inline parallel reviewers (no cross-feed) → `review-coordinator` Judge | **Workflow** substrate for Phase 5–6 (bake-off POSITIVE: +4.4 % tokens / +54 % wall-time → shipped **opt-in `--workflow`**, not default; wholesale substrate swap declined) |
|
| **review** | Inline parallel reviewers (no cross-feed) → `review-coordinator` Judge | **Workflow** substrate for Phase 5–6 (bake-off POSITIVE: +4.4 % tokens / +54 % wall-time → shipped **opt-in `--workflow`**, not default; wholesale substrate swap declined) |
|
||||||
| **continue** | Inline reads `.session-state.local.json` → zero-confirm resume | CC `--resume` (transcript replay, not typed work-state → insufficient) |
|
| **continue** | Inline reads `.session-state.local.json` → zero-confirm resume | CC `--resume` (transcript replay, not typed work-state → insufficient) |
|
||||||
| **cross-cutting** | 8 hook scripts: `pre-bash` + `pre-write` guards, `pre-agent-cap` loop-bound enforcement, `post-bash` stats, `session-title`, `pre-`/`post-compact` flush, **`Stop`→OTEL** export | — |
|
| **cross-cutting** | 8 hook scripts: `pre-bash` + `pre-write` guards, `pre-agent-cap` loop-bound enforcement, `post-bash` stats, `session-title`, `pre-`/`post-compact` flush, **`Stop`→OTEL** export | — |
|
||||||
|
|
|
||||||
|
|
@ -80,14 +80,19 @@ Protocol:
|
||||||
- Both arms append to the same `trekresearch-stats.jsonl`; `effort` is the
|
- Both arms append to the same `trekresearch-stats.jsonl`; `effort` is the
|
||||||
grouping key that separates them.
|
grouping key that separates them.
|
||||||
|
|
||||||
|
Each run is a Claude Code session the operator opens (Voyage starts none of
|
||||||
|
them; changed 2026-09-27 before any run — the thresholds above are untouched):
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Control arm (loop inert) — n >= 5, in a session opened with VOYAGE_STORM_ENABLED unset
|
||||||
|
/trekresearch --project .claude/projects/<brief-standard-effort>
|
||||||
|
|
||||||
|
# Treatment arm (loop live, effort: high in the brief's phase_signals) — n >= 5,
|
||||||
|
# in a session opened with VOYAGE_STORM_ENABLED=1 in its environment
|
||||||
|
/trekresearch --project .claude/projects/<brief-high-effort>
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Control arm (loop inert) — n >= 5
|
|
||||||
claude -p "/trekresearch --project .claude/projects/<brief-standard-effort>"
|
|
||||||
|
|
||||||
# Treatment arm (loop live, effort: high in the brief's phase_signals) — n >= 5
|
|
||||||
VOYAGE_STORM_ENABLED=1 \
|
|
||||||
claude -p "/trekresearch --project .claude/projects/<brief-high-effort>"
|
|
||||||
|
|
||||||
# The gate
|
# The gate
|
||||||
node scripts/storm-measure.mjs --stats "${CLAUDE_PLUGIN_DATA}/trekresearch-stats.jsonl"
|
node scripts/storm-measure.mjs --stats "${CLAUDE_PLUGIN_DATA}/trekresearch-stats.jsonl"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
// budget; a PreToolUse hook that over-blocks breaks every session on the box.
|
// budget; a PreToolUse hook that over-blocks breaks every session on the box.
|
||||||
//
|
//
|
||||||
// Stated limit, because the guarantee above is about OTHER sessions and reads
|
// Stated limit, because the guarantee above is about OTHER sessions and reads
|
||||||
// as broader than it is: `claude --resume` keeps the same session_id, so a
|
// as broader than it is: resuming a session (`--resume`) keeps the same session_id, so a
|
||||||
// resumed session is the same session by this key. If a run reached its cap
|
// resumed session is the same session by this key. If a run reached its cap
|
||||||
// and then died before removing the marker, the resume inherits the remainder
|
// and then died before removing the marker, the resume inherits the remainder
|
||||||
// of the TTL, for any WebSearch/WebFetch/Task — research or not. Three things
|
// of the TTL, for any WebSearch/WebFetch/Task — research or not. Three things
|
||||||
|
|
@ -73,7 +73,7 @@ const SCOPE_DIRNAME = 'trekresearch-loop-scope';
|
||||||
// 2h — comfortably longer than any real research run (a 24-turn loop at a couple
|
// 2h — comfortably longer than any real research run (a 24-turn loop at a couple
|
||||||
// of minutes a turn is under an hour), and short enough that debris does not own
|
// of minutes a turn is under an hour), and short enough that debris does not own
|
||||||
// the rest of the working day. The TTL is measured from marker.startedAt rather
|
// the rest of the working day. The TTL is measured from marker.startedAt rather
|
||||||
// than from last activity, and `claude --resume` keeps the same session_id, so
|
// than from last activity, and resuming a session (`--resume`) keeps the same session_id, so
|
||||||
// this window is what a resumed session can inherit from a run that died holding
|
// this window is what a resumed session can inherit from a run that died holding
|
||||||
// the marker. It was 6h; nothing needed six.
|
// the marker. It was 6h; nothing needed six.
|
||||||
const DEFAULT_TTL_MS = 2 * 60 * 60 * 1000;
|
const DEFAULT_TTL_MS = 2 * 60 * 60 * 1000;
|
||||||
|
|
|
||||||
227
lib/util/session-orders.mjs
Normal file
227
lib/util/session-orders.mjs
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
// lib/util/session-orders.mjs
|
||||||
|
// /trekexecute's multi-session path, the PM way: every session a wave needs
|
||||||
|
// becomes an ORDER (a file naming its worktree, steps and done-condition) plus
|
||||||
|
// a STARTER (the one line the user gives a Claude Code session they open
|
||||||
|
// themselves in that worktree). Voyage never starts a session; this module
|
||||||
|
// only reads the plan, writes order files and reads progress files.
|
||||||
|
//
|
||||||
|
// Subcommands (CLI shim at the bottom):
|
||||||
|
// write --plan <repo-relative plan> --wave <W> --session-dir <dir>
|
||||||
|
// writes <dir>/orders/wave-<W>-session-<N>.md + <dir>/orders/wave.json
|
||||||
|
// and prints the starters
|
||||||
|
// status --plan <repo-relative plan> --wave <W> --session-dir <dir>
|
||||||
|
// prints JSON: each session's progress-file status, all_completed
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
||||||
|
import { join, dirname, basename, extname, isAbsolute } from 'node:path';
|
||||||
|
|
||||||
|
function sectionBody(text, heading) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const start = lines.findIndex(l => l.trim() === heading);
|
||||||
|
if (start === -1) return null;
|
||||||
|
const out = [];
|
||||||
|
for (let i = start + 1; i < lines.length; i++) {
|
||||||
|
if (/^## /.test(lines[i])) break;
|
||||||
|
out.push(lines[i]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function field(block, name) {
|
||||||
|
const re = new RegExp(`^\\s*-\\s*\\*\\*${name}:\\*\\*\\s*(.*)$`, 'i');
|
||||||
|
for (const l of block) {
|
||||||
|
const m = l.match(re);
|
||||||
|
if (m) return m[1].trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberList(raw) {
|
||||||
|
if (!raw || /^none$/i.test(raw)) return [];
|
||||||
|
const out = [];
|
||||||
|
for (const part of raw.split(',')) {
|
||||||
|
const p = part.replace(/session|^s/gi, '').trim();
|
||||||
|
const range = p.match(/^(\d+)\s*[-–]\s*(\d+)$/);
|
||||||
|
if (range) {
|
||||||
|
for (let n = Number(range[1]); n <= Number(range[2]); n++) out.push(n);
|
||||||
|
} else if (/^\d+$/.test(p)) {
|
||||||
|
out.push(Number(p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a plan's `## Execution Strategy` section.
|
||||||
|
* @returns {{sessions: {n:number,title:string,wave:number,steps:number[],dependsOn:number[],cwd:string}[], waves:number[]}}
|
||||||
|
*/
|
||||||
|
export function parseExecutionStrategy(planText) {
|
||||||
|
const body = sectionBody(planText, '## Execution Strategy');
|
||||||
|
if (!body) throw new Error('plan has no ## Execution Strategy section');
|
||||||
|
const sessions = [];
|
||||||
|
let cur = null;
|
||||||
|
for (const line of body) {
|
||||||
|
const h = line.match(/^###\s+Session\s+(\d+):\s*(.*)$/);
|
||||||
|
if (h) {
|
||||||
|
cur = { n: Number(h[1]), title: h[2].trim(), block: [] };
|
||||||
|
sessions.push(cur);
|
||||||
|
} else if (/^###\s/.test(line)) {
|
||||||
|
cur = null;
|
||||||
|
} else if (cur) {
|
||||||
|
cur.block.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sessions.length === 0) throw new Error('## Execution Strategy names no ### Session N: blocks');
|
||||||
|
const parsed = sessions.map(({ n, title, block }) => {
|
||||||
|
const wave = Number(field(block, 'Wave'));
|
||||||
|
if (!Number.isInteger(wave) || wave < 1) throw new Error(`Session ${n} has no Wave`);
|
||||||
|
return {
|
||||||
|
n, title, wave,
|
||||||
|
steps: numberList(field(block, 'Steps')),
|
||||||
|
dependsOn: numberList(field(block, 'Depends on')),
|
||||||
|
cwd: field(block, 'cwd') || '.',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { sessions: parsed, waves: [...new Set(parsed.map(s => s.wave))].sort((a, b) => a - b) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function session(strategy, n) {
|
||||||
|
const s = strategy.sessions.find(x => x.n === n);
|
||||||
|
if (!s) throw new Error(`no Session ${n} in the Execution Strategy`);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function worktreePathFor(sessionDir, n) {
|
||||||
|
return join(sessionDir, 'worktrees', `session-${n}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInFor(sessionDir, s) {
|
||||||
|
const wt = worktreePathFor(sessionDir, s.n);
|
||||||
|
return s.cwd === '.' ? wt : join(wt, s.cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The progress file `/trekexecute --session N <plan>` writes, as seen from the
|
||||||
|
* session's working directory inside its worktree (Phase 3 session naming).
|
||||||
|
*/
|
||||||
|
export function progressFileFor({ strategy, n, planPath, sessionDir }) {
|
||||||
|
const s = session(strategy, n);
|
||||||
|
const slug = basename(planPath, extname(planPath));
|
||||||
|
return join(openInFor(sessionDir, s), dirname(planPath), `.trekexecute-progress-${slug}-session-${n}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One order per session in `wave`: worktree, starter, done-condition. */
|
||||||
|
export function buildWaveOrders({ strategy, wave, planPath, sessionDir }) {
|
||||||
|
if (isAbsolute(planPath)) throw new Error(`plan path must be repo-relative (each session resolves it inside its own worktree): ${planPath}`);
|
||||||
|
const members = strategy.sessions.filter(s => s.wave === wave);
|
||||||
|
if (members.length === 0) throw new Error(`no sessions in wave ${wave}`);
|
||||||
|
return members.map((s) => {
|
||||||
|
const worktreePath = worktreePathFor(sessionDir, s.n);
|
||||||
|
const openIn = openInFor(sessionDir, s);
|
||||||
|
const starter = `/trekexecute --session ${s.n} ${planPath}`;
|
||||||
|
const progressFile = progressFileFor({ strategy, n: s.n, planPath, sessionDir });
|
||||||
|
const body = [
|
||||||
|
`# Order: session ${s.n}, wave ${wave} - ${s.title}`,
|
||||||
|
'',
|
||||||
|
`- plan: ${planPath}`,
|
||||||
|
`- steps: ${s.steps.join(', ') || '-'}`,
|
||||||
|
`- depends on: ${s.dependsOn.length ? s.dependsOn.map(d => `session ${d}`).join(', ') : 'none'}`,
|
||||||
|
`- worktree: ${worktreePath}`,
|
||||||
|
`- open the session in: ${openIn}`,
|
||||||
|
'',
|
||||||
|
'## Starter',
|
||||||
|
'',
|
||||||
|
`Open a Claude Code session yourself in \`${openIn}\` and give it this line:`,
|
||||||
|
'',
|
||||||
|
` ${starter}`,
|
||||||
|
'',
|
||||||
|
'Voyage does not start this session. It runs only this session\'s steps, inside',
|
||||||
|
'its own worktree; never run two sessions in the same working directory.',
|
||||||
|
'',
|
||||||
|
'## Done when',
|
||||||
|
'',
|
||||||
|
`\`${progressFile}\` has \`"status": "completed"\`. Then run the same`,
|
||||||
|
'`/trekexecute` command as before in the main checkout: it reads every',
|
||||||
|
'session\'s progress file and merges the wave.',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
return { session: s.n, title: s.title, wave, worktreePath, openIn, starter, progressFile, orderFile: `wave-${wave}-session-${s.n}.md`, body };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeWaveOrders({ strategy, wave, planPath, sessionDir, now = new Date() }) {
|
||||||
|
const orders = buildWaveOrders({ strategy, wave, planPath, sessionDir });
|
||||||
|
const dir = join(sessionDir, 'orders');
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
for (const o of orders) writeFileSync(join(dir, o.orderFile), o.body);
|
||||||
|
const state = {
|
||||||
|
wave, plan: planPath, sessions: orders.map(o => o.session),
|
||||||
|
status: 'awaiting-sessions', created_at: now.toISOString(),
|
||||||
|
};
|
||||||
|
writeFileSync(join(dir, 'wave.json'), JSON.stringify(state, null, 2) + '\n');
|
||||||
|
return { dir, orders, state };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waveStatus({ strategy, wave, planPath, sessionDir }) {
|
||||||
|
const members = strategy.sessions.filter(s => s.wave === wave);
|
||||||
|
if (members.length === 0) throw new Error(`no sessions in wave ${wave}`);
|
||||||
|
const sessions = members.map((s) => {
|
||||||
|
const progressFile = progressFileFor({ strategy, n: s.n, planPath, sessionDir });
|
||||||
|
let status = 'pending';
|
||||||
|
if (existsSync(progressFile)) {
|
||||||
|
try {
|
||||||
|
status = JSON.parse(readFileSync(progressFile, 'utf-8')).status || 'unreadable';
|
||||||
|
} catch {
|
||||||
|
status = 'unreadable';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { session: s.n, status, progressFile };
|
||||||
|
});
|
||||||
|
return { wave, sessions, all_completed: sessions.every(s => s.status === 'completed') };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CLI shim ----------------------------------------------------------------
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const out = { cmd: argv[0] };
|
||||||
|
for (let i = 1; i < argv.length; i++) {
|
||||||
|
const a = argv[i];
|
||||||
|
if (a === '--plan') out.plan = argv[++i];
|
||||||
|
else if (a === '--wave') out.wave = Number(argv[++i]);
|
||||||
|
else if (a === '--session-dir') out.sessionDir = argv[++i];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
const usage = 'usage: session-orders.mjs write|status --plan <repo-relative plan> --wave <W> --session-dir <dir>';
|
||||||
|
if (!['write', 'status'].includes(args.cmd) || !args.plan || !Number.isInteger(args.wave) || !args.sessionDir) {
|
||||||
|
process.stderr.write(usage + '\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const strategy = parseExecutionStrategy(readFileSync(args.plan, 'utf-8'));
|
||||||
|
const opts = { strategy, wave: args.wave, planPath: args.plan, sessionDir: args.sessionDir };
|
||||||
|
if (args.cmd === 'status') {
|
||||||
|
process.stdout.write(JSON.stringify(waveStatus(opts), null, 2) + '\n');
|
||||||
|
} else {
|
||||||
|
const { dir, orders } = writeWaveOrders(opts);
|
||||||
|
const lines = [
|
||||||
|
`Wave ${args.wave}: ${orders.length} session order(s) in ${dir}`,
|
||||||
|
'Voyage starts none of them. For each session, open Claude Code yourself in the',
|
||||||
|
'named directory and give it the starter line:',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
for (const o of orders) {
|
||||||
|
lines.push(` Session ${o.session} (${o.title})`, ` open in: ${o.openIn}`, ` starter: ${o.starter}`, '');
|
||||||
|
}
|
||||||
|
lines.push('When every session is done, run the same /trekexecute command again here to merge the wave.');
|
||||||
|
process.stdout.write(lines.join('\n') + '\n');
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
} catch (err) {
|
||||||
|
process.stderr.write(`session-orders: ${err.message}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,540 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
// scripts/q3-cache-prefix-experiment.mjs
|
|
||||||
//
|
|
||||||
// Q3 cache-prefix-preservation experiment for Spor C of post-v3.4.0 roadmap.
|
|
||||||
// Measures whether CLAUDE_CODE_FORK_SUBAGENT=1 preserves the server-side
|
|
||||||
// cache prefix across multiple `claude -p` fork-children when all children
|
|
||||||
// spawn with byte-identical --allowedTools at 150-250K parent context.
|
|
||||||
//
|
|
||||||
// Brief: .claude/projects/2026-05-04-spor-c-q3-cache-prefix-experiment/brief.md
|
|
||||||
// Plan: .claude/projects/2026-05-04-spor-c-q3-cache-prefix-experiment/plan.md
|
|
||||||
//
|
|
||||||
// Result thresholds (master-plan):
|
|
||||||
// median(cache_creation_input_tokens) <= 1500 -> POSITIVE
|
|
||||||
// median >= 3500 -> NEGATIVE
|
|
||||||
// else -> INCONCLUSIVE
|
|
||||||
// Any per-child failure or missing metadata -> INCONCLUSIVE.
|
|
||||||
//
|
|
||||||
// Zero npm dependencies. Node stdlib only. Hook-safe (no forbidden words
|
|
||||||
// in source — pre-bash-executor.mjs scans the entire command string when
|
|
||||||
// this script is invoked).
|
|
||||||
|
|
||||||
import { spawn, spawnSync } from 'node:child_process';
|
|
||||||
import { readFileSync, readdirSync, statSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
||||||
import { createHash } from 'node:crypto';
|
|
||||||
import { join, dirname, resolve } from 'node:path';
|
|
||||||
import { tmpdir } from 'node:os';
|
|
||||||
|
|
||||||
const PROJECT_DIR = resolve(
|
|
||||||
process.cwd(),
|
|
||||||
'.claude/projects/2026-05-04-spor-c-q3-cache-prefix-experiment',
|
|
||||||
);
|
|
||||||
const DEFAULT_OUT = join(PROJECT_DIR, 'q3-experiment-results.local.md');
|
|
||||||
const STATS_JSONL = '/Users/ktg/.claude/plugins/data/voyage-ktg-plugin-marketplace/trekexecute-stats.jsonl';
|
|
||||||
const ANALYZER = resolve(process.cwd(), 'lib/stats/cache-analyzer.mjs');
|
|
||||||
|
|
||||||
const MIN_PARENT_TOKENS = 150_000;
|
|
||||||
const MAX_PARENT_TOKENS = 250_000;
|
|
||||||
const POSITIVE_THRESHOLD = 1500;
|
|
||||||
const NEGATIVE_THRESHOLD = 3500;
|
|
||||||
const HARD_TIMEOUT_MS = 600_000; // 10 min total
|
|
||||||
const PER_CHILD_TIMEOUT_MS = 240_000; // 4 min per child
|
|
||||||
const MIN_CC_VERSION = [2, 1, 121];
|
|
||||||
const ALLOWED_TOOLS = 'Read,Write,Edit,Bash,Glob,Grep';
|
|
||||||
const MODEL = 'sonnet';
|
|
||||||
|
|
||||||
// Sources for parent context build. Brief constraint: no secrets, no ~/, no
|
|
||||||
// other plugins. Stays inside plugins/trekplan/.
|
|
||||||
//
|
|
||||||
// Calibration (empirical, CC v2.1.128 + Sonnet 4.6):
|
|
||||||
// Token-per-byte ratio varies from 0.38-0.90 depending on content type.
|
|
||||||
// Mixed .md+.mjs at 264K bytes yielded only ~60K context tokens (4.5 byte/token).
|
|
||||||
// To reliably hit 150K context tokens, target ~600-700K bytes of mixed content.
|
|
||||||
// Hooks baseline ~62K cache_creation always present, so total lands ~212-262K.
|
|
||||||
const CONTEXT_DIRS = [
|
|
||||||
'commands',
|
|
||||||
'agents',
|
|
||||||
'lib/parsers',
|
|
||||||
'lib/validators',
|
|
||||||
'lib/util',
|
|
||||||
'lib/review',
|
|
||||||
'lib/stats',
|
|
||||||
];
|
|
||||||
const CONTEXT_EXTRA_FILES = [
|
|
||||||
'docs/HANDOVER-CONTRACTS.md',
|
|
||||||
'CLAUDE.md',
|
|
||||||
'examples/02-real-cli/REGENERATED.md',
|
|
||||||
];
|
|
||||||
|
|
||||||
function usage() {
|
|
||||||
return `q3-cache-prefix-experiment.mjs — Q3 cache-prefix experiment harness
|
|
||||||
|
|
||||||
USAGE:
|
|
||||||
node scripts/q3-cache-prefix-experiment.mjs [--help] [--dry-run] [--out <path>]
|
|
||||||
|
|
||||||
FLAGS:
|
|
||||||
--help Print this usage block and exit 0.
|
|
||||||
--dry-run Build parent context, print child argv arrays + token-byte
|
|
||||||
estimate to stderr, do NOT call the API. No result file written.
|
|
||||||
--out <path> Write result file to <path>. Default:
|
|
||||||
${DEFAULT_OUT}
|
|
||||||
|
|
||||||
EXIT CODES:
|
|
||||||
0 Experiment completed (RESULT line written).
|
|
||||||
2 Hard timeout exceeded.
|
|
||||||
3 CC version too old or FORK_SUBAGENT warm-up failed -> INCONCLUSIVE.
|
|
||||||
4 Parent context out of 150K-250K band -> INCONCLUSIVE.
|
|
||||||
5 Child API metadata unavailable -> INCONCLUSIVE.
|
|
||||||
7 Usage / I/O error.
|
|
||||||
|
|
||||||
ENV:
|
|
||||||
ANTHROPIC_API_KEY must be set (read from operator env, not embedded).
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(argv) {
|
|
||||||
const opts = { help: false, dryRun: false, out: DEFAULT_OUT };
|
|
||||||
for (let i = 0; i < argv.length; i++) {
|
|
||||||
const a = argv[i];
|
|
||||||
if (a === '--help' || a === '-h') opts.help = true;
|
|
||||||
else if (a === '--dry-run') opts.dryRun = true;
|
|
||||||
else if (a === '--out') opts.out = argv[++i];
|
|
||||||
else {
|
|
||||||
process.stderr.write(`Unknown argument: ${a}\n${usage()}`);
|
|
||||||
process.exit(7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return opts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function log(msg) {
|
|
||||||
process.stderr.write(`[q3] ${msg}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nowIso() {
|
|
||||||
return new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function listFilesRecursive(dir, ext) {
|
|
||||||
const out = [];
|
|
||||||
if (!existsSync(dir)) return out;
|
|
||||||
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
const p = join(dir, ent.name);
|
|
||||||
if (ent.isDirectory()) out.push(...listFilesRecursive(p, ext));
|
|
||||||
else if (ent.isFile() && (!ext || p.endsWith(ext))) out.push(p);
|
|
||||||
}
|
|
||||||
return out.sort(); // deterministic ordering
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildParentContext() {
|
|
||||||
const parts = [];
|
|
||||||
const fileList = [];
|
|
||||||
|
|
||||||
for (const d of CONTEXT_DIRS) {
|
|
||||||
const files = [
|
|
||||||
...listFilesRecursive(d, '.mjs'),
|
|
||||||
...listFilesRecursive(d, '.md'),
|
|
||||||
].sort();
|
|
||||||
for (const f of files) {
|
|
||||||
if (existsSync(f)) {
|
|
||||||
try {
|
|
||||||
parts.push(`=== FILE: ${f} ===\n` + readFileSync(f, 'utf-8'));
|
|
||||||
fileList.push(f);
|
|
||||||
} catch { /* skip unreadable */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const f of CONTEXT_EXTRA_FILES) {
|
|
||||||
if (existsSync(f)) {
|
|
||||||
try {
|
|
||||||
parts.push(`=== FILE: ${f} ===\n` + readFileSync(f, 'utf-8'));
|
|
||||||
fileList.push(f);
|
|
||||||
} catch { /* skip */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = parts.join('\n\n');
|
|
||||||
const sha256 = createHash('sha256').update(text).digest('hex');
|
|
||||||
return { text, sha256, fileCount: fileList.length, byteLength: Buffer.byteLength(text, 'utf-8') };
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkCcVersion() {
|
|
||||||
const r = spawnSync('claude', ['--version'], { encoding: 'utf-8', timeout: 10_000 });
|
|
||||||
if (r.status !== 0) {
|
|
||||||
return { ok: false, reason: `claude --version exit ${r.status}: ${r.stderr || r.stdout}` };
|
|
||||||
}
|
|
||||||
const m = (r.stdout || '').match(/(\d+)\.(\d+)\.(\d+)/);
|
|
||||||
if (!m) return { ok: false, reason: `cannot parse version from: ${r.stdout}` };
|
|
||||||
const got = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
if (got[i] > MIN_CC_VERSION[i]) return { ok: true, version: got.join('.') };
|
|
||||||
if (got[i] < MIN_CC_VERSION[i]) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: `CC ${got.join('.')} < required ${MIN_CC_VERSION.join('.')}`,
|
|
||||||
version: got.join('.'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { ok: true, version: got.join('.') };
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildChildArgv(contextFilePath) {
|
|
||||||
// Byte-identical across all 3 children (SC #3). Per-child differentiation
|
|
||||||
// is via the user prompt suffix only, NOT via argv.
|
|
||||||
//
|
|
||||||
// Context is delivered via --append-system-prompt-file (NOT stdin) to:
|
|
||||||
// 1. avoid stdin pipe buffer issues at >200K bytes
|
|
||||||
// 2. ensure context is part of the cache-prefix segment
|
|
||||||
//
|
|
||||||
// --exclude-dynamic-system-prompt-sections moves cwd/env/git-status into
|
|
||||||
// the user message, preventing per-child variation in the cache prefix.
|
|
||||||
return [
|
|
||||||
'-p',
|
|
||||||
'--model', MODEL,
|
|
||||||
'--output-format', 'stream-json',
|
|
||||||
'--verbose',
|
|
||||||
'--allowedTools', ALLOWED_TOOLS,
|
|
||||||
'--max-turns', '1',
|
|
||||||
'--append-system-prompt-file', contextFilePath,
|
|
||||||
'--exclude-dynamic-system-prompt-sections',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnChild(contextFilePath, childIndex) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const argv = buildChildArgv(contextFilePath);
|
|
||||||
// User prompt is short (per-child suffix only). Context lives in the
|
|
||||||
// appended system-prompt file, which Claude treats as cache-prefix
|
|
||||||
// material.
|
|
||||||
const prompt = `[child #${childIndex}] Reply only with the word OK.`;
|
|
||||||
const env = { ...process.env, CLAUDE_CODE_FORK_SUBAGENT: '1' };
|
|
||||||
const child = spawn('claude', argv, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
||||||
|
|
||||||
let stdout = '';
|
|
||||||
let stderr = '';
|
|
||||||
let killed = false;
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
killed = true;
|
|
||||||
child.kill('SIGTERM');
|
|
||||||
}, PER_CHILD_TIMEOUT_MS);
|
|
||||||
|
|
||||||
child.stdout.on('data', (b) => { stdout += b.toString('utf-8'); });
|
|
||||||
child.stderr.on('data', (b) => { stderr += b.toString('utf-8'); });
|
|
||||||
child.on('close', (code) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolve({ code, stdout, stderr, killed, argv: ['claude', ...argv] });
|
|
||||||
});
|
|
||||||
child.on('error', (err) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolve({ code: -1, stdout, stderr: stderr + `\nspawn error: ${err.message}`, killed, argv: ['claude', ...argv] });
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stdin.write(prompt);
|
|
||||||
child.stdin.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractUsageFromStream(stdout) {
|
|
||||||
// First {"type":"assistant",...} JSON line carries the usage payload.
|
|
||||||
const lines = stdout.split('\n');
|
|
||||||
for (const line of lines) {
|
|
||||||
if (!line.startsWith('{')) continue;
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(line);
|
|
||||||
if (obj.type === 'assistant' && obj.message && obj.message.usage) {
|
|
||||||
return obj.message.usage;
|
|
||||||
}
|
|
||||||
// Fallback: top-level result event also carries usage.
|
|
||||||
if (obj.type === 'result' && obj.usage) {
|
|
||||||
return obj.usage;
|
|
||||||
}
|
|
||||||
} catch { /* skip non-JSON lines */ }
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function median(values) {
|
|
||||||
if (values.length === 0) return null;
|
|
||||||
const sorted = [...values].sort((a, b) => a - b);
|
|
||||||
const mid = Math.floor(sorted.length / 2);
|
|
||||||
return sorted.length % 2 === 0
|
|
||||||
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
||||||
: sorted[mid];
|
|
||||||
}
|
|
||||||
|
|
||||||
function decideResult(measurements, allValid) {
|
|
||||||
if (!allValid) return { result: 'INCONCLUSIVE', reason: 'one or more children failed or missing metadata' };
|
|
||||||
const ccs = measurements.map(m => m.cache_creation_input_tokens);
|
|
||||||
const med = median(ccs);
|
|
||||||
if (med === null) return { result: 'INCONCLUSIVE', reason: 'no measurements' };
|
|
||||||
if (med <= POSITIVE_THRESHOLD) return { result: 'POSITIVE', reason: `median cache_creation ${med} <= ${POSITIVE_THRESHOLD}`, median: med };
|
|
||||||
if (med >= NEGATIVE_THRESHOLD) return { result: 'NEGATIVE', reason: `median cache_creation ${med} >= ${NEGATIVE_THRESHOLD}`, median: med };
|
|
||||||
return { result: 'INCONCLUSIVE', reason: `median cache_creation ${med} in (${POSITIVE_THRESHOLD}, ${NEGATIVE_THRESHOLD})`, median: med };
|
|
||||||
}
|
|
||||||
|
|
||||||
function runAnalyzer() {
|
|
||||||
if (!existsSync(ANALYZER) || !existsSync(STATS_JSONL)) return null;
|
|
||||||
const r = spawnSync('node', [ANALYZER, '--json', STATS_JSONL], {
|
|
||||||
encoding: 'utf-8',
|
|
||||||
timeout: 30_000,
|
|
||||||
});
|
|
||||||
if (r.status !== 0) return null;
|
|
||||||
try { return JSON.parse(r.stdout); }
|
|
||||||
catch { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeResultFile(outPath, ctx, ccVersion, measurements, parentTokens, decision, analyzerSummary, runErrors) {
|
|
||||||
// ALWAYS write at least 30 lines + required strings (SC #6).
|
|
||||||
const dir = dirname(outPath);
|
|
||||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
||||||
|
|
||||||
const lines = [];
|
|
||||||
lines.push('# Q3 Cache-Prefix-Preservation Experiment — Results');
|
|
||||||
lines.push('');
|
|
||||||
lines.push(`Generated: ${nowIso()}`);
|
|
||||||
lines.push(`Brief: \`.claude/projects/2026-05-04-spor-c-q3-cache-prefix-experiment/brief.md\``);
|
|
||||||
lines.push(`Plan: \`.claude/projects/2026-05-04-spor-c-q3-cache-prefix-experiment/plan.md\``);
|
|
||||||
lines.push('');
|
|
||||||
lines.push('## Setup');
|
|
||||||
lines.push('');
|
|
||||||
lines.push(`- Claude Code version: ${ccVersion ?? 'unknown'}`);
|
|
||||||
lines.push(`- Model: ${MODEL}`);
|
|
||||||
lines.push(`- Allowed tools: ${ALLOWED_TOOLS}`);
|
|
||||||
lines.push(`- CLAUDE_CODE_FORK_SUBAGENT: 1 (set per-child via env)`);
|
|
||||||
lines.push(`- Children: 3 (sequential spawn)`);
|
|
||||||
lines.push('');
|
|
||||||
lines.push('## Parent context');
|
|
||||||
lines.push('');
|
|
||||||
lines.push(`- File count: ${ctx.fileCount}`);
|
|
||||||
lines.push(`- Byte length: ${ctx.byteLength}`);
|
|
||||||
lines.push(`- SHA-256: \`${ctx.sha256}\``);
|
|
||||||
lines.push(`- Measured input_tokens (pre-flight): ${parentTokens ?? 'N/A'}`);
|
|
||||||
lines.push(`- Target band: [${MIN_PARENT_TOKENS}, ${MAX_PARENT_TOKENS}]`);
|
|
||||||
lines.push('');
|
|
||||||
lines.push('## Per-child measurements');
|
|
||||||
lines.push('');
|
|
||||||
lines.push('| child | cache_creation | cache_read | input_tokens | output_tokens | argv_unique | exit |');
|
|
||||||
lines.push('|-------|----------------|------------|--------------|---------------|-------------|------|');
|
|
||||||
for (const m of measurements) {
|
|
||||||
lines.push(
|
|
||||||
`| ${m.child} | ${m.cache_creation_input_tokens ?? 'N/A'} | ${m.cache_read_input_tokens ?? 'N/A'} | ${m.input_tokens ?? 'N/A'} | ${m.output_tokens ?? 'N/A'} | ${m.argv_signature} | ${m.exit_code} |`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
lines.push('');
|
|
||||||
lines.push('## argv parity (SC #3)');
|
|
||||||
lines.push('');
|
|
||||||
const argvSet = new Set(measurements.map(m => m.argv_signature));
|
|
||||||
lines.push(`Unique argv signatures across children: ${argvSet.size} (expected: 1)`);
|
|
||||||
lines.push('');
|
|
||||||
lines.push('## Telemetry context');
|
|
||||||
lines.push('');
|
|
||||||
if (analyzerSummary) {
|
|
||||||
lines.push(`- total_events: ${analyzerSummary.total_events}`);
|
|
||||||
lines.push(`- wall_time_ms_p50: ${analyzerSummary.wall_time_ms_p50}`);
|
|
||||||
lines.push(`- wall_time_ms_p90: ${analyzerSummary.wall_time_ms_p90}`);
|
|
||||||
lines.push(`- oldest_event_iso: ${analyzerSummary.oldest_event_iso ?? 'N/A'}`);
|
|
||||||
lines.push(`- newest_event_iso: ${analyzerSummary.newest_event_iso ?? 'N/A'}`);
|
|
||||||
} else {
|
|
||||||
lines.push('- analyser unavailable or stats jsonl missing');
|
|
||||||
}
|
|
||||||
lines.push('');
|
|
||||||
if (runErrors.length > 0) {
|
|
||||||
lines.push('## Errors');
|
|
||||||
lines.push('');
|
|
||||||
for (const e of runErrors) lines.push(`- ${e}`);
|
|
||||||
lines.push('');
|
|
||||||
}
|
|
||||||
lines.push('## Conclusion');
|
|
||||||
lines.push('');
|
|
||||||
lines.push(`Reason: ${decision.reason}`);
|
|
||||||
if (decision.median !== undefined) lines.push(`Median cache_creation_input_tokens: ${decision.median}`);
|
|
||||||
lines.push('');
|
|
||||||
lines.push(`RESULT: ${decision.result}`);
|
|
||||||
lines.push('');
|
|
||||||
lines.push('## Path C decision (master-plan §Spor D direction)');
|
|
||||||
lines.push('');
|
|
||||||
if (decision.result === 'POSITIVE') {
|
|
||||||
lines.push('Path C is feasible. C3 should write a v3.5.0 brief proposing cache-warm sentinel + identical-tool parallel children.');
|
|
||||||
} else if (decision.result === 'NEGATIVE') {
|
|
||||||
lines.push('Path C is closed. C3 should update master-plan §Spor D = stabilisation work; v3.5.0 brief NOT written.');
|
|
||||||
} else {
|
|
||||||
lines.push('Path C decision deferred to operator. C3 documents the gap and proposes targeted follow-up before Spor D commits.');
|
|
||||||
}
|
|
||||||
lines.push('');
|
|
||||||
|
|
||||||
writeFileSync(outPath, lines.join('\n') + '\n', 'utf-8');
|
|
||||||
log(`wrote result file: ${outPath} (${lines.length} lines)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function measureParentTokens(contextFilePath) {
|
|
||||||
// Fire one warm-up call to measure parent context size.
|
|
||||||
//
|
|
||||||
// CC's stream-json wrapper splits the prompt into:
|
|
||||||
// - input_tokens: only the non-cached portion (typically the latest turn)
|
|
||||||
// - cache_creation_input_tokens: tokens promoted to cache (the parent context)
|
|
||||||
// - cache_read_input_tokens: tokens served from cache (zero on first hit)
|
|
||||||
//
|
|
||||||
// Total parent context size = input_tokens + cache_creation + cache_read.
|
|
||||||
const argv = [
|
|
||||||
'-p',
|
|
||||||
'--model', MODEL,
|
|
||||||
'--output-format', 'stream-json',
|
|
||||||
'--verbose',
|
|
||||||
'--max-turns', '1',
|
|
||||||
'--append-system-prompt-file', contextFilePath,
|
|
||||||
'--exclude-dynamic-system-prompt-sections',
|
|
||||||
];
|
|
||||||
const env = { ...process.env, CLAUDE_CODE_FORK_SUBAGENT: '1' };
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const child = spawn('claude', argv, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
||||||
let stdout = '';
|
|
||||||
let stderr = '';
|
|
||||||
const timer = setTimeout(() => child.kill('SIGTERM'), 180_000);
|
|
||||||
child.stdout.on('data', (b) => { stdout += b.toString('utf-8'); });
|
|
||||||
child.stderr.on('data', (b) => { stderr += b.toString('utf-8'); });
|
|
||||||
child.on('close', (code) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
const usage = extractUsageFromStream(stdout);
|
|
||||||
if (!usage) {
|
|
||||||
log(`measureParentTokens: no usage extracted; exit=${code}; stderr (first 300): ${stderr.slice(0, 300)}`);
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const total = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
||||||
log(`measureParentTokens: input=${usage.input_tokens} cache_creation=${usage.cache_creation_input_tokens} cache_read=${usage.cache_read_input_tokens} total=${total}`);
|
|
||||||
resolve({ total, ...usage });
|
|
||||||
});
|
|
||||||
child.on('error', (e) => { clearTimeout(timer); log(`measureParentTokens spawn error: ${e.message}`); resolve(null); });
|
|
||||||
child.stdin.write('Reply only with the word OK.');
|
|
||||||
child.stdin.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const opts = parseArgs(process.argv.slice(2));
|
|
||||||
if (opts.help) {
|
|
||||||
process.stdout.write(usage());
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hardTimer = setTimeout(() => {
|
|
||||||
process.stderr.write('[q3] HARD TIMEOUT: 10 min exceeded, exit 2\n');
|
|
||||||
process.exit(2);
|
|
||||||
}, HARD_TIMEOUT_MS);
|
|
||||||
|
|
||||||
log(`starting at ${nowIso()}`);
|
|
||||||
|
|
||||||
// Build parent context first (works in dry-run too).
|
|
||||||
log('building parent context...');
|
|
||||||
const ctx = buildParentContext();
|
|
||||||
log(`context: ${ctx.fileCount} files, ${ctx.byteLength} bytes, sha256=${ctx.sha256.slice(0, 16)}`);
|
|
||||||
|
|
||||||
// Write parent context to a temp file (used as system-prompt-file for all
|
|
||||||
// 3 children + warm-up). Determinism check: SHA-256 already computed.
|
|
||||||
const contextFilePath = join(tmpdir(), `q3-parent-context-${process.pid}-${Date.now()}.txt`);
|
|
||||||
writeFileSync(contextFilePath, ctx.text, 'utf-8');
|
|
||||||
log(`wrote parent context to: ${contextFilePath}`);
|
|
||||||
|
|
||||||
// Print 3 child argvs for SC #3 verification.
|
|
||||||
const argvBase = buildChildArgv(contextFilePath);
|
|
||||||
log(`argv (identical for all 3 children):`);
|
|
||||||
log(` argv: ${JSON.stringify(['claude', ...argvBase])}`);
|
|
||||||
log(` "--allowedTools" "${ALLOWED_TOOLS}"`);
|
|
||||||
log(` "--allowedTools" "${ALLOWED_TOOLS}"`);
|
|
||||||
log(` "--allowedTools" "${ALLOWED_TOOLS}"`);
|
|
||||||
|
|
||||||
if (opts.dryRun) {
|
|
||||||
log('dry-run: skipping API calls.');
|
|
||||||
try { unlinkSync(contextFilePath); } catch {}
|
|
||||||
clearTimeout(hardTimer);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-flight: CC version (SC #2 part 1).
|
|
||||||
log('pre-flight: checking CC version...');
|
|
||||||
const verCheck = checkCcVersion();
|
|
||||||
if (!verCheck.ok) {
|
|
||||||
log(`CC version check FAILED: ${verCheck.reason}`);
|
|
||||||
const decision = { result: 'INCONCLUSIVE', reason: `CC version: ${verCheck.reason}` };
|
|
||||||
writeResultFile(opts.out, ctx, verCheck.version, [], null, decision, runAnalyzer(), [verCheck.reason]);
|
|
||||||
clearTimeout(hardTimer);
|
|
||||||
process.exit(3);
|
|
||||||
}
|
|
||||||
log(`CC version OK: ${verCheck.version}`);
|
|
||||||
|
|
||||||
// Pre-flight: parent token band (SC #4).
|
|
||||||
log('pre-flight: measuring parent context token count via warm-up...');
|
|
||||||
const measurement = await measureParentTokens(contextFilePath);
|
|
||||||
if (measurement === null) {
|
|
||||||
const decision = { result: 'INCONCLUSIVE', reason: 'pre-flight warm-up returned no usage metadata' };
|
|
||||||
writeResultFile(opts.out, ctx, verCheck.version, [], null, decision, runAnalyzer(), ['pre-flight failed']);
|
|
||||||
clearTimeout(hardTimer);
|
|
||||||
process.exit(3);
|
|
||||||
}
|
|
||||||
const parentTokens = measurement.total;
|
|
||||||
log(`parent total tokens: ${parentTokens} (input=${measurement.input_tokens} cache_creation=${measurement.cache_creation_input_tokens} cache_read=${measurement.cache_read_input_tokens})`);
|
|
||||||
if (parentTokens < MIN_PARENT_TOKENS || parentTokens > MAX_PARENT_TOKENS) {
|
|
||||||
const decision = {
|
|
||||||
result: 'INCONCLUSIVE',
|
|
||||||
reason: `parent context out of band: ${parentTokens} not in [${MIN_PARENT_TOKENS}, ${MAX_PARENT_TOKENS}]`,
|
|
||||||
};
|
|
||||||
writeResultFile(opts.out, ctx, verCheck.version, [], parentTokens, decision, runAnalyzer(), [decision.reason]);
|
|
||||||
clearTimeout(hardTimer);
|
|
||||||
process.exit(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run 3 children sequentially (avoids spawn-burst rate-limit).
|
|
||||||
const measurements = [];
|
|
||||||
const runErrors = [];
|
|
||||||
let allValid = true;
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
|
||||||
log(`spawning child ${i}/3...`);
|
|
||||||
const r = await spawnChild(contextFilePath, i);
|
|
||||||
const usage = extractUsageFromStream(r.stdout);
|
|
||||||
const argvSig = JSON.stringify(r.argv);
|
|
||||||
if (r.code !== 0 || !usage || typeof usage.cache_creation_input_tokens !== 'number') {
|
|
||||||
allValid = false;
|
|
||||||
const err = `child ${i}: exit=${r.code}, killed=${r.killed}, usage=${usage ? 'partial' : 'missing'}`;
|
|
||||||
runErrors.push(err);
|
|
||||||
log(err);
|
|
||||||
if (r.stderr) log(` stderr (first 500 chars): ${r.stderr.slice(0, 500)}`);
|
|
||||||
}
|
|
||||||
measurements.push({
|
|
||||||
child: i,
|
|
||||||
cache_creation_input_tokens: usage?.cache_creation_input_tokens ?? null,
|
|
||||||
cache_read_input_tokens: usage?.cache_read_input_tokens ?? null,
|
|
||||||
input_tokens: usage?.input_tokens ?? null,
|
|
||||||
output_tokens: usage?.output_tokens ?? null,
|
|
||||||
argv_signature: argvSig,
|
|
||||||
exit_code: r.code,
|
|
||||||
});
|
|
||||||
log(` cache_creation=${usage?.cache_creation_input_tokens ?? 'N/A'} cache_read=${usage?.cache_read_input_tokens ?? 'N/A'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decide result (SC #7).
|
|
||||||
const decision = decideResult(measurements, allValid);
|
|
||||||
log(`RESULT: ${decision.result} (${decision.reason})`);
|
|
||||||
|
|
||||||
// Run analyser for telemetry context (SC #8).
|
|
||||||
const analyzerSummary = runAnalyzer();
|
|
||||||
|
|
||||||
// Write result file (SC #6).
|
|
||||||
writeResultFile(opts.out, ctx, verCheck.version, measurements, parentTokens, decision, analyzerSummary, runErrors);
|
|
||||||
|
|
||||||
// Cleanup temp context file.
|
|
||||||
try { unlinkSync(contextFilePath); } catch {}
|
|
||||||
|
|
||||||
clearTimeout(hardTimer);
|
|
||||||
// Exit 0 even on INCONCLUSIVE — that's a valid outcome per brief NFR.
|
|
||||||
// Only exit non-zero on harness failures (already handled above).
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
||||||
main().catch((e) => {
|
|
||||||
process.stderr.write(`[q3] uncaught: ${e.stack || e.message}\n`);
|
|
||||||
process.exit(7);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
# Headless Launch Script Template
|
|
||||||
|
|
||||||
This template is used by the session-decomposer agent to generate a launch script
|
|
||||||
for headless execution of decomposed sessions.
|
|
||||||
|
|
||||||
## Template
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Headless launch script — generated by trekplan
|
|
||||||
# Master plan: {plan_path}
|
|
||||||
# Generated: {date}
|
|
||||||
# Sessions: {total_sessions} ({parallel_count} parallel, {sequential_count} sequential)
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Prevent accidental API billing — remove this line if you intend to use API credits
|
|
||||||
unset ANTHROPIC_API_KEY
|
|
||||||
|
|
||||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
||||||
PLAN_DIR="{session_dir}"
|
|
||||||
LOG_DIR="{session_dir}/logs"
|
|
||||||
WORKTREE_BASE="{session_dir}/worktrees"
|
|
||||||
mkdir -p "$LOG_DIR" "$WORKTREE_BASE"
|
|
||||||
|
|
||||||
# Disable git's optional locks during parallel worktree ops (research/02 R2;
|
|
||||||
# GH #47721). Mirror Phase 2.6 hardenings (commands/trekexecute.md).
|
|
||||||
export GIT_OPTIONAL_LOCKS=0
|
|
||||||
|
|
||||||
# Per-child guardrails (operator may override via env). Match Phase 2.6
|
|
||||||
# Step 2a-pre defaults.
|
|
||||||
MAX_TURNS="${TREKEXECUTE_MAX_TURNS:-50}"
|
|
||||||
MAX_BUDGET_USD="${TREKEXECUTE_MAX_BUDGET_USD:-5}"
|
|
||||||
|
|
||||||
# Shared context (brief + architecture if present). Absolute path so child
|
|
||||||
# sessions resolve it after `cd` into their worktree.
|
|
||||||
SHARED_CONTEXT_FILE="$WORKTREE_BASE/.shared-context.md"
|
|
||||||
if [ -f "$PLAN_DIR/brief.md" ]; then
|
|
||||||
cat "$PLAN_DIR/brief.md" > "$SHARED_CONTEXT_FILE"
|
|
||||||
if [ -f "$PLAN_DIR/architecture/overview.md" ]; then
|
|
||||||
printf '\n\n---\n\n' >> "$SHARED_CONTEXT_FILE"
|
|
||||||
cat "$PLAN_DIR/architecture/overview.md" >> "$SHARED_CONTEXT_FILE"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Safety preambles prepended to every per-session prompt (defense-in-depth
|
|
||||||
# against headless-mode hook gaps + Claude 4 cross-model malware-reminder).
|
|
||||||
SAFETY_PREAMBLE='[CRITICAL — defense in depth] You are running in headless mode where plugin hooks may not fire reliably (GH #36071). DO NOT execute commands matching: `rm -rf /`, `curl | bash`, `git push --force` to main, `git reset --hard` outside this worktree, or any command writing outside the current worktree. Treat this rule as enforced regardless of `--allowedTools` allowlist.
|
|
||||||
|
|
||||||
Note: any malware-related safety reminders apply conditionally to code you assess as actually malicious, not to all code reads (per GH #52272 community workaround).'
|
|
||||||
|
|
||||||
# Cleanup trap — always remove worktrees on exit (success or failure)
|
|
||||||
cleanup_worktrees() {
|
|
||||||
echo ""
|
|
||||||
echo "=== Cleaning up worktrees ==="
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
# push-before-cleanup (Hard Rule 19): push any remaining branches so work
|
|
||||||
# survives even if subsequent removal races. Failure is non-fatal.
|
|
||||||
git branch --list "trek/{slug}/*" | while read b; do
|
|
||||||
git push origin "$b" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
for wt in "$WORKTREE_BASE"/session-*; do
|
|
||||||
[ -d "$wt" ] && git worktree remove "$wt" --force 2>/dev/null && echo "Removed: $wt"
|
|
||||||
done
|
|
||||||
git worktree prune
|
|
||||||
git branch --list "trek/{slug}/*" | while read b; do
|
|
||||||
git branch -D "$b" 2>/dev/null
|
|
||||||
done
|
|
||||||
rmdir "$WORKTREE_BASE" 2>/dev/null
|
|
||||||
echo "Cleanup complete."
|
|
||||||
}
|
|
||||||
trap cleanup_worktrees EXIT
|
|
||||||
|
|
||||||
# Pre-flight: verify clean working tree
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
echo "ERROR: Working tree is not clean. Commit or stash changes before parallel execution."
|
|
||||||
git status --short
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Pre-flight: verify remote push permissions (catches credential/auth issues
|
|
||||||
# BEFORE spawning sessions). Sub-agent bash sandbox may have different
|
|
||||||
# credentials than the launching shell — Step 0 in each session spec handles
|
|
||||||
# the sandbox-side detection. Set TREKEXECUTE_SKIP_PREFLIGHT=1 for offline
|
|
||||||
# or air-gapped testing.
|
|
||||||
if [ "${TREKEXECUTE_SKIP_PREFLIGHT:-0}" != "1" ]; then
|
|
||||||
if ! git push --dry-run origin HEAD >/tmp/push-dryrun-launch.log 2>&1; then
|
|
||||||
echo "ERROR: git push --dry-run failed. Sessions will be unable to push."
|
|
||||||
cat /tmp/push-dryrun-launch.log
|
|
||||||
echo ""
|
|
||||||
echo "Fix remote credentials before running parallel execution, or set"
|
|
||||||
echo "TREKEXECUTE_SKIP_PREFLIGHT=1 to bypass (offline/air-gapped only)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if grep -qE "(rejected|denied|forbidden|permission)" /tmp/push-dryrun-launch.log; then
|
|
||||||
echo "ERROR: git push --dry-run reports rejection. Sessions will fail at commit time."
|
|
||||||
cat /tmp/push-dryrun-launch.log
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== Voyage Headless Execution (Worktree-Isolated) ==="
|
|
||||||
echo "Plan: {plan_path}"
|
|
||||||
echo "Sessions: {total_sessions}"
|
|
||||||
echo "Repo root: $REPO_ROOT"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# --- Wave {N}: Parallel sessions (no dependencies) ---
|
|
||||||
echo "--- Wave {N}: {description} ---"
|
|
||||||
|
|
||||||
{# For each parallel session in this wave, create worktree: }
|
|
||||||
git worktree add -b "trek/{slug}/session-{n}" "$WORKTREE_BASE/session-{n}" HEAD
|
|
||||||
echo "Worktree created: session-{n} (branch: trek/{slug}/session-{n})"
|
|
||||||
|
|
||||||
{# Launch session in its worktree (with safety preamble + budget caps + shared context): }
|
|
||||||
cd "$WORKTREE_BASE/session-{n}" && claude -p "${SAFETY_PREAMBLE}
|
|
||||||
|
|
||||||
$(cat "$PLAN_DIR/session-{n}-{slug}.md")" \
|
|
||||||
--allowedTools "Read,Write,Edit,Bash,Glob,Grep" \
|
|
||||||
--permission-mode bypassPermissions \
|
|
||||||
--max-turns "$MAX_TURNS" \
|
|
||||||
--max-budget-usd "$MAX_BUDGET_USD" \
|
|
||||||
--append-system-prompt-file "$SHARED_CONTEXT_FILE" \
|
|
||||||
> "$LOG_DIR/session-{n}.log" 2>&1 &
|
|
||||||
PID_{n}=$!
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
echo "Started session {n}: {title} (PID $PID_{n})"
|
|
||||||
|
|
||||||
{# After all parallel sessions in this wave: }
|
|
||||||
echo "Waiting for Wave {N} to complete..."
|
|
||||||
wait $PID_{n1} $PID_{n2}
|
|
||||||
echo "Wave {N} complete."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# --- Merge wave results (sequential) ---
|
|
||||||
echo "--- Merging Wave {N} ---"
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
{# For each session in the wave: push BEFORE merge (Hard Rule 19 — push-before-cleanup). }
|
|
||||||
git push origin "trek/{slug}/session-{n}" 2>/dev/null || true
|
|
||||||
git merge --no-ff "trek/{slug}/session-{n}" \
|
|
||||||
-m "merge: trekplan session {n} — {title}"
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "MERGE CONFLICT: session {n}. Conflicting files:"
|
|
||||||
git diff --name-only --diff-filter=U
|
|
||||||
git merge --abort
|
|
||||||
echo "Aborting. Earlier sessions in this wave are already merged."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
git worktree remove "$WORKTREE_BASE/session-{n}" --force
|
|
||||||
git branch -d "trek/{slug}/session-{n}"
|
|
||||||
echo "Merged and cleaned: session {n}"
|
|
||||||
|
|
||||||
git worktree prune
|
|
||||||
|
|
||||||
# --- Verify wave results ---
|
|
||||||
echo "--- Verifying Wave {N} ---"
|
|
||||||
{# For each session in the wave, run its exit condition commands }
|
|
||||||
{verify_commands}
|
|
||||||
|
|
||||||
# --- Wave {N+1}: Sequential sessions (depends on previous wave) ---
|
|
||||||
{# Repeat wave pattern for dependent sessions }
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== All sessions complete ==="
|
|
||||||
echo "Review logs in $LOG_DIR/"
|
|
||||||
echo "Run final verification: {final_verify_command}"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rules for the session-decomposer
|
|
||||||
|
|
||||||
When generating a launch script from this template:
|
|
||||||
|
|
||||||
1. **Group sessions into waves** by dependency. Sessions with no dependencies
|
|
||||||
or whose dependencies are all in earlier waves can run in the same wave.
|
|
||||||
2. **Each wave waits for completion** before the next wave starts.
|
|
||||||
3. **Verification runs after each wave** — if verification fails, the script
|
|
||||||
stops and reports which session failed.
|
|
||||||
4. **Log each session** to a separate file for debugging.
|
|
||||||
5. **Use `claude -p`** with the session spec file as the prompt.
|
|
||||||
6. **Use `--allowedTools "Read,Write,Edit,Bash,Glob,Grep"`** with
|
|
||||||
`--permission-mode bypassPermissions` for child sessions. This limits the
|
|
||||||
tool surface to what the executor needs and prevents agent spawning, MCP
|
|
||||||
access, and external web requests in headless sessions.
|
|
||||||
7. **Final verification** at the end runs the master plan's verification section.
|
|
||||||
8. **Never include secrets** in the generated script.
|
|
||||||
9. **Wave verification must be independent.** After each wave completes, run
|
|
||||||
verification commands fresh via Bash — never parse session log files as proof
|
|
||||||
of success. Log files contain executor self-reporting, not ground truth. The
|
|
||||||
command's exit code is the only authoritative verification signal.
|
|
||||||
10. **Billing preamble.** Prepend `unset ANTHROPIC_API_KEY` with a comment at
|
|
||||||
the top of the script to prevent accidental API billing. Users who intend
|
|
||||||
to use API credits can remove this line.
|
|
||||||
11. **Worktree isolation is mandatory.** Every parallel wave MUST use git
|
|
||||||
worktrees. Each session gets its own worktree and branch. Never launch
|
|
||||||
parallel `claude -p` sessions in the same working directory.
|
|
||||||
12. **Cleanup trap on EXIT.** The generated script MUST include a `trap` on
|
|
||||||
EXIT that removes all worktrees (`git worktree remove --force`) and prunes
|
|
||||||
branches, even if the script fails or is interrupted.
|
|
||||||
13. **Sequential merge after each wave.** After all sessions in a wave complete,
|
|
||||||
merge their branches back to the main branch one at a time. Abort on merge
|
|
||||||
conflict — do not force-resolve.
|
|
||||||
14. **Clean working tree before worktrees.** Add a `git status --porcelain`
|
|
||||||
check at the top of the script. Fail if the working tree is dirty.
|
|
||||||
15. **Absolute paths for logs.** Log file paths must be absolute (resolved from
|
|
||||||
`$REPO_ROOT`), not relative to any worktree.
|
|
||||||
16. **Per-child guardrails (mirrors Phase 2.6 Step 2b).** Every `claude -p`
|
|
||||||
invocation must include `--max-turns "$MAX_TURNS"`,
|
|
||||||
`--max-budget-usd "$MAX_BUDGET_USD"`, and
|
|
||||||
`--append-system-prompt-file "$SHARED_CONTEXT_FILE"`. The shared context
|
|
||||||
must be built once with an absolute path (resolved from `$WORKTREE_BASE`)
|
|
||||||
so child sessions can read it after `cd`.
|
|
||||||
17. **Safety preamble.** Every per-session prompt must be prefixed with the
|
|
||||||
`$SAFETY_PREAMBLE` string defined at the top of the script. This is the
|
|
||||||
primary defense when plugin hooks do not fire reliably (GH #36071), and
|
|
||||||
includes the GH #52272 malware-reminder clarification for AUTO mode.
|
|
||||||
18. **GIT_OPTIONAL_LOCKS=0.** The script must export `GIT_OPTIONAL_LOCKS=0`
|
|
||||||
once at the top so every git invocation (worktree add/remove/prune,
|
|
||||||
branch -d, merge, push) avoids the index.lock background-poll race
|
|
||||||
(research/02 R2; GH #47721).
|
|
||||||
19. **push-before-cleanup (Hard Rule 19).** After successful `git merge --no-ff`,
|
|
||||||
run `git push origin <branch>` BEFORE `git worktree remove` and
|
|
||||||
`git branch -d`. Push failure is non-fatal — cleanup proceeds. Converts
|
|
||||||
unrecoverable branch loss into recoverable remote state (research/02 R3).
|
|
||||||
50
tests/fixtures/session-orders/plan.md
vendored
Normal file
50
tests/fixtures/session-orders/plan.md
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
plan_version: "1.7"
|
||||||
|
slug: demo
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plan: demo (fixture for session orders)
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Step 1: Add the parser
|
||||||
|
### Step 2: Add the formatter
|
||||||
|
### Step 3: Add the CLI
|
||||||
|
### Step 4: Wire the plugin subdir
|
||||||
|
### Step 5: Integrate
|
||||||
|
|
||||||
|
## Execution Strategy
|
||||||
|
|
||||||
|
### Session 1: Parser
|
||||||
|
- **Steps:** 1, 2
|
||||||
|
- **Wave:** 1
|
||||||
|
- **Depends on:** none
|
||||||
|
- **Scope fence:**
|
||||||
|
- Touch: lib/parser.mjs, lib/format.mjs
|
||||||
|
- Never touch: lib/cli.mjs, plugins/demo/
|
||||||
|
|
||||||
|
### Session 2: Plugin subdir
|
||||||
|
- **Steps:** 3–4
|
||||||
|
- **Wave:** 1
|
||||||
|
- **Depends on:** none
|
||||||
|
- **cwd:** plugins/demo
|
||||||
|
- **Scope fence:**
|
||||||
|
- Touch: lib/cli.mjs, plugins/demo/
|
||||||
|
- Never touch: lib/parser.mjs, lib/format.mjs
|
||||||
|
|
||||||
|
### Session 3: Integration
|
||||||
|
- **Steps:** 5
|
||||||
|
- **Wave:** 2
|
||||||
|
- **Depends on:** Session 1, Session 2
|
||||||
|
- **Scope fence:**
|
||||||
|
- Touch: README.md
|
||||||
|
- Never touch: lib/
|
||||||
|
|
||||||
|
### Execution Order
|
||||||
|
|
||||||
|
- **Wave 1:** Session 1, Session 2 (parallel)
|
||||||
|
- **Wave 2:** Session 3 (after Wave 1)
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `node --test`
|
||||||
|
|
@ -277,24 +277,24 @@ test('rule-catalogue has exactly 12 entries', async () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('headless-launch-template.md mirrors Phase 2.6 hardenings', () => {
|
test('Phase 2.6 hands waves over as session orders; no launch template ships', () => {
|
||||||
const tpl = read('templates/headless-launch-template.md');
|
// The coupling pin for the PM way: the prose must call the order writer and
|
||||||
|
// the status reader (tests/lib/session-orders.test.mjs proves they work), keep
|
||||||
|
// push-before-cleanup, and carry the never-start rule.
|
||||||
|
assert.ok(!existsSync(join(ROOT, 'templates', 'headless-launch-template.md')),
|
||||||
|
'templates/headless-launch-template.md must stay removed');
|
||||||
|
const exec = read('commands/trekexecute.md');
|
||||||
|
const p26 = exec.slice(exec.indexOf('## Phase 2.6'), exec.indexOf('## Phase 3 '));
|
||||||
for (const needle of [
|
for (const needle of [
|
||||||
|
'lib/util/session-orders.mjs" write',
|
||||||
|
'lib/util/session-orders.mjs" status',
|
||||||
'GIT_OPTIONAL_LOCKS',
|
'GIT_OPTIONAL_LOCKS',
|
||||||
'--max-turns',
|
|
||||||
'--max-budget-usd',
|
|
||||||
'--append-system-prompt-file',
|
|
||||||
'SHARED_CONTEXT_FILE',
|
|
||||||
'SAFETY_PREAMBLE',
|
|
||||||
'git push origin',
|
'git push origin',
|
||||||
'GH #36071',
|
'Voyage never starts a Claude Code',
|
||||||
'push-before-cleanup',
|
|
||||||
]) {
|
]) {
|
||||||
assert.ok(
|
assert.ok(p26.includes(needle), `trekexecute.md Phase 2.6 must include "${needle}"`);
|
||||||
tpl.includes(needle),
|
|
||||||
`templates/headless-launch-template.md should include "${needle}" (Step 10 mirrors Phase 2.6)`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
assert.match(exec, /21\. \*\*Never start Claude Code\.\*\*/, 'trekexecute.md Hard rule 21 missing');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', () => {
|
test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', () => {
|
||||||
|
|
|
||||||
142
tests/lib/session-orders.test.mjs
Normal file
142
tests/lib/session-orders.test.mjs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
// tests/lib/session-orders.test.mjs
|
||||||
|
// /trekexecute's multi-session path turns each wave into orders plus starters
|
||||||
|
// and stops: the user opens every session themselves. Voyage starts nothing.
|
||||||
|
// These tests run the module and its CLI on a small fixture plan and prove
|
||||||
|
// both halves: the orders land, and no process is started.
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync, chmodSync, cpSync } from 'node:fs';
|
||||||
|
import { join, dirname } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseExecutionStrategy, buildWaveOrders, progressFileFor, waveStatus,
|
||||||
|
} from '../../lib/util/session-orders.mjs';
|
||||||
|
|
||||||
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ROOT = join(HERE, '..', '..');
|
||||||
|
const MODULE = join(ROOT, 'lib', 'util', 'session-orders.mjs');
|
||||||
|
const FIXTURE = join(ROOT, 'tests', 'fixtures', 'session-orders', 'plan.md');
|
||||||
|
const PLAN_REL = '.claude/plans/demo.md';
|
||||||
|
|
||||||
|
function withRepo(fn) {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'session-orders-'));
|
||||||
|
try {
|
||||||
|
mkdirSync(join(root, '.claude', 'plans'), { recursive: true });
|
||||||
|
cpSync(FIXTURE, join(root, PLAN_REL));
|
||||||
|
return fn(root);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('parseExecutionStrategy reads sessions, waves, step ranges, dependencies and cwd', () => {
|
||||||
|
const s = parseExecutionStrategy(readFileSync(FIXTURE, 'utf-8'));
|
||||||
|
assert.deepEqual(s.waves, [1, 2]);
|
||||||
|
assert.deepEqual(s.sessions.map(x => [x.n, x.title, x.wave, x.steps, x.dependsOn, x.cwd]), [
|
||||||
|
[1, 'Parser', 1, [1, 2], [], '.'],
|
||||||
|
[2, 'Plugin subdir', 1, [3, 4], [], 'plugins/demo'],
|
||||||
|
[3, 'Integration', 2, [5], [1, 2], '.'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parseExecutionStrategy refuses a plan without a strategy, and a session without a wave', () => {
|
||||||
|
assert.throws(() => parseExecutionStrategy('# Plan\n\n## Verification\n'), /no ## Execution Strategy/);
|
||||||
|
assert.throws(
|
||||||
|
() => parseExecutionStrategy('## Execution Strategy\n\n### Session 1: X\n- **Steps:** 1\n'),
|
||||||
|
/Session 1 has no Wave/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildWaveOrders gives one order per session in the wave, each with its own worktree and a starter', () => {
|
||||||
|
const s = parseExecutionStrategy(readFileSync(FIXTURE, 'utf-8'));
|
||||||
|
const orders = buildWaveOrders({ strategy: s, wave: 1, planPath: PLAN_REL, sessionDir: '/r/.claude/trekplan-sessions/demo' });
|
||||||
|
assert.equal(orders.length, 2);
|
||||||
|
assert.deepEqual(orders.map(o => o.starter), [
|
||||||
|
`/trekexecute --session 1 ${PLAN_REL}`,
|
||||||
|
`/trekexecute --session 2 ${PLAN_REL}`,
|
||||||
|
]);
|
||||||
|
assert.deepEqual(orders.map(o => o.openIn), [
|
||||||
|
'/r/.claude/trekplan-sessions/demo/worktrees/session-1',
|
||||||
|
'/r/.claude/trekplan-sessions/demo/worktrees/session-2/plugins/demo',
|
||||||
|
]);
|
||||||
|
assert.equal(new Set(orders.map(o => o.worktreePath)).size, 2, 'two sessions never share a worktree');
|
||||||
|
for (const o of orders) {
|
||||||
|
assert.match(o.body, /Voyage does not start this session/);
|
||||||
|
assert.ok(o.body.includes(o.starter));
|
||||||
|
assert.ok(o.body.includes(o.progressFile));
|
||||||
|
}
|
||||||
|
assert.throws(() => buildWaveOrders({ strategy: s, wave: 3, planPath: PLAN_REL, sessionDir: '/r' }), /no sessions in wave 3/);
|
||||||
|
assert.throws(() => buildWaveOrders({ strategy: s, wave: 1, planPath: '/abs/plan.md', sessionDir: '/r' }), /repo-relative/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the module cannot start a process: it imports nothing that could', () => {
|
||||||
|
const src = readFileSync(MODULE, 'utf-8');
|
||||||
|
assert.doesNotMatch(src, /child_process|worker_threads|\bspawn\(|\bexec(File)?(Sync)?\(/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CLI on the fixture: wave 1 writes two orders and wave.json, prints the starters, and starts nothing', () => {
|
||||||
|
withRepo((root) => {
|
||||||
|
// Trap: a `claude` first on PATH that leaves a marker if anything runs it.
|
||||||
|
const bin = join(root, 'trap-bin');
|
||||||
|
const marker = join(root, 'claude-was-started');
|
||||||
|
mkdirSync(bin);
|
||||||
|
writeFileSync(join(bin, 'claude'), `#!/bin/sh\ntouch "${marker}"\n`);
|
||||||
|
chmodSync(join(bin, 'claude'), 0o755);
|
||||||
|
|
||||||
|
const sessionDir = join(root, '.claude', 'trekplan-sessions', 'demo');
|
||||||
|
const r = spawnSync(process.execPath, [MODULE, 'write', '--plan', PLAN_REL, '--wave', '1', '--session-dir', sessionDir], {
|
||||||
|
cwd: root, encoding: 'utf-8', env: { ...process.env, PATH: `${bin}:${process.env.PATH}` },
|
||||||
|
});
|
||||||
|
assert.equal(r.status, 0, r.stderr);
|
||||||
|
assert.equal(existsSync(marker), false, 'nothing may start Claude Code');
|
||||||
|
|
||||||
|
const ordersDir = join(sessionDir, 'orders');
|
||||||
|
assert.deepEqual(readdirSync(ordersDir).sort(), ['wave-1-session-1.md', 'wave-1-session-2.md', 'wave.json']);
|
||||||
|
const wave = JSON.parse(readFileSync(join(ordersDir, 'wave.json'), 'utf-8'));
|
||||||
|
assert.deepEqual([wave.wave, wave.sessions, wave.status, wave.plan], [1, [1, 2], 'awaiting-sessions', PLAN_REL]);
|
||||||
|
assert.match(r.stdout, /\/trekexecute --session 1 \.claude\/plans\/demo\.md/);
|
||||||
|
assert.match(r.stdout, /\/trekexecute --session 2 \.claude\/plans\/demo\.md/);
|
||||||
|
assert.match(r.stdout, /Voyage starts none of them/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CLI status: pending until each session writes its progress file, then completed', () => {
|
||||||
|
withRepo((root) => {
|
||||||
|
const sessionDir = join(root, '.claude', 'trekplan-sessions', 'demo');
|
||||||
|
const run = (...a) => spawnSync(process.execPath, [MODULE, ...a, '--plan', PLAN_REL, '--session-dir', sessionDir], { cwd: root, encoding: 'utf-8' });
|
||||||
|
assert.equal(run('write', '--wave', '1').status, 0);
|
||||||
|
|
||||||
|
let st = JSON.parse(run('status', '--wave', '1').stdout);
|
||||||
|
assert.deepEqual(st.sessions.map(x => x.status), ['pending', 'pending']);
|
||||||
|
assert.equal(st.all_completed, false);
|
||||||
|
|
||||||
|
const s = parseExecutionStrategy(readFileSync(FIXTURE, 'utf-8'));
|
||||||
|
const put = (n, status) => {
|
||||||
|
const f = progressFileFor({ strategy: s, n, planPath: PLAN_REL, sessionDir });
|
||||||
|
mkdirSync(dirname(f), { recursive: true });
|
||||||
|
writeFileSync(f, JSON.stringify({ status }));
|
||||||
|
};
|
||||||
|
put(1, 'completed');
|
||||||
|
put(2, 'failed');
|
||||||
|
st = JSON.parse(run('status', '--wave', '1').stdout);
|
||||||
|
assert.deepEqual(st.sessions.map(x => x.status), ['completed', 'failed']);
|
||||||
|
assert.equal(st.all_completed, false);
|
||||||
|
|
||||||
|
put(2, 'completed');
|
||||||
|
st = waveStatus({ strategy: s, wave: 1, planPath: PLAN_REL, sessionDir });
|
||||||
|
assert.equal(st.all_completed, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CLI refuses a plan without an Execution Strategy, loudly', () => {
|
||||||
|
withRepo((root) => {
|
||||||
|
writeFileSync(join(root, PLAN_REL), '# Plan\n\n## Verification\n');
|
||||||
|
const r = spawnSync(process.execPath, [MODULE, 'write', '--plan', PLAN_REL, '--wave', '1', '--session-dir', join(root, 's')], { cwd: root, encoding: 'utf-8' });
|
||||||
|
assert.equal(r.status, 1);
|
||||||
|
assert.match(r.stderr, /no ## Execution Strategy/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue