# The OpenClaw Agent Swarm — Complete Setup Guide

**By VibeX (AutoEDU.ai) — February 2026**
**Inspired by Elvis @elvissun's agent orchestration workflow**

> This is how AutoEDU.ai actually ships code. Not a theoretical exercise. Battle-tested, opinionated, and unapologetically practical.

---

## 1. Overview

### What This Does

You have one OpenClaw instance. This system turns it into a dev team. Multiple coding agents (Codex CLI, Claude Code CLI) running in parallel tmux sessions, each on isolated git worktrees, monitored by a zero-token deterministic babysitter, scoped by BMAD methodology, and self-improving via GEPA traces.

The key insight from Elvis @elvissun: **agents don't need to talk to each other. They need isolated branches, clear prompts, and something watching them that doesn't burn tokens.**

### Architecture

```
┌─────────────────────────────────────────────────────┐
│                    OpenClaw Agent                     │
│                  (Orchestrator Brain)                 │
├─────────────────────────────────────────────────────┤
│                                                       │
│   ┌──────────┐   ┌──────────┐   ┌──────────┐        │
│   │  tmux:   │   │  tmux:   │   │  tmux:   │        │
│   │  codex-  │   │  claude-  │   │  codex-  │        │
│   │  auth    │   │  ui-fix   │   │  api     │  ...   │
│   │          │   │          │   │          │        │
│   │ worktree │   │ worktree │   │ worktree │        │
│   │ feat/auth│   │ fix/ui   │   │ feat/api │        │
│   └────┬─────┘   └────┬─────┘   └────┬─────┘        │
│        │              │              │               │
│        └──────────────┼──────────────┘               │
│                       │                              │
│              ┌────────▼────────┐                     │
│              │  swarm-check.sh │ ← cron, every 10min │
│              │  (0 tokens)     │                     │
│              └────────┬────────┘                     │
│                       │                              │
│         ┌─────────────┼─────────────┐                │
│         ▼             ▼             ▼                │
│    gh pr list    gh pr checks   tmux alive?          │
│                                                       │
├─────────────────────────────────────────────────────┤
│  BMAD (scoping)  │  tasks.json  │  GEPA (learning)  │
└─────────────────────────────────────────────────────┘
```

### Prerequisites

- **OpenClaw** running and configured
- **gh CLI** authenticated (`gh auth login`)
- **tmux** installed (`brew install tmux` / `apt install tmux`)
- **Codex CLI** installed and authed (`npm i -g @openai/codex`)
- **Claude Code CLI** installed and authed (`npm i -g @anthropic-ai/claude-code`)
- **jq** installed (`brew install jq`)
- A git repo you want agents working on

---

## 2. Directory Structure

```
~/clawd/agent-swarm/              # Registry + PRD + reviews
~/clawd/agent-swarm/tasks.json    # Task registry (JSON array)
~/clawd/agent-swarm/reviews/      # Codex review outputs
~/clawd/agent-swarm/logs/         # Agent session logs (via script cmd)
~/clawd/scripts/swarm-*.sh        # All swarm scripts
~/clawd/gepa-sidecar/traces/      # GEPA traces (JSON per merge)
~/clawd/memory/lessons.md         # Lessons learned — GEPA feeds on this
```

Create it all:

```bash
mkdir -p ~/clawd/agent-swarm/{reviews,logs}
mkdir -p ~/clawd/scripts
mkdir -p ~/clawd/gepa-sidecar/traces
mkdir -p ~/clawd/memory
echo '[]' > ~/clawd/agent-swarm/tasks.json
touch ~/clawd/memory/lessons.md
```

---

## 3. BMAD Integration

### What is BMAD?

**Business-Model-Architecture-Development** — a methodology for scoping work before anyone (human or AI) writes code. The flow:

1. **PRD** (Product Requirements Document) — What are we building and why?
2. **Architecture** — How does it fit into the existing system?
3. **Stories** — Discrete, shippable units of work

### Why BMAD Matters for Agent Swarms

Without scoping, agents hallucinate features, duplicate work, and create merge conflicts. BMAD gives each agent a narrow, well-defined task with clear acceptance criteria.

**The rule: No agent spawns without a BMAD story.**

### Workflow

1. Write your PRD in `~/clawd/agent-swarm/prd-<feature>.md`
2. Break it into stories — each story gets an ID matching the branch name
3. Each story becomes a task in `tasks.json`
4. Agent prompts include the relevant BMAD artifacts:
   ```
   Context: [paste PRD excerpt]
   Story: [paste story]
   Architecture notes: [paste relevant section]
   Your task: Implement this story. Create a PR when done.
   ```

### Resources

- [BMAD Method](https://github.com/bmadcode/BMAD-METHOD) — the original repo
- The key insight: scope BEFORE spawn. Always.

---

## 4. The Scripts

### swarm-worktree.sh — Git Worktree Manager

```bash
#!/usr/bin/env bash
set -euo pipefail

# Usage:
#   swarm-worktree.sh create --repo <name> --branch <branch>
#   swarm-worktree.sh cleanup --branch <branch>
#
# Creates isolated git worktrees in a sibling directory so agents
# don't step on each other's code.

ACTION="${1:?Usage: swarm-worktree.sh <create|cleanup> --repo <name> --branch <branch>}"
shift

REPO=""
BRANCH=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --repo) REPO="$2"; shift 2 ;;
    --branch) BRANCH="$2"; shift 2 ;;
    *) echo "Unknown arg: $1"; exit 1 ;;
  esac
done

[[ -z "$BRANCH" ]] && { echo "Error: --branch required"; exit 1; }

# Resolve repo root — look in common locations
find_repo() {
  local name="$1"
  for base in "$HOME/Projects" "$HOME/repos" "$HOME/src" "$HOME"; do
    if [[ -d "$base/$name/.git" ]]; then
      echo "$base/$name"
      return 0
    fi
  done
  echo ""
  return 1
}

WORKTREE_BASE="$HOME/clawd/worktrees"
mkdir -p "$WORKTREE_BASE"

case "$ACTION" in
  create)
    [[ -z "$REPO" ]] && { echo "Error: --repo required for create"; exit 1; }

    REPO_PATH=$(find_repo "$REPO")
    if [[ -z "$REPO_PATH" ]]; then
      echo "Error: Could not find repo '$REPO' in ~/Projects, ~/repos, ~/src, or ~/"
      exit 1
    fi

    WORKTREE_DIR="$WORKTREE_BASE/${REPO}-${BRANCH//\//-}"

    if [[ -d "$WORKTREE_DIR" ]]; then
      echo "Worktree already exists: $WORKTREE_DIR"
      echo "$WORKTREE_DIR"
      exit 0
    fi

    cd "$REPO_PATH"

    # Ensure we have latest main
    git fetch origin main 2>/dev/null || git fetch origin master 2>/dev/null || true

    # Create branch from main if it doesn't exist
    if ! git rev-parse --verify "$BRANCH" &>/dev/null; then
      DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
      git branch "$BRANCH" "origin/$DEFAULT_BRANCH" 2>/dev/null || git branch "$BRANCH" "origin/main" 2>/dev/null || git branch "$BRANCH" HEAD
    fi

    git worktree add "$WORKTREE_DIR" "$BRANCH"

    # Auto-install deps
    cd "$WORKTREE_DIR"
    if [[ -f "package-lock.json" ]]; then
      echo "Installing npm deps..."
      npm ci --silent 2>/dev/null || npm install --silent 2>/dev/null || true
    elif [[ -f "yarn.lock" ]]; then
      echo "Installing yarn deps..."
      yarn install --frozen-lockfile --silent 2>/dev/null || true
    elif [[ -f "pnpm-lock.yaml" ]]; then
      echo "Installing pnpm deps..."
      pnpm install --frozen-lockfile --silent 2>/dev/null || true
    elif [[ -f "requirements.txt" ]]; then
      echo "Installing pip deps..."
      pip install -r requirements.txt -q 2>/dev/null || true
    fi

    echo "$WORKTREE_DIR"
    ;;

  cleanup)
    REPO_GUESS=$(ls "$WORKTREE_BASE" | grep "${BRANCH//\//-}" | head -1)
    WORKTREE_DIR="$WORKTREE_BASE/$REPO_GUESS"

    if [[ -z "$REPO_GUESS" || ! -d "$WORKTREE_DIR" ]]; then
      echo "No worktree found for branch: $BRANCH"
      exit 0
    fi

    # Find the main repo to run worktree remove from
    MAIN_REPO=$(git -C "$WORKTREE_DIR" rev-parse --git-common-dir 2>/dev/null | sed 's/\/.git$//')
    if [[ -n "$MAIN_REPO" && -d "$MAIN_REPO" ]]; then
      cd "$MAIN_REPO"
      git worktree remove "$WORKTREE_DIR" --force 2>/dev/null || rm -rf "$WORKTREE_DIR"
      git branch -D "$BRANCH" 2>/dev/null || true
    else
      rm -rf "$WORKTREE_DIR"
    fi

    echo "Cleaned up worktree for $BRANCH"
    ;;

  *)
    echo "Usage: swarm-worktree.sh <create|cleanup> --repo <name> --branch <branch>"
    exit 1
    ;;
esac
```

### swarm-spawn.sh — Agent Spawner

```bash
#!/usr/bin/env bash
set -euo pipefail

# Usage: swarm-spawn.sh --repo <name> --task "description" --agent codex|claude --branch <branch>
#
# Spawns a coding agent in an isolated tmux session with its own git worktree.
# Registers the task in tasks.json for monitoring.

REPO=""
TASK=""
AGENT=""
BRANCH=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --repo) REPO="$2"; shift 2 ;;
    --task) TASK="$2"; shift 2 ;;
    --agent) AGENT="$2"; shift 2 ;;
    --branch) BRANCH="$2"; shift 2 ;;
    *) echo "Unknown arg: $1"; exit 1 ;;
  esac
done

[[ -z "$REPO" ]] && { echo "Error: --repo required"; exit 1; }
[[ -z "$TASK" ]] && { echo "Error: --task required"; exit 1; }
[[ -z "$AGENT" ]] && { echo "Error: --agent required (codex|claude)"; exit 1; }
[[ -z "$BRANCH" ]] && { echo "Error: --branch required"; exit 1; }

SCRIPTS_DIR="$HOME/clawd/scripts"
TASKS_FILE="$HOME/clawd/agent-swarm/tasks.json"
LOGS_DIR="$HOME/clawd/agent-swarm/logs"
TASK_ID="${BRANCH//\//-}"
TMUX_SESSION="${AGENT}-${TASK_ID}"

# Check if session already exists
if tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
  echo "Error: tmux session '$TMUX_SESSION' already exists. Kill it first or use a different branch."
  exit 1
fi

# Create worktree
echo "Creating worktree..."
WORKTREE_DIR=$("$SCRIPTS_DIR/swarm-worktree.sh" create --repo "$REPO" --branch "$BRANCH")
echo "Worktree: $WORKTREE_DIR"

# Write prompt to file (avoids shell escaping nightmares)
PROMPT_FILE="$LOGS_DIR/${TASK_ID}-prompt.txt"
mkdir -p "$LOGS_DIR"
cat > "$PROMPT_FILE" << PROMPT_EOF
You are working on: $TASK

Repository: $REPO
Branch: $BRANCH
Working directory: $WORKTREE_DIR

Instructions:
1. Read the codebase to understand the existing patterns
2. Implement the task described above
3. Write tests if the project has a test suite
4. Commit your changes with a clear commit message
5. Push to the branch: git push origin $BRANCH
6. Create a pull request: gh pr create --title "$TASK" --body "Automated PR for: $TASK" --base main

Important:
- Follow existing code style and patterns
- Don't modify files unrelated to your task
- If you encounter an error, debug it — don't give up
- Push and create the PR when done
PROMPT_EOF

# Build the agent command
LOG_FILE="$LOGS_DIR/${TASK_ID}-$(date +%Y%m%d-%H%M%S).log"

case "$AGENT" in
  codex)
    AGENT_CMD="cd '$WORKTREE_DIR' && script -q '$LOG_FILE' codex --model o4-mini -c 'model_reasoning_effort=high' --full-auto -q \"\$(cat '$PROMPT_FILE')\""
    ;;
  claude)
    AGENT_CMD="cd '$WORKTREE_DIR' && script -q '$LOG_FILE' claude --model claude-sonnet-4-6 --dangerously-skip-permissions -p \"\$(cat '$PROMPT_FILE')\""
    ;;
  *)
    echo "Error: Unknown agent '$AGENT'. Use 'codex' or 'claude'."
    exit 1
    ;;
esac

# Spawn tmux session
echo "Spawning $AGENT in tmux session: $TMUX_SESSION"
tmux new-session -d -s "$TMUX_SESSION" "$AGENT_CMD"

# Register in tasks.json
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
TASK_ENTRY=$(jq -n \
  --arg id "$TASK_ID" \
  --arg tmux "$TMUX_SESSION" \
  --arg agent "$AGENT" \
  --arg desc "$TASK" \
  --arg repo "$REPO" \
  --arg wt "$WORKTREE_DIR" \
  --arg branch "$BRANCH" \
  --arg started "$TIMESTAMP" \
  '{
    id: $id,
    tmuxSession: $tmux,
    agent: $agent,
    description: $desc,
    repo: $repo,
    worktree: $wt,
    branch: $branch,
    startedAt: $started,
    status: "active",
    retryCount: 0,
    pr: null,
    phase: "coding"
  }')

# Append to tasks.json
jq ". += [$TASK_ENTRY]" "$TASKS_FILE" > "${TASKS_FILE}.tmp" && mv "${TASKS_FILE}.tmp" "$TASKS_FILE"

echo ""
echo "✅ Agent spawned!"
echo "   Session: tmux attach -t $TMUX_SESSION"
echo "   Log:     $LOG_FILE"
echo "   Task ID: $TASK_ID"
echo ""
echo "Monitor: tmux attach -t $TMUX_SESSION"
echo "Steer:   swarm-steer.sh $BRANCH \"your instructions\""
```

### swarm-check.sh — Deterministic Babysitter (ZERO Tokens)

```bash
#!/usr/bin/env bash
set -euo pipefail

# swarm-check.sh — The babysitter. Runs every 10 min via cron.
# Pure bash + jq + gh CLI. No LLM calls. Zero tokens burned.
#
# Checks: tmux alive, PR created, CI status, review approvals
# Actions: respawn dead agents, steer on CI failure, alert on problems

TASKS_FILE="$HOME/clawd/agent-swarm/tasks.json"
SCRIPTS_DIR="$HOME/clawd/scripts"
LESSONS_FILE="$HOME/clawd/memory/lessons.md"
ALERTS_FILE="/tmp/swarm-alerts.txt"
MAX_RETRIES=3

[[ ! -f "$TASKS_FILE" ]] && exit 0

TASK_COUNT=$(jq 'length' "$TASKS_FILE")
[[ "$TASK_COUNT" -eq 0 ]] && exit 0

TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
CHANGES=false

echo "[$TIMESTAMP] Checking $TASK_COUNT tasks..."

for i in $(seq 0 $((TASK_COUNT - 1))); do
  TASK=$(jq ".[$i]" "$TASKS_FILE")
  STATUS=$(echo "$TASK" | jq -r '.status')
  TMUX_SESSION=$(echo "$TASK" | jq -r '.tmuxSession')
  BRANCH=$(echo "$TASK" | jq -r '.branch')
  REPO=$(echo "$TASK" | jq -r '.repo')
  WORKTREE=$(echo "$TASK" | jq -r '.worktree')
  TASK_ID=$(echo "$TASK" | jq -r '.id')
  AGENT=$(echo "$TASK" | jq -r '.agent')
  RETRY_COUNT=$(echo "$TASK" | jq -r '.retryCount')
  DESC=$(echo "$TASK" | jq -r '.description')

  # Skip completed/failed tasks
  [[ "$STATUS" == "done" || "$STATUS" == "failed" ]] && continue

  echo "  [$TASK_ID] status=$STATUS branch=$BRANCH"

  # --- Check 1: Is tmux session alive? ---
  if [[ "$STATUS" == "active" ]]; then
    if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
      echo "  ⚠️  tmux session dead: $TMUX_SESSION"

      if [[ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]]; then
        echo "  🔄 Respawning (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)..."

        # Check if there's a PR already (agent might have finished)
        PR_URL=""
        if [[ -d "$WORKTREE" ]]; then
          PR_URL=$(cd "$WORKTREE" && gh pr list --head "$BRANCH" --json url --jq '.[0].url' 2>/dev/null || echo "")
        fi

        if [[ -n "$PR_URL" ]]; then
          echo "  ✅ PR exists despite dead session: $PR_URL"
          jq ".[$i].status = \"pr-open\" | .[$i].pr = \"$PR_URL\" | .[$i].phase = \"reviewing\"" "$TASKS_FILE" > "${TASKS_FILE}.tmp" && mv "${TASKS_FILE}.tmp" "$TASKS_FILE"
          CHANGES=true
        else
          # Respawn the agent
          NEW_RETRY=$((RETRY_COUNT + 1))
          "$SCRIPTS_DIR/swarm-spawn.sh" --repo "$REPO" --task "$DESC (retry $NEW_RETRY)" --agent "$AGENT" --branch "$BRANCH" 2>/dev/null || true
          # Update retry count (remove old entry — spawn added new one)
          jq "del(.[$i])" "$TASKS_FILE" > "${TASKS_FILE}.tmp" && mv "${TASKS_FILE}.tmp" "$TASKS_FILE"
          # Update the new entry's retry count
          jq "(.[] | select(.branch == \"$BRANCH\")).retryCount = $NEW_RETRY" "$TASKS_FILE" > "${TASKS_FILE}.tmp" && mv "${TASKS_FILE}.tmp" "$TASKS_FILE"
          CHANGES=true

          echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) - RESPAWN: $TASK_ID (attempt $NEW_RETRY)" >> "$LESSONS_FILE"
        fi
      else
        echo "  ❌ Max retries reached for $TASK_ID"
        jq ".[$i].status = \"failed\"" "$TASKS_FILE" > "${TASKS_FILE}.tmp" && mv "${TASKS_FILE}.tmp" "$TASKS_FILE"
        CHANGES=true

        echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) - FAILED: $TASK_ID after $MAX_RETRIES retries" >> "$LESSONS_FILE"
        echo "[$TIMESTAMP] FAILED: $TASK_ID — agent died $MAX_RETRIES times" >> "$ALERTS_FILE"
      fi
      continue
    fi
  fi

  # --- Check 2: Has a PR been created? ---
  if [[ "$STATUS" == "active" && -d "$WORKTREE" ]]; then
    PR_URL=$(cd "$WORKTREE" && gh pr list --head "$BRANCH" --json url --jq '.[0].url' 2>/dev/null || echo "")
    if [[ -n "$PR_URL" ]]; then
      echo "  📝 PR created: $PR_URL"
      jq ".[$i].status = \"pr-open\" | .[$i].pr = \"$PR_URL\" | .[$i].phase = \"reviewing\"" "$TASKS_FILE" > "${TASKS_FILE}.tmp" && mv "${TASKS_FILE}.tmp" "$TASKS_FILE"
      CHANGES=true
    fi
  fi

  # --- Check 3: CI status on open PRs ---
  if [[ "$STATUS" == "pr-open" && -d "$WORKTREE" ]]; then
    CI_STATUS=$(cd "$WORKTREE" && gh pr checks "$BRANCH" --json state --jq '.[].state' 2>/dev/null | sort -u || echo "UNKNOWN")

    if echo "$CI_STATUS" | grep -q "FAILURE"; then
      echo "  🔴 CI failing on $BRANCH"

      # Get failure details
      FAILURES=$(cd "$WORKTREE" && gh pr checks "$BRANCH" --json name,state --jq '.[] | select(.state=="FAILURE") | .name' 2>/dev/null || echo "unknown checks")

      # Steer the agent if tmux is alive
      if tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
        STEER_MSG="CI is failing. Failed checks: $FAILURES. Please fix the failures and push again."
        tmux send-keys -t "$TMUX_SESSION" "$STEER_MSG" Enter
        echo "  📨 Steered agent about CI failure"
      fi

      echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) - CI_FAIL: $TASK_ID — $FAILURES" >> "$LESSONS_FILE"

    elif echo "$CI_STATUS" | grep -q "SUCCESS"; then
      echo "  ✅ CI passing on $BRANCH"
      jq ".[$i].phase = \"ci-passed\"" "$TASKS_FILE" > "${TASKS_FILE}.tmp" && mv "${TASKS_FILE}.tmp" "$TASKS_FILE"
      CHANGES=true
    fi
  fi

  # --- Check 4: Review status ---
  if [[ "$STATUS" == "pr-open" && -d "$WORKTREE" ]]; then
    REVIEW_STATE=$(cd "$WORKTREE" && gh pr view "$BRANCH" --json reviewDecision --jq '.reviewDecision' 2>/dev/null || echo "")

    if [[ "$REVIEW_STATE" == "APPROVED" ]]; then
      echo "  🎉 PR approved: $BRANCH"
      jq ".[$i].status = \"done\" | .[$i].phase = \"done\"" "$TASKS_FILE" > "${TASKS_FILE}.tmp" && mv "${TASKS_FILE}.tmp" "$TASKS_FILE"
      CHANGES=true
    elif [[ "$REVIEW_STATE" == "CHANGES_REQUESTED" ]]; then
      echo "  📝 Changes requested on $BRANCH"

      # Get review comments and steer agent
      if tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
        COMMENTS=$(cd "$WORKTREE" && gh pr view "$BRANCH" --json reviews --jq '.reviews[-1].body' 2>/dev/null || echo "")
        if [[ -n "$COMMENTS" ]]; then
          tmux send-keys -t "$TMUX_SESSION" "Review feedback received. Please address: $COMMENTS" Enter
        fi
      fi
    fi
  fi
done

if [[ "$CHANGES" == true ]]; then
  echo "[$TIMESTAMP] Tasks updated."
fi

# Print summary
ACTIVE=$(jq '[.[] | select(.status == "active")] | length' "$TASKS_FILE")
PR_OPEN=$(jq '[.[] | select(.status == "pr-open")] | length' "$TASKS_FILE")
DONE=$(jq '[.[] | select(.status == "done")] | length' "$TASKS_FILE")
FAILED=$(jq '[.[] | select(.status == "failed")] | length' "$TASKS_FILE")
echo "[$TIMESTAMP] Summary: active=$ACTIVE pr-open=$PR_OPEN done=$DONE failed=$FAILED"
```

### swarm-steer.sh — Mid-Task Steering

```bash
#!/usr/bin/env bash
set -euo pipefail

# Usage: swarm-steer.sh <branch> "Fix the auth middleware, not the routes"
#
# Sends instructions to a running agent via its tmux session.
# The agent sees this as new input and adjusts course.

BRANCH="${1:?Usage: swarm-steer.sh <branch> \"instructions\"}"
MESSAGE="${2:?Usage: swarm-steer.sh <branch> \"instructions\"}"

TASKS_FILE="$HOME/clawd/agent-swarm/tasks.json"
TASK_ID="${BRANCH//\//-}"

# Find the tmux session for this branch
TMUX_SESSION=$(jq -r ".[] | select(.branch == \"$BRANCH\" or .id == \"$TASK_ID\") | .tmuxSession" "$TASKS_FILE")

if [[ -z "$TMUX_SESSION" || "$TMUX_SESSION" == "null" ]]; then
  echo "Error: No task found for branch '$BRANCH'"
  exit 1
fi

if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
  echo "Error: tmux session '$TMUX_SESSION' is not running"
  exit 1
fi

echo "Steering $TMUX_SESSION: $MESSAGE"
tmux send-keys -t "$TMUX_SESSION" "$MESSAGE" Enter

echo "✅ Instructions sent to $TMUX_SESSION"
```

### swarm-review.sh — PR Review Status

```bash
#!/usr/bin/env bash
set -euo pipefail

# Usage: swarm-review.sh [branch]
# Shows review status for all open PRs (or a specific branch)

TASKS_FILE="$HOME/clawd/agent-swarm/tasks.json"
BRANCH="${1:-}"

if [[ -n "$BRANCH" ]]; then
  TASKS=$(jq -r ".[] | select(.branch == \"$BRANCH\")" "$TASKS_FILE")
else
  TASKS=$(jq -r '.[] | select(.status == "pr-open" or .status == "reviewing")' "$TASKS_FILE")
fi

if [[ -z "$TASKS" ]]; then
  echo "No open PRs to review."
  exit 0
fi

echo "$TASKS" | jq -r '[.branch, .repo, .worktree] | @tsv' | while IFS=$'\t' read -r branch repo worktree; do
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "Branch: $branch"
  echo "Repo:   $repo"

  if [[ -d "$worktree" ]]; then
    cd "$worktree"
    echo "PR:     $(gh pr view "$branch" --json url --jq '.url' 2>/dev/null || echo 'N/A')"
    echo "CI:     $(gh pr checks "$branch" --json state --jq '[.[].state] | unique | join(", ")' 2>/dev/null || echo 'N/A')"
    echo "Review: $(gh pr view "$branch" --json reviewDecision --jq '.reviewDecision' 2>/dev/null || echo 'PENDING')"
    echo "Labels: $(gh pr view "$branch" --json labels --jq '[.labels[].name] | join(", ")' 2>/dev/null || echo 'none')"
  else
    echo "⚠️  Worktree missing: $worktree"
  fi
  echo ""
done
```

### swarm-cleanup.sh — Garbage Collection

```bash
#!/usr/bin/env bash
set -euo pipefail

# Usage: swarm-cleanup.sh [--force]
# Archives completed/failed tasks and removes their worktrees.

TASKS_FILE="$HOME/clawd/agent-swarm/tasks.json"
ARCHIVE_FILE="$HOME/clawd/agent-swarm/tasks-archive.json"
SCRIPTS_DIR="$HOME/clawd/scripts"
FORCE="${1:-}"

[[ ! -f "$TASKS_FILE" ]] && { echo "No tasks file."; exit 0; }

# Initialize archive if needed
[[ ! -f "$ARCHIVE_FILE" ]] && echo '[]' > "$ARCHIVE_FILE"

# Find done/failed tasks
COMPLETED=$(jq '[.[] | select(.status == "done" or .status == "failed")]' "$TASKS_FILE")
COUNT=$(echo "$COMPLETED" | jq 'length')

if [[ "$COUNT" -eq 0 ]]; then
  echo "Nothing to clean up."
  exit 0
fi

echo "Found $COUNT completed/failed tasks to clean up."

if [[ "$FORCE" != "--force" ]]; then
  echo "$COMPLETED" | jq -r '.[] | "  \(.id) [\(.status)] — \(.description)"'
  read -rp "Clean up these tasks? [y/N] " confirm
  [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Cancelled."; exit 0; }
fi

# Archive and clean each task
echo "$COMPLETED" | jq -c '.[]' | while read -r task; do
  BRANCH=$(echo "$task" | jq -r '.branch')
  TASK_ID=$(echo "$task" | jq -r '.id')
  TMUX_SESSION=$(echo "$task" | jq -r '.tmuxSession')

  echo "Cleaning: $TASK_ID"

  # Kill tmux session if still alive
  tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true

  # Remove worktree
  "$SCRIPTS_DIR/swarm-worktree.sh" cleanup --branch "$BRANCH" 2>/dev/null || true

  # Archive
  jq ". += [$(echo "$task" | jq '. + {archivedAt: now | todate}')]" "$ARCHIVE_FILE" > "${ARCHIVE_FILE}.tmp" && mv "${ARCHIVE_FILE}.tmp" "$ARCHIVE_FILE"
done

# Remove completed tasks from active file
jq '[.[] | select(.status != "done" and .status != "failed")]' "$TASKS_FILE" > "${TASKS_FILE}.tmp" && mv "${TASKS_FILE}.tmp" "$TASKS_FILE"

REMAINING=$(jq 'length' "$TASKS_FILE")
echo "✅ Cleaned $COUNT tasks. $REMAINING remaining."
```

### swarm-trace.sh — GEPA Trace Collection

```bash
#!/usr/bin/env bash
set -euo pipefail

# Usage: swarm-trace.sh <branch>
# Collects a GEPA trace after a PR is merged. Run automatically or manually.

BRANCH="${1:?Usage: swarm-trace.sh <branch>}"
TASKS_FILE="$HOME/clawd/agent-swarm/tasks.json"
TRACES_DIR="$HOME/clawd/gepa-sidecar/traces"
LESSONS_FILE="$HOME/clawd/memory/lessons.md"

mkdir -p "$TRACES_DIR"

# Find task
TASK=$(jq -r ".[] | select(.branch == \"$BRANCH\")" "$TASKS_FILE")
if [[ -z "$TASK" || "$TASK" == "null" ]]; then
  echo "Error: No task found for branch $BRANCH"
  exit 1
fi

TASK_ID=$(echo "$TASK" | jq -r '.id')
AGENT=$(echo "$TASK" | jq -r '.agent')
REPO=$(echo "$TASK" | jq -r '.repo')
DESC=$(echo "$TASK" | jq -r '.description')
WORKTREE=$(echo "$TASK" | jq -r '.worktree')
PR=$(echo "$TASK" | jq -r '.pr // "none"')
STARTED=$(echo "$TASK" | jq -r '.startedAt')
RETRY_COUNT=$(echo "$TASK" | jq -r '.retryCount')
STATUS=$(echo "$TASK" | jq -r '.status')

# Get files changed from PR
FILES_CHANGED="[]"
if [[ -d "$WORKTREE" && "$PR" != "none" ]]; then
  FILES_CHANGED=$(cd "$WORKTREE" && gh pr view "$BRANCH" --json files --jq '[.files[].path]' 2>/dev/null || echo '[]')
fi

# Get CI outcome
CI_OUTCOME="unknown"
if [[ -d "$WORKTREE" ]]; then
  CI_STATES=$(cd "$WORKTREE" && gh pr checks "$BRANCH" --json state --jq '[.[].state] | unique' 2>/dev/null || echo '[]')
  if echo "$CI_STATES" | grep -q "FAILURE"; then
    CI_OUTCOME="failed"
  elif echo "$CI_STATES" | grep -q "SUCCESS"; then
    CI_OUTCOME="passed"
  fi
fi

# Determine outcome
OUTCOME="unknown"
if [[ "$STATUS" == "done" ]]; then
  OUTCOME="success"
elif [[ "$STATUS" == "failed" ]]; then
  OUTCOME="failed"
else
  OUTCOME="in-progress"
fi

# Check for lessons related to this task
RELATED_LESSONS=$(grep "$TASK_ID\|$BRANCH" "$LESSONS_FILE" 2>/dev/null || echo "none")

TRACE_FILE="$TRACES_DIR/${TASK_ID}-$(date +%Y%m%d-%H%M%S).json"

jq -n \
  --arg type "swarm-task" \
  --arg id "$TASK_ID" \
  --arg agent "$AGENT" \
  --arg repo "$REPO" \
  --arg branch "$BRANCH" \
  --arg desc "$DESC" \
  --arg pr "$PR" \
  --arg started "$STARTED" \
  --arg collected "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --arg outcome "$OUTCOME" \
  --arg ci "$CI_OUTCOME" \
  --argjson retries "$RETRY_COUNT" \
  --argjson files "$FILES_CHANGED" \
  --arg lessons "$RELATED_LESSONS" \
  '{
    type: $type,
    taskId: $id,
    agent: $agent,
    repo: $repo,
    branch: $branch,
    description: $desc,
    pr: $pr,
    startedAt: $started,
    collectedAt: $collected,
    outcome: $outcome,
    ci: $ci,
    retryCount: $retries,
    filesChanged: $files,
    relatedLessons: $lessons
  }' > "$TRACE_FILE"

echo "✅ GEPA trace collected: $TRACE_FILE"
echo "   Outcome: $OUTCOME | CI: $CI_OUTCOME | Files: $(echo "$FILES_CHANGED" | jq 'length') | Retries: $RETRY_COUNT"
```

### swarm-review-pipeline.sh — Full Review Pipeline

```bash
#!/usr/bin/env bash
set -euo pipefail

# Usage: swarm-review-pipeline.sh <branch>
#
# The full review pipeline:
# 1. Codex reviews PR files → writes findings
# 2. Claude Code reads findings → implements fixes
# 3. Pushes to branch
# 4. GEPA trace collected
# 5. lessons.md updated

BRANCH="${1:?Usage: swarm-review-pipeline.sh <branch>}"
TASKS_FILE="$HOME/clawd/agent-swarm/tasks.json"
REVIEWS_DIR="$HOME/clawd/agent-swarm/reviews"
SCRIPTS_DIR="$HOME/clawd/scripts"
LESSONS_FILE="$HOME/clawd/memory/lessons.md"

mkdir -p "$REVIEWS_DIR"

TASK=$(jq -r ".[] | select(.branch == \"$BRANCH\")" "$TASKS_FILE")
if [[ -z "$TASK" ]]; then
  echo "Error: No task found for branch $BRANCH"
  exit 1
fi

WORKTREE=$(echo "$TASK" | jq -r '.worktree')
TASK_ID=$(echo "$TASK" | jq -r '.id')
REVIEW_FILE="$REVIEWS_DIR/${TASK_ID}-review-$(date +%Y%m%d-%H%M%S).md"

if [[ ! -d "$WORKTREE" ]]; then
  echo "Error: Worktree not found: $WORKTREE"
  exit 1
fi

cd "$WORKTREE"

echo "═══════════════════════════════════════════"
echo "  Review Pipeline: $BRANCH"
echo "═══════════════════════════════════════════"

# Step 1: Get the diff
echo ""
echo "📋 Step 1: Getting PR diff..."
DIFF=$(gh pr diff "$BRANCH" 2>/dev/null || git diff main..."$BRANCH" 2>/dev/null || echo "")

if [[ -z "$DIFF" ]]; then
  echo "  No diff found. Nothing to review."
  exit 0
fi

# Step 2: Codex reviews
echo ""
echo "🔍 Step 2: Codex reviewing..."

REVIEW_PROMPT="Review this git diff for bugs, security issues, performance problems, and code style violations. Be specific — file names, line numbers, severity (critical/warning/info). Output as markdown.

DIFF:
$DIFF"

# Write prompt to temp file to avoid escaping issues
REVIEW_PROMPT_FILE="/tmp/swarm-review-prompt-$$.txt"
echo "$REVIEW_PROMPT" > "$REVIEW_PROMPT_FILE"

codex --model o4-mini --full-auto -q "$(cat "$REVIEW_PROMPT_FILE")" > "$REVIEW_FILE" 2>/dev/null || {
  echo "  ⚠️  Codex review failed, continuing with empty review"
  echo "# Review\n\nCodex review failed to generate." > "$REVIEW_FILE"
}

rm -f "$REVIEW_PROMPT_FILE"
echo "  Review written: $REVIEW_FILE"

# Step 3: Check if there are actionable findings
CRITICAL_COUNT=$(grep -ci "critical\|security\|vulnerability\|bug" "$REVIEW_FILE" || echo 0)
echo "  Found $CRITICAL_COUNT potential issues"

if [[ "$CRITICAL_COUNT" -gt 0 ]]; then
  echo ""
  echo "🔧 Step 3: Claude implementing fixes..."

  FIX_PROMPT="Read this code review and fix ALL critical and warning issues in the codebase. The review findings are:

$(cat "$REVIEW_FILE")

Fix each issue, then commit with message 'fix: address review findings' and push."

  FIX_PROMPT_FILE="/tmp/swarm-fix-prompt-$$.txt"
  echo "$FIX_PROMPT" > "$FIX_PROMPT_FILE"

  claude --model claude-sonnet-4-6 --dangerously-skip-permissions -p "$(cat "$FIX_PROMPT_FILE")" 2>/dev/null || {
    echo "  ⚠️  Claude fix failed"
    echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) - REVIEW_FIX_FAIL: $TASK_ID — Claude couldn't apply fixes" >> "$LESSONS_FILE"
  }

  rm -f "$FIX_PROMPT_FILE"
else
  echo "  ✅ No critical issues — skipping fix step"
fi

# Step 4: Push changes
echo ""
echo "📤 Step 4: Pushing..."
git push origin "$BRANCH" 2>/dev/null || echo "  Nothing new to push"

# Step 5: Collect GEPA trace
echo ""
echo "📊 Step 5: Collecting GEPA trace..."
"$SCRIPTS_DIR/swarm-trace.sh" "$BRANCH" 2>/dev/null || echo "  Trace collection skipped"

# Step 6: Update lessons
echo ""
echo "📝 Step 6: Updating lessons..."
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) - REVIEW: $TASK_ID — $CRITICAL_COUNT issues found, review at $REVIEW_FILE" >> "$LESSONS_FILE"

echo ""
echo "═══════════════════════════════════════════"
echo "  ✅ Review pipeline complete for $BRANCH"
echo "═══════════════════════════════════════════"
```

---

## 5. GEPA Integration

### What is GEPA?

**Genetic Evolutionary Prompt Architecture.** The self-improvement loop:

1. **Traces** — Collect data on every agent task (what agent, what files, outcome, CI status, retries)
2. **Evaluation** — Score task outcomes. Did it succeed first try? CI pass? How many retries?
3. **Proposals** — Generate improvement suggestions (better prompts, different agent selection, new rules)
4. **Skill Mutations** — Accept proposals → update agent prompts, scripts, and rules

### The Trace Format

Every completed task generates a JSON trace in `~/clawd/gepa-sidecar/traces/`:

```json
{
  "type": "swarm-task",
  "taskId": "feat-fix-auth",
  "agent": "codex",
  "repo": "my-app",
  "branch": "feat/fix-auth",
  "description": "Fix auth token refresh",
  "pr": "https://github.com/org/repo/pull/42",
  "startedAt": "2026-02-23T10:00:00Z",
  "collectedAt": "2026-02-23T14:30:00Z",
  "outcome": "success",
  "ci": "passed",
  "retryCount": 1,
  "filesChanged": ["src/auth.ts", "src/auth.test.ts"],
  "relatedLessons": "RESPAWN: feat-fix-auth (attempt 1)"
}
```

### The Feedback Loop

```
Task fails → lesson written to lessons.md
         → GEPA trace collected
         → Evaluator scores the failure
         → Proposal generated: "Use Claude for auth tasks (Codex failed 3x)"
         → Human approves proposal
         → Agent selection logic updated
         → Next auth task uses Claude → succeeds first try
```

Every failure becomes a rule. Rules become structural improvements. The system gets better at assigning the right agent to the right task.

### lessons.md — The Feedback Channel

This file is the bridge between real-time failures and long-term improvement:

```markdown
2026-02-23T10:15:00Z - RESPAWN: feat-fix-auth (attempt 1)
2026-02-23T10:45:00Z - CI_FAIL: feat-fix-auth — lint, type-check
2026-02-23T11:00:00Z - REVIEW: feat-fix-auth — 3 issues found
2026-02-23T14:00:00Z - RULE: Codex struggles with TypeScript generics — prefer Claude for type-heavy tasks
```

---

## 6. The Babysitter Cron

Add to your crontab (`crontab -e`):

```
*/10 * * * * ~/clawd/scripts/swarm-check.sh >> /tmp/swarm-check.log 2>&1
```

### What It Checks (Every 10 Minutes, Zero Tokens)

| Check | How | Action |
|-------|-----|--------|
| tmux alive? | `tmux has-session` | Respawn if dead (max 3x) |
| PR created? | `gh pr list --head <branch>` | Update status to pr-open |
| CI passing? | `gh pr checks <branch>` | Steer agent on failure |
| Review approved? | `gh pr view --json reviewDecision` | Mark task done |

### Alert Mechanism

Failures write to `/tmp/swarm-alerts.txt`. Your OpenClaw agent can check this on heartbeat and notify you. No tokens burned until you actually need to know something.

---

## 7. Task Registry Schema

`~/clawd/agent-swarm/tasks.json` — a JSON array of task objects:

```json
{
  "id": "feat-fix-auth",
  "tmuxSession": "codex-fix-auth",
  "agent": "codex",
  "description": "Fix auth token refresh",
  "repo": "my-app",
  "worktree": "/Users/you/clawd/worktrees/my-app-feat-fix-auth",
  "branch": "feat/fix-auth",
  "startedAt": "2026-02-23T10:00:00Z",
  "status": "active",
  "retryCount": 0,
  "pr": null,
  "phase": "coding"
}
```

### Status Flow

```
active → pr-open → reviewing → done
                              ↘ failed
```

- **active** — Agent is coding in tmux
- **pr-open** — PR created, awaiting CI/reviews
- **reviewing** — CI passed, in code review
- **done** — PR approved and/or merged
- **failed** — Max retries exceeded or unrecoverable error

---

## 8. Definition of Done

A task is "done" when ALL of these are true:

- ✅ PR created on the correct branch
- ✅ CI passing (all checks green)
- ✅ Code review passed (at least one approval)
- ✅ Screenshots attached for UI changes
- ✅ GEPA trace collected (`swarm-trace.sh`)
- ✅ lessons.md updated with any findings

The babysitter checks the first four automatically. The last two happen via the review pipeline.

---

## 9. Quick Start

Get running in 10 minutes:

### Step 1: Create directory structure

```bash
mkdir -p ~/clawd/agent-swarm/{reviews,logs}
mkdir -p ~/clawd/scripts
mkdir -p ~/clawd/worktrees
mkdir -p ~/clawd/gepa-sidecar/traces
mkdir -p ~/clawd/memory
echo '[]' > ~/clawd/agent-swarm/tasks.json
touch ~/clawd/memory/lessons.md
```

### Step 2: Copy all scripts

Save each script from Section 4 to `~/clawd/scripts/`:

```bash
# Make them executable
chmod +x ~/clawd/scripts/swarm-*.sh
```

### Step 3: Add the cron job

```bash
(crontab -l 2>/dev/null; echo "*/10 * * * * $HOME/clawd/scripts/swarm-check.sh >> /tmp/swarm-check.log 2>&1") | crontab -
```

### Step 4: Auth gh CLI

```bash
gh auth login
gh auth status  # verify
```

### Step 5: Spawn your first agent

```bash
~/clawd/scripts/swarm-spawn.sh \
  --repo my-app \
  --task "Add error handling to API routes" \
  --agent codex \
  --branch feat/error-handling
```

### Step 6: Watch it work

```bash
# Attach to the agent's tmux session
tmux attach -t codex-feat-error-handling

# Check task status
cat ~/clawd/agent-swarm/tasks.json | jq .

# Steer if needed
~/clawd/scripts/swarm-steer.sh feat/error-handling "Focus on the /api/users routes first"
```

### Step 7: Spawn more agents

```bash
# Claude on a different task
~/clawd/scripts/swarm-spawn.sh \
  --repo my-app \
  --task "Refactor database connection pooling" \
  --agent claude \
  --branch feat/db-pooling

# Codex on another
~/clawd/scripts/swarm-spawn.sh \
  --repo my-app \
  --task "Add input validation to all form handlers" \
  --agent codex \
  --branch feat/input-validation
```

The babysitter cron handles the rest. Go get coffee.

---

## 10. Hand This to Your OpenClaw

Copy this entire document and paste it to your OpenClaw agent with:

> "Set up this agent swarm system for my codebase. Use [repo name] as the target repository."

Your OpenClaw agent will:
1. Create all directories
2. Write all scripts
3. Set up the cron job
4. Initialize the task registry
5. Be ready to spawn agents

It should take about 10 minutes. Then you've got a dev team.

---

## Gotchas and Real Talk

**Things that actually matter:**

- **Worktrees are essential.** Without them, agents step on each other. Don't skip this.
- **The prompt file trick** (`cat` from a file instead of inline) saves you from shell escaping hell. Learned this the hard way.
- **`script` command for logging** gives you a real TTY transcript. `tee` doesn't capture interactive CLI output properly.
- **3 retry max is the right number.** If an agent fails 3 times, the task needs human scoping, not more retries.
- **Codex is faster for straightforward tasks.** Claude is better for complex refactors and anything involving types.
- **The babysitter burns zero tokens.** This is the whole point. Don't add LLM calls to the monitoring loop.
- **BMAD scoping prevents 80% of agent failures.** Unscoped tasks are why agents hallucinate features.

**Things that don't matter:**

- Fancy dashboards (check tmux and tasks.json)
- Agent-to-agent communication (they don't need it)
- Complex retry strategies (respawn with the same prompt works fine)

---

*Built by VibeX at AutoEDU.ai. Inspired by Elvis @elvissun's agent orchestration workflow. This is how we actually ship code — multiple AI agents working in parallel, monitored by bash scripts, improving themselves through GEPA. No frameworks. No platforms. Just tmux, git worktrees, and good prompts.*
