@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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/VERSION +1 -0
- package/bin/attach.sh +98 -0
- package/bin/check-agents.sh +343 -0
- package/bin/clawforge +257 -0
- package/bin/clawforge-dashboard +0 -0
- package/bin/clean.sh +257 -0
- package/bin/config.sh +111 -0
- package/bin/conflicts.sh +224 -0
- package/bin/cost.sh +273 -0
- package/bin/dashboard.sh +557 -0
- package/bin/diff.sh +109 -0
- package/bin/doctor.sh +196 -0
- package/bin/eval.sh +217 -0
- package/bin/history.sh +91 -0
- package/bin/init.sh +182 -0
- package/bin/learn.sh +230 -0
- package/bin/logs.sh +126 -0
- package/bin/memory.sh +207 -0
- package/bin/merge-helper.sh +174 -0
- package/bin/multi-review.sh +215 -0
- package/bin/notify.sh +93 -0
- package/bin/on-complete.sh +149 -0
- package/bin/parse-cost.sh +205 -0
- package/bin/pr.sh +167 -0
- package/bin/resume.sh +183 -0
- package/bin/review-mode.sh +163 -0
- package/bin/review-pr.sh +145 -0
- package/bin/routing.sh +88 -0
- package/bin/scope-task.sh +169 -0
- package/bin/spawn-agent.sh +190 -0
- package/bin/sprint.sh +320 -0
- package/bin/steer.sh +107 -0
- package/bin/stop.sh +136 -0
- package/bin/summary.sh +182 -0
- package/bin/swarm.sh +525 -0
- package/bin/templates.sh +244 -0
- package/lib/common.sh +302 -0
- package/lib/templates/bugfix.json +6 -0
- package/lib/templates/migration.json +7 -0
- package/lib/templates/refactor.json +6 -0
- package/lib/templates/security-audit.json +5 -0
- package/lib/templates/test-coverage.json +6 -0
- package/package.json +31 -0
- package/registry/conflicts.jsonl +0 -0
- package/registry/costs.jsonl +0 -0
- package/tui/PRD.md +106 -0
- package/tui/agent.go +266 -0
- package/tui/animation.go +192 -0
- package/tui/dashboard.go +219 -0
- package/tui/filter.go +68 -0
- package/tui/go.mod +25 -0
- package/tui/go.sum +46 -0
- package/tui/keybindings.go +229 -0
- package/tui/main.go +61 -0
- package/tui/model.go +166 -0
- package/tui/steer.go +69 -0
- 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
|