@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
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-agents.sh — Module 4: Health check all running agents
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
7
|
+
|
|
8
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
9
|
+
PID_FILE="${CLAWFORGE_DIR}/watch.pid"
|
|
10
|
+
|
|
11
|
+
usage() {
|
|
12
|
+
cat <<EOF
|
|
13
|
+
Usage: check-agents.sh [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--json Output as JSON
|
|
17
|
+
--dry-run Don't auto-respawn or update registry
|
|
18
|
+
--daemon Run continuously in background (interval: 5 min)
|
|
19
|
+
--interval N Daemon check interval in seconds (default: 300)
|
|
20
|
+
--stop Stop the running daemon
|
|
21
|
+
--help Show this help
|
|
22
|
+
EOF
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
JSON_OUTPUT=false
|
|
26
|
+
DRY_RUN=false
|
|
27
|
+
DAEMON=false
|
|
28
|
+
DAEMON_INTERVAL=300
|
|
29
|
+
STOP_DAEMON=false
|
|
30
|
+
|
|
31
|
+
while [[ $# -gt 0 ]]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--json) JSON_OUTPUT=true; shift ;;
|
|
34
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
35
|
+
--daemon) DAEMON=true; shift ;;
|
|
36
|
+
--interval) DAEMON_INTERVAL="$2"; shift 2 ;;
|
|
37
|
+
--stop) STOP_DAEMON=true; shift ;;
|
|
38
|
+
--help|-h) usage; exit 0 ;;
|
|
39
|
+
*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# ── Stop daemon ───────────────────────────────────────────────────────
|
|
44
|
+
if $STOP_DAEMON; then
|
|
45
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
46
|
+
PID=$(cat "$PID_FILE")
|
|
47
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
48
|
+
kill "$PID"
|
|
49
|
+
rm -f "$PID_FILE"
|
|
50
|
+
echo "Daemon stopped (PID: $PID)"
|
|
51
|
+
else
|
|
52
|
+
rm -f "$PID_FILE"
|
|
53
|
+
echo "Daemon was not running (stale PID file removed)"
|
|
54
|
+
fi
|
|
55
|
+
else
|
|
56
|
+
echo "No daemon running."
|
|
57
|
+
fi
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# ── CI auto-feedback loop ────────────────────────────────────────────
|
|
62
|
+
_ci_feedback() {
|
|
63
|
+
local id="$1" repo="$2" pr="$3" tmux_session="$4" branch="$5"
|
|
64
|
+
local ci_retries ci_limit
|
|
65
|
+
|
|
66
|
+
ci_retries=$(registry_get "$id" | jq -r '.ci_retries // 0')
|
|
67
|
+
# Use per-task max_ci_retries if set, otherwise fall back to config
|
|
68
|
+
ci_limit=$(registry_get "$id" | jq -r '.max_ci_retries // empty' 2>/dev/null || true)
|
|
69
|
+
[[ -z "$ci_limit" ]] && ci_limit=$(config_get ci_retry_limit 2)
|
|
70
|
+
|
|
71
|
+
if [[ "$ci_retries" -ge "$ci_limit" ]]; then
|
|
72
|
+
log_warn "CI retry limit reached for $id ($ci_retries/$ci_limit)"
|
|
73
|
+
return 0
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Fetch failed CI log
|
|
77
|
+
local ci_log
|
|
78
|
+
ci_log=$(gh run list --repo "$repo" --branch "$branch" --status failure --limit 1 --json databaseId -q '.[0].databaseId' 2>/dev/null || true)
|
|
79
|
+
if [[ -z "$ci_log" ]]; then
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
local error_output
|
|
84
|
+
error_output=$(gh run view "$ci_log" --repo "$repo" --log-failed 2>/dev/null | tail -50 || true)
|
|
85
|
+
if [[ -z "$error_output" ]]; then
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Auto-steer the agent
|
|
90
|
+
local steer_msg="CI failed on your PR. Here is the error output:
|
|
91
|
+
|
|
92
|
+
${error_output}
|
|
93
|
+
|
|
94
|
+
Fix the issues and push again."
|
|
95
|
+
|
|
96
|
+
if tmux has-session -t "$tmux_session" 2>/dev/null; then
|
|
97
|
+
if [[ ${#steer_msg} -gt 200 ]]; then
|
|
98
|
+
local tmpfile
|
|
99
|
+
tmpfile=$(mktemp)
|
|
100
|
+
echo "$steer_msg" > "$tmpfile"
|
|
101
|
+
tmux load-buffer "$tmpfile"
|
|
102
|
+
tmux paste-buffer -t "$tmux_session"
|
|
103
|
+
tmux send-keys -t "$tmux_session" Enter
|
|
104
|
+
rm -f "$tmpfile"
|
|
105
|
+
else
|
|
106
|
+
tmux send-keys -t "$tmux_session" "$steer_msg" Enter
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Increment ci_retries
|
|
110
|
+
local new_retries=$((ci_retries + 1))
|
|
111
|
+
registry_update "$id" "ci_retries" "$new_retries"
|
|
112
|
+
log_info "CI feedback sent to $id (retry $new_retries/$ci_limit)"
|
|
113
|
+
|
|
114
|
+
# Notify
|
|
115
|
+
"${SCRIPT_DIR}/notify.sh" --type task-failed --description "CI failed for $id (auto-retry $new_retries)" --dry-run 2>/dev/null || true
|
|
116
|
+
fi
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_ensure_registry
|
|
120
|
+
|
|
121
|
+
# ── Check each task ───────────────────────────────────────────────────
|
|
122
|
+
TASKS=$(jq -c '.tasks[]' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
123
|
+
if [[ -z "$TASKS" ]]; then
|
|
124
|
+
if $JSON_OUTPUT; then
|
|
125
|
+
echo '{"tasks":[],"summary":"No active tasks"}'
|
|
126
|
+
else
|
|
127
|
+
echo "No active tasks in registry."
|
|
128
|
+
fi
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
RESULTS="[]"
|
|
133
|
+
|
|
134
|
+
while IFS= read -r task; do
|
|
135
|
+
id=$(echo "$task" | jq -r '.id')
|
|
136
|
+
tmux_session=$(echo "$task" | jq -r '.tmuxSession')
|
|
137
|
+
branch=$(echo "$task" | jq -r '.branch')
|
|
138
|
+
repo=$(echo "$task" | jq -r '.repo')
|
|
139
|
+
status=$(echo "$task" | jq -r '.status')
|
|
140
|
+
retries=$(echo "$task" | jq -r '.retries')
|
|
141
|
+
max_retries=$(echo "$task" | jq -r '.maxRetries')
|
|
142
|
+
|
|
143
|
+
new_status="$status"
|
|
144
|
+
pr_number=""
|
|
145
|
+
ci_status=""
|
|
146
|
+
|
|
147
|
+
# Skip completed tasks
|
|
148
|
+
if [[ "$status" == "done" ]]; then
|
|
149
|
+
RESULTS=$(echo "$RESULTS" | jq --argjson t "$task" '. += [$t]')
|
|
150
|
+
continue
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# 1. Check tmux session
|
|
154
|
+
tmux_alive=false
|
|
155
|
+
if tmux has-session -t "$tmux_session" 2>/dev/null; then
|
|
156
|
+
tmux_alive=true
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# 2. Check for PR
|
|
160
|
+
if [[ -d "$repo" ]]; then
|
|
161
|
+
pr_info=$(gh pr list --repo "$repo" --head "$branch" --json number,state 2>/dev/null || echo "[]")
|
|
162
|
+
pr_number=$(echo "$pr_info" | jq -r '.[0].number // empty' 2>/dev/null || true)
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# 3. Determine status
|
|
166
|
+
if [[ -n "$pr_number" ]]; then
|
|
167
|
+
new_status="pr-created"
|
|
168
|
+
if ! $DRY_RUN; then
|
|
169
|
+
registry_update "$id" "pr" "$pr_number"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Check CI
|
|
173
|
+
ci_result=$(gh pr checks "$pr_number" --repo "$repo" 2>/dev/null || true)
|
|
174
|
+
if echo "$ci_result" | grep -q "pass"; then
|
|
175
|
+
if ! echo "$ci_result" | grep -qE "fail|pending"; then
|
|
176
|
+
new_status="ci-passing"
|
|
177
|
+
fi
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# CI auto-feedback loop: if CI failed and agent is alive, steer with error context
|
|
181
|
+
if echo "$ci_result" | grep -q "fail" && $tmux_alive && ! $DRY_RUN; then
|
|
182
|
+
_ci_feedback "$id" "$repo" "$pr_number" "$tmux_session" "$branch"
|
|
183
|
+
fi
|
|
184
|
+
elif ! $tmux_alive; then
|
|
185
|
+
# tmux dead + no PR = failed
|
|
186
|
+
new_status="failed"
|
|
187
|
+
if ! $DRY_RUN && [[ "$retries" -lt "$max_retries" ]]; then
|
|
188
|
+
log_warn "Task $id failed, respawning (retry $((retries + 1))/$max_retries)..."
|
|
189
|
+
new_retries=$((retries + 1))
|
|
190
|
+
registry_update "$id" "retries" "$new_retries"
|
|
191
|
+
registry_update "$id" "status" '"spawned"'
|
|
192
|
+
|
|
193
|
+
# Respawn
|
|
194
|
+
worktree=$(echo "$task" | jq -r '.worktree')
|
|
195
|
+
agent=$(echo "$task" | jq -r '.agent')
|
|
196
|
+
model=$(echo "$task" | jq -r '.model')
|
|
197
|
+
desc=$(echo "$task" | jq -r '.description')
|
|
198
|
+
|
|
199
|
+
FULL_PROMPT="$desc
|
|
200
|
+
|
|
201
|
+
When complete:
|
|
202
|
+
1. Commit your changes with a descriptive message
|
|
203
|
+
2. Push the branch: git push origin $branch
|
|
204
|
+
3. Create a PR: gh pr create --fill --base main"
|
|
205
|
+
|
|
206
|
+
if [[ "$agent" == "claude" ]]; then
|
|
207
|
+
AGENT_CMD="claude --model ${model} --dangerously-skip-permissions -p \"$(echo "$FULL_PROMPT" | sed 's/"/\\"/g')\""
|
|
208
|
+
else
|
|
209
|
+
AGENT_CMD="codex --model ${model} --dangerously-bypass-approvals-and-sandbox \"$(echo "$FULL_PROMPT" | sed 's/"/\\"/g')\""
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
tmux kill-session -t "$tmux_session" 2>/dev/null || true
|
|
213
|
+
tmux new-session -d -s "$tmux_session" -c "$worktree" "$AGENT_CMD"
|
|
214
|
+
new_status="running"
|
|
215
|
+
log_info "Respawned task $id"
|
|
216
|
+
elif ! $DRY_RUN && [[ "$retries" -ge "$max_retries" ]]; then
|
|
217
|
+
log_error "Task $id failed after $max_retries retries"
|
|
218
|
+
registry_update "$id" "note" '"Max retries exceeded"'
|
|
219
|
+
fi
|
|
220
|
+
elif $tmux_alive; then
|
|
221
|
+
if [[ "$status" == "spawned" ]]; then
|
|
222
|
+
new_status="running"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# Update status
|
|
227
|
+
if [[ "$new_status" != "$status" ]] && ! $DRY_RUN; then
|
|
228
|
+
registry_update "$id" "status" "\"$new_status\""
|
|
229
|
+
if [[ "$new_status" == "done" || "$new_status" == "failed" || "$new_status" == "timeout" || "$new_status" == "cancelled" ]]; then
|
|
230
|
+
registry_update "$id" "completedAt" "$(epoch_ms)"
|
|
231
|
+
"${SCRIPT_DIR}/on-complete.sh" "$id" 2>/dev/null &
|
|
232
|
+
log_info "Fired on-complete for $id ($new_status)"
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# Build result entry
|
|
237
|
+
result=$(echo "$task" | jq \
|
|
238
|
+
--arg ns "$new_status" \
|
|
239
|
+
--arg ta "$tmux_alive" \
|
|
240
|
+
--arg pr "${pr_number:-null}" \
|
|
241
|
+
'. + {currentStatus: $ns, tmuxAlive: ($ta == "true"), detectedPR: (if $pr == "null" then null else ($pr | tonumber) end)}')
|
|
242
|
+
|
|
243
|
+
RESULTS=$(echo "$RESULTS" | jq --argjson r "$result" '. += [$r]')
|
|
244
|
+
done <<< "$TASKS"
|
|
245
|
+
|
|
246
|
+
# ── Output ─────────────────────────────────────────────────────────────
|
|
247
|
+
# ── Output ─────────────────────────────────────────────────────────────
|
|
248
|
+
_output_results() {
|
|
249
|
+
if $JSON_OUTPUT; then
|
|
250
|
+
echo "$RESULTS" | jq '{tasks: ., summary: {total: length, running: [.[] | select(.currentStatus == "running")] | length, failed: [.[] | select(.currentStatus == "failed")] | length, done: [.[] | select(.currentStatus == "done")] | length}}'
|
|
251
|
+
else
|
|
252
|
+
echo "=== Agent Status Check ==="
|
|
253
|
+
echo ""
|
|
254
|
+
echo "$RESULTS" | jq -r '.[] | " [\(.currentStatus)] \(.id) — \(.agent)/\(.model) (tmux: \(.tmuxAlive), PR: \(.detectedPR // "none"))"'
|
|
255
|
+
echo ""
|
|
256
|
+
total=$(echo "$RESULTS" | jq 'length')
|
|
257
|
+
running=$(echo "$RESULTS" | jq '[.[] | select(.currentStatus == "running")] | length')
|
|
258
|
+
echo "Total: $total | Running: $running"
|
|
259
|
+
fi
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_output_results
|
|
263
|
+
|
|
264
|
+
# ── Daemon mode ───────────────────────────────────────────────────────
|
|
265
|
+
if $DAEMON; then
|
|
266
|
+
# Write PID file
|
|
267
|
+
echo $$ > "$PID_FILE"
|
|
268
|
+
log_info "Watch daemon started (PID: $$, interval: ${DAEMON_INTERVAL}s)"
|
|
269
|
+
log_info "PID file: $PID_FILE"
|
|
270
|
+
log_info "Stop with: clawforge watch --stop"
|
|
271
|
+
|
|
272
|
+
_daemon_cleanup() {
|
|
273
|
+
rm -f "$PID_FILE"
|
|
274
|
+
log_info "Watch daemon stopped"
|
|
275
|
+
}
|
|
276
|
+
trap _daemon_cleanup EXIT INT TERM
|
|
277
|
+
|
|
278
|
+
while true; do
|
|
279
|
+
sleep "$DAEMON_INTERVAL"
|
|
280
|
+
|
|
281
|
+
# Re-run the check (re-read tasks from registry)
|
|
282
|
+
TASKS=$(jq -c '.tasks[]' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
283
|
+
if [[ -z "$TASKS" ]]; then
|
|
284
|
+
log_debug "Daemon: no active tasks"
|
|
285
|
+
continue
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
RESULTS="[]"
|
|
289
|
+
while IFS= read -r task; do
|
|
290
|
+
id=$(echo "$task" | jq -r '.id')
|
|
291
|
+
tmux_session=$(echo "$task" | jq -r '.tmuxSession')
|
|
292
|
+
branch=$(echo "$task" | jq -r '.branch')
|
|
293
|
+
repo=$(echo "$task" | jq -r '.repo')
|
|
294
|
+
status=$(echo "$task" | jq -r '.status')
|
|
295
|
+
|
|
296
|
+
if [[ "$status" == "done" || "$status" == "stopped" || "$status" == "archived" ]]; then
|
|
297
|
+
continue
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
new_status="$status"
|
|
301
|
+
tmux_alive=false
|
|
302
|
+
tmux has-session -t "$tmux_session" 2>/dev/null && tmux_alive=true
|
|
303
|
+
|
|
304
|
+
pr_number=""
|
|
305
|
+
if [[ -d "$repo" ]]; then
|
|
306
|
+
pr_info=$(gh pr list --repo "$repo" --head "$branch" --json number,state 2>/dev/null || echo "[]")
|
|
307
|
+
pr_number=$(echo "$pr_info" | jq -r '.[0].number // empty' 2>/dev/null || true)
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
if [[ -n "$pr_number" ]]; then
|
|
311
|
+
if [[ "$status" != "pr-created" && "$status" != "ci-passing" && "$status" != "reviewing" ]]; then
|
|
312
|
+
new_status="pr-created"
|
|
313
|
+
registry_update "$id" "pr" "$pr_number"
|
|
314
|
+
"${SCRIPT_DIR}/notify.sh" --type pr-ready --description "$(echo "$task" | jq -r '.description')" --pr "$pr_number" 2>/dev/null || true
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
ci_result=$(gh pr checks "$pr_number" --repo "$repo" 2>/dev/null || true)
|
|
318
|
+
if echo "$ci_result" | grep -q "pass" && ! echo "$ci_result" | grep -qE "fail|pending"; then
|
|
319
|
+
new_status="ci-passing"
|
|
320
|
+
elif echo "$ci_result" | grep -q "fail" && $tmux_alive; then
|
|
321
|
+
_ci_feedback "$id" "$repo" "$pr_number" "$tmux_session" "$branch"
|
|
322
|
+
fi
|
|
323
|
+
elif ! $tmux_alive && [[ "$status" == "running" || "$status" == "spawned" ]]; then
|
|
324
|
+
new_status="failed"
|
|
325
|
+
"${SCRIPT_DIR}/notify.sh" --type task-failed --description "$(echo "$task" | jq -r '.description')" 2>/dev/null || true
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
if [[ "$new_status" != "$status" ]]; then
|
|
329
|
+
registry_update "$id" "status" "\"$new_status\""
|
|
330
|
+
log_info "Daemon: $id status changed: $status → $new_status"
|
|
331
|
+
|
|
332
|
+
# Fire completion hooks on terminal states
|
|
333
|
+
case "$new_status" in
|
|
334
|
+
done|failed|timeout|cancelled)
|
|
335
|
+
registry_update "$id" "completedAt" "$(epoch_ms)"
|
|
336
|
+
"${SCRIPT_DIR}/on-complete.sh" "$id" 2>/dev/null &
|
|
337
|
+
log_info "Daemon: fired on-complete for $id ($new_status)"
|
|
338
|
+
;;
|
|
339
|
+
esac
|
|
340
|
+
fi
|
|
341
|
+
done <<< "$TASKS"
|
|
342
|
+
done
|
|
343
|
+
fi
|
package/bin/clawforge
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# clawforge — Unified CLI for agent swarm workflow
|
|
3
|
+
# Routes subcommands to individual module scripts.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# Resolve symlinks to find the real install location
|
|
7
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
8
|
+
while [[ -L "$SOURCE" ]]; do
|
|
9
|
+
DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
10
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
11
|
+
[[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
|
|
12
|
+
done
|
|
13
|
+
CLAWFORGE_DIR="$(cd "$(dirname "$SOURCE")/.." && pwd)"
|
|
14
|
+
BIN_DIR="${CLAWFORGE_DIR}/bin"
|
|
15
|
+
|
|
16
|
+
# ── Global flags ───────────────────────────────────────────────────────
|
|
17
|
+
PASSTHROUGH_ARGS=()
|
|
18
|
+
for arg in "$@"; do
|
|
19
|
+
if [[ "$arg" == "--verbose" ]]; then
|
|
20
|
+
export CLAWFORGE_DEBUG=1
|
|
21
|
+
else
|
|
22
|
+
PASSTHROUGH_ARGS+=("$arg")
|
|
23
|
+
fi
|
|
24
|
+
done
|
|
25
|
+
set -- "${PASSTHROUGH_ARGS[@]+"${PASSTHROUGH_ARGS[@]}"}"
|
|
26
|
+
|
|
27
|
+
# ── Version ────────────────────────────────────────────────────────────
|
|
28
|
+
show_version() {
|
|
29
|
+
local ver
|
|
30
|
+
ver=$(cat "${CLAWFORGE_DIR}/VERSION" 2>/dev/null || echo "unknown")
|
|
31
|
+
echo "clawforge v${ver}"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
35
|
+
show_help() {
|
|
36
|
+
local show_all=false
|
|
37
|
+
[[ "${1:-}" == "--all" ]] && show_all=true
|
|
38
|
+
|
|
39
|
+
cat <<EOF
|
|
40
|
+
$(show_version)
|
|
41
|
+
Multi-mode coding workflow CLI — from quick patches to parallel agent orchestration.
|
|
42
|
+
|
|
43
|
+
Usage: clawforge <command> [options]
|
|
44
|
+
|
|
45
|
+
Workflow Modes:
|
|
46
|
+
sprint Single agent, full dev cycle (the workhorse)
|
|
47
|
+
review Quality gate on an existing PR (analysis only)
|
|
48
|
+
swarm Parallel multi-agent orchestration
|
|
49
|
+
|
|
50
|
+
Management:
|
|
51
|
+
status Show all tracked tasks with short IDs
|
|
52
|
+
resume Resume a failed/timeout task from where it left off
|
|
53
|
+
diff Show git diff for a task without attaching
|
|
54
|
+
pr Create a PR from a task branch
|
|
55
|
+
attach Attach to a running agent's tmux session
|
|
56
|
+
steer Send course correction to a running agent
|
|
57
|
+
stop Stop a running agent
|
|
58
|
+
watch Monitor agent health (supports --daemon)
|
|
59
|
+
dashboard Live TUI dashboard with vim keybindings
|
|
60
|
+
|
|
61
|
+
Observability (v0.5):
|
|
62
|
+
cost Token/cost tracking and budget management
|
|
63
|
+
conflicts Swarm conflict detection and resolution
|
|
64
|
+
templates Task template management
|
|
65
|
+
|
|
66
|
+
Fleet Ops (v0.6):
|
|
67
|
+
memory Per-repo agent memory (show, add, search, forget, clear)
|
|
68
|
+
init Scan project and generate initial memory entries
|
|
69
|
+
history Show completed task history
|
|
70
|
+
|
|
71
|
+
Reliability (v0.7):
|
|
72
|
+
doctor Diagnose orphaned sessions, dangling worktrees, stale tasks
|
|
73
|
+
|
|
74
|
+
Observability (v0.9):
|
|
75
|
+
logs Capture agent output from tmux (without attaching)
|
|
76
|
+
on-complete Fire completion hooks for finished tasks
|
|
77
|
+
|
|
78
|
+
Power Features (v1.2):
|
|
79
|
+
config Manage user configuration (~/.clawforge/config.json)
|
|
80
|
+
multi-review Run PR through multiple models and compare feedback
|
|
81
|
+
summary AI-generated summary of what an agent did
|
|
82
|
+
parse-cost Parse real cost/token data from agent output
|
|
83
|
+
|
|
84
|
+
Evaluation (v0.6.2):
|
|
85
|
+
eval Run-eval logging and weekly summaries
|
|
86
|
+
|
|
87
|
+
Lifecycle:
|
|
88
|
+
clean Clean up completed tasks
|
|
89
|
+
learn Record learnings from completed tasks
|
|
90
|
+
EOF
|
|
91
|
+
|
|
92
|
+
if $show_all; then
|
|
93
|
+
cat <<EOF
|
|
94
|
+
|
|
95
|
+
Direct Module Access:
|
|
96
|
+
scope Assemble a prompt with context (Module 1)
|
|
97
|
+
spawn Create worktree + launch coding agent (Module 2)
|
|
98
|
+
notify Send Discord notification (Module 6)
|
|
99
|
+
merge Merge helper with safety checks (Module 7)
|
|
100
|
+
run Scope + spawn in one step (legacy shortcut)
|
|
101
|
+
EOF
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
cat <<EOF
|
|
105
|
+
|
|
106
|
+
Meta:
|
|
107
|
+
help Show this help (use --all for direct module commands)
|
|
108
|
+
version Show version
|
|
109
|
+
|
|
110
|
+
Global flags:
|
|
111
|
+
--verbose Enable debug logging
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
clawforge sprint "Add JWT authentication middleware"
|
|
115
|
+
clawforge sprint "Fix typo in readme" --quick
|
|
116
|
+
clawforge sprint --template refactor "Refactor auth module"
|
|
117
|
+
clawforge review --pr 42
|
|
118
|
+
clawforge swarm "Migrate all tests from jest to vitest"
|
|
119
|
+
clawforge swarm --template migration "Migrate to TypeScript" --ci-loop
|
|
120
|
+
clawforge swarm --repos ~/api,~/web,~/shared "Upgrade auth library"
|
|
121
|
+
clawforge sprint --routing auto "Refactor auth service"
|
|
122
|
+
clawforge steer 1 "Use bcrypt instead of md5"
|
|
123
|
+
clawforge dashboard
|
|
124
|
+
clawforge cost --summary
|
|
125
|
+
clawforge memory show
|
|
126
|
+
clawforge memory add "Always run tests first"
|
|
127
|
+
clawforge init
|
|
128
|
+
clawforge history --repo api
|
|
129
|
+
clawforge eval log --command sprint --mode quick --repo api --status ok --duration-ms 420000
|
|
130
|
+
clawforge eval weekly
|
|
131
|
+
clawforge templates
|
|
132
|
+
EOF
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# ── Dashboard (v0.5: routes to TUI, legacy fallback kept as show_dashboard_legacy) ─
|
|
136
|
+
|
|
137
|
+
# ── Run shortcut (scope + spawn) ──────────────────────────────────────
|
|
138
|
+
run_combined() {
|
|
139
|
+
# Parse --repo, --branch, --task from args, pass rest to spawn
|
|
140
|
+
local repo="" branch="" task=""
|
|
141
|
+
local extra_args=()
|
|
142
|
+
|
|
143
|
+
while [[ $# -gt 0 ]]; do
|
|
144
|
+
case "$1" in
|
|
145
|
+
--repo) repo="$2"; shift 2 ;;
|
|
146
|
+
--branch) branch="$2"; shift 2 ;;
|
|
147
|
+
--task) task="$2"; shift 2 ;;
|
|
148
|
+
*) extra_args+=("$1"); shift ;;
|
|
149
|
+
esac
|
|
150
|
+
done
|
|
151
|
+
|
|
152
|
+
if [[ -z "$repo" || -z "$branch" || -z "$task" ]]; then
|
|
153
|
+
echo "Usage: clawforge run --repo <path> --branch <name> --task \"description\"" >&2
|
|
154
|
+
echo "All three flags are required." >&2
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
echo "── Step 1: Scope ─────────────────────────────────────────────"
|
|
159
|
+
local prompt
|
|
160
|
+
prompt=$("${BIN_DIR}/scope-task.sh" --task "$task" "${extra_args[@]+"${extra_args[@]}"}" 2>/dev/null || echo "$task")
|
|
161
|
+
|
|
162
|
+
echo "── Step 2: Spawn ─────────────────────────────────────────────"
|
|
163
|
+
"${BIN_DIR}/spawn-agent.sh" --repo "$repo" --branch "$branch" --task "$prompt" "${extra_args[@]+"${extra_args[@]}"}"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# ── Status (pretty wrapper around registry) ────────────────────────────
|
|
167
|
+
show_status() {
|
|
168
|
+
source "${CLAWFORGE_DIR}/lib/common.sh"
|
|
169
|
+
local tasks
|
|
170
|
+
tasks=$(registry_list "$@" 2>/dev/null || echo "[]")
|
|
171
|
+
local count
|
|
172
|
+
count=$(echo "$tasks" | jq 'length' 2>/dev/null || echo 0)
|
|
173
|
+
|
|
174
|
+
if [[ "$count" == "0" ]]; then
|
|
175
|
+
echo "No active tasks."
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# Enhanced status with short IDs and mode column
|
|
180
|
+
echo "$tasks" | jq -r '.[] |
|
|
181
|
+
def sid: if .short_id then "#\(.short_id)" else .id end;
|
|
182
|
+
def mode: .mode // "—";
|
|
183
|
+
def elapsed:
|
|
184
|
+
(now * 1000 - (.startedAt // 0)) / 60000 | floor | tostring + "m ago";
|
|
185
|
+
def extra:
|
|
186
|
+
if .mode == "swarm" and .sub_task_count then " (\(.sub_task_count) agents)" else "" end;
|
|
187
|
+
" \(sid) \(mode) \(.status // "?") \(.repo // "" | split("/") | last) \"\(.description // "")[0:50]\"\(extra)"
|
|
188
|
+
' 2>/dev/null || \
|
|
189
|
+
echo "$tasks" | jq -r '.[] | "[\(.status // "?")] \(.id) \(.agent // "")/\(.model // "") \(.task // .description // "")[0:60]"' 2>/dev/null || echo "$tasks"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# ── Route subcommand ───────────────────────────────────────────────────
|
|
193
|
+
COMMAND="${1:-help}"
|
|
194
|
+
shift 2>/dev/null || true
|
|
195
|
+
|
|
196
|
+
case "$COMMAND" in
|
|
197
|
+
# Workflow modes (v0.4)
|
|
198
|
+
sprint) exec "${BIN_DIR}/sprint.sh" "$@" ;;
|
|
199
|
+
review) exec "${BIN_DIR}/review-mode.sh" "$@" ;;
|
|
200
|
+
swarm) exec "${BIN_DIR}/swarm.sh" "$@" ;;
|
|
201
|
+
|
|
202
|
+
# Management commands (v0.4)
|
|
203
|
+
steer) exec "${BIN_DIR}/steer.sh" "$@" ;;
|
|
204
|
+
attach) exec "${BIN_DIR}/attach.sh" "$@" ;;
|
|
205
|
+
stop) exec "${BIN_DIR}/stop.sh" "$@" ;;
|
|
206
|
+
|
|
207
|
+
# Existing commands
|
|
208
|
+
status) show_status "$@" ;;
|
|
209
|
+
watch) exec "${BIN_DIR}/check-agents.sh" "$@" ;;
|
|
210
|
+
dashboard)
|
|
211
|
+
# Prefer Go TUI binary if available, fallback to dashboard.sh
|
|
212
|
+
if [[ -x "${BIN_DIR}/clawforge-dashboard" ]]; then
|
|
213
|
+
exec "${BIN_DIR}/clawforge-dashboard" "$@"
|
|
214
|
+
else
|
|
215
|
+
exec "${BIN_DIR}/dashboard.sh" "$@"
|
|
216
|
+
fi
|
|
217
|
+
;;
|
|
218
|
+
clean) exec "${BIN_DIR}/clean.sh" "$@" ;;
|
|
219
|
+
learn) exec "${BIN_DIR}/learn.sh" "$@" ;;
|
|
220
|
+
|
|
221
|
+
# v0.5 observability + intelligence
|
|
222
|
+
cost) exec "${BIN_DIR}/cost.sh" "$@" ;;
|
|
223
|
+
conflicts) exec "${BIN_DIR}/conflicts.sh" "$@" ;;
|
|
224
|
+
templates) exec "${BIN_DIR}/templates.sh" "$@" ;;
|
|
225
|
+
|
|
226
|
+
# v0.6 fleet operations
|
|
227
|
+
memory) exec "${BIN_DIR}/memory.sh" "$@" ;;
|
|
228
|
+
init) exec "${BIN_DIR}/init.sh" "$@" ;;
|
|
229
|
+
history) exec "${BIN_DIR}/history.sh" "$@" ;;
|
|
230
|
+
eval) exec "${BIN_DIR}/eval.sh" "$@" ;;
|
|
231
|
+
doctor) exec "${BIN_DIR}/doctor.sh" "$@" ;;
|
|
232
|
+
resume) exec "${BIN_DIR}/resume.sh" "$@" ;;
|
|
233
|
+
diff) exec "${BIN_DIR}/diff.sh" "$@" ;;
|
|
234
|
+
pr) exec "${BIN_DIR}/pr.sh" "$@" ;;
|
|
235
|
+
config) exec "${BIN_DIR}/config.sh" "$@" ;;
|
|
236
|
+
multi-review) exec "${BIN_DIR}/multi-review.sh" "$@" ;;
|
|
237
|
+
summary) exec "${BIN_DIR}/summary.sh" "$@" ;;
|
|
238
|
+
parse-cost) exec "${BIN_DIR}/parse-cost.sh" "$@" ;;
|
|
239
|
+
logs) exec "${BIN_DIR}/logs.sh" "$@" ;;
|
|
240
|
+
on-complete) exec "${BIN_DIR}/on-complete.sh" "$@" ;;
|
|
241
|
+
|
|
242
|
+
# Direct module access (hidden from main help)
|
|
243
|
+
scope) exec "${BIN_DIR}/scope-task.sh" "$@" ;;
|
|
244
|
+
spawn) exec "${BIN_DIR}/spawn-agent.sh" "$@" ;;
|
|
245
|
+
notify) exec "${BIN_DIR}/notify.sh" "$@" ;;
|
|
246
|
+
merge) exec "${BIN_DIR}/merge-helper.sh" "$@" ;;
|
|
247
|
+
run) run_combined "$@" ;;
|
|
248
|
+
|
|
249
|
+
# Meta
|
|
250
|
+
version|-v|--version) show_version ;;
|
|
251
|
+
help|-h|--help) show_help "$@" ;;
|
|
252
|
+
*)
|
|
253
|
+
echo "Unknown command: $COMMAND" >&2
|
|
254
|
+
echo "Run 'clawforge help' for usage." >&2
|
|
255
|
+
exit 1
|
|
256
|
+
;;
|
|
257
|
+
esac
|
|
Binary file
|