@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
@@ -0,0 +1,557 @@
1
+ #!/usr/bin/env bash
2
+ # dashboard.sh — Live TUI dashboard with vim keybindings and forge animation
3
+ # Usage: clawforge dashboard [--no-anim]
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ source "${SCRIPT_DIR}/../lib/common.sh"
8
+
9
+ # ── Help ───────────────────────────────────────────────────────────────
10
+ usage() {
11
+ cat <<EOF
12
+ Usage: clawforge dashboard [options]
13
+
14
+ Live terminal UI for monitoring all ClawForge agents.
15
+
16
+ Options:
17
+ --no-anim Skip startup animation
18
+ --help Show this help
19
+
20
+ Keybindings:
21
+ j/k Navigate agent list
22
+ Enter Attach to selected agent's tmux session
23
+ s Steer selected agent (prompts for message)
24
+ x Stop selected agent
25
+ / Filter agents
26
+ r Force refresh
27
+ ? Show help overlay
28
+ q Quit dashboard
29
+ EOF
30
+ }
31
+
32
+ # ── Parse args ─────────────────────────────────────────────────────────
33
+ SKIP_ANIM=false
34
+ while [[ $# -gt 0 ]]; do
35
+ case "$1" in
36
+ --no-anim) SKIP_ANIM=true; shift ;;
37
+ --help|-h) usage; exit 0 ;;
38
+ --*) log_error "Unknown option: $1"; usage; exit 1 ;;
39
+ *) shift ;;
40
+ esac
41
+ done
42
+
43
+ # ── Terminal setup ─────────────────────────────────────────────────────
44
+ COLS=$(tput cols 2>/dev/null || echo 80)
45
+ ROWS=$(tput lines 2>/dev/null || echo 24)
46
+ SELECTED=0
47
+ FILTER=""
48
+ SHOW_HELP=false
49
+ REFRESH_INTERVAL=2
50
+
51
+ # Amber/orange forge colors via tput
52
+ COLOR_RESET=$(tput sgr0 2>/dev/null || echo "")
53
+ COLOR_AMBER=$(tput setaf 208 2>/dev/null || tput setaf 3 2>/dev/null || echo "")
54
+ COLOR_ORANGE=$(tput setaf 166 2>/dev/null || tput setaf 1 2>/dev/null || echo "")
55
+ COLOR_DIM=$(tput dim 2>/dev/null || echo "")
56
+ COLOR_BOLD=$(tput bold 2>/dev/null || echo "")
57
+ COLOR_REV=$(tput rev 2>/dev/null || echo "")
58
+ COLOR_GREEN=$(tput setaf 2 2>/dev/null || echo "")
59
+ COLOR_RED=$(tput setaf 1 2>/dev/null || echo "")
60
+ COLOR_YELLOW=$(tput setaf 3 2>/dev/null || echo "")
61
+ COLOR_CYAN=$(tput setaf 6 2>/dev/null || echo "")
62
+
63
+ # ── Forge ASCII art frames ────────────────────────────────────────────
64
+ FORGE_FRAME_1() {
65
+ cat <<'ART'
66
+ ╔═══════════════════════════════╗
67
+ ║ ClawForge ║
68
+ ╚═══════════════════════════════╝
69
+
70
+ _______________
71
+ / \
72
+ / ███████████ \
73
+ │ ███████████████ │
74
+ │ ████ ▓▓ █████│
75
+ │ ███████████████ │
76
+ \ ███████████ /
77
+ \_____ _____/
78
+ │ │
79
+ ═════╧═══╧═════
80
+ / ░░░░░░░░░░░░ \
81
+ /_________________ \
82
+ ╔═══════════════════╗
83
+ ║ ▒▒▒▒▒▒▒▒▒▒▒▒▒ ║
84
+ ╚═══════════════════╝
85
+ ART
86
+ }
87
+
88
+ FORGE_FRAME_2() {
89
+ cat <<'ART'
90
+ ╔═══════════════════════════════╗
91
+ ║ ClawForge ⚒ ║
92
+ ╚═══════════════════════════════╝
93
+
94
+ _______________
95
+ / \
96
+ / ▓▓▓▓▓▓▓▓▓▓▓ \
97
+ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
98
+ │ ▓▓▓▓ ██ ▓▓▓▓▓│
99
+ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
100
+ \ ▓▓▓▓▓▓▓▓▓▓▓ /
101
+ \_____ _____/
102
+ │ ⚡│
103
+ ═════╧═══╧═════
104
+ / ▓▓▓▓▓▓▓▓▓▓▓▓ \
105
+ /_________________ \
106
+ ╔═══════════════════╗
107
+ ║ ████████████████ ║
108
+ ╚═══════════════════╝
109
+ ART
110
+ }
111
+
112
+ FORGE_FRAME_3() {
113
+ cat <<'ART'
114
+ ╔═══════════════════════════════╗
115
+ ║ ClawForge ⚒ ⚒ ║
116
+ ╚═══════════════════════════════╝
117
+ * * *
118
+ _______________
119
+ / * * * * \
120
+ / ████████████ \
121
+ │ █████████████████│
122
+ │ █████ ░░ ██████│
123
+ │ █████████████████│
124
+ \ ████████████ /
125
+ \_____ _____/
126
+ │⚡⚡│
127
+ ═════╧═══╧═════
128
+ / ████████████████\
129
+ /_________________ \
130
+ ╔═══════════════════╗
131
+ ║ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ║
132
+ ╚═══════════════════╝
133
+ ART
134
+ }
135
+
136
+ # ── Startup animation ─────────────────────────────────────────────────
137
+ _forge_animation() {
138
+ tput civis 2>/dev/null || true # hide cursor
139
+ local frames=("1" "2" "3" "2" "3" "1" "3" "2")
140
+ for frame in "${frames[@]}"; do
141
+ tput clear 2>/dev/null || clear
142
+ echo ""
143
+ echo "${COLOR_AMBER}"
144
+ case "$frame" in
145
+ 1) FORGE_FRAME_1 ;;
146
+ 2) FORGE_FRAME_2 ;;
147
+ 3) FORGE_FRAME_3 ;;
148
+ esac
149
+ echo "${COLOR_RESET}"
150
+ sleep 0.2
151
+ done
152
+ # Final flash
153
+ tput clear 2>/dev/null || clear
154
+ echo ""
155
+ echo "${COLOR_ORANGE}${COLOR_BOLD}"
156
+ FORGE_FRAME_3
157
+ echo "${COLOR_RESET}"
158
+ sleep 0.4
159
+ }
160
+
161
+ # ── Data fetching ──────────────────────────────────────────────────────
162
+ TASK_DATA="[]"
163
+ TASK_COUNT=0
164
+
165
+ _refresh_data() {
166
+ _ensure_registry
167
+ TASK_DATA=$(jq '.tasks' "$REGISTRY_FILE" 2>/dev/null || echo "[]")
168
+
169
+ # Apply filter
170
+ if [[ -n "$FILTER" ]]; then
171
+ TASK_DATA=$(echo "$TASK_DATA" | jq --arg f "$FILTER" \
172
+ '[.[] | select(
173
+ (.id | ascii_downcase | contains($f | ascii_downcase)) or
174
+ (.description // "" | ascii_downcase | contains($f | ascii_downcase)) or
175
+ (.mode // "" | ascii_downcase | contains($f | ascii_downcase)) or
176
+ (.status // "" | ascii_downcase | contains($f | ascii_downcase))
177
+ )]' 2>/dev/null || echo "$TASK_DATA")
178
+ fi
179
+
180
+ TASK_COUNT=$(echo "$TASK_DATA" | jq 'length' 2>/dev/null || echo 0)
181
+
182
+ # Clamp selection
183
+ if [[ "$TASK_COUNT" -gt 0 ]]; then
184
+ if [[ "$SELECTED" -ge "$TASK_COUNT" ]]; then
185
+ SELECTED=$((TASK_COUNT - 1))
186
+ fi
187
+ [[ "$SELECTED" -lt 0 ]] && SELECTED=0
188
+ else
189
+ SELECTED=0
190
+ fi
191
+ }
192
+
193
+ # ── Cost data ──────────────────────────────────────────────────────────
194
+ _get_task_cost() {
195
+ local task_id="$1"
196
+ local costs_file="${CLAWFORGE_DIR}/registry/costs.jsonl"
197
+ if [[ -f "$costs_file" ]]; then
198
+ grep "\"taskId\":\"${task_id}\"" "$costs_file" 2>/dev/null | tail -1 | jq -r '.totalCost // 0' 2>/dev/null || echo "—"
199
+ else
200
+ echo "—"
201
+ fi
202
+ }
203
+
204
+ # ── Conflict data ──────────────────────────────────────────────────────
205
+ _get_conflict_count() {
206
+ local conflicts_file="${CLAWFORGE_DIR}/registry/conflicts.jsonl"
207
+ if [[ -f "$conflicts_file" ]]; then
208
+ wc -l < "$conflicts_file" 2>/dev/null | tr -d ' '
209
+ else
210
+ echo "0"
211
+ fi
212
+ }
213
+
214
+ # ── Render functions ───────────────────────────────────────────────────
215
+ _render_header() {
216
+ local ver
217
+ ver=$(cat "${CLAWFORGE_DIR}/VERSION" 2>/dev/null || echo "?")
218
+ echo "${COLOR_AMBER}${COLOR_BOLD}╔══════════════════════════════════════════════════════════════════════════╗${COLOR_RESET}"
219
+ printf "${COLOR_AMBER}${COLOR_BOLD}║${COLOR_RESET} ${COLOR_ORANGE}${COLOR_BOLD}ClawForge Dashboard${COLOR_RESET} ${COLOR_DIM}v${ver}${COLOR_RESET}"
220
+ # Right-align timestamp
221
+ local ts
222
+ ts=$(date +"%H:%M:%S")
223
+ local pad=$((72 - 22 - ${#ver} - ${#ts}))
224
+ printf "%*s" "$pad" ""
225
+ printf "${COLOR_DIM}${ts}${COLOR_RESET} ${COLOR_AMBER}${COLOR_BOLD}║${COLOR_RESET}\n"
226
+ echo "${COLOR_AMBER}${COLOR_BOLD}╚══════════════════════════════════════════════════════════════════════════╝${COLOR_RESET}"
227
+ }
228
+
229
+ _render_status_bar() {
230
+ local running=0 spawned=0 done_count=0 failed=0
231
+ running=$(echo "$TASK_DATA" | jq '[.[] | select(.status == "running")] | length' 2>/dev/null || echo 0)
232
+ spawned=$(echo "$TASK_DATA" | jq '[.[] | select(.status == "spawned")] | length' 2>/dev/null || echo 0)
233
+ done_count=$(echo "$TASK_DATA" | jq '[.[] | select(.status == "done")] | length' 2>/dev/null || echo 0)
234
+ failed=$(echo "$TASK_DATA" | jq '[.[] | select(.status == "failed")] | length' 2>/dev/null || echo 0)
235
+ local conflicts
236
+ conflicts=$(_get_conflict_count)
237
+
238
+ printf " ${COLOR_GREEN}●${COLOR_RESET} Running: ${running} "
239
+ printf "${COLOR_YELLOW}○${COLOR_RESET} Spawned: ${spawned} "
240
+ printf "${COLOR_CYAN}✓${COLOR_RESET} Done: ${done_count} "
241
+ printf "${COLOR_RED}✗${COLOR_RESET} Failed: ${failed}"
242
+ if [[ "$conflicts" -gt 0 ]]; then
243
+ printf " ${COLOR_RED}${COLOR_BOLD}⚠ Conflicts: ${conflicts}${COLOR_RESET}"
244
+ fi
245
+ if [[ -n "$FILTER" ]]; then
246
+ printf " ${COLOR_DIM}Filter: ${FILTER}${COLOR_RESET}"
247
+ fi
248
+ echo ""
249
+ }
250
+
251
+ _status_color() {
252
+ case "$1" in
253
+ running) echo "${COLOR_GREEN}" ;;
254
+ spawned) echo "${COLOR_YELLOW}" ;;
255
+ pr-created) echo "${COLOR_CYAN}" ;;
256
+ ci-passing) echo "${COLOR_GREEN}" ;;
257
+ done) echo "${COLOR_DIM}" ;;
258
+ failed) echo "${COLOR_RED}" ;;
259
+ stopped) echo "${COLOR_RED}" ;;
260
+ *) echo "" ;;
261
+ esac
262
+ }
263
+
264
+ _render_table() {
265
+ echo ""
266
+ # Table header
267
+ printf " ${COLOR_DIM}%-5s %-8s %-12s %-12s %-30s %-10s %-8s${COLOR_RESET}\n" \
268
+ "ID" "Mode" "Status" "Branch" "Description" "Cost" "CI"
269
+ printf " ${COLOR_DIM}%-5s %-8s %-12s %-12s %-30s %-10s %-8s${COLOR_RESET}\n" \
270
+ "─────" "────────" "────────────" "────────────" "──────────────────────────────" "──────────" "────────"
271
+
272
+ if [[ "$TASK_COUNT" -eq 0 ]]; then
273
+ echo ""
274
+ echo " ${COLOR_DIM}No active tasks.${COLOR_RESET}"
275
+ echo " ${COLOR_DIM}Start one: clawforge sprint \"<task>\"${COLOR_RESET}"
276
+ return
277
+ fi
278
+
279
+ local i=0
280
+ while IFS= read -r task_line; do
281
+ local id mode status branch desc cost ci_status
282
+ id=$(echo "$task_line" | jq -r 'if .short_id then "#\(.short_id)" else .id[0:8] end' 2>/dev/null)
283
+ mode=$(echo "$task_line" | jq -r '.mode // "—"' 2>/dev/null)
284
+ status=$(echo "$task_line" | jq -r '.status // "?"' 2>/dev/null)
285
+ branch=$(echo "$task_line" | jq -r '.branch // "" | split("/") | last | .[0:12]' 2>/dev/null)
286
+ desc=$(echo "$task_line" | jq -r '.description // "" | .[0:30]' 2>/dev/null)
287
+ local task_id
288
+ task_id=$(echo "$task_line" | jq -r '.id' 2>/dev/null)
289
+ cost=$(_get_task_cost "$task_id")
290
+ ci_status=$(echo "$task_line" | jq -r 'if .ci_retries and .ci_retries > 0 then "retry/\(.ci_retries)" else "—" end' 2>/dev/null)
291
+
292
+ local sc
293
+ sc=$(_status_color "$status")
294
+ if [[ "$i" -eq "$SELECTED" ]]; then
295
+ printf " ${COLOR_REV}${COLOR_BOLD}%-5s %-8s ${sc}%-12s${COLOR_RESET}${COLOR_REV} %-12s %-30s %-10s %-8s${COLOR_RESET}\n" \
296
+ "$id" "$mode" "$status" "$branch" "$desc" "$cost" "$ci_status"
297
+ else
298
+ printf " %-5s %-8s ${sc}%-12s${COLOR_RESET} %-12s %-30s %-10s %-8s\n" \
299
+ "$id" "$mode" "$status" "$branch" "$desc" "$cost" "$ci_status"
300
+ fi
301
+ ((i++)) || true
302
+ done < <(echo "$TASK_DATA" | jq -c '.[]' 2>/dev/null)
303
+ }
304
+
305
+ _render_system_info() {
306
+ echo ""
307
+ echo " ${COLOR_DIM}── System ──────────────────────────────────────────────────────────${COLOR_RESET}"
308
+ local tmux_count
309
+ tmux_count=$(tmux list-sessions 2>/dev/null | grep -c "agent" 2>/dev/null || echo "0")
310
+ local daemon_status="off"
311
+ local pid_file="${CLAWFORGE_DIR}/watch.pid"
312
+ if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
313
+ daemon_status="${COLOR_GREEN}on${COLOR_RESET}"
314
+ fi
315
+ printf " tmux agents: %s │ watch daemon: %s │ tasks: %s\n" \
316
+ "${tmux_count:-0}" "$daemon_status" "$TASK_COUNT"
317
+ }
318
+
319
+ _render_help_overlay() {
320
+ echo ""
321
+ echo " ${COLOR_AMBER}${COLOR_BOLD}╔═══════════════════════════╗${COLOR_RESET}"
322
+ echo " ${COLOR_AMBER}${COLOR_BOLD}║ Dashboard Help ║${COLOR_RESET}"
323
+ echo " ${COLOR_AMBER}${COLOR_BOLD}╠═══════════════════════════╣${COLOR_RESET}"
324
+ echo " ${COLOR_AMBER}║${COLOR_RESET} j/k Navigate up/down ${COLOR_AMBER}║${COLOR_RESET}"
325
+ echo " ${COLOR_AMBER}║${COLOR_RESET} Enter Attach to agent ${COLOR_AMBER}║${COLOR_RESET}"
326
+ echo " ${COLOR_AMBER}║${COLOR_RESET} s Steer agent ${COLOR_AMBER}║${COLOR_RESET}"
327
+ echo " ${COLOR_AMBER}║${COLOR_RESET} x Stop agent ${COLOR_AMBER}║${COLOR_RESET}"
328
+ echo " ${COLOR_AMBER}║${COLOR_RESET} / Filter agents ${COLOR_AMBER}║${COLOR_RESET}"
329
+ echo " ${COLOR_AMBER}║${COLOR_RESET} r Force refresh ${COLOR_AMBER}║${COLOR_RESET}"
330
+ echo " ${COLOR_AMBER}║${COLOR_RESET} ? Toggle this help ${COLOR_AMBER}║${COLOR_RESET}"
331
+ echo " ${COLOR_AMBER}║${COLOR_RESET} q Quit ${COLOR_AMBER}║${COLOR_RESET}"
332
+ echo " ${COLOR_AMBER}${COLOR_BOLD}╚═══════════════════════════╝${COLOR_RESET}"
333
+ }
334
+
335
+ _render_footer() {
336
+ echo ""
337
+ echo " ${COLOR_DIM}[j/k] navigate [Enter] attach [s] steer [x] stop [/] filter [?] help [q] quit${COLOR_RESET}"
338
+ }
339
+
340
+ # ── Full render ────────────────────────────────────────────────────────
341
+ _render() {
342
+ tput cup 0 0 2>/dev/null || true
343
+ tput ed 2>/dev/null || true # clear to end of screen
344
+ _render_header
345
+ _render_status_bar
346
+ _render_table
347
+ if $SHOW_HELP; then
348
+ _render_help_overlay
349
+ fi
350
+ _render_system_info
351
+ _render_footer
352
+ }
353
+
354
+ # ── Selected task helpers ──────────────────────────────────────────────
355
+ _selected_task_id() {
356
+ if [[ "$TASK_COUNT" -eq 0 ]]; then
357
+ echo ""
358
+ return
359
+ fi
360
+ echo "$TASK_DATA" | jq -r ".[$SELECTED].id // empty" 2>/dev/null || echo ""
361
+ }
362
+
363
+ _selected_short_id() {
364
+ if [[ "$TASK_COUNT" -eq 0 ]]; then
365
+ echo ""
366
+ return
367
+ fi
368
+ echo "$TASK_DATA" | jq -r ".[$SELECTED].short_id // empty" 2>/dev/null || echo ""
369
+ }
370
+
371
+ _selected_tmux_session() {
372
+ if [[ "$TASK_COUNT" -eq 0 ]]; then
373
+ echo ""
374
+ return
375
+ fi
376
+ echo "$TASK_DATA" | jq -r ".[$SELECTED].tmuxSession // empty" 2>/dev/null || echo ""
377
+ }
378
+
379
+ # ── Action handlers ────────────────────────────────────────────────────
380
+ _action_attach() {
381
+ local session
382
+ session=$(_selected_tmux_session)
383
+ if [[ -n "$session" ]] && tmux has-session -t "$session" 2>/dev/null; then
384
+ # Restore terminal before attaching
385
+ tput cnorm 2>/dev/null || true
386
+ tput rmcup 2>/dev/null || true
387
+ stty echo 2>/dev/null || true
388
+ tmux attach-session -t "$session"
389
+ # Re-enter alt screen after detach
390
+ tput smcup 2>/dev/null || true
391
+ tput civis 2>/dev/null || true
392
+ stty -echo 2>/dev/null || true
393
+ fi
394
+ }
395
+
396
+ _action_steer() {
397
+ local sid
398
+ sid=$(_selected_short_id)
399
+ if [[ -z "$sid" ]]; then return; fi
400
+
401
+ # Restore terminal for input
402
+ tput cnorm 2>/dev/null || true
403
+ stty echo 2>/dev/null || true
404
+ tput cup $((ROWS - 2)) 0 2>/dev/null || true
405
+ tput el 2>/dev/null || true
406
+ printf " Steer #${sid}: "
407
+ local msg
408
+ read -r msg
409
+ stty -echo 2>/dev/null || true
410
+ tput civis 2>/dev/null || true
411
+
412
+ if [[ -n "$msg" ]]; then
413
+ "${SCRIPT_DIR}/steer.sh" "$sid" "$msg" 2>/dev/null || true
414
+ fi
415
+ }
416
+
417
+ _action_stop() {
418
+ local sid
419
+ sid=$(_selected_short_id)
420
+ if [[ -z "$sid" ]]; then return; fi
421
+
422
+ tput cnorm 2>/dev/null || true
423
+ stty echo 2>/dev/null || true
424
+ tput cup $((ROWS - 2)) 0 2>/dev/null || true
425
+ tput el 2>/dev/null || true
426
+ printf " Stop #${sid}? [y/N]: "
427
+ local confirm
428
+ read -r -n 1 confirm
429
+ stty -echo 2>/dev/null || true
430
+ tput civis 2>/dev/null || true
431
+
432
+ if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
433
+ "${SCRIPT_DIR}/stop.sh" "$sid" --yes 2>/dev/null || true
434
+ _refresh_data
435
+ fi
436
+ }
437
+
438
+ _action_filter() {
439
+ tput cnorm 2>/dev/null || true
440
+ stty echo 2>/dev/null || true
441
+ tput cup $((ROWS - 2)) 0 2>/dev/null || true
442
+ tput el 2>/dev/null || true
443
+ printf " Filter: "
444
+ local f
445
+ read -r f
446
+ stty -echo 2>/dev/null || true
447
+ tput civis 2>/dev/null || true
448
+ FILTER="$f"
449
+ SELECTED=0
450
+ _refresh_data
451
+ }
452
+
453
+ # ── Static (non-interactive) dashboard ─────────────────────────────────
454
+ _static_dashboard() {
455
+ _refresh_data
456
+ echo "╔══════════════════════════════════════════════════════════════╗"
457
+ echo "║ ClawForge Dashboard ║"
458
+ echo "╚══════════════════════════════════════════════════════════════╝"
459
+ echo ""
460
+ echo "── Active Tasks ──────────────────────────────────────────────"
461
+ if [[ "$TASK_COUNT" -eq 0 ]]; then
462
+ echo " No active tasks."
463
+ else
464
+ echo "$TASK_DATA" | jq -r '.[] |
465
+ def sid: if .short_id then "#\(.short_id)" else .id end;
466
+ def mode: .mode // "—";
467
+ " \(sid) \(mode) [\(.status // "unknown")] \(.description // "no description")[0:55]"
468
+ ' 2>/dev/null || echo " (error reading tasks)"
469
+ fi
470
+ echo ""
471
+ echo " Tip: Run in a terminal for interactive TUI with vim keybindings"
472
+ }
473
+
474
+ # ── Terminal cleanup ───────────────────────────────────────────────────
475
+ _cleanup() {
476
+ tput cnorm 2>/dev/null || true # show cursor
477
+ tput rmcup 2>/dev/null || true # exit alt screen
478
+ stty echo 2>/dev/null || true # restore echo
479
+ echo ""
480
+ }
481
+
482
+ # ── Main ───────────────────────────────────────────────────────────────
483
+
484
+ # Non-interactive: print static dashboard and exit
485
+ if [[ ! -t 1 ]]; then
486
+ _static_dashboard
487
+ exit 0
488
+ fi
489
+
490
+ trap _cleanup EXIT INT TERM
491
+
492
+ # Run startup animation
493
+ if ! $SKIP_ANIM; then
494
+ _forge_animation
495
+ fi
496
+
497
+ # Enter alt screen buffer
498
+ tput smcup 2>/dev/null || true
499
+ tput civis 2>/dev/null || true # hide cursor
500
+ stty -echo 2>/dev/null || true # disable echo
501
+
502
+ # Initial data load
503
+ _refresh_data
504
+
505
+ # Main loop
506
+ LAST_REFRESH=$(date +%s)
507
+ while true; do
508
+ _render
509
+
510
+ # Non-blocking read with timeout for auto-refresh
511
+ if read -t "$REFRESH_INTERVAL" -n 1 key 2>/dev/null; then
512
+ case "$key" in
513
+ q)
514
+ break
515
+ ;;
516
+ j)
517
+ if [[ "$TASK_COUNT" -gt 0 && "$SELECTED" -lt $((TASK_COUNT - 1)) ]]; then
518
+ SELECTED=$((SELECTED + 1))
519
+ fi
520
+ ;;
521
+ k)
522
+ if [[ "$SELECTED" -gt 0 ]]; then
523
+ SELECTED=$((SELECTED - 1))
524
+ fi
525
+ ;;
526
+ "") # Enter key
527
+ _action_attach
528
+ ;;
529
+ s)
530
+ _action_steer
531
+ ;;
532
+ x)
533
+ _action_stop
534
+ ;;
535
+ /)
536
+ _action_filter
537
+ ;;
538
+ r)
539
+ _refresh_data
540
+ ;;
541
+ "?")
542
+ if $SHOW_HELP; then
543
+ SHOW_HELP=false
544
+ else
545
+ SHOW_HELP=true
546
+ fi
547
+ ;;
548
+ esac
549
+ fi
550
+
551
+ # Auto-refresh data
552
+ NOW_TS=$(date +%s)
553
+ if [[ $((NOW_TS - LAST_REFRESH)) -ge "$REFRESH_INTERVAL" ]]; then
554
+ _refresh_data
555
+ LAST_REFRESH=$NOW_TS
556
+ fi
557
+ done
package/bin/diff.sh ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ # diff.sh — Show what an agent has changed without attaching
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "${SCRIPT_DIR}/../lib/common.sh"
7
+
8
+ usage() {
9
+ cat <<EOF
10
+ Usage: clawforge diff <id> [options]
11
+
12
+ Show git diff for a task's worktree without attaching to the agent.
13
+
14
+ Arguments:
15
+ <id> Task ID or short ID
16
+
17
+ Options:
18
+ --stat Show diffstat only (files changed summary)
19
+ --staged Show staged changes only
20
+ --name-only Show only file names
21
+ --save <path> Save diff to file
22
+ --help Show this help
23
+
24
+ Examples:
25
+ clawforge diff 1
26
+ clawforge diff 1 --stat
27
+ clawforge diff sprint-jwt --save /tmp/changes.diff
28
+ EOF
29
+ }
30
+
31
+ TASK_REF="" STAT=false STAGED=false NAME_ONLY=false SAVE_PATH=""
32
+
33
+ while [[ $# -gt 0 ]]; do
34
+ case "$1" in
35
+ --stat) STAT=true; shift ;;
36
+ --staged) STAGED=true; shift ;;
37
+ --name-only) NAME_ONLY=true; shift ;;
38
+ --save) SAVE_PATH="$2"; shift 2 ;;
39
+ --help|-h) usage; exit 0 ;;
40
+ --*) log_error "Unknown option: $1"; usage; exit 1 ;;
41
+ *) TASK_REF="$1"; shift ;;
42
+ esac
43
+ done
44
+
45
+ [[ -z "$TASK_REF" ]] && { log_error "Task ID required"; usage; exit 1; }
46
+
47
+ _ensure_registry
48
+
49
+ # Resolve task
50
+ TASK_DATA=""
51
+ if [[ "$TASK_REF" =~ ^[0-9]+$ ]]; then
52
+ TASK_DATA=$(jq -r --argjson sid "$TASK_REF" '.tasks[] | select(.short_id == $sid)' "$REGISTRY_FILE" 2>/dev/null || true)
53
+ fi
54
+ if [[ -z "$TASK_DATA" ]]; then
55
+ TASK_DATA=$(registry_get "$TASK_REF" 2>/dev/null || true)
56
+ fi
57
+ if [[ -z "$TASK_DATA" ]]; then
58
+ log_error "Task '$TASK_REF' not found"
59
+ exit 1
60
+ fi
61
+
62
+ WORKTREE=$(echo "$TASK_DATA" | jq -r '.worktree // empty')
63
+ BRANCH=$(echo "$TASK_DATA" | jq -r '.branch // empty')
64
+ SHORT_ID=$(echo "$TASK_DATA" | jq -r '.short_id // 0')
65
+ TASK_ID=$(echo "$TASK_DATA" | jq -r '.id')
66
+
67
+ if [[ -z "$WORKTREE" || ! -d "$WORKTREE" ]]; then
68
+ log_error "Worktree not found: ${WORKTREE:-'(none)'}. Task may have been cleaned."
69
+ exit 1
70
+ fi
71
+
72
+ # Build git diff args
73
+ DIFF_ARGS=()
74
+ $STAT && DIFF_ARGS+=(--stat)
75
+ $STAGED && DIFF_ARGS+=(--staged)
76
+ $NAME_ONLY && DIFF_ARGS+=(--name-only)
77
+
78
+ # Get default branch for comparison
79
+ DEFAULT_BRANCH=$(git -C "$WORKTREE" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || echo "main")
80
+
81
+ echo "── Diff for #${SHORT_ID} ($TASK_ID) ──"
82
+ echo "Branch: $BRANCH"
83
+ echo "Worktree: $WORKTREE"
84
+ echo ""
85
+
86
+ # Show uncommitted changes
87
+ UNCOMMITTED=$(git -C "$WORKTREE" diff "${DIFF_ARGS[@]}" 2>/dev/null || true)
88
+ COMMITTED=$(git -C "$WORKTREE" diff "${DIFF_ARGS[@]}" "${DEFAULT_BRANCH}...HEAD" 2>/dev/null || true)
89
+
90
+ OUTPUT=""
91
+ if [[ -n "$COMMITTED" ]]; then
92
+ OUTPUT+="── Committed changes (vs ${DEFAULT_BRANCH}) ──"$'\n'
93
+ OUTPUT+="$COMMITTED"$'\n'
94
+ fi
95
+ if [[ -n "$UNCOMMITTED" ]]; then
96
+ OUTPUT+="── Uncommitted changes ──"$'\n'
97
+ OUTPUT+="$UNCOMMITTED"$'\n'
98
+ fi
99
+
100
+ if [[ -z "$OUTPUT" ]]; then
101
+ echo "(no changes detected)"
102
+ else
103
+ if [[ -n "$SAVE_PATH" ]]; then
104
+ echo "$OUTPUT" > "$SAVE_PATH"
105
+ echo "Saved diff to $SAVE_PATH"
106
+ else
107
+ echo "$OUTPUT"
108
+ fi
109
+ fi