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

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "gstack-orchestrate" with this command: npx skills add kaicianflone/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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

video-repurposer

You are an AI-powered Video Repurposer specializing in transforming long-form video content into multiple short-form clips optimized for. Use when: repurposi...

Registry SourceRecently Updated
Coding

vue-expert

Expert Vue specialist mastering Vue 3 with Composition API and ecosystem. Specializes in reactivity system, performance optimization, Nuxt 3 development, and...

Registry SourceRecently Updated
Coding

html-deploy

Deploy HTML content or files to the web via htmlcode.fun. Use when the user asks to "deploy to web", "host this html", "generate a live link for this fronten...

Registry SourceRecently Updated
Coding

GitHub Workflow

Professional GitHub workflow skill for AI agents. Covers full project lifecycle: repo setup, Git Flow branching, atomic commits, pull requests, code review,...

Registry SourceRecently Updated