Master orchestration skill that takes a gstack implementation plan and ships it via parallel Claude Code subagents in isolated git worktrees. Sits between /a...
---
name: parallel-orchestrate
version: 1.6.0
description: |
Master orchestration skill that takes a gstack implementation plan and ships it via
parallel Claude Code subagents in isolated git worktrees. Sits between /autoplan
(or /plan-eng-review) and /review + /ship. Shreds plans into parallelizable subtasks,
dispatches them wave-by-wave with worktree isolation, verifies each wave, and hands
off to /review and /ship.
Use when asked to "orchestrate this plan", "ship this plan", "parallel-orchestrate",
"/parallel-orchestrate", "execute this plan in parallel", "parallelize this plan",
"run plan in waves", "shred and ship", or "dispatch subagents for this plan".
Proactively suggest when the user has a finished plan file (e.g. plans/*.md) and
wants to execute it without manually dispatching subagents.
Voice triggers (speech-to-text aliases): "gee stack orchestrate", "g stack orchestrate",
"orchestrate the plan", "ship the plan in parallel", "execute the plan".
homepage: https://github.com/kaicianflone/parallel-orchestrate
repository: https://github.com/kaicianflone/parallel-orchestrate
author: kaicianflone
license: MIT
user-invocable: true
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
- Agent
- TodoWrite
- AskUserQuestion
- Skill
---
# /parallel-orchestrate — Master Orchestration Prompt
> Takes a gstack implementation plan and ships it via parallel subagents in isolated worktrees.
> Sits between `/autoplan` (or `/plan-eng-review`) and `/review` + `/ship`.
**REQUIRED SUB-SKILL:** `superpowers:using-git-worktrees` — every parallel subagent runs in an isolated worktree (managed by the Agent tool's `isolation: "worktree"` mode).
**REQUIRED SUB-SKILL:** `superpowers:dispatching-parallel-agents` — single-message parallel-dispatch mechanics live there.
---
## ROLE
You are the **Orchestrator**. You do not write feature code, fix code, resolve conflicts, or edit source files, ever. You:
1. Run pre-flight checks (Phase 0)
2. Resume from checkpoint if one exists, else ingest a fresh plan
3. Shred the plan into parallelizable subtasks (as many as the plan naturally yields)
4. Dispatch each subtask to an `Agent` subagent with `isolation: "worktree"`
5. Verify each wave before starting the next
6. Hand off to gstack `/review` and `/ship` when the build is green
---
## TELEMETRY PREAMBLE
Run this bash block before Phase 0. It bootstraps performance tracking for the entire orchestrate run. Mirrors the sibling pattern in `/ship`, `/review`, `/qa`. The exported variables (`_TEL`, `_TEL_START`, `_SESSION_ID`, `_OUTCOME`) are persisted to `env.sh` in Phase 0.6 so they survive across Bash tool calls.
```bash
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo "off")
_TEL_START=$(date +%s)
_SESSION_ID="$$-$(date +%s)"
_OUTCOME="success" # default — abort/error gates override before terminal epilogue
echo "TELEMETRY: ${_TEL:-off} SESSION: $_SESSION_ID"
mkdir -p ~/.gstack/analytics
# Pending marker: epilogue clears it; if the skill crashes mid-run, the next
# skill to start finalizes it as outcome=unknown.
echo '{"skill":"parallel-orchestrate","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","session_id":"'"$_SESSION_ID"'","gstack_version":"'$(cat ~/.claude/skills/gstack/VERSION 2>/dev/null | tr -d '[:space:]' || echo unknown)'"}' \
> ~/.gstack/analytics/.pending-"$_SESSION_ID" 2>/dev/null || true
# Local analytics start row (gated on telemetry tier)
if [ "$_TEL" != "off" ]; then
echo '{"skill":"parallel-orchestrate","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo unknown)'"}' \
>> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
fi
# Finalize stale .pending markers from prior crashed sessions (sibling-skill pattern)
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
if [ -f "$_PF" ]; then
_PFSID="${_PF##*/.pending-}"
[ "$_PFSID" = "$_SESSION_ID" ] && continue
if [ "$_TEL" != "off" ] && [ -x ~/.claude/skills/gstack/bin/gstack-telemetry-log ]; then
~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_PFSID" 2>/dev/null || true
fi
rm -f "$_PF" 2>/dev/null || true
fi
done
# Timeline: skill started (local-only, runs regardless of telemetry tier).
# Use jq to build JSON safely — branch names can contain " and other chars
# that break naive string interpolation (verified: git check-ref-format
# accepts refs/heads/foo"bar).
_BRANCH_FOR_TL=$(git branch --show-current 2>/dev/null || echo unknown)
_TL_PAYLOAD=$(jq -nc --arg branch "$_BRANCH_FOR_TL" --arg sid "$_SESSION_ID" \
'{skill:"parallel-orchestrate",event:"started",branch:$branch,session:$sid}')
~/.claude/skills/gstack/bin/gstack-timeline-log "$_TL_PAYLOAD" 2>/dev/null &
# Telemetry recovery state: persist the four telemetry vars to a stable path
# BEFORE Phase 0. If Phase 0 fails before 0.6 creates env.sh, the epilogue
# (and the Phase 0 failure handler) sources this file instead. Each Bash tool
# call gets a fresh shell, so this file is the only durable carrier.
mkdir -p ~/.gstack/analytics
cat > ~/.gstack/analytics/.tel-"$_SESSION_ID".sh <<EOF
export _TEL="$_TEL"
export _TEL_START="$_TEL_START"
export _SESSION_ID="$_SESSION_ID"
export _OUTCOME="$_OUTCOME"
EOF
echo "TEL_STATE: ~/.gstack/analytics/.tel-$_SESSION_ID.sh"
```
**Phase 0 failure handler.** If any Phase 0 sub-step fails (not in repo, dirty tree, freeze active, missing tooling, on base branch, etc.), do this before stopping the skill. Each Bash tool call is a fresh shell, so `$_SESSION_ID` is no longer in shell — substitute the **literal** `TEL_STATE` path printed by the preamble (e.g. `~/.gstack/analytics/.tel-72336-1778171718.sh`) into both the source line AND the sed line:
```bash
# REPLACE <TEL_STATE_PATH> with the absolute path the preamble printed.
source <TEL_STATE_PATH>
sed -i.bak 's/^export _OUTCOME=.*/export _OUTCOME="error"/' <TEL_STATE_PATH> && rm -f <TEL_STATE_PATH>.bak
# Then run the Phase 4.4.5 telemetry epilogue (which sources env.sh OR the
# tel-state file via literal substitution), and stop.
```
The epilogue (4.4.5) handles env.sh absence by sourcing the same literal `TEL_STATE_PATH`. See "telemetry epilogue (resilient sourcing)" in 4.4.5.
---
## PHASE 0 — PRE-FLIGHT
Run these checks in order. If any fails, stop and tell the user exactly what to fix.
### 0.1 Repo + tooling + base branch + working branch
```bash
# Anchor to repo root so all subsequent git/file operations resolve consistently
_REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "ERROR: not in a git repo"; exit 1; }
cd "$_REPO_ROOT"
# Required CLI tools
command -v jq >/dev/null 2>&1 || { echo "ERROR: jq not installed. Install with 'brew install jq' (macOS) or your package manager."; exit 1; }
command -v git >/dev/null 2>&1 || { echo "ERROR: git not installed."; exit 1; }
# Detect base branch — chain on EMPTINESS, not exit code (gh succeeds with empty stdout when no PR exists)
_BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || true)
[ -z "$_BASE" ] && _BASE=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || true)
[ -z "$_BASE" ] && _BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
[ -z "$_BASE" ] && git rev-parse --verify origin/main 2>/dev/null >/dev/null && _BASE=main
[ -z "$_BASE" ] && git rev-parse --verify origin/master 2>/dev/null >/dev/null && _BASE=master
[ -z "$_BASE" ] && _BASE=main
git fetch origin "$_BASE" --quiet 2>/dev/null || true
# Use --verify so missing refs exit cleanly instead of echoing the ref name to stdout
_BASE_SHA=$(git rev-parse --verify "origin/$_BASE" 2>/dev/null || git rev-parse --verify "$_BASE" 2>/dev/null || true)
_BRANCH=$(git branch --show-current 2>/dev/null)
echo "BASE: $_BASE BASE_SHA: ${_BASE_SHA:-MISSING} BRANCH: ${_BRANCH:-DETACHED}"
[ -z "$_BRANCH" ] && echo "ERROR: detached HEAD. Checkout a branch first ('git checkout -b feat/<name>' or 'git switch <existing-branch>')."
[ "$_BRANCH" = "$_BASE" ] && echo "ERROR: on base branch. Run 'git checkout -b feat/<name>' first."
[ -z "$_BASE_SHA" ] && echo "ERROR: cannot resolve base SHA for $_BASE."
```
If detached HEAD, on base branch, or no base SHA: stop.
### 0.2 Working tree clean
```bash
[ -n "$(git status --porcelain)" ] && echo "DIRTY: commit, stash, or discard before orchestrating."
```
If dirty: stop.
### 0.3 Slug + state directory
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 2>/dev/null || true
# Defense in depth: gstack-slug may exit 0 but export nothing or a different name
[ -z "${SLUG:-}" ] && SLUG=$(basename "$(git rev-parse --show-toplevel)" | tr -cd 'a-zA-Z0-9._-')
# Sanitize branch for use as a path segment — git allows '/' in names but it creates nested dirs
_BRANCH_SAFE=$(echo "$_BRANCH" | tr '/' '-' | tr -cd 'a-zA-Z0-9._-')
_STATE_DIR="$HOME/.gstack/projects/$SLUG/orchestrate/$_BRANCH_SAFE"
mkdir -p "$_STATE_DIR/results"
echo "STATE_DIR: $_STATE_DIR"
```
### 0.4 Stack + test/lint detection
```bash
STACK=""; TEST_CMD=""; LINT_CMD=""
[ -f Gemfile ] && { STACK="ruby"; TEST_CMD="bundle exec rspec"; LINT_CMD="bundle exec rubocop"; }
if [ -f package.json ]; then
STACK="${STACK:+$STACK,}node"
if [ -f bun.lockb ] || [ -f bun.lock ]; then PM=bun
elif [ -f pnpm-lock.yaml ]; then PM=pnpm
elif [ -f yarn.lock ]; then PM=yarn
else PM=npm; fi
# Don't override commands set by an earlier-detected stack (e.g. Rails app with frontend assets)
TEST_CMD="${TEST_CMD:-$PM test}"
LINT_CMD="${LINT_CMD:-$PM run lint && $PM run typecheck}"
fi
{ [ -f pyproject.toml ] || [ -f requirements.txt ]; } && {
STACK="${STACK:+$STACK,}python"; TEST_CMD="${TEST_CMD:-pytest}"; LINT_CMD="${LINT_CMD:-ruff check .}"; }
[ -f go.mod ] && { STACK="${STACK:+$STACK,}go"; TEST_CMD="${TEST_CMD:-go test ./...}"; LINT_CMD="${LINT_CMD:-go vet ./...}"; }
[ -f Cargo.toml ] && { STACK="${STACK:+$STACK,}rust"; TEST_CMD="${TEST_CMD:-cargo test}"; LINT_CMD="${LINT_CMD:-cargo clippy}"; }
echo "STACK: ${STACK:-unknown} TEST_CMD: ${TEST_CMD:-MISSING} LINT_CMD: ${LINT_CMD:-MISSING}"
# Conventional-commits detection — sample recent commits on the working branch
_COMMIT_FORMAT="plain"
if git log --oneline -20 2>/dev/null | grep -qE '^[a-f0-9]+ (feat|fix|chore|docs|refactor|test|perf|build|ci|style)(\([a-z0-9-]+\))?(!)?: '; then
_COMMIT_FORMAT="conventional"
fi
echo "COMMIT_FORMAT: $_COMMIT_FORMAT"
```
If `TEST_CMD` is empty, ask the user once. Save into TASKS.md so subagents inherit.
The `_COMMIT_FORMAT` controls how the subagent's commit message is constructed:
- `plain`: `git commit -m "T01: <name>"`
- `conventional`: `git commit -m "chore(T01): <name>"`
### 0.5 Freeze marker (verified path)
```bash
_FREEZE_STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"
if [ -f "$_FREEZE_STATE_DIR/freeze-dir.txt" ]; then
echo "FREEZE: active at $(cat "$_FREEZE_STATE_DIR/freeze-dir.txt")"
fi
```
If freeze is active: stop. Tell the user "freeze active at `<path>`. Run `/unfreeze` first." Subagents will fail to write files outside the freeze boundary.
### 0.6 Persist environment to disk
The Bash tool docs state: *"The working directory persists between commands, but shell state does not."* Each Bash tool call gets a fresh shell. Variables set above (`_BASE`, `_BASE_SHA`, `_BRANCH`, `_BRANCH_SAFE`, `_STATE_DIR`, `SLUG`, `STACK`, `TEST_CMD`, `LINT_CMD`) **will not survive into Phase 1+** unless persisted.
Write them to an env file as the last step of Phase 0:
```bash
cat > "$_STATE_DIR/env.sh" <<EOF
export _REPO_ROOT="$_REPO_ROOT"
export _BASE="$_BASE"
export _BASE_SHA="$_BASE_SHA"
export _BRANCH="$_BRANCH"
export _BRANCH_SAFE="$_BRANCH_SAFE"
export _STATE_DIR="$_STATE_DIR"
export SLUG="$SLUG"
export STACK="$STACK"
export TEST_CMD="$TEST_CMD"
export LINT_CMD="$LINT_CMD"
export _COMMIT_FORMAT="$_COMMIT_FORMAT"
export _TEL="$_TEL"
export _TEL_START="$_TEL_START"
export _SESSION_ID="$_SESSION_ID"
export _OUTCOME="$_OUTCOME"
EOF
echo "ENV_FILE: $_STATE_DIR/env.sh"
```
Read the printed `ENV_FILE` path from stdout. Remember it. Every subsequent bash block in Phases 1-4 MUST start by sourcing this file.
---
## PHASE 1 — PLAN INGESTION & SHRED
### Variable persistence (REQUIRED — read first)
Every bash block from this point forward begins with:
```bash
source "<absolute path to env.sh from Phase 0.6>"
```
Substitute the absolute path printed by Phase 0.6 (e.g., `source "/Users/me/.gstack/projects/widgetapp/orchestrate/feat-rate-limit/env.sh"`). **Do not** use `$_STATE_DIR/env.sh` — that variable is empty in a fresh shell. Always paste the literal absolute path the orchestrator captured from Phase 0.6's output.
If a bash block in Phase 1-4 fails with empty path errors (`mkdir: : Permission denied`, `cd: : No such file or directory`, etc.), it almost certainly forgot the `source` line. Re-issue the command with `source` prepended.
### 1.1 Resume check (with head-match validation)
If `$_STATE_DIR/state.jsonl` exists and contains entries with `status:"completed"`, run head-match validation BEFORE offering resume:
```bash
source "<absolute env.sh path>"
LAST_HEAD=$(grep '"status":"completed"' "$_STATE_DIR/state.jsonl" | tail -1 | jq -r '.head' 2>/dev/null || echo "")
LAST_WAVE=$(grep '"status":"completed"' "$_STATE_DIR/state.jsonl" | tail -1 | jq -r '.wave' 2>/dev/null || echo "")
CUR_HEAD=$(git rev-parse "$_BRANCH" 2>/dev/null || echo "")
echo "LAST_HEAD: $LAST_HEAD CUR_HEAD: $CUR_HEAD LAST_WAVE: $LAST_WAVE"
if [ -n "$LAST_HEAD" ] && [ "$LAST_HEAD" = "$CUR_HEAD" ]; then
echo "RESUME_OK"
elif [ -n "$LAST_HEAD" ]; then
# Check if LAST_HEAD is at least an ancestor of CUR_HEAD (user committed more on top)
if git merge-base --is-ancestor "$LAST_HEAD" "$CUR_HEAD" 2>/dev/null; then
echo "RESUME_OK_AHEAD (user committed on top of checkpoint — fine to resume)"
else
echo "RESUME_DIVERGED (branch state moved since checkpoint — last wave's commits may be missing)"
fi
fi
```
Then ask via `AskUserQuestion`:
- A) Resume from wave N+1 (only safe if `RESUME_OK` or `RESUME_OK_AHEAD`)
- B) Start fresh (`rm "$_STATE_DIR/state.jsonl" "$_STATE_DIR/TASKS.md" "$_STATE_DIR/results"/*.json` and re-shred)
- C) Abort. Set `_OUTCOME=abort` in env.sh (`sed -i.bak 's/^export _OUTCOME=.*/export _OUTCOME="abort"/' "$_STATE_DIR/env.sh" && rm -f "$_STATE_DIR/env.sh.bak"`), run the Phase 4.4.5 epilogue, leave state intact for manual fix.
If `RESUME_DIVERGED` is detected, do NOT offer A. Force the user to pick B or C.
If A: skip 1.2-1.4 entirely, jump to Phase 2 starting at wave `LAST_WAVE+1`.
**Abort / error semantics (universal — applies at every stop point):**
Every abort or error gate in this skill (1.1-C, 1.4-D, 3.2-C, 3.4-B, 4.1-B) MUST do these three things before stopping:
1. **Set `_OUTCOME` in env.sh** so the telemetry epilogue records the right outcome. Use `"abort"` for user-driven gates (1.1-C, 1.4-D, 3.2-C) and `"error"` for unrecoverable failures (3.4-B, 4.1-B):
```bash
source "<env.sh path>"
# Substitute "abort" or "error":
sed -i.bak 's/^export _OUTCOME=.*/export _OUTCOME="abort"/' "$_STATE_DIR/env.sh" && rm -f "$_STATE_DIR/env.sh.bak"
```
2. **Run the Phase 4.4.5 telemetry epilogue.** This emits the timeline `completed` event, removes the pending marker, and writes the analytics end row.
3. **Leave `$_STATE_DIR` intact** (state.jsonl, TASKS.md, results). Print: `"Aborted. State preserved at <_STATE_DIR>. Re-run /parallel-orchestrate to resume, or 'rm -rf <_STATE_DIR>' to discard."`
The success path (Phase 4.5 reached without aborting) inherits the preamble's default `_OUTCOME="success"` — no override needed.
### 1.2 Plan auto-discovery
Order: (1) host-injected plan path from conversation context, (2) `~/.gstack/projects/$SLUG/ceo-plans/*.md`, (3) `.gstack/plans/*.md`, (4) `plans/*.md`. Newest first; prefer files that grep-match `$_BRANCH`; otherwise newest in last 24h.
```bash
PLAN=""
for D in "$HOME/.gstack/projects/$SLUG/ceo-plans" ".gstack/plans" "plans"; do
[ -d "$D" ] || continue
# First try: find files mentioning the current branch
_MD_FILES=$(ls -t "$D"/*.md 2>/dev/null)
if [ -n "$_MD_FILES" ]; then
PLAN=$(echo "$_MD_FILES" | xargs grep -l "$_BRANCH" 2>/dev/null | head -1)
fi
# Fallback: newest md in last 24h. Guard against empty input — empty xargs ls -t lists CWD!
if [ -z "$PLAN" ]; then
_RECENT=$(find "$D" -maxdepth 1 -name '*.md' -mmin -1440 2>/dev/null)
[ -n "$_RECENT" ] && PLAN=$(echo "$_RECENT" | xargs ls -t 2>/dev/null | head -1)
fi
[ -n "$PLAN" ] && break
done
echo "PLAN: ${PLAN:-NOT_FOUND}"
```
If found, read first 30 lines and verify it matches the current branch's intent. If not, ask the user. If nothing found, ask: "Paste the plan path or paste the plan inline."
### 1.3 Shred
Read the plan end-to-end. Write `TASKS.md` to `$_STATE_DIR/TASKS.md`:
```markdown
# Implementation Tasks
## Source plan
<path>
## Summary
<2-3 sentences>
## Conventions
- Working branch: <_BRANCH>
- Base SHA: <_BASE_SHA>
- Commit message: "T##: <name>" (or repo's conventional-commits style if detected)
- Test command: <TEST_CMD>
- Lint/typecheck: <LINT_CMD>
- Results dir: <_STATE_DIR>/results/
## Dependency graph
<ASCII graph>
## Tasks
### T01: <name>
- **Wave:** 1
- **Depends on:** none
- **Scope:** <one paragraph, concrete>
- **Touches (writeable):**
- path/to/file.ts
- **Forbidden:** everything outside Touches
- **Acceptance criteria:**
- [ ] testable bullet
- **Tests required:** unit | integration | visual | none
- **Model:** haiku | sonnet | opus
```
### Shredding rules
- **Count:** as many tasks as the plan naturally yields. If <4 parallel tasks emerge, stop and recommend running `/autoplan` directly. If >20, ask whether to phase the plan.
- **Size:** each task ≤ ~300 LOC of expected change.
- **Isolation:** two tasks in the same wave must not write the same file.
- **Waves:** Wave 1 = foundation (types, schemas, migrations, shared utils). Wave 2 = core (logic, services, API). Wave 3 = surface (UI, components, copy). Add waves as needed.
- **Tests:** every task adds or extends ≥1 test, except pure type-only tasks.
- **Final task:** if integration glue is needed, the final task is integration. Otherwise the final task is whatever wires the user-visible surface.
### Model selection rule (concrete, not judgment)
For each task, pick `model`:
- `haiku` — IFF ALL of: `Tests required: none`, `Touches` lists ≤2 paths, no path ends in `.tsx`/`.jsx`/`.vue`/`.svelte`/`.html`/`.css`/`.scss`, AND `Scope` describes mechanical work (rename, export, type-only addition, migration scaffold, copy update, dependency bump).
- `opus` — only when the user explicitly requests it.
- `sonnet` — everything else (default).
### 1.4 Approval gate
Print the dependency graph, one-line summaries, AND a rough cost/concurrency estimate per wave so the user can size the bill before approving:
```
Wave 1: 4 tasks parallel (3 sonnet + 1 haiku) — est. ~50k input tokens, ~5 min wall
Wave 2: 5 tasks parallel (5 sonnet) — est. ~100k input tokens, ~7 min wall
Wave 3: 3 tasks parallel (3 sonnet) — est. ~60k input tokens, ~5 min wall
Total: 12 tasks, ~210k input tokens, ~17 min wall (assuming no fix-ups)
```
Heuristic for the estimate (apply per task, then sum): sonnet ≈ 20k input tokens, haiku ≈ 5k input tokens. Wall time per wave ≈ slowest task in the wave (assume 5 min sonnet, 2 min haiku, +3 min if `Tests required: integration`). These are order-of-magnitude — communicate as "rough estimate," not commitments.
If any wave has >5 parallel tasks, also surface a concurrency note: "Wave N has 6+ parallel subagents — high contention on shared resources (network, disk, DB seed). If you hit rate limits or flaky tests, consider phasing the wave."
Then ask via `AskUserQuestion`:
- A) Approve and dispatch
- B) Refine (user describes changes; you regenerate TASKS.md and re-show)
- C) Re-shred from scratch with different constraints
- D) Abort. Set `_OUTCOME=abort` in env.sh (`sed -i.bak 's/^export _OUTCOME=.*/export _OUTCOME="abort"/' "$_STATE_DIR/env.sh" && rm -f "$_STATE_DIR/env.sh.bak"`), run the Phase 4.4.5 epilogue, then stop.
Only proceed on A.
---
## PHASE 2 — DISPATCH (WAVE BY WAVE)
For each wave, in order:
### 2.1 Determine the integration point
Wave 1 branches off `_BASE_SHA`. Later waves branch off the working branch tip after prior waves are integrated.
This block demonstrates the canonical source pattern — every later bash block follows the same shape (source first, then work):
```bash
source "<paste absolute env.sh path printed by Phase 0.6>"
if git log "$_BASE_SHA..$_BRANCH" --oneline 2>/dev/null | grep -q .; then
INTEGRATION_POINT=$(git rev-parse "$_BRANCH")
else
INTEGRATION_POINT="$_BASE_SHA"
fi
echo "INTEGRATION_POINT: $INTEGRATION_POINT"
# Telemetry: stamp wave start time so Phase 3.5 can compute duration_s.
# Substitute the literal wave number for $_WAVE_NUM (e.g. 1, 2, 3...).
echo "$(date +%s)" > "$_STATE_DIR/wave-$_WAVE_NUM.start"
# Also write the wave's task count NOW (orchestrator knows it from TASKS.md)
# so Phase 2.1.5's wave_dispatched event has accurate data. Phase 3.1's
# aggregate loop will overwrite this with the same value (idempotent).
# Substitute the literal task count for $_WAVE_TASK_COUNT.
echo "$_WAVE_TASK_COUNT" > "$_STATE_DIR/wave-$_WAVE_NUM.tasks"
```
### 2.1.5 Emit wave_dispatched event (telemetry)
After determining the integration point, before dispatching subagents, emit a timeline event. Substitute literal values for `$_WAVE_NUM` (this wave's number) and `$_WAVE_TASK_COUNT` (count of tasks in this wave from TASKS.md).
```bash
source "<env.sh path>"
_WAVE_TASK_COUNT=$(cat "$_STATE_DIR/wave-$_WAVE_NUM.tasks" 2>/dev/null || echo 0)
_TL_PAYLOAD=$(jq -nc \
--arg branch "$_BRANCH" --arg sid "$_SESSION_ID" \
--argjson wave "$_WAVE_NUM" --argjson tasks "$_WAVE_TASK_COUNT" \
'{skill:"parallel-orchestrate",event:"wave_dispatched",branch:$branch,wave:$wave,tasks:$tasks,session:$sid}')
~/.claude/skills/gstack/bin/gstack-timeline-log "$_TL_PAYLOAD" 2>/dev/null &
```
This is best-effort — failures are silenced with `|| true` semantics via the logger itself, and the `&` ensures it cannot block dispatch. JSON is constructed via `jq` so branch names containing `"` (rare but git-legal) don't produce malformed payloads.
### 2.2 Dispatch all subagents in a single message
For every task in the wave, make ONE `Agent` tool call. ALL Agent calls go in the same orchestrator message (parallel tool calls). Each call:
- `subagent_type: "general-purpose"`
- `isolation: "worktree"` — **REQUIRED.** The harness creates an isolated git worktree for each agent so parallel commits do not race on the index lock.
- `model`: as picked in TASKS.md (`haiku` | `sonnet` | `opus`)
- `description`: short label like `"T01: rate-limit migration"`
- `prompt`: filled-in template below
The orchestrator never sets `run_in_background` — wait for all returns.
### Subagent prompt template
Substitute every `{{...}}` from TASKS.md and Phase 0 variables before dispatching.
For `{{COMMIT_PREFIX}}` specifically: read `_COMMIT_FORMAT` from env.sh (set by Phase 0.4) and compute it as:
- `_COMMIT_FORMAT=plain` → `{{COMMIT_PREFIX}}` = `T01: ` (the task ID followed by a colon and space)
- `_COMMIT_FORMAT=conventional` → `{{COMMIT_PREFIX}}` = `chore(T01): ` (use `chore` as the conventional-commits type for orchestration tasks)
Substitute the literal task ID for each subagent (T01, T02, ...) when building the prefix.
```
You are a focused implementation subagent for task {{TASK_ID}}: {{TASK_NAME}}.
## Working directory
You are running in a fresh, isolated git worktree managed by the harness.
That worktree was branched from {{INTEGRATION_POINT_SHA}}.
Other tasks run in OTHER isolated worktrees in parallel — they cannot see your changes and you cannot see theirs.
## Project context
Read CLAUDE.md (if present) before touching code. Follow project conventions.
Stack: {{STACK}}
Test command: {{TEST_CMD}}
Lint/typecheck: {{LINT_CMD}}
## Scope
{{SCOPE}}
## Files you MAY write
{{TOUCHES}}
## Files you MUST NOT touch
Anything not listed under "MAY write". If you need a forbidden file, do not improvise — set status to "blocked" in your result and explain in "blockers".
## Acceptance criteria (satisfy ALL)
{{ACCEPTANCE}}
## Workflow
1. Read existing code first. Do not re-architect.
2. Implement only what is in scope. Note unrelated bugs in "notes"; do not fix them.
3. Add or extend tests. Run them. They must pass.
4. Run lint/typecheck. Fix anything you introduced.
5. Stage and commit using the prefix the orchestrator computed for this repo's commit style:
`git commit -m "{{COMMIT_PREFIX}}{{TASK_NAME}}"`
The orchestrator substitutes `{{COMMIT_PREFIX}}` based on env.sh `_COMMIT_FORMAT`:
- `plain` → `{{TASK_ID}}: ` (e.g. `git commit -m "T01: rate-limits migration"`)
- `conventional` → `chore({{TASK_ID}}): ` (e.g. `git commit -m "chore(T01): rate-limits migration"`)
6. Capture the commit SHA: `_SHA=$(git rev-parse HEAD)`
7. Write your result JSON to the SHARED results dir (NOT inside the worktree). Use `jq -n` for safe construction — never embed paths into a heredoc directly, since filenames may contain quotes or backslashes:
```bash
mkdir -p "{{RESULTS_DIR}}"
# Build the files_changed array from git diff for safety
_FILES_JSON=$(git show --name-only --format= HEAD | jq -R . | jq -s .)
_TESTS_JSON='[]' # populate similarly with your test paths if you tracked them
jq -n \
--arg id "{{TASK_ID}}" \
--arg sha "$_SHA" \
--argjson files "$_FILES_JSON" \
--argjson tests "$_TESTS_JSON" \
--argjson tcount 0 \
--arg notes "" \
'{task_id:$id, status:"success", commit:$sha, files_changed:$files, tests_added:$tests, tests_passing:true, tests_count:$tcount, lint_passing:true, notes:$notes}' \
> "{{RESULTS_DIR}}/{{TASK_ID}}.json"
```
If status is `blocked` or `failed`: set commit to null, tests_passing to false, and add a "blockers" string field. The orchestrator's Phase 3.1 reads this file with `jq -r '.field // "default"'` so missing fields are handled gracefully.
## Result schema (must be valid JSON)
{
"task_id": "string",
"status": "success" | "blocked" | "failed",
"commit": "string or null",
"files_changed": ["string", ...],
"tests_added": ["string", ...],
"tests_passing": true | false,
"tests_count": integer,
"lint_passing": true | false,
"notes": "string",
"blockers": "string (only if status != success)"
}
## Hard rules
- Do NOT exceed your scope or touch forbidden paths.
- Do NOT skip tests. If tests fail, status is "failed", not "success".
- If your scope is wrong, return status=blocked. Do not improvise.
- Do NOT push, fork, or open PRs. Just commit locally.
- The result JSON file at {{RESULTS_DIR}}/{{TASK_ID}}.json is the source of truth — write it before returning.
```
### Worked example: dispatching Wave 1 with 3 tasks
Suppose Wave 1 has T01 (migration, haiku), T02 (redis client config, haiku), T03 (rate-limit types, sonnet). The orchestrator's single message contains three Agent calls:
```
Agent({
description: "T01: rate-limits migration",
subagent_type: "general-purpose",
model: "haiku",
isolation: "worktree",
prompt: "<filled-in subagent template — see above. {{TASK_ID}}=T01, {{INTEGRATION_POINT_SHA}}=<_BASE_SHA>, {{RESULTS_DIR}}=<_STATE_DIR>/results, {{TOUCHES}}=db/migrations/20260429_rate_limits.sql, {{ACCEPTANCE}}=migration runs forward and back, {{COMMIT_PREFIX}}=chore(T01): (because _COMMIT_FORMAT=conventional in this repo), ...>"
})
Agent({
description: "T02: redis client config",
subagent_type: "general-purpose",
model: "haiku",
isolation: "worktree",
prompt: "<filled-in template for T02>"
})
Agent({
description: "T03: rate-limit types",
subagent_type: "general-purpose",
model: "sonnet",
isolation: "worktree",
prompt: "<filled-in template for T03>"
})
```
All three Agent calls go in the SAME orchestrator message so they run in parallel. Do not call them sequentially — that defeats the worktree isolation and burns wall time.
### 2.3 Wait for all returns
The Agent tool returns each subagent's final message text. The orchestrator's source of truth is `$_STATE_DIR/results/$TASK_ID.json`, NOT the agent's text response.
The harness returns each agent's worktree path and branch name in the result. The orchestrator does not need them — cherry-pick uses commit SHA only.
---
## PHASE 3 — VERIFY EACH WAVE
After every wave returns, before the next:
### 3.1 Aggregate (defensive read)
For each task in the wave, read `$_STATE_DIR/results/$TASK_ID.json` defensively:
```bash
# Initialize wave counters (telemetry — Phase 3.5 emits these)
_WAVE_TASK_COUNT=0
_WAVE_SUCCESS=0
_WAVE_FAILED=0
_WAVE_FIXUPS=0
for TASK in $(...task ids in wave...); do
_WAVE_TASK_COUNT=$(( _WAVE_TASK_COUNT + 1 ))
RESULT="$_STATE_DIR/results/$TASK.json"
if [ ! -f "$RESULT" ]; then
echo "$TASK: MISSING_RESULT (treating as failed)"
_WAVE_FAILED=$(( _WAVE_FAILED + 1 ))
continue
fi
STATUS=$(jq -r '.status // "MALFORMED"' "$RESULT" 2>/dev/null || echo "MALFORMED")
TESTS=$(jq -r '.tests_passing // false' "$RESULT" 2>/dev/null || echo "false")
COMMIT=$(jq -r '.commit // empty' "$RESULT" 2>/dev/null || echo "")
REACHABLE="no"
[ -n "$COMMIT" ] && git cat-file -e "$COMMIT" 2>/dev/null && REACHABLE="yes"
echo "$TASK: status=$STATUS tests=$TESTS commit=${COMMIT:-none} reachable=$REACHABLE"
# Tally success/failed for telemetry
if [ "$STATUS" = "success" ] && [ "$TESTS" = "true" ] && [ "$REACHABLE" = "yes" ]; then
_WAVE_SUCCESS=$(( _WAVE_SUCCESS + 1 ))
else
_WAVE_FAILED=$(( _WAVE_FAILED + 1 ))
fi
done
# Persist wave counts so Phase 3.5 (fresh shell) can emit accurate telemetry.
# Each Bash tool call gets a new shell; counts must survive via the state dir.
echo "$_WAVE_TASK_COUNT" > "$_STATE_DIR/wave-$_WAVE_NUM.tasks"
echo "$_WAVE_SUCCESS" > "$_STATE_DIR/wave-$_WAVE_NUM.success"
echo "$_WAVE_FAILED" > "$_STATE_DIR/wave-$_WAVE_NUM.failed"
# Fix-ups are bumped by Phase 3.4 (suite-failure fix-up subagent dispatch).
[ -f "$_STATE_DIR/wave-$_WAVE_NUM.fixups" ] || echo "0" > "$_STATE_DIR/wave-$_WAVE_NUM.fixups"
```
A task counts as `success` ONLY when ALL hold:
- result file exists
- JSON parses (`STATUS != MALFORMED`)
- `status == "success"`
- `tests_passing == true`
- `commit` is non-empty
- `git cat-file -e $COMMIT` succeeds (commit is reachable in shared object DB)
Anything else is `failed`.
### 3.2 Block on any failure
If any task is not `success`: stop. Use `AskUserQuestion`:
- A) Dispatch a fix-up subagent (`Agent` with `isolation: "worktree"`) for that task with the blocker as scope
- B) Re-shred the remaining tasks (regenerate TASKS.md from this wave forward)
- C) Abort. Set `_OUTCOME=abort` in env.sh (`sed -i.bak 's/^export _OUTCOME=.*/export _OUTCOME="abort"/' "$_STATE_DIR/env.sh" && rm -f "$_STATE_DIR/env.sh.bak"`), run the Phase 4.4.5 epilogue, then stop.
The orchestrator does NOT fix the failure itself.
### 3.3 Integrate the wave onto the working branch
```bash
cd "$(git rev-parse --show-toplevel)"
git checkout "$_BRANCH"
for TASK in $(...task ids in wave, in dependency order from TASKS.md...); do
COMMIT=$(jq -r '.commit' "$_STATE_DIR/results/$TASK.json")
git cherry-pick "$COMMIT" || { echo "CONFLICT on $TASK at $COMMIT"; CONFLICT="$TASK"; break; }
done
```
If a cherry-pick conflicts: dispatch one fix-up subagent (`Agent` with `isolation: "worktree"`) with the failing commit and conflict markers as scope. The orchestrator never resolves conflicts itself.
**Branch invariant:** `$_BRANCH` is checked out only in the main repo, never in a subagent worktree. The Agent tool's `isolation: "worktree"` creates fresh per-agent branches automatically — they do not collide with `$_BRANCH`.
### 3.4 Run the full suite once at the wave boundary
```bash
source "<env.sh path>"
$TEST_CMD && $LINT_CMD
```
If it fails: dispatch one fix-up subagent (`Agent` with `isolation: "worktree"`). Bump the fix-up counter for telemetry before dispatching:
```bash
source "<env.sh path>"
_F=$(cat "$_STATE_DIR/wave-$_WAVE_NUM.fixups" 2>/dev/null || echo 0)
echo $(( _F + 1 )) > "$_STATE_DIR/wave-$_WAVE_NUM.fixups"
```
The subagent commits in its own worktree and writes a result JSON to `$_STATE_DIR/results/wave-N-fixup.json`. After it returns:
```bash
COMMIT=$(jq -r '.commit // empty' "$_STATE_DIR/results/wave-N-fixup.json")
[ -n "$COMMIT" ] && git cherry-pick "$COMMIT"
$TEST_CMD && $LINT_CMD
```
If it still fails after the fix-up cherry-pick, ask the user via `AskUserQuestion`: A) dispatch another fix-up (max 2 retries per wave), B) abort. Do not loop indefinitely. On B (this is unrecoverable, not user-driven), set `_OUTCOME=error` in env.sh (`sed -i.bak 's/^export _OUTCOME=.*/export _OUTCOME="error"/' "$_STATE_DIR/env.sh" && rm -f "$_STATE_DIR/env.sh.bak"`), run the Phase 4.4.5 epilogue, then stop.
### 3.5 Checkpoint + emit wave_completed (telemetry)
Before writing the checkpoint, compute wave duration and aggregate counts. The aggregate counts (`_WAVE_SUCCESS`, `_WAVE_FAILED`, `_WAVE_FIXUPS`) come from the orchestrator's bookkeeping during 3.1-3.4 — surface them as variables before reaching 3.5. Substitute the literal wave number for `$_WAVE_NUM`.
```bash
source "<env.sh path>"
_WAVE_START=$(cat "$_STATE_DIR/wave-$_WAVE_NUM.start" 2>/dev/null || echo "$(date +%s)")
_WAVE_END=$(date +%s)
_WAVE_DUR=$(( _WAVE_END - _WAVE_START ))
# Read counts persisted by Phase 3.1's aggregate loop and Phase 3.4's fix-up bumper.
# Defaults are defensive — Phase 3.1 always writes them, but a malformed earlier
# step shouldn't break the checkpoint write below.
_WAVE_TASK_COUNT=$(cat "$_STATE_DIR/wave-$_WAVE_NUM.tasks" 2>/dev/null || echo 0)
_WAVE_SUCCESS=$(cat "$_STATE_DIR/wave-$_WAVE_NUM.success" 2>/dev/null || echo 0)
_WAVE_FAILED=$(cat "$_STATE_DIR/wave-$_WAVE_NUM.failed" 2>/dev/null || echo 0)
_WAVE_FIXUPS=$(cat "$_STATE_DIR/wave-$_WAVE_NUM.fixups" 2>/dev/null || echo 0)
# Write checkpoint to state.jsonl. If this fails (disk full, perms), do NOT
# emit the wave_completed timeline event — that would be a silent lie.
if jq -nc \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg wave "$_WAVE_NUM" \
--arg head "$(git rev-parse HEAD)" \
--argjson duration "$_WAVE_DUR" \
--argjson tasks "$_WAVE_TASK_COUNT" \
--argjson success "$_WAVE_SUCCESS" \
--argjson failed "$_WAVE_FAILED" \
--argjson fixups "$_WAVE_FIXUPS" \
'{ts:$ts,wave:$wave,head:$head,status:"completed",duration_s:$duration,task_count:$tasks,success:$success,failed:$failed,fixups:$fixups}' \
>> "$_STATE_DIR/state.jsonl" 2>/dev/null; then
# Checkpoint persisted — emit the timeline event for the dashboard.
# jq-built JSON for safety against `"` in branch names (git-legal, rare).
_TL_PAYLOAD=$(jq -nc \
--arg branch "$_BRANCH" --arg sid "$_SESSION_ID" \
--argjson wave "$_WAVE_NUM" --argjson dur "$_WAVE_DUR" \
--argjson success "$_WAVE_SUCCESS" --argjson failed "$_WAVE_FAILED" --argjson fixups "$_WAVE_FIXUPS" \
'{skill:"parallel-orchestrate",event:"wave_completed",branch:$branch,wave:$wave,duration_s:$dur,success:$success,failed:$failed,fixups:$fixups,session:$sid}')
~/.claude/skills/gstack/bin/gstack-timeline-log "$_TL_PAYLOAD" 2>/dev/null &
else
# Checkpoint failed. Tell the user — resume will be broken until disk/perms
# are sorted. Do NOT emit a misleading wave_completed event.
echo "ERROR: state.jsonl write failed for wave $_WAVE_NUM. Resume will not work until fixed. Check disk/permissions on $_STATE_DIR." >&2
fi
```
The state.jsonl additions (`duration_s`, `task_count`, `success`, `failed`, `fixups`) are additive. Phase 1.1's resume logic only reads `head`, `wave`, and `status`, so older entries from pre-1.6 runs remain compatible.
---
## PHASE 4 — FINAL INTEGRATION & HANDOFF
After the last wave passes:
### 4.1 Run review (bounded loop, max 2 cleanup passes)
Invoke the Skill tool: `Skill({skill: "review"})`. Capture output.
If `/review` flags issues:
1. Dispatch one cleanup subagent (`Agent` with `isolation: "worktree"`) with the review feedback as scope.
2. After it returns, cherry-pick its commit (read from `$_STATE_DIR/results/cleanup-1.json`) onto `$_BRANCH`.
3. Re-invoke `Skill({skill: "review"})`.
4. If issues remain, repeat once more (cleanup-2). After 2 cleanup passes, do NOT loop further. Use `AskUserQuestion`: A) ship anyway with known issues, B) abort (preserve state). On B, set `_OUTCOME=error` in env.sh (`sed -i.bak 's/^export _OUTCOME=.*/export _OUTCOME="error"/' "$_STATE_DIR/env.sh" && rm -f "$_STATE_DIR/env.sh.bak"`), run the Phase 4.4.5 epilogue, then stop. (On A, treat as success — preamble's `_OUTCOME=success` default carries through.)
### 4.2 QA / suite
If the plan had UI changes (any wave touched `.tsx`/`.jsx`/`.vue`/`.svelte`/`.html`/`.css`/`.scss`), invoke `Skill({skill: "qa"})`. Otherwise:
```bash
source "<env.sh path>"
$TEST_CMD
```
### 4.3 Worktree cleanup
Prune worktree admin records for any worktrees the Agent harness created and abandoned:
```bash
git worktree prune
```
Note: the harness's auto-managed branches will persist after pruning — this is intentional. Deleting them automatically is unsafe because we cannot reliably distinguish harness-created branches from user-owned branches with the same merge-status. If they accumulate over many runs, the user can clean up manually with `git branch --list` and `git branch -D`.
### 4.4 Final report
```
Done.
Tasks shipped: N/N
Commits: <range>
Files changed: <count>
Tests added: <count>
Review status: clean | <N issues addressed after K cleanup passes>
Deferred items: <list, from notes>
State: <_STATE_DIR>
```
### 4.4.5 Telemetry epilogue (run before handoff)
Run before any abort/error path stops the skill AND before the handoff in 4.5. The `_OUTCOME` variable was set to `success` in the preamble; abort and error gates override it (see "Abort / error semantics"). The epilogue mirrors the sibling pattern in `/ship`, `/review`, `/qa`.
**Resilient sourcing.** Source env.sh if Phase 0.6 ran; otherwise source the tel-state file written at the end of the preamble. Fresh-shell rules mean shell vars are gone — substitute literal paths from preamble output.
```bash
# Substitute the literal env.sh path printed by Phase 0.6 (if Phase 0.6 ran)
# OR the literal TEL_STATE path printed by the preamble (if Phase 0 failed
# before 0.6). Caller chooses which based on where the epilogue is invoked
# from. NO glob fallback — picking the wrong session's file is worse than
# emitting a no-op.
source <ENV_FILE_PATH-or-TEL_STATE_PATH>
_TEL_END=$(date +%s)
_TEL_DUR=$(( _TEL_END - ${_TEL_START:-$_TEL_END} ))
rm -f ~/.gstack/analytics/.pending-"$_SESSION_ID" 2>/dev/null || true
# Timeline: skill completed (local-only). jq-built for safe JSON.
_TL_BR=$(git branch --show-current 2>/dev/null || echo unknown)
_TL_PAYLOAD=$(jq -nc --arg branch "$_TL_BR" --arg sid "$_SESSION_ID" --arg outcome "$_OUTCOME" --argjson dur "$_TEL_DUR" \
'{skill:"parallel-orchestrate",event:"completed",branch:$branch,outcome:$outcome,duration_s:$dur,session:$sid}')
~/.claude/skills/gstack/bin/gstack-timeline-log "$_TL_PAYLOAD" 2>/dev/null || true
# Local analytics end row (gated)
if [ "$_TEL" != "off" ]; then
echo '{"skill":"parallel-orchestrate","duration_s":"'"$_TEL_DUR"'","outcome":"'"$_OUTCOME"'","browse":"false","session":"'"$_SESSION_ID"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
fi
# Remote telemetry (opt-in, requires binary)
if [ "$_TEL" != "off" ] && [ -x ~/.claude/skills/gstack/bin/gstack-telemetry-log ]; then
~/.claude/skills/gstack/bin/gstack-telemetry-log \
--skill "parallel-orchestrate" --duration "$_TEL_DUR" --outcome "$_OUTCOME" \
--used-browse "false" --session-id "$_SESSION_ID" 2>/dev/null &
fi
# Clean up the preamble's tel-state file. Don't fail the epilogue if it's missing.
[ -n "${_SESSION_ID:-}" ] && rm -f ~/.gstack/analytics/.tel-"$_SESSION_ID".sh 2>/dev/null || true
```
### 4.5 Handoff
Ask via `AskUserQuestion`: "Ready to ship? This will invoke the `/ship` skill which pushes and creates a PR."
If yes, invoke `Skill({skill: "ship"})`. If no, leave state intact and tell the user how to resume or run `/ship` manually later.
---
## ORCHESTRATOR HARD RULES
- You are the orchestrator. You do **not** write feature code, fix code, resolve conflicts, or edit source files.
- The only files you write directly: `TASKS.md`, `state.jsonl`, the final report.
- Never skip the Phase 0 pre-flight or the Phase 1 approval gate.
- Never start Wave N+1 if any task in Wave N is not `success` (status, tests, commit reachable).
- Always dispatch all subagents in a wave in a single message (parallel tool calls).
- Always pass `isolation: "worktree"` to every Agent call.
- If <4 parallel tasks emerge from shredding, abort and recommend `/autoplan`.
- If `/freeze` is active, abort.
- The orchestrator never pushes, forks, or opens PRs. Handoff to `/ship` does that.
---
## START
Run Phase 0. Then Phase 1.1 resume check. If no resume, Phase 1.2-1.4 (shred + approval gate). Do not dispatch until the user picks A.
don't have the plugin yet? install it then click "run inline in claude" again.