shipwright-cli 1.7.1 → 1.9.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 (105) hide show
  1. package/.claude/agents/code-reviewer.md +90 -0
  2. package/.claude/agents/devops-engineer.md +142 -0
  3. package/.claude/agents/pipeline-agent.md +80 -0
  4. package/.claude/agents/shell-script-specialist.md +150 -0
  5. package/.claude/agents/test-specialist.md +196 -0
  6. package/.claude/hooks/post-tool-use.sh +38 -0
  7. package/.claude/hooks/pre-tool-use.sh +25 -0
  8. package/.claude/hooks/session-started.sh +37 -0
  9. package/README.md +212 -814
  10. package/claude-code/CLAUDE.md.shipwright +54 -0
  11. package/claude-code/hooks/notify-idle.sh +2 -2
  12. package/claude-code/hooks/session-start.sh +24 -0
  13. package/claude-code/hooks/task-completed.sh +6 -2
  14. package/claude-code/settings.json.template +12 -0
  15. package/dashboard/public/app.js +4422 -0
  16. package/dashboard/public/index.html +816 -0
  17. package/dashboard/public/styles.css +4755 -0
  18. package/dashboard/server.ts +4315 -0
  19. package/docs/KNOWN-ISSUES.md +18 -10
  20. package/docs/TIPS.md +38 -26
  21. package/docs/patterns/README.md +33 -23
  22. package/package.json +9 -5
  23. package/scripts/adapters/iterm2-adapter.sh +1 -1
  24. package/scripts/adapters/tmux-adapter.sh +52 -23
  25. package/scripts/adapters/wezterm-adapter.sh +26 -14
  26. package/scripts/lib/compat.sh +200 -0
  27. package/scripts/lib/helpers.sh +72 -0
  28. package/scripts/postinstall.mjs +72 -13
  29. package/scripts/{cct → sw} +109 -21
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +390 -0
  33. package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
  37. package/scripts/sw-dashboard.sh +477 -0
  38. package/scripts/sw-developer-simulation.sh +252 -0
  39. package/scripts/sw-docs.sh +635 -0
  40. package/scripts/sw-doctor.sh +907 -0
  41. package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
  42. package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
  43. package/scripts/sw-github-checks.sh +521 -0
  44. package/scripts/sw-github-deploy.sh +533 -0
  45. package/scripts/sw-github-graphql.sh +972 -0
  46. package/scripts/sw-heartbeat.sh +293 -0
  47. package/scripts/{cct-init.sh → sw-init.sh} +144 -11
  48. package/scripts/sw-intelligence.sh +1196 -0
  49. package/scripts/sw-jira.sh +643 -0
  50. package/scripts/sw-launchd.sh +364 -0
  51. package/scripts/sw-linear.sh +648 -0
  52. package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
  53. package/scripts/{cct-loop.sh → sw-loop.sh} +534 -44
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
  58. package/scripts/sw-predictive.sh +820 -0
  59. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  60. package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
  61. package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
  62. package/scripts/sw-remote.sh +687 -0
  63. package/scripts/sw-self-optimize.sh +947 -0
  64. package/scripts/sw-session.sh +519 -0
  65. package/scripts/sw-setup.sh +234 -0
  66. package/scripts/sw-status.sh +605 -0
  67. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  68. package/scripts/sw-tmux.sh +591 -0
  69. package/scripts/sw-tracker-jira.sh +277 -0
  70. package/scripts/sw-tracker-linear.sh +292 -0
  71. package/scripts/sw-tracker.sh +409 -0
  72. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  73. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  74. package/templates/pipelines/autonomous.json +27 -5
  75. package/templates/pipelines/full.json +12 -0
  76. package/templates/pipelines/standard.json +12 -0
  77. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  78. package/tmux/templates/accessibility.json +34 -0
  79. package/tmux/templates/api-design.json +35 -0
  80. package/tmux/templates/architecture.json +1 -0
  81. package/tmux/templates/bug-fix.json +9 -0
  82. package/tmux/templates/code-review.json +1 -0
  83. package/tmux/templates/compliance.json +36 -0
  84. package/tmux/templates/data-pipeline.json +36 -0
  85. package/tmux/templates/debt-paydown.json +34 -0
  86. package/tmux/templates/devops.json +1 -0
  87. package/tmux/templates/documentation.json +1 -0
  88. package/tmux/templates/exploration.json +1 -0
  89. package/tmux/templates/feature-dev.json +1 -0
  90. package/tmux/templates/full-stack.json +8 -0
  91. package/tmux/templates/i18n.json +34 -0
  92. package/tmux/templates/incident-response.json +36 -0
  93. package/tmux/templates/migration.json +1 -0
  94. package/tmux/templates/observability.json +35 -0
  95. package/tmux/templates/onboarding.json +33 -0
  96. package/tmux/templates/performance.json +35 -0
  97. package/tmux/templates/refactor.json +1 -0
  98. package/tmux/templates/release.json +35 -0
  99. package/tmux/templates/security-audit.json +8 -0
  100. package/tmux/templates/spike.json +34 -0
  101. package/tmux/templates/testing.json +1 -0
  102. package/tmux/tmux.conf +98 -9
  103. package/scripts/cct-doctor.sh +0 -414
  104. package/scripts/cct-session.sh +0 -284
  105. package/scripts/cct-status.sh +0 -169
@@ -0,0 +1,605 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ sw-status.sh — Dashboard showing Claude Code team status ║
4
+ # ║ ║
5
+ # ║ Shows running teams, agent windows, and task progress. ║
6
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
7
+ VERSION="1.9.0"
8
+ set -euo pipefail
9
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
+
11
+ # ─── Colors ──────────────────────────────────────────────────────────────────
12
+ CYAN='\033[38;2;0;212;255m'
13
+ PURPLE='\033[38;2;124;58;237m'
14
+ BLUE='\033[38;2;0;102;255m'
15
+ GREEN='\033[38;2;74;222;128m'
16
+ YELLOW='\033[38;2;250;204;21m'
17
+ RED='\033[38;2;248;113;113m'
18
+ DIM='\033[2m'
19
+ BOLD='\033[1m'
20
+ RESET='\033[0m'
21
+
22
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
23
+ _COMPAT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/compat.sh"
24
+ # shellcheck source=lib/compat.sh
25
+ [[ -f "$_COMPAT" ]] && source "$_COMPAT"
26
+
27
+ # ─── Header ──────────────────────────────────────────────────────────────────
28
+
29
+ echo ""
30
+ echo -e "${CYAN}${BOLD} Shipwright — Status Dashboard${RESET}"
31
+ echo -e "${DIM} $(date '+%Y-%m-%d %H:%M:%S')${RESET}"
32
+ echo -e "${DIM} ══════════════════════════════════════════${RESET}"
33
+ echo ""
34
+
35
+ # ─── 1. Tmux Windows ────────────────────────────────────────────────────────
36
+
37
+ echo -e "${PURPLE}${BOLD} TMUX WINDOWS${RESET}"
38
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
39
+
40
+ # Get all windows, highlight Claude-related ones
41
+ HAS_CLAUDE_WINDOWS=false
42
+ while IFS= read -r line; do
43
+ session_window="$(echo "$line" | cut -d'|' -f1)"
44
+ window_name="$(echo "$line" | cut -d'|' -f2)"
45
+ pane_count="$(echo "$line" | cut -d'|' -f3)"
46
+ active="$(echo "$line" | cut -d'|' -f4)"
47
+
48
+ if echo "$window_name" | grep -qi "claude"; then
49
+ HAS_CLAUDE_WINDOWS=true
50
+ if [[ "$active" == "1" ]]; then
51
+ status_icon="${GREEN}●${RESET}"
52
+ status_label="${GREEN}active${RESET}"
53
+ else
54
+ status_icon="${YELLOW}●${RESET}"
55
+ status_label="${YELLOW}idle${RESET}"
56
+ fi
57
+ echo -e " ${status_icon} ${BOLD}${window_name}${RESET} ${DIM}${session_window}${RESET} panes:${pane_count} ${status_label}"
58
+ fi
59
+ done < <(tmux list-windows -a -F '#{session_name}:#{window_index}|#{window_name}|#{window_panes}|#{window_active}' 2>/dev/null || true)
60
+
61
+ if ! $HAS_CLAUDE_WINDOWS; then
62
+ echo -e " ${DIM}No Claude team windows found.${RESET}"
63
+ echo -e " ${DIM}Start one with: ${CYAN}shipwright session <name>${RESET}"
64
+ fi
65
+
66
+ # ─── 2. Team Configurations ─────────────────────────────────────────────────
67
+
68
+ echo ""
69
+ echo -e "${PURPLE}${BOLD} TEAM CONFIGS${RESET} ${DIM}~/.claude/teams/${RESET}"
70
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
71
+
72
+ TEAMS_DIR="${HOME}/.claude/teams"
73
+ HAS_TEAMS=false
74
+
75
+ if [[ -d "$TEAMS_DIR" ]]; then
76
+ while IFS= read -r team_dir; do
77
+ [[ -z "$team_dir" ]] && continue
78
+ HAS_TEAMS=true
79
+ team_name="$(basename "$team_dir")"
80
+
81
+ # Try to read config.json for member info
82
+ config_file="${team_dir}/config.json"
83
+ if [[ -f "$config_file" ]]; then
84
+ # Count members from JSON (look for "name" keys in members array)
85
+ member_count=$(grep -c '"name"' "$config_file" 2>/dev/null || true)
86
+ member_count="${member_count:-0}"
87
+ # Extract member names
88
+ member_names=$(grep '"name"' "$config_file" 2>/dev/null | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | tr '\n' ', ' | sed 's/,$//' | sed 's/,/, /g')
89
+
90
+ echo -e " ${GREEN}●${RESET} ${BOLD}${team_name}${RESET} ${DIM}members:${member_count}${RESET}"
91
+ if [[ -n "$member_names" ]]; then
92
+ echo -e " ${DIM}└─ ${member_names}${RESET}"
93
+ fi
94
+ else
95
+ # Directory exists but no config — possibly orphaned
96
+ echo -e " ${RED}●${RESET} ${BOLD}${team_name}${RESET} ${DIM}(no config — possibly orphaned)${RESET}"
97
+ fi
98
+ done < <(find "$TEAMS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
99
+ fi
100
+
101
+ if ! $HAS_TEAMS; then
102
+ echo -e " ${DIM}No team configs found.${RESET}"
103
+ fi
104
+
105
+ # ─── 3. Task Lists ──────────────────────────────────────────────────────────
106
+
107
+ echo ""
108
+ echo -e "${PURPLE}${BOLD} TASK LISTS${RESET} ${DIM}~/.claude/tasks/${RESET}"
109
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
110
+
111
+ TASKS_DIR="${HOME}/.claude/tasks"
112
+ HAS_TASKS=false
113
+
114
+ if [[ -d "$TASKS_DIR" ]]; then
115
+ while IFS= read -r task_dir; do
116
+ [[ -z "$task_dir" ]] && continue
117
+ HAS_TASKS=true
118
+ task_team="$(basename "$task_dir")"
119
+
120
+ # Count tasks by status
121
+ total=0
122
+ completed=0
123
+ in_progress=0
124
+ pending=0
125
+
126
+ while IFS= read -r task_file; do
127
+ [[ -z "$task_file" ]] && continue
128
+ total=$((total + 1))
129
+ status=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$task_file" 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
130
+ case "$status" in
131
+ completed) completed=$((completed + 1)) ;;
132
+ in_progress) in_progress=$((in_progress + 1)) ;;
133
+ pending) pending=$((pending + 1)) ;;
134
+ esac
135
+ done < <(find "$task_dir" -type f -name '*.json' 2>/dev/null)
136
+
137
+ # Build progress bar
138
+ if [[ $total -gt 0 ]]; then
139
+ pct=$((completed * 100 / total))
140
+ bar_width=20
141
+ filled=$((pct * bar_width / 100))
142
+ empty=$((bar_width - filled))
143
+ bar="${GREEN}"
144
+ for ((i=0; i<filled; i++)); do bar+="█"; done
145
+ bar+="${DIM}"
146
+ for ((i=0; i<empty; i++)); do bar+="░"; done
147
+ bar+="${RESET}"
148
+
149
+ echo -e " ${BLUE}●${RESET} ${BOLD}${task_team}${RESET} ${bar} ${pct}% ${DIM}(${completed}/${total} done)${RESET}"
150
+
151
+ # Show breakdown if there are active tasks
152
+ details=""
153
+ [[ $in_progress -gt 0 ]] && details+="${GREEN}${in_progress} active${RESET} "
154
+ [[ $pending -gt 0 ]] && details+="${YELLOW}${pending} pending${RESET} "
155
+ [[ $completed -gt 0 ]] && details+="${DIM}${completed} done${RESET}"
156
+ [[ -n "$details" ]] && echo -e " ${DIM}└─${RESET} ${details}"
157
+ else
158
+ echo -e " ${DIM}●${RESET} ${BOLD}${task_team}${RESET} ${DIM}(no tasks)${RESET}"
159
+ fi
160
+ done < <(find "$TASKS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
161
+ fi
162
+
163
+ if ! $HAS_TASKS; then
164
+ echo -e " ${DIM}No task lists found.${RESET}"
165
+ fi
166
+
167
+ # ─── 4. Daemon Pipelines ──────────────────────────────────────────────────
168
+
169
+ DAEMON_DIR="${HOME}/.shipwright"
170
+ STATE_FILE="${DAEMON_DIR}/daemon-state.json"
171
+ PID_FILE="${DAEMON_DIR}/daemon.pid"
172
+ EVENTS_FILE="${DAEMON_DIR}/events.jsonl"
173
+ HAS_DAEMON=false
174
+
175
+ if [[ -f "$STATE_FILE" ]]; then
176
+ # Check daemon process
177
+ daemon_pid=""
178
+ daemon_running=false
179
+ if [[ -f "$PID_FILE" ]]; then
180
+ daemon_pid=$(cat "$PID_FILE" 2>/dev/null || true)
181
+ if [[ -n "$daemon_pid" ]] && kill -0 "$daemon_pid" 2>/dev/null; then
182
+ daemon_running=true
183
+ fi
184
+ fi
185
+
186
+ active_count=$(jq -r '.active_jobs | length' "$STATE_FILE" 2>/dev/null || echo 0)
187
+ queue_count=$(jq -r '.queued | length' "$STATE_FILE" 2>/dev/null || echo 0)
188
+ completed_count=$(jq -r '.completed | length' "$STATE_FILE" 2>/dev/null || echo 0)
189
+
190
+ if $daemon_running || [[ "$active_count" -gt 0 ]] || [[ "$queue_count" -gt 0 ]] || [[ "$completed_count" -gt 0 ]]; then
191
+ HAS_DAEMON=true
192
+ echo ""
193
+ echo -e "${PURPLE}${BOLD} DAEMON PIPELINES${RESET} ${DIM}~/.shipwright/${RESET}"
194
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
195
+
196
+ # ── Daemon Health ──
197
+ if $daemon_running; then
198
+ started_at=$(jq -r '.started_at // "unknown"' "$STATE_FILE" 2>/dev/null)
199
+ last_poll=$(jq -r '.last_poll // "never"' "$STATE_FILE" 2>/dev/null)
200
+ # Calculate uptime
201
+ uptime_str=""
202
+ if [[ "$started_at" != "unknown" && "$started_at" != "null" ]]; then
203
+ start_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$started_at" +%s 2>/dev/null || echo 0)
204
+ if [[ "$start_epoch" -gt 0 ]]; then
205
+ now_e=$(date +%s)
206
+ elapsed=$((now_e - start_epoch))
207
+ if [[ "$elapsed" -ge 3600 ]]; then
208
+ uptime_str=$(printf "%dh %dm" $((elapsed/3600)) $((elapsed%3600/60)))
209
+ elif [[ "$elapsed" -ge 60 ]]; then
210
+ uptime_str=$(printf "%dm %ds" $((elapsed/60)) $((elapsed%60)))
211
+ else
212
+ uptime_str=$(printf "%ds" "$elapsed")
213
+ fi
214
+ fi
215
+ fi
216
+ echo -e " ${GREEN}●${RESET} ${BOLD}Running${RESET} ${DIM}PID:${daemon_pid}${RESET} ${DIM}up:${uptime_str:-?}${RESET} ${DIM}poll:${last_poll}${RESET}"
217
+ else
218
+ echo -e " ${RED}●${RESET} ${BOLD}Stopped${RESET}"
219
+ fi
220
+
221
+ # ── Active Jobs ──
222
+ if [[ "$active_count" -gt 0 ]]; then
223
+ echo ""
224
+ echo -e " ${BOLD}Active Jobs (${active_count})${RESET}"
225
+ while IFS= read -r job; do
226
+ [[ -z "$job" ]] && continue
227
+ a_issue=$(echo "$job" | jq -r '.issue')
228
+ a_title=$(echo "$job" | jq -r '.title // ""')
229
+ a_worktree=$(echo "$job" | jq -r '.worktree // ""')
230
+ a_started=$(echo "$job" | jq -r '.started_at // ""')
231
+ a_goal=$(echo "$job" | jq -r '.goal // ""')
232
+
233
+ # Look up title from title cache if empty
234
+ if [[ -z "$a_title" ]]; then
235
+ a_title=$(jq -r --arg n "$a_issue" '.titles[$n] // ""' "$STATE_FILE" 2>/dev/null || true)
236
+ fi
237
+
238
+ # Time elapsed
239
+ age_str=""
240
+ if [[ -n "$a_started" && "$a_started" != "null" ]]; then
241
+ s_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$a_started" +%s 2>/dev/null || echo 0)
242
+ if [[ "$s_epoch" -gt 0 ]]; then
243
+ now_e=$(date +%s)
244
+ el=$((now_e - s_epoch))
245
+ if [[ "$el" -ge 3600 ]]; then
246
+ age_str=$(printf "%dh %dm" $((el/3600)) $((el%3600/60)))
247
+ elif [[ "$el" -ge 60 ]]; then
248
+ age_str=$(printf "%dm %ds" $((el/60)) $((el%60)))
249
+ else
250
+ age_str=$(printf "%ds" "$el")
251
+ fi
252
+ fi
253
+ fi
254
+
255
+ # Read enriched pipeline state from worktree
256
+ stage_str=""
257
+ stage_desc=""
258
+ stage_progress=""
259
+ goal_from_state=""
260
+ if [[ -n "$a_worktree" && -f "${a_worktree}/.claude/pipeline-state.md" ]]; then
261
+ ps_file="${a_worktree}/.claude/pipeline-state.md"
262
+ stage_str=$(grep -E '^current_stage:' "$ps_file" 2>/dev/null | head -1 | sed 's/^current_stage:[[:space:]]*//' || true)
263
+ stage_desc=$(grep -E '^current_stage_description:' "$ps_file" 2>/dev/null | head -1 | sed 's/^current_stage_description:[[:space:]]*"//;s/"$//' || true)
264
+ stage_progress=$(grep -E '^stage_progress:' "$ps_file" 2>/dev/null | head -1 | sed 's/^stage_progress:[[:space:]]*"//;s/"$//' || true)
265
+ goal_from_state=$(grep -E '^goal:' "$ps_file" 2>/dev/null | head -1 | sed 's/^goal:[[:space:]]*"//;s/"$//' || true)
266
+ fi
267
+
268
+ # Use goal from state file if not in daemon job data
269
+ display_goal="${a_goal:-$goal_from_state}"
270
+
271
+ # Title line
272
+ echo -e " ${CYAN}#${a_issue}${RESET} ${BOLD}${a_title}${RESET}"
273
+
274
+ # Goal line (if different from title)
275
+ if [[ -n "$display_goal" && "$display_goal" != "$a_title" ]]; then
276
+ echo -e " ${DIM}Delivering: ${display_goal}${RESET}"
277
+ fi
278
+
279
+ # Stage + description line
280
+ if [[ -n "$stage_str" ]]; then
281
+ stage_icon="🔄"
282
+ stage_line=" ${stage_icon} ${BLUE}${stage_str}${RESET}"
283
+ [[ -n "$stage_desc" ]] && stage_line="${stage_line} ${DIM}— ${stage_desc}${RESET}"
284
+ echo -e "$stage_line"
285
+ fi
286
+
287
+ # Inline progress bar from stage_progress
288
+ if [[ -n "$stage_progress" ]]; then
289
+ progress_bar=""
290
+ entry=""
291
+ # Parse space-separated "stage:status" pairs
292
+ for entry in $stage_progress; do
293
+ s_name="${entry%%:*}"
294
+ s_stat="${entry#*:}"
295
+ s_icon=""
296
+ case "$s_stat" in
297
+ complete) s_icon="✅" ;;
298
+ running) s_icon="🔄" ;;
299
+ failed) s_icon="❌" ;;
300
+ *) s_icon="⬜" ;;
301
+ esac
302
+ if [[ -n "$progress_bar" ]]; then
303
+ progress_bar="${progress_bar} → ${s_icon}${s_name}"
304
+ else
305
+ progress_bar="${s_icon}${s_name}"
306
+ fi
307
+ done
308
+ echo -e " ${DIM}${progress_bar}${RESET}"
309
+ fi
310
+
311
+ # Elapsed time
312
+ [[ -n "$age_str" ]] && echo -e " ${DIM}Elapsed: ${age_str}${RESET}"
313
+ done < <(jq -c '.active_jobs[]' "$STATE_FILE" 2>/dev/null)
314
+ fi
315
+
316
+ # ── Queued Issues ──
317
+ if [[ "$queue_count" -gt 0 ]]; then
318
+ echo ""
319
+ echo -e " ${BOLD}Queued (${queue_count})${RESET}"
320
+ while read -r q_num; do
321
+ [[ -z "$q_num" ]] && continue
322
+ q_title=$(jq -r --arg n "$q_num" '.titles[$n] // ""' "$STATE_FILE" 2>/dev/null || true)
323
+ title_display=""
324
+ [[ -n "$q_title" ]] && title_display=" ${q_title}"
325
+ echo -e " ${YELLOW}#${q_num}${RESET}${title_display}"
326
+ done < <(jq -r '.queued[]' "$STATE_FILE" 2>/dev/null)
327
+ fi
328
+
329
+ # ── Recent Completions ──
330
+ if [[ "$completed_count" -gt 0 ]]; then
331
+ echo ""
332
+ echo -e " ${BOLD}Recent Completions${RESET}"
333
+ while IFS=$'\t' read -r c_num c_result c_dur c_at; do
334
+ [[ -z "$c_num" ]] && continue
335
+ if [[ "$c_result" == "success" ]]; then
336
+ c_icon="${GREEN}✓${RESET}"
337
+ else
338
+ c_icon="${RED}✗${RESET}"
339
+ fi
340
+ echo -e " ${c_icon} ${CYAN}#${c_num}${RESET} ${c_result} ${DIM}(${c_dur})${RESET}"
341
+ done < <(jq -r '.completed | reverse | .[:5][] | "\(.issue)\t\(.result)\t\(.duration // "—")\t\(.completed_at // "")"' "$STATE_FILE" 2>/dev/null)
342
+ fi
343
+
344
+ # ── Recent Activity (from events.jsonl) ──
345
+ if [[ -f "$EVENTS_FILE" ]]; then
346
+ # Get last 8 relevant events (spawns, stage changes, completions)
347
+ recent_events=$(tail -200 "$EVENTS_FILE" 2>/dev/null | \
348
+ grep -E '"type":"(daemon\.spawn|daemon\.reap|stage\.(started|completed)|daemon\.poll)"' 2>/dev/null | \
349
+ tail -8 || true)
350
+ if [[ -n "$recent_events" ]]; then
351
+ echo ""
352
+ echo -e " ${BOLD}Recent Activity${RESET}"
353
+ while IFS= read -r evt; do
354
+ [[ -z "$evt" ]] && continue
355
+ evt_ts=$(echo "$evt" | jq -r '.ts // ""' 2>/dev/null)
356
+ evt_type=$(echo "$evt" | jq -r '.type // ""' 2>/dev/null)
357
+ evt_issue=$(echo "$evt" | jq -r '.issue // ""' 2>/dev/null)
358
+
359
+ # Format timestamp as HH:MM
360
+ evt_time=""
361
+ if [[ -n "$evt_ts" && "$evt_ts" != "null" ]]; then
362
+ evt_time=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$evt_ts" +"%H:%M" 2>/dev/null || echo "")
363
+ fi
364
+
365
+ case "$evt_type" in
366
+ daemon.spawn)
367
+ echo -e " ${DIM}${evt_time}${RESET} ${GREEN}↳${RESET} Spawned pipeline for #${evt_issue}"
368
+ ;;
369
+ daemon.reap)
370
+ evt_result=$(echo "$evt" | jq -r '.result // ""' 2>/dev/null)
371
+ evt_dur=$(echo "$evt" | jq -r '.duration_s // 0' 2>/dev/null)
372
+ dur_display=""
373
+ if [[ "$evt_dur" -gt 0 ]] 2>/dev/null; then
374
+ if [[ "$evt_dur" -ge 3600 ]]; then
375
+ dur_display=$(printf " (%dh %dm)" $((evt_dur/3600)) $((evt_dur%3600/60)))
376
+ elif [[ "$evt_dur" -ge 60 ]]; then
377
+ dur_display=$(printf " (%dm %ds)" $((evt_dur/60)) $((evt_dur%60)))
378
+ else
379
+ dur_display=$(printf " (%ds)" "$evt_dur")
380
+ fi
381
+ fi
382
+ if [[ "$evt_result" == "success" ]]; then
383
+ echo -e " ${DIM}${evt_time}${RESET} ${GREEN}●${RESET} #${evt_issue} completed${dur_display}"
384
+ else
385
+ echo -e " ${DIM}${evt_time}${RESET} ${RED}●${RESET} #${evt_issue} failed${dur_display}"
386
+ fi
387
+ ;;
388
+ stage.started)
389
+ evt_stage=$(echo "$evt" | jq -r '.stage // ""' 2>/dev/null)
390
+ echo -e " ${DIM}${evt_time}${RESET} ${BLUE}●${RESET} #${evt_issue} started ${evt_stage}"
391
+ ;;
392
+ stage.completed)
393
+ evt_stage=$(echo "$evt" | jq -r '.stage // ""' 2>/dev/null)
394
+ echo -e " ${DIM}${evt_time}${RESET} ${DIM}●${RESET} #${evt_issue} completed ${evt_stage}"
395
+ ;;
396
+ daemon.poll)
397
+ evt_found=$(echo "$evt" | jq -r '.issues_found // 0' 2>/dev/null)
398
+ echo -e " ${DIM}${evt_time} ⟳ Polled — ${evt_found} issue(s) found${RESET}"
399
+ ;;
400
+ esac
401
+ done <<< "$recent_events"
402
+ fi
403
+ fi
404
+ fi
405
+ fi
406
+
407
+ # ─── Issue Tracker ─────────────────────────────────────────────────────────
408
+
409
+ TRACKER_CONFIG="${HOME}/.shipwright/tracker-config.json"
410
+ if [[ -f "$TRACKER_CONFIG" ]]; then
411
+ TRACKER_PROVIDER=$(jq -r '.provider // "none"' "$TRACKER_CONFIG" 2>/dev/null || echo "none")
412
+ if [[ "$TRACKER_PROVIDER" != "none" && -n "$TRACKER_PROVIDER" ]]; then
413
+ echo ""
414
+ echo -e "${PURPLE}${BOLD} ISSUE TRACKER${RESET}"
415
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
416
+ case "$TRACKER_PROVIDER" in
417
+ linear)
418
+ echo -e " ${GREEN}●${RESET} ${BOLD}Linear${RESET} ${DIM}(run shipwright linear status for details)${RESET}"
419
+ ;;
420
+ jira)
421
+ JIRA_URL=$(jq -r '.jira.base_url // ""' "$TRACKER_CONFIG" 2>/dev/null || true)
422
+ echo -e " ${GREEN}●${RESET} ${BOLD}Jira${RESET} ${DIM}${JIRA_URL}${RESET} ${DIM}(run shipwright jira status for details)${RESET}"
423
+ ;;
424
+ esac
425
+ fi
426
+ fi
427
+
428
+ # ─── Agent Heartbeats ──────────────────────────────────────────────────────
429
+
430
+ HEARTBEAT_DIR="$HOME/.shipwright/heartbeats"
431
+ HAS_HEARTBEATS=false
432
+
433
+ if [[ -d "$HEARTBEAT_DIR" ]]; then
434
+ hb_count=0
435
+ for hb_file in "${HEARTBEAT_DIR}"/*.json; do
436
+ [[ -f "$hb_file" ]] || continue
437
+ hb_count=$((hb_count + 1))
438
+ done
439
+
440
+ if [[ "$hb_count" -gt 0 ]]; then
441
+ HAS_HEARTBEATS=true
442
+ echo ""
443
+ echo -e "${PURPLE}${BOLD} AGENT HEARTBEATS${RESET} ${DIM}(${hb_count} active)${RESET}"
444
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
445
+
446
+ for hb_file in "${HEARTBEAT_DIR}"/*.json; do
447
+ [[ -f "$hb_file" ]] || continue
448
+ local_job_id="$(basename "$hb_file" .json)"
449
+ hb_pid=$(jq -r '.pid // ""' "$hb_file" 2>/dev/null || true)
450
+ hb_stage=$(jq -r '.stage // ""' "$hb_file" 2>/dev/null || true)
451
+ hb_issue=$(jq -r '.issue // ""' "$hb_file" 2>/dev/null || true)
452
+ hb_iter=$(jq -r '.iteration // ""' "$hb_file" 2>/dev/null || true)
453
+ hb_activity=$(jq -r '.last_activity // ""' "$hb_file" 2>/dev/null || true)
454
+ hb_updated=$(jq -r '.updated_at // ""' "$hb_file" 2>/dev/null || true)
455
+ hb_mem=$(jq -r '.memory_mb // 0' "$hb_file" 2>/dev/null || true)
456
+
457
+ # Check if process is still alive
458
+ hb_alive=false
459
+ if [[ -n "$hb_pid" && "$hb_pid" != "null" ]] && kill -0 "$hb_pid" 2>/dev/null; then
460
+ hb_alive=true
461
+ fi
462
+
463
+ # Calculate age
464
+ hb_age_str=""
465
+ if [[ -n "$hb_updated" && "$hb_updated" != "null" ]]; then
466
+ hb_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$hb_updated" +%s 2>/dev/null || echo 0)
467
+ if [[ "$hb_epoch" -gt 0 ]]; then
468
+ now_e=$(date +%s)
469
+ hb_age=$((now_e - hb_epoch))
470
+ if [[ "$hb_age" -ge 120 ]]; then
471
+ hb_age_str="${RED}${hb_age}s ago (STALE)${RESET}"
472
+ else
473
+ hb_age_str="${DIM}${hb_age}s ago${RESET}"
474
+ fi
475
+ fi
476
+ fi
477
+
478
+ if $hb_alive; then
479
+ hb_icon="${GREEN}●${RESET}"
480
+ else
481
+ hb_icon="${RED}●${RESET}"
482
+ fi
483
+
484
+ echo -e " ${hb_icon} ${BOLD}${local_job_id}${RESET} ${DIM}pid:${hb_pid}${RESET}"
485
+ detail_line=" "
486
+ [[ -n "$hb_issue" && "$hb_issue" != "null" && "$hb_issue" != "0" ]] && detail_line+="${CYAN}#${hb_issue}${RESET} "
487
+ [[ -n "$hb_stage" && "$hb_stage" != "null" ]] && detail_line+="${BLUE}${hb_stage}${RESET} "
488
+ [[ -n "$hb_iter" && "$hb_iter" != "null" ]] && detail_line+="${DIM}iter:${hb_iter}${RESET} "
489
+ [[ -n "$hb_age_str" ]] && detail_line+="${hb_age_str} "
490
+ [[ "${hb_mem:-0}" -gt 0 ]] && detail_line+="${DIM}${hb_mem}MB${RESET}"
491
+ echo -e "$detail_line"
492
+ [[ -n "$hb_activity" && "$hb_activity" != "null" ]] && echo -e " ${DIM}${hb_activity}${RESET}"
493
+ done
494
+ fi
495
+ fi
496
+
497
+ # ─── Remote Machines ──────────────────────────────────────────────────────
498
+
499
+ MACHINES_FILE="$HOME/.shipwright/machines.json"
500
+ if [[ -f "$MACHINES_FILE" ]]; then
501
+ machine_count=$(jq '.machines | length' "$MACHINES_FILE" 2>/dev/null || echo 0)
502
+ if [[ "$machine_count" -gt 0 ]]; then
503
+ echo ""
504
+ echo -e "${PURPLE}${BOLD} REMOTE MACHINES${RESET} ${DIM}(${machine_count} registered)${RESET}"
505
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
506
+
507
+ while IFS= read -r machine; do
508
+ [[ -z "$machine" ]] && continue
509
+ m_name=$(echo "$machine" | jq -r '.name // ""')
510
+ m_host=$(echo "$machine" | jq -r '.host // ""')
511
+ m_cores=$(echo "$machine" | jq -r '.cores // "?"')
512
+ m_mem=$(echo "$machine" | jq -r '.memory_gb // "?"')
513
+ m_workers=$(echo "$machine" | jq -r '.max_workers // "?"')
514
+
515
+ echo -e " ${BLUE}●${RESET} ${BOLD}${m_name}${RESET} ${DIM}${m_host}${RESET} ${DIM}cores:${m_cores} mem:${m_mem}GB workers:${m_workers}${RESET}"
516
+ done < <(jq -c '.machines[]' "$MACHINES_FILE" 2>/dev/null)
517
+ fi
518
+ fi
519
+
520
+ # ─── Connected Developers ─────────────────────────────────────────────────
521
+
522
+ # Check if curl and jq are available
523
+ if command -v curl &>/dev/null && command -v jq &>/dev/null; then
524
+ # Read dashboard URL from config, fall back to default
525
+ TEAM_CONFIG="${HOME}/.shipwright/team-config.json"
526
+ DASHBOARD_URL=""
527
+ if [[ -f "$TEAM_CONFIG" ]]; then
528
+ DASHBOARD_URL=$(jq -r '.dashboard_url // ""' "$TEAM_CONFIG" 2>/dev/null || true)
529
+ fi
530
+ [[ -z "$DASHBOARD_URL" ]] && DASHBOARD_URL="http://localhost:8767"
531
+
532
+ # Try to reach the dashboard /api/team endpoint with 3s timeout
533
+ api_response=$(curl -s --max-time 3 "$DASHBOARD_URL/api/team" 2>/dev/null || true)
534
+
535
+ # Check if we got a valid response
536
+ if [[ -n "$api_response" ]] && echo "$api_response" | jq empty 2>/dev/null; then
537
+ echo ""
538
+ echo -e "${PURPLE}${BOLD} CONNECTED DEVELOPERS${RESET}"
539
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
540
+
541
+ # Parse total_online count
542
+ total_online=$(echo "$api_response" | jq -r '.total_online // 0' 2>/dev/null)
543
+
544
+ # Parse developers array and display table
545
+ dev_count=$(echo "$api_response" | jq '.developers | length' 2>/dev/null || echo 0)
546
+ if [[ "$dev_count" -gt 0 ]]; then
547
+ while IFS= read -r developer; do
548
+ [[ -z "$developer" ]] && continue
549
+
550
+ dev_id=$(echo "$developer" | jq -r '.developer_id // "?"')
551
+ dev_machine=$(echo "$developer" | jq -r '.machine_name // "?"')
552
+ dev_status=$(echo "$developer" | jq -r '.status // "offline"')
553
+ active_jobs=$(echo "$developer" | jq '.active_jobs | length' 2>/dev/null || echo 0)
554
+ queued=$(echo "$developer" | jq '.queued | length' 2>/dev/null || echo 0)
555
+
556
+ # Status indicator and color
557
+ case "$dev_status" in
558
+ online)
559
+ status_icon="${GREEN}●${RESET}"
560
+ status_label="${GREEN}online${RESET}"
561
+ ;;
562
+ idle)
563
+ status_icon="${YELLOW}●${RESET}"
564
+ status_label="${YELLOW}idle${RESET}"
565
+ ;;
566
+ offline|*)
567
+ status_icon="${DIM}●${RESET}"
568
+ status_label="${DIM}offline${RESET}"
569
+ ;;
570
+ esac
571
+
572
+ echo -e " ${status_icon} ${BOLD}${dev_id}${RESET} ${DIM}${dev_machine}${RESET} ${status_label} ${DIM}active:${active_jobs} queued:${queued}${RESET}"
573
+ done < <(echo "$api_response" | jq -c '.developers[]' 2>/dev/null)
574
+
575
+ # Display total online count
576
+ echo -e " ${DIM}────────────────────────────────────────────${RESET}"
577
+ echo -e " ${DIM}Total online: ${GREEN}${total_online}${RESET}${DIM} / ${dev_count}${RESET}"
578
+ else
579
+ echo -e " ${DIM}No developers connected${RESET}"
580
+ fi
581
+ else
582
+ # Dashboard not reachable — show dim message
583
+ echo ""
584
+ echo -e "${PURPLE}${BOLD} CONNECTED DEVELOPERS${RESET}"
585
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
586
+ echo -e " ${DIM}Dashboard not reachable (${DASHBOARD_URL})${RESET}"
587
+ fi
588
+ elif [[ -f "$HOME/.shipwright/team-config.json" ]] || [[ -f "$HOME/.shipwright/daemon-state.json" ]]; then
589
+ # If we have shipwright config but curl/jq missing, show info
590
+ echo ""
591
+ echo -e "${PURPLE}${BOLD} CONNECTED DEVELOPERS${RESET}"
592
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
593
+ echo -e " ${DIM}curl or jq not available to check dashboard${RESET}"
594
+ fi
595
+
596
+ # ─── Footer ──────────────────────────────────────────────────────────────────
597
+
598
+ echo ""
599
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
600
+ if $HAS_CLAUDE_WINDOWS || $HAS_TEAMS || $HAS_TASKS || $HAS_DAEMON || ${HAS_HEARTBEATS:-false}; then
601
+ echo -e " ${DIM}Clean up:${RESET} ${CYAN}shipwright cleanup${RESET} ${DIM}|${RESET} ${DIM}New session:${RESET} ${CYAN}shipwright session <name>${RESET}"
602
+ else
603
+ echo -e " ${DIM}No active teams. Start one:${RESET} ${CYAN}shipwright session <name>${RESET}"
604
+ fi
605
+ echo ""
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env bash
2
2
  # ╔═══════════════════════════════════════════════════════════════════════════╗
3
- # ║ cct-templates.sh — Browse and inspect team templates ║
3
+ # ║ sw-templates.sh — Browse and inspect team templates ║
4
4
  # ║ ║
5
5
  # ║ Templates define reusable agent team configurations (roles, layout, ║
6
6
  # ║ focus areas) that shipwright session --template can use to scaffold teams. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
+ VERSION="1.9.0"
8
9
  set -euo pipefail
10
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
9
11
 
10
12
  # ─── Colors ──────────────────────────────────────────────────────────────────
11
13
  CYAN='\033[38;2;0;212;255m'
@@ -18,6 +20,9 @@ DIM='\033[2m'
18
20
  BOLD='\033[1m'
19
21
  RESET='\033[0m'
20
22
 
23
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
24
+ # shellcheck source=lib/compat.sh
25
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
21
26
  info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
22
27
  success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
23
28
  warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
@@ -27,7 +32,7 @@ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
27
32
 
28
33
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
34
  REPO_TEMPLATES_DIR="$(cd "$SCRIPT_DIR/../tmux/templates" 2>/dev/null && pwd)" || REPO_TEMPLATES_DIR=""
30
- USER_TEMPLATES_DIR="${HOME}/.claude-teams/templates"
35
+ USER_TEMPLATES_DIR="${HOME}/.shipwright/templates"
31
36
 
32
37
  # Find all template directories (user dir takes priority)
33
38
  find_template_dirs() {
@@ -203,11 +208,11 @@ show_help() {
203
208
  echo -e " ${CYAN}shipwright templates${RESET} show <name> Show template details"
204
209
  echo ""
205
210
  echo -e " ${BOLD}TEMPLATE LOCATIONS${RESET}"
206
- echo -e " ${DIM}~/.claude-teams/templates/${RESET} Custom templates ${DIM}(takes priority)${RESET}"
211
+ echo -e " ${DIM}~/.shipwright/templates/${RESET} Custom templates ${DIM}(takes priority)${RESET}"
207
212
  [[ -n "$REPO_TEMPLATES_DIR" ]] && echo -e " ${DIM}${REPO_TEMPLATES_DIR}/${RESET} Built-in templates"
208
213
  echo ""
209
214
  echo -e " ${BOLD}CREATING TEMPLATES${RESET}"
210
- echo -e " Drop a JSON file in ${DIM}~/.claude-teams/templates/${RESET}:"
215
+ echo -e " Drop a JSON file in ${DIM}~/.shipwright/templates/${RESET}:"
211
216
  echo ""
212
217
  echo -e " ${DIM}{"
213
218
  echo -e " \"name\": \"my-template\","