Gstack Orchestrate

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", "gstack-orchestrate", "/gstack-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".

Audits

Pass

Install

openclaw skills install gstack-orchestrate

/gstack-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.

_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":"gstack-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":"gstack-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:"gstack-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:

# 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

# 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

[ -n "$(git status --porcelain)" ] && echo "DIRTY: commit, stash, or discard before orchestrating."

If dirty: stop.

0.3 Slug + state directory

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

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)

_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:

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:

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:

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):
    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 /gstack-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.

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:

# 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):

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).

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:"gstack-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

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

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:

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:

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.

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:"gstack-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:

source "<env.sh path>"
$TEST_CMD

4.3 Worktree cleanup

Prune worktree admin records for any worktrees the Agent harness created and abandoned:

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.

# 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:"gstack-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":"gstack-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 "gstack-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.