@cyperx/clawforge 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/VERSION +1 -0
  4. package/bin/attach.sh +98 -0
  5. package/bin/check-agents.sh +343 -0
  6. package/bin/clawforge +257 -0
  7. package/bin/clawforge-dashboard +0 -0
  8. package/bin/clean.sh +257 -0
  9. package/bin/config.sh +111 -0
  10. package/bin/conflicts.sh +224 -0
  11. package/bin/cost.sh +273 -0
  12. package/bin/dashboard.sh +557 -0
  13. package/bin/diff.sh +109 -0
  14. package/bin/doctor.sh +196 -0
  15. package/bin/eval.sh +217 -0
  16. package/bin/history.sh +91 -0
  17. package/bin/init.sh +182 -0
  18. package/bin/learn.sh +230 -0
  19. package/bin/logs.sh +126 -0
  20. package/bin/memory.sh +207 -0
  21. package/bin/merge-helper.sh +174 -0
  22. package/bin/multi-review.sh +215 -0
  23. package/bin/notify.sh +93 -0
  24. package/bin/on-complete.sh +149 -0
  25. package/bin/parse-cost.sh +205 -0
  26. package/bin/pr.sh +167 -0
  27. package/bin/resume.sh +183 -0
  28. package/bin/review-mode.sh +163 -0
  29. package/bin/review-pr.sh +145 -0
  30. package/bin/routing.sh +88 -0
  31. package/bin/scope-task.sh +169 -0
  32. package/bin/spawn-agent.sh +190 -0
  33. package/bin/sprint.sh +320 -0
  34. package/bin/steer.sh +107 -0
  35. package/bin/stop.sh +136 -0
  36. package/bin/summary.sh +182 -0
  37. package/bin/swarm.sh +525 -0
  38. package/bin/templates.sh +244 -0
  39. package/lib/common.sh +302 -0
  40. package/lib/templates/bugfix.json +6 -0
  41. package/lib/templates/migration.json +7 -0
  42. package/lib/templates/refactor.json +6 -0
  43. package/lib/templates/security-audit.json +5 -0
  44. package/lib/templates/test-coverage.json +6 -0
  45. package/package.json +31 -0
  46. package/registry/conflicts.jsonl +0 -0
  47. package/registry/costs.jsonl +0 -0
  48. package/tui/PRD.md +106 -0
  49. package/tui/agent.go +266 -0
  50. package/tui/animation.go +192 -0
  51. package/tui/dashboard.go +219 -0
  52. package/tui/filter.go +68 -0
  53. package/tui/go.mod +25 -0
  54. package/tui/go.sum +46 -0
  55. package/tui/keybindings.go +229 -0
  56. package/tui/main.go +61 -0
  57. package/tui/model.go +166 -0
  58. package/tui/steer.go +69 -0
  59. package/tui/styles.go +69 -0
package/bin/swarm.sh ADDED
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env bash
2
+ # swarm.sh — Swarm mode: parallel multi-agent orchestration
3
+ # Usage: clawforge swarm [repo] "<task>" [flags]
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ source "${SCRIPT_DIR}/../lib/common.sh"
8
+ source "${SCRIPT_DIR}/routing.sh"
9
+
10
+ # ── Help ───────────────────────────────────────────────────────────────
11
+ usage() {
12
+ cat <<EOF
13
+ Usage: clawforge swarm [repo] "<task>" [flags]
14
+
15
+ Parallel multi-agent orchestration. Decomposes task, spawns N agents, coordinates.
16
+
17
+ Arguments:
18
+ [repo] Path to git repository (default: auto-detect from cwd)
19
+ "<task>" Task description (required)
20
+
21
+ Flags:
22
+ --repos <paths> Comma-separated repo paths (one agent per repo, skips decomposition)
23
+ --repos-file <path> File with repo paths, one per line
24
+ --routing <strategy> Model routing: auto, cheap, or quality
25
+ --max-agents <N> Cap parallel agents (default: 3)
26
+ --agent <name> Force specific agent for all sub-tasks
27
+ --auto-merge Merge each PR automatically after CI + review
28
+ --template <name> Apply a task template
29
+ --ci-loop Enable CI auto-fix feedback loop
30
+ --max-ci-retries <N> Max CI auto-fix retries (default: 3)
31
+ --budget <dollars> Kill agents if total cost exceeds budget
32
+ --json Output structured JSON
33
+ --notify Send OpenClaw event on completion
34
+ --webhook <url> POST completion payload to URL
35
+ --auto-clean Clean up worktrees+sessions on completion
36
+ --timeout <minutes> Kill agents after N minutes
37
+ --dry-run Show decomposition plan without spawning
38
+ --yes Skip RAM confirmation prompt
39
+ --help Show this help
40
+
41
+ Examples:
42
+ clawforge swarm "Migrate all tests from jest to vitest"
43
+ clawforge swarm "Add i18n to all user-facing strings" --max-agents 4
44
+ clawforge swarm --repos ~/api,~/web,~/shared "Upgrade auth v2 to v3"
45
+ clawforge swarm --repos-file repos.txt "Add health endpoint" --routing cheap
46
+ clawforge swarm --template migration "Migrate to TypeScript"
47
+ EOF
48
+ }
49
+
50
+ # ── Parse args ────────────────────────────────────────────────────────
51
+ REPO="" TASK="" MAX_AGENTS=3 AGENT="" AUTO_MERGE=false DRY_RUN=false SKIP_CONFIRM=false
52
+ TEMPLATE="" CI_LOOP=false MAX_CI_RETRIES=3 BUDGET="" JSON_OUTPUT=false NOTIFY=false WEBHOOK=""
53
+ REPOS="" REPOS_FILE="" ROUTING="" MULTI_REPO=false AUTO_CLEAN=false TIMEOUT_MIN=""
54
+ POSITIONAL=()
55
+
56
+ while [[ $# -gt 0 ]]; do
57
+ case "$1" in
58
+ --repos) REPOS="$2"; shift 2 ;;
59
+ --repos-file) REPOS_FILE="$2"; shift 2 ;;
60
+ --routing) ROUTING="$2"; shift 2 ;;
61
+ --max-agents) MAX_AGENTS="$2"; shift 2 ;;
62
+ --agent) AGENT="$2"; shift 2 ;;
63
+ --auto-merge) AUTO_MERGE=true; shift ;;
64
+ --template) TEMPLATE="$2"; shift 2 ;;
65
+ --ci-loop) CI_LOOP=true; shift ;;
66
+ --max-ci-retries) MAX_CI_RETRIES="$2"; shift 2 ;;
67
+ --budget) BUDGET="$2"; shift 2 ;;
68
+ --json) JSON_OUTPUT=true; shift ;;
69
+ --notify) NOTIFY=true; shift ;;
70
+ --webhook) WEBHOOK="$2"; shift 2 ;;
71
+ --auto-clean) AUTO_CLEAN=true; shift ;;
72
+ --timeout) TIMEOUT_MIN="$2"; shift 2 ;;
73
+ --dry-run) DRY_RUN=true; shift ;;
74
+ --yes) SKIP_CONFIRM=true; shift ;;
75
+ --help|-h) usage; exit 0 ;;
76
+ --*) log_error "Unknown option: $1"; usage; exit 1 ;;
77
+ *) POSITIONAL+=("$1"); shift ;;
78
+ esac
79
+ done
80
+
81
+ # ── Apply template (template < CLI flags) ─────────────────────────────
82
+ if [[ -n "$TEMPLATE" ]]; then
83
+ TMPL_FILE=""
84
+ if [[ -f "${CLAWFORGE_DIR}/lib/templates/${TEMPLATE}.json" ]]; then
85
+ TMPL_FILE="${CLAWFORGE_DIR}/lib/templates/${TEMPLATE}.json"
86
+ elif [[ -f "${HOME}/.clawforge/templates/${TEMPLATE}.json" ]]; then
87
+ TMPL_FILE="${HOME}/.clawforge/templates/${TEMPLATE}.json"
88
+ else
89
+ log_error "Template '$TEMPLATE' not found"; exit 1
90
+ fi
91
+ log_info "Applying template: $TEMPLATE"
92
+ TMPL_MAX_AGENTS=$(jq -r '.maxAgents // empty' "$TMPL_FILE" 2>/dev/null || true)
93
+ TMPL_AUTO_MERGE=$(jq -r '.autoMerge // false' "$TMPL_FILE")
94
+ TMPL_CI_LOOP=$(jq -r '.ciLoop // false' "$TMPL_FILE")
95
+ [[ -n "$TMPL_MAX_AGENTS" ]] && MAX_AGENTS="$TMPL_MAX_AGENTS"
96
+ [[ "$TMPL_AUTO_MERGE" == "true" ]] && AUTO_MERGE=true
97
+ [[ "$TMPL_CI_LOOP" == "true" ]] && CI_LOOP=true
98
+ fi
99
+
100
+ # Parse positional args: [repo] "<task>"
101
+ case ${#POSITIONAL[@]} in
102
+ 0) log_error "Task description is required"; usage; exit 1 ;;
103
+ 1) TASK="${POSITIONAL[0]}" ;;
104
+ 2) REPO="${POSITIONAL[0]}"; TASK="${POSITIONAL[1]}" ;;
105
+ *) log_error "Too many positional arguments"; usage; exit 1 ;;
106
+ esac
107
+
108
+ if [[ -n "$TIMEOUT_MIN" && ! "$TIMEOUT_MIN" =~ ^[0-9]+$ ]]; then
109
+ log_error "--timeout must be an integer number of minutes"
110
+ exit 1
111
+ fi
112
+
113
+ # ── Resolve multi-repo paths ─────────────────────────────────────────
114
+ REPO_LIST=()
115
+ if [[ -n "$REPOS" ]]; then
116
+ IFS=',' read -ra REPO_LIST <<< "$REPOS"
117
+ MULTI_REPO=true
118
+ elif [[ -n "$REPOS_FILE" ]]; then
119
+ [[ -f "$REPOS_FILE" ]] || { log_error "Repos file not found: $REPOS_FILE"; exit 1; }
120
+ while IFS= read -r line; do
121
+ line=$(echo "$line" | sed 's/#.*//' | xargs) # strip comments + whitespace
122
+ [[ -n "$line" ]] && REPO_LIST+=("$line")
123
+ done < "$REPOS_FILE"
124
+ MULTI_REPO=true
125
+ fi
126
+
127
+ # Resolve absolute paths for multi-repo
128
+ if $MULTI_REPO; then
129
+ RESOLVED_REPOS=()
130
+ for rp in "${REPO_LIST[@]}"; do
131
+ expanded=$(eval echo "$rp") # expand ~ and env vars
132
+ [[ -d "$expanded" ]] || { log_error "Repo path not found: $rp"; exit 1; }
133
+ RESOLVED_REPOS+=("$(cd "$expanded" && pwd)")
134
+ done
135
+ REPO_LIST=("${RESOLVED_REPOS[@]}")
136
+ # Use first repo as the "primary" for parent task
137
+ REPO="${REPO_LIST[0]}"
138
+ fi
139
+
140
+ # ── Resolve repo (single-repo mode) ─────────────────────────────────
141
+ if [[ -z "$REPO" ]]; then
142
+ REPO=$(detect_repo) || { log_error "No --repo and no git repo found from cwd"; exit 1; }
143
+ fi
144
+ REPO_ABS=$(cd "$REPO" && pwd)
145
+
146
+ # Disk safety check before spawn/decompose
147
+ disk_check "$REPO_ABS" || { log_error "Aborting due to low disk space"; exit 1; }
148
+
149
+ # ── Resolve agent ─────────────────────────────────────────────────────
150
+ RESOLVED_AGENT=$(detect_agent "${AGENT:-}")
151
+ if [[ "$RESOLVED_AGENT" == "claude" ]]; then
152
+ MODEL=$(config_get default_model_claude "claude-sonnet-4-5")
153
+ else
154
+ MODEL=$(config_get default_model_codex "gpt-5.3-codex")
155
+ fi
156
+
157
+ # ── Load routing ─────────────────────────────────────────────────────
158
+ if [[ -n "$ROUTING" ]]; then
159
+ load_routing "$ROUTING"
160
+ log_info "Routing: strategy=$ROUTING"
161
+ IMPL_MODEL=$(get_model_for_phase "implement")
162
+ [[ -n "$IMPL_MODEL" ]] && MODEL="$IMPL_MODEL"
163
+ fi
164
+
165
+ # ── RAM warning ───────────────────────────────────────────────────────
166
+ AGENT_COUNT=$MAX_AGENTS
167
+ $MULTI_REPO && AGENT_COUNT=${#REPO_LIST[@]}
168
+ RAM_THRESHOLD=$(config_get ram_warn_threshold 3)
169
+ if [[ "$AGENT_COUNT" -gt "$RAM_THRESHOLD" ]] && ! $SKIP_CONFIRM && ! $DRY_RUN; then
170
+ ESTIMATED_RAM=$((AGENT_COUNT * 2))
171
+ echo ""
172
+ echo " Warning: $AGENT_COUNT agents will use ~${ESTIMATED_RAM}GB RAM (estimated). Continue? [Y/n]"
173
+ read -r confirm
174
+ if [[ "$confirm" =~ ^[nN] ]]; then
175
+ echo "Aborted."
176
+ exit 0
177
+ fi
178
+ fi
179
+
180
+ # ── Multi-repo mode ──────────────────────────────────────────────────
181
+ SPAWN_FAILED=0
182
+ if $MULTI_REPO; then
183
+ # ── Multi-repo: one agent per repo, skip decomposition ────────────
184
+ SUB_TASK_COUNT=${#REPO_LIST[@]}
185
+ log_info "Multi-repo swarm: $SUB_TASK_COUNT repos"
186
+
187
+ # Build repo name list for context injection
188
+ REPO_NAMES=()
189
+ for rp in "${REPO_LIST[@]}"; do
190
+ REPO_NAMES+=("$(basename "$rp")")
191
+ done
192
+ ALL_REPO_NAMES=$(IFS=', '; echo "${REPO_NAMES[*]}")
193
+
194
+ # ── Assign parent short ID ───────────────────────────────────────
195
+ PARENT_SHORT_ID=$(_next_short_id)
196
+ PARENT_ID="swarm-$(slugify_task "$TASK" 30)"
197
+
198
+ # ── Register parent task ─────────────────────────────────────────
199
+ NOW=$(epoch_ms)
200
+ PARENT_JSON=$(jq -n \
201
+ --arg id "$PARENT_ID" \
202
+ --argjson sid "$PARENT_SHORT_ID" \
203
+ --arg desc "$TASK" \
204
+ --arg repo "$REPO_ABS" \
205
+ --argjson started "$NOW" \
206
+ --argjson subcount "$SUB_TASK_COUNT" \
207
+ --arg repos "$ALL_REPO_NAMES" \
208
+ '{
209
+ id: $id,
210
+ short_id: $sid,
211
+ mode: "swarm",
212
+ tmuxSession: "",
213
+ agent: "multi",
214
+ model: "multi",
215
+ description: $desc,
216
+ repo: $repo,
217
+ worktree: "",
218
+ branch: "",
219
+ startedAt: $started,
220
+ status: "running",
221
+ retries: 0,
222
+ maxRetries: 0,
223
+ pr: null,
224
+ checks: {},
225
+ completedAt: null,
226
+ note: null,
227
+ files_touched: [],
228
+ ci_retries: 0,
229
+ sub_task_count: $subcount,
230
+ auto_merge: false,
231
+ multi_repo: true,
232
+ repos: $repos
233
+ }')
234
+ registry_add "$PARENT_JSON"
235
+ $AUTO_MERGE && registry_update "$PARENT_ID" "auto_merge" 'true'
236
+ $CI_LOOP && registry_update "$PARENT_ID" "ci_loop" 'true'
237
+ registry_update "$PARENT_ID" "max_ci_retries" "$MAX_CI_RETRIES"
238
+ [[ -n "$BUDGET" ]] && registry_update "$PARENT_ID" "budget" "$BUDGET"
239
+ [[ -n "$TIMEOUT_MIN" ]] && registry_update "$PARENT_ID" "timeout_minutes" "$TIMEOUT_MIN"
240
+ $AUTO_CLEAN && registry_update "$PARENT_ID" "auto_clean" "true"
241
+ [[ -n "$TIMEOUT_MIN" ]] && registry_update "$PARENT_ID" "timeout_minutes" "$TIMEOUT_MIN"
242
+ $AUTO_CLEAN && registry_update "$PARENT_ID" "auto_clean" "true"
243
+
244
+ # ── Dry-run output ───────────────────────────────────────────────
245
+ if $DRY_RUN; then
246
+ echo "=== Swarm Dry Run (Multi-Repo) ==="
247
+ echo " Task: $TASK"
248
+ echo " Repos: $ALL_REPO_NAMES"
249
+ echo " Agent: $RESOLVED_AGENT ($MODEL)"
250
+ echo " Short ID: #$PARENT_SHORT_ID"
251
+ echo " Sub-tasks: $SUB_TASK_COUNT (one per repo)"
252
+ echo " Auto-merge: $AUTO_MERGE"
253
+ [[ -n "$TIMEOUT_MIN" ]] && echo " Timeout: ${TIMEOUT_MIN}m"
254
+ $AUTO_CLEAN && echo " Auto-clean: true"
255
+ [[ -n "$TIMEOUT_MIN" ]] && echo " Timeout: ${TIMEOUT_MIN}m"
256
+ $AUTO_CLEAN && echo " Auto-clean: true"
257
+ [[ -n "$ROUTING" ]] && echo " Routing: $ROUTING"
258
+ echo ""
259
+ echo "Repos:"
260
+ for i in $(seq 0 $((SUB_TASK_COUNT - 1))); do
261
+ echo " #${PARENT_SHORT_ID}.${REPO_NAMES[$i]}: ${REPO_LIST[$i]}"
262
+ done
263
+ echo ""
264
+ echo "Would spawn $SUB_TASK_COUNT agents, one per repo."
265
+ exit 0
266
+ fi
267
+
268
+ # ── Spawn one agent per repo ─────────────────────────────────────
269
+ echo ""
270
+ echo " #${PARENT_SHORT_ID} swarm running multi-repo \"$(echo "$TASK" | head -c 50)\" ($SUB_TASK_COUNT repos)"
271
+ echo ""
272
+
273
+ for i in $(seq 0 $((SUB_TASK_COUNT - 1))); do
274
+ SUB_INDEX=$((i + 1))
275
+ SUB_REPO="${REPO_LIST[$i]}"
276
+ SUB_REPO_NAME="${REPO_NAMES[$i]}"
277
+ SUB_SHORT_ID=$(_next_short_id)
278
+
279
+ # Build repo-aware task prompt
280
+ OTHER_REPOS=$(printf '%s\n' "${REPO_NAMES[@]}" | grep -v "^${SUB_REPO_NAME}$" | paste -sd ', ' -)
281
+ SUB_TASK="You are working on repo: ${SUB_REPO_NAME}. Other repos in this task: ${OTHER_REPOS}.
282
+
283
+ ${TASK}"
284
+
285
+ SUB_BRANCH=$(auto_branch_name "swarm" "$TASK" "$SUB_REPO")
286
+ SUB_SAFE=$(sanitize_branch "$SUB_BRANCH")
287
+
288
+ log_info "Spawning sub-agent #${PARENT_SHORT_ID}.${SUB_REPO_NAME}: $SUB_REPO"
289
+
290
+ # Spawn agent
291
+ SPAWN_ARGS=(--repo "$SUB_REPO" --branch "$SUB_BRANCH" --task "$SUB_TASK")
292
+ [[ -n "${AGENT:-}" ]] && SPAWN_ARGS+=(--agent "$AGENT")
293
+ [[ -n "$MODEL" ]] && SPAWN_ARGS+=(--model "$MODEL")
294
+
295
+ if ! "${SCRIPT_DIR}/spawn-agent.sh" "${SPAWN_ARGS[@]}" 2>/dev/null; then
296
+ log_error "Failed to spawn sub-agent for repo ${SUB_REPO_NAME}"
297
+ SPAWN_FAILED=$((SPAWN_FAILED + 1))
298
+ continue
299
+ fi
300
+
301
+ # Enhance registry entry with swarm + repo metadata
302
+ registry_update "$SUB_SAFE" "short_id" "$SUB_SHORT_ID"
303
+ registry_update "$SUB_SAFE" "mode" '"swarm"'
304
+ registry_update "$SUB_SAFE" "parent_id" "\"$PARENT_ID\""
305
+ registry_update "$SUB_SAFE" "sub_index" "$SUB_INDEX"
306
+ registry_update "$SUB_SAFE" "repo_name" "\"$SUB_REPO_NAME\""
307
+ registry_update "$SUB_SAFE" "repo" "\"$SUB_REPO\""
308
+ registry_update "$SUB_SAFE" "files_touched" '[]'
309
+ registry_update "$SUB_SAFE" "ci_retries" '0'
310
+
311
+ echo " #${PARENT_SHORT_ID}.${SUB_REPO_NAME} swarm spawned $(basename "$SUB_REPO") \"$(echo "$TASK" | head -c 40)\""
312
+ done
313
+
314
+ else
315
+ # ── Standard mode: decompose task into sub-tasks ───────────────────
316
+ log_info "Swarm mode: decomposing task into sub-tasks..."
317
+ log_info "Max agents: $MAX_AGENTS"
318
+
319
+ # Use Claude to decompose the task into sub-tasks
320
+ DECOMPOSE_PROMPT="Decompose this coding task into ${MAX_AGENTS} or fewer independent sub-tasks that can be worked on in parallel by separate coding agents. Each sub-task should be self-contained and not depend on others.
321
+
322
+ Task: ${TASK}
323
+
324
+ Respond with ONLY a JSON array of sub-task descriptions. Example:
325
+ [\"Sub-task 1 description\", \"Sub-task 2 description\", \"Sub-task 3 description\"]"
326
+
327
+ # Try to decompose using agent, fall back to splitting by sentence
328
+ SUB_TASKS="[]"
329
+ if command -v claude &>/dev/null && ! $DRY_RUN; then
330
+ # Guard decomposition so swarm doesn't stall builds if model call hangs.
331
+ DECOMP_TIMEOUT_SEC=$(( $(config_get decompose_timeout_minutes 2) * 60 ))
332
+ TMP_DECOMP=$(mktemp)
333
+ (claude --model "$MODEL" -p "$DECOMPOSE_PROMPT" >"$TMP_DECOMP" 2>/dev/null || true) &
334
+ CLAUDE_PID=$!
335
+ (
336
+ sleep "$DECOMP_TIMEOUT_SEC"
337
+ if kill -0 "$CLAUDE_PID" 2>/dev/null; then
338
+ log_warn "Swarm decomposition timed out after ${DECOMP_TIMEOUT_SEC}s; using fallback split"
339
+ kill "$CLAUDE_PID" 2>/dev/null || true
340
+ fi
341
+ ) &
342
+ WATCHDOG_PID=$!
343
+ wait "$CLAUDE_PID" 2>/dev/null || true
344
+ kill "$WATCHDOG_PID" 2>/dev/null || true
345
+ DECOMPOSED=$(cat "$TMP_DECOMP" 2>/dev/null || true)
346
+ rm -f "$TMP_DECOMP"
347
+
348
+ # Try to extract JSON array from response
349
+ if echo "$DECOMPOSED" | jq -e 'type == "array"' >/dev/null 2>&1; then
350
+ SUB_TASKS="$DECOMPOSED"
351
+ elif echo "$DECOMPOSED" | grep -o '\[.*\]' | jq -e 'type == "array"' >/dev/null 2>&1; then
352
+ SUB_TASKS=$(echo "$DECOMPOSED" | grep -o '\[.*\]' | head -1)
353
+ else
354
+ # Fallback: split task into N generic parts to keep pipeline moving
355
+ SUB_TASKS=$(jq -n --arg t "$TASK" --argjson n "$MAX_AGENTS" '[range($n) | "\($t) (part \(. + 1))"]')
356
+ fi
357
+ else
358
+ # Dry-run or no agent: create placeholder sub-tasks
359
+ SUB_TASKS=$(jq -n --arg t "$TASK" --argjson n "$MAX_AGENTS" \
360
+ '[range($n) | "\($t) (part \(. + 1))"]')
361
+ fi
362
+
363
+ # Cap to max-agents
364
+ SUB_TASK_COUNT=$(echo "$SUB_TASKS" | jq 'length')
365
+ if [[ "$SUB_TASK_COUNT" -gt "$MAX_AGENTS" ]]; then
366
+ SUB_TASKS=$(echo "$SUB_TASKS" | jq --argjson n "$MAX_AGENTS" '.[0:$n]')
367
+ SUB_TASK_COUNT=$MAX_AGENTS
368
+ fi
369
+
370
+ # ── Assign parent short ID ───────────────────────────────────────
371
+ PARENT_SHORT_ID=$(_next_short_id)
372
+ PARENT_ID="swarm-$(slugify_task "$TASK" 30)"
373
+
374
+ # ── Register parent task ─────────────────────────────────────────
375
+ NOW=$(epoch_ms)
376
+ PARENT_JSON=$(jq -n \
377
+ --arg id "$PARENT_ID" \
378
+ --argjson sid "$PARENT_SHORT_ID" \
379
+ --arg desc "$TASK" \
380
+ --arg repo "$REPO_ABS" \
381
+ --argjson started "$NOW" \
382
+ --argjson subcount "$SUB_TASK_COUNT" \
383
+ '{
384
+ id: $id,
385
+ short_id: $sid,
386
+ mode: "swarm",
387
+ tmuxSession: "",
388
+ agent: "multi",
389
+ model: "multi",
390
+ description: $desc,
391
+ repo: $repo,
392
+ worktree: "",
393
+ branch: "",
394
+ startedAt: $started,
395
+ status: "running",
396
+ retries: 0,
397
+ maxRetries: 0,
398
+ pr: null,
399
+ checks: {},
400
+ completedAt: null,
401
+ note: null,
402
+ files_touched: [],
403
+ ci_retries: 0,
404
+ sub_task_count: $subcount,
405
+ auto_merge: false
406
+ }')
407
+ registry_add "$PARENT_JSON"
408
+ $AUTO_MERGE && registry_update "$PARENT_ID" "auto_merge" 'true'
409
+ $CI_LOOP && registry_update "$PARENT_ID" "ci_loop" 'true'
410
+ registry_update "$PARENT_ID" "max_ci_retries" "$MAX_CI_RETRIES"
411
+ [[ -n "$BUDGET" ]] && registry_update "$PARENT_ID" "budget" "$BUDGET"
412
+
413
+ # ── Dry-run output ───────────────────────────────────────────────
414
+ if $DRY_RUN; then
415
+ echo "=== Swarm Dry Run ==="
416
+ echo " Task: $TASK"
417
+ echo " Repo: $REPO_ABS"
418
+ echo " Agent: $RESOLVED_AGENT ($MODEL)"
419
+ echo " Short ID: #$PARENT_SHORT_ID"
420
+ echo " Sub-tasks: $SUB_TASK_COUNT"
421
+ echo " Auto-merge: $AUTO_MERGE"
422
+ [[ -n "$ROUTING" ]] && echo " Routing: $ROUTING"
423
+ echo ""
424
+ echo "Decomposition:"
425
+ echo "$SUB_TASKS" | jq -r 'to_entries[] | " #\(.key + 1): \(.value)"'
426
+ echo ""
427
+ echo "Would spawn $SUB_TASK_COUNT agents, each in own worktree."
428
+ exit 0
429
+ fi
430
+
431
+ # ── Spawn sub-agents ─────────────────────────────────────────────
432
+ echo ""
433
+ echo " #${PARENT_SHORT_ID} swarm running $(basename "$REPO_ABS") \"$(echo "$TASK" | head -c 50)\" ($SUB_TASK_COUNT agents)"
434
+ echo ""
435
+
436
+ for i in $(seq 0 $((SUB_TASK_COUNT - 1))); do
437
+ SUB_INDEX=$((i + 1))
438
+ SUB_TASK=$(echo "$SUB_TASKS" | jq -r ".[$i]")
439
+ SUB_BRANCH=$(auto_branch_name "swarm" "$SUB_TASK" "$REPO_ABS")
440
+ SUB_SAFE=$(sanitize_branch "$SUB_BRANCH")
441
+ SUB_SHORT_ID=$(_next_short_id)
442
+
443
+ log_info "Spawning sub-agent #${PARENT_SHORT_ID}.${SUB_INDEX}: $SUB_TASK"
444
+
445
+ # Spawn agent
446
+ SPAWN_ARGS=(--repo "$REPO_ABS" --branch "$SUB_BRANCH" --task "$SUB_TASK")
447
+ [[ -n "${AGENT:-}" ]] && SPAWN_ARGS+=(--agent "$AGENT")
448
+ [[ -n "$MODEL" ]] && SPAWN_ARGS+=(--model "$MODEL")
449
+
450
+ if ! "${SCRIPT_DIR}/spawn-agent.sh" "${SPAWN_ARGS[@]}" 2>/dev/null; then
451
+ log_error "Failed to spawn sub-agent ${SUB_INDEX}"
452
+ SPAWN_FAILED=$((SPAWN_FAILED + 1))
453
+ continue
454
+ fi
455
+
456
+ # Enhance registry entry with swarm metadata
457
+ registry_update "$SUB_SAFE" "short_id" "$SUB_SHORT_ID"
458
+ registry_update "$SUB_SAFE" "mode" '"swarm"'
459
+ registry_update "$SUB_SAFE" "parent_id" "\"$PARENT_ID\""
460
+ registry_update "$SUB_SAFE" "sub_index" "$SUB_INDEX"
461
+ registry_update "$SUB_SAFE" "files_touched" '[]'
462
+ registry_update "$SUB_SAFE" "ci_retries" '0'
463
+
464
+ echo " #${PARENT_SHORT_ID}.${SUB_INDEX} swarm spawned \"$(echo "$SUB_TASK" | head -c 50)\""
465
+ done
466
+ fi
467
+
468
+ if [[ "$SPAWN_FAILED" -gt 0 ]]; then
469
+ registry_update "$PARENT_ID" "spawn_failed" "$SPAWN_FAILED" || true
470
+ if [[ "$SPAWN_FAILED" -ge "$SUB_TASK_COUNT" ]]; then
471
+ registry_update "$PARENT_ID" "status" '"failed"' || true
472
+ log_error "All sub-agent spawns failed (${SPAWN_FAILED}/${SUB_TASK_COUNT})."
473
+ else
474
+ log_warn "Some sub-agent spawns failed (${SPAWN_FAILED}/${SUB_TASK_COUNT})."
475
+ fi
476
+ fi
477
+
478
+ # ── Notify ────────────────────────────────────────────────────────────
479
+ "${SCRIPT_DIR}/notify.sh" --type task-started --description "Swarm: $TASK ($SUB_TASK_COUNT agents)" --dry-run 2>/dev/null || true
480
+
481
+ # ── OpenClaw notify ──────────────────────────────────────────────────
482
+ if $NOTIFY; then
483
+ openclaw system event --text "ClawForge: swarm started — $TASK (#$PARENT_SHORT_ID, $SUB_TASK_COUNT agents)" --mode now 2>/dev/null || true
484
+ fi
485
+
486
+ # ── Webhook ──────────────────────────────────────────────────────────
487
+ if [[ -n "$WEBHOOK" ]]; then
488
+ PAYLOAD=$(jq -cn \
489
+ --arg taskId "$PARENT_ID" \
490
+ --argjson shortId "$PARENT_SHORT_ID" \
491
+ --arg mode "swarm" \
492
+ --arg status "running" \
493
+ --argjson subTaskCount "$SUB_TASK_COUNT" \
494
+ --arg description "$TASK" \
495
+ --arg repo "$REPO_ABS" \
496
+ '{taskId: $taskId, shortId: $shortId, mode: $mode, status: $status, subTaskCount: $subTaskCount, description: $description, repo: $repo}')
497
+ curl -s -X POST -H "Content-Type: application/json" -d "$PAYLOAD" "$WEBHOOK" >/dev/null 2>&1 || log_warn "Webhook POST failed"
498
+ fi
499
+
500
+ # ── Output ────────────────────────────────────────────────────────────
501
+ if $JSON_OUTPUT; then
502
+ jq -cn \
503
+ --arg taskId "$PARENT_ID" \
504
+ --argjson shortId "$PARENT_SHORT_ID" \
505
+ --arg mode "swarm" \
506
+ --arg status "running" \
507
+ --argjson subTaskCount "$SUB_TASK_COUNT" \
508
+ --arg description "$TASK" \
509
+ --arg repo "$REPO_ABS" \
510
+ --argjson autoMerge "$AUTO_MERGE" \
511
+ --argjson ciLoop "$CI_LOOP" \
512
+ '{taskId: $taskId, shortId: $shortId, mode: $mode, status: $status, subTaskCount: $subTaskCount, description: $description, repo: $repo, autoMerge: $autoMerge, ciLoop: $ciLoop}'
513
+ else
514
+ echo ""
515
+ echo " Spawned $((SUB_TASK_COUNT - SPAWN_FAILED))/$SUB_TASK_COUNT agents."
516
+ echo " Status: clawforge status"
517
+ echo " Attach: clawforge attach ${PARENT_SHORT_ID}.N (where N is the agent number)"
518
+ echo " Steer: clawforge steer ${PARENT_SHORT_ID}.N \"<message>\""
519
+ echo ""
520
+ $CI_LOOP && echo " CI feedback loop: enabled (max retries: $MAX_CI_RETRIES)"
521
+ [[ -n "$BUDGET" ]] && echo " Budget cap: \$$BUDGET"
522
+ [[ -n "$ROUTING" ]] && echo " Routing: $ROUTING"
523
+ [[ "$SPAWN_FAILED" -gt 0 ]] && echo " ⚠️ Spawn failures: $SPAWN_FAILED (check clawforge doctor + logs)"
524
+ echo " Tip: Run 'clawforge watch --daemon' in another pane for auto-monitoring"
525
+ fi