shipwright-cli 1.9.0 → 1.10.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 (53) hide show
  1. package/.claude/hooks/post-tool-use.sh +12 -5
  2. package/package.json +2 -2
  3. package/scripts/sw +9 -1
  4. package/scripts/sw-adversarial.sh +1 -1
  5. package/scripts/sw-architecture-enforcer.sh +1 -1
  6. package/scripts/sw-checkpoint.sh +79 -1
  7. package/scripts/sw-cleanup.sh +192 -7
  8. package/scripts/sw-connect.sh +1 -1
  9. package/scripts/sw-cost.sh +1 -1
  10. package/scripts/sw-daemon.sh +409 -37
  11. package/scripts/sw-dashboard.sh +1 -1
  12. package/scripts/sw-developer-simulation.sh +1 -1
  13. package/scripts/sw-docs.sh +1 -1
  14. package/scripts/sw-doctor.sh +1 -1
  15. package/scripts/sw-fix.sh +1 -1
  16. package/scripts/sw-fleet.sh +1 -1
  17. package/scripts/sw-github-checks.sh +1 -1
  18. package/scripts/sw-github-deploy.sh +1 -1
  19. package/scripts/sw-github-graphql.sh +1 -1
  20. package/scripts/sw-heartbeat.sh +1 -1
  21. package/scripts/sw-init.sh +1 -1
  22. package/scripts/sw-intelligence.sh +1 -1
  23. package/scripts/sw-jira.sh +1 -1
  24. package/scripts/sw-launchd.sh +4 -4
  25. package/scripts/sw-linear.sh +1 -1
  26. package/scripts/sw-logs.sh +1 -1
  27. package/scripts/sw-loop.sh +444 -49
  28. package/scripts/sw-memory.sh +198 -3
  29. package/scripts/sw-pipeline-composer.sh +8 -8
  30. package/scripts/sw-pipeline-vitals.sh +1096 -0
  31. package/scripts/sw-pipeline.sh +1692 -84
  32. package/scripts/sw-predictive.sh +1 -1
  33. package/scripts/sw-prep.sh +1 -1
  34. package/scripts/sw-ps.sh +4 -3
  35. package/scripts/sw-reaper.sh +5 -3
  36. package/scripts/sw-remote.sh +1 -1
  37. package/scripts/sw-self-optimize.sh +109 -8
  38. package/scripts/sw-session.sh +31 -9
  39. package/scripts/sw-setup.sh +1 -1
  40. package/scripts/sw-status.sh +192 -1
  41. package/scripts/sw-templates.sh +1 -1
  42. package/scripts/sw-tmux.sh +1 -1
  43. package/scripts/sw-tracker.sh +1 -1
  44. package/scripts/sw-upgrade.sh +1 -1
  45. package/scripts/sw-worktree.sh +1 -1
  46. package/templates/pipelines/autonomous.json +8 -1
  47. package/templates/pipelines/cost-aware.json +21 -0
  48. package/templates/pipelines/deployed.json +40 -6
  49. package/templates/pipelines/enterprise.json +16 -2
  50. package/templates/pipelines/fast.json +19 -0
  51. package/templates/pipelines/full.json +16 -2
  52. package/templates/pipelines/hotfix.json +19 -0
  53. package/templates/pipelines/standard.json +19 -0
@@ -19,11 +19,18 @@ if [[ "$tool_name" == "Bash" ]] && [[ "${exit_code:-0}" != "0" ]]; then
19
19
  # Classify error type
20
20
  error_type="unknown"
21
21
  case "$error_snippet" in
22
- *"test"*|*"FAIL"*|*"assert"*) error_type="test" ;;
23
- *"syntax"*|*"unexpected"*) error_type="syntax" ;;
24
- *"not found"*|*"No such"*) error_type="missing" ;;
25
- *"permission"*|*"denied"*) error_type="permission" ;;
26
- *"timeout"*|*"timed out"*) error_type="timeout" ;;
22
+ *"test"*|*"FAIL"*|*"assert"*|*"expect"*) error_type="test" ;;
23
+ *"syntax"*|*"unexpected"*|*"parse error"*) error_type="syntax" ;;
24
+ *"not found"*|*"No such"*|*"ENOENT"*) error_type="missing" ;;
25
+ *"permission"*|*"denied"*|*"EACCES"*) error_type="permission" ;;
26
+ *"timeout"*|*"timed out"*|*"ETIMEDOUT"*) error_type="timeout" ;;
27
+ *"injection"*|*"XSS"*|*"CSRF"*|*"CVE-"*) error_type="security" ;;
28
+ *"TypeError"*|*"ReferenceError"*|*"null"*|*"undefined is not"*) error_type="logic" ;;
29
+ *"ERESOLVE"*|*"peer dep"*|*"version"*|*"incompatible"*) error_type="dependency" ;;
30
+ *"flaky"*|*"intermittent"*|*"race condition"*) error_type="flaky" ;;
31
+ *"config"*|*"env"*|*"missing key"*|*"invalid option"*) error_type="config" ;;
32
+ *"ECONNREFUSED"*|*"503"*|*"502"*|*"rate limit"*) error_type="api" ;;
33
+ *"ENOMEM"*|*"disk full"*|*"quota"*) error_type="resource" ;;
27
34
  esac
28
35
 
29
36
  # Append to JSONL log
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipwright-cli",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Orchestrate autonomous Claude Code agent teams in tmux",
5
5
  "bin": {
6
6
  "shipwright": "./scripts/sw",
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "scripts": {
31
31
  "postinstall": "node scripts/postinstall.mjs",
32
- "test": "bash scripts/sw-pipeline-test.sh && bash scripts/sw-daemon-test.sh && bash scripts/sw-prep-test.sh && bash scripts/sw-fleet-test.sh && bash scripts/sw-fix-test.sh && bash scripts/sw-memory-test.sh && bash scripts/sw-session-test.sh && bash scripts/sw-init-test.sh && bash scripts/sw-tracker-test.sh && bash scripts/sw-heartbeat-test.sh && bash scripts/sw-remote-test.sh && bash scripts/sw-intelligence-test.sh && bash scripts/sw-pipeline-composer-test.sh && bash scripts/sw-self-optimize-test.sh && bash scripts/sw-predictive-test.sh && bash scripts/sw-frontier-test.sh && bash scripts/sw-connect-test.sh && bash scripts/sw-github-graphql-test.sh && bash scripts/sw-github-checks-test.sh && bash scripts/sw-github-deploy-test.sh && bash scripts/sw-docs-test.sh && bash scripts/sw-tmux-test.sh"
32
+ "test": "bash scripts/sw-pipeline-test.sh && bash scripts/sw-daemon-test.sh && bash scripts/sw-prep-test.sh && bash scripts/sw-fleet-test.sh && bash scripts/sw-fix-test.sh && bash scripts/sw-memory-test.sh && bash scripts/sw-session-test.sh && bash scripts/sw-init-test.sh && bash scripts/sw-tracker-test.sh && bash scripts/sw-heartbeat-test.sh && bash scripts/sw-remote-test.sh && bash scripts/sw-intelligence-test.sh && bash scripts/sw-pipeline-composer-test.sh && bash scripts/sw-self-optimize-test.sh && bash scripts/sw-predictive-test.sh && bash scripts/sw-frontier-test.sh && bash scripts/sw-connect-test.sh && bash scripts/sw-github-graphql-test.sh && bash scripts/sw-github-checks-test.sh && bash scripts/sw-github-deploy-test.sh && bash scripts/sw-docs-test.sh && bash scripts/sw-tmux-test.sh && bash scripts/sw-status-test.sh"
33
33
  },
34
34
  "keywords": [
35
35
  "claude",
package/scripts/sw CHANGED
@@ -67,7 +67,7 @@ show_help() {
67
67
  echo ""
68
68
  echo -e "${BOLD}COMMANDS${RESET}"
69
69
  echo -e " ${CYAN}session${RESET} [name] Create a new tmux window for a Claude team"
70
- echo -e " ${CYAN}status${RESET} Show dashboard of running teams and agents"
70
+ echo -e " ${CYAN}status${RESET} [--json] Show dashboard of running teams and agents"
71
71
  echo -e " ${CYAN}ps${RESET} Show running agent processes and status"
72
72
  echo -e " ${CYAN}logs${RESET} [team] [opts] View and search agent pane logs"
73
73
  echo -e " ${CYAN}templates${RESET} [list|show] Manage team composition templates"
@@ -95,6 +95,7 @@ show_help() {
95
95
  echo -e " ${CYAN}launchd${RESET} <cmd> ${BOLD}Process supervision${RESET} — auto-start daemon + dashboard on boot"
96
96
  echo -e " ${CYAN}docs${RESET} <cmd> ${BOLD}Documentation keeper${RESET} — auto-sync docs from source"
97
97
  echo -e " ${CYAN}tmux${RESET} <cmd> ${BOLD}tmux health${RESET} — doctor, install plugins, fix issues"
98
+ echo -e " ${CYAN}vitals${RESET} ${BOLD}Pipeline vitals${RESET} — real-time health scoring and dashboard"
98
99
  echo -e " ${CYAN}github${RESET} ${BOLD}GitHub context${RESET} — repo metadata, security, blame"
99
100
  echo -e " ${CYAN}checks${RESET} ${BOLD}GitHub checks${RESET} — CI check runs and status"
100
101
  echo -e " ${CYAN}deploys${RESET} ${BOLD}Deployments${RESET} — deployment history and environments"
@@ -102,6 +103,7 @@ show_help() {
102
103
  echo -e " ${CYAN}setup${RESET} ${BOLD}Guided setup${RESET} — prerequisites, init, doctor, quick start"
103
104
  echo -e " ${CYAN}help${RESET} Show this help message"
104
105
  echo -e " ${CYAN}version${RESET} Show version"
106
+ echo -e " ${CYAN}hello${RESET} Say hello world"
105
107
  echo ""
106
108
  echo -e "${BOLD}CONTINUOUS LOOP${RESET} ${DIM}(autonomous agent operation)${RESET}"
107
109
  echo -e " ${DIM}shipwright loop \"Build auth\" --test-cmd \"npm test\"${RESET}"
@@ -297,6 +299,9 @@ main() {
297
299
  architecture)
298
300
  exec "$SCRIPT_DIR/sw-architecture-enforcer.sh" "$@"
299
301
  ;;
302
+ vitals)
303
+ exec "$SCRIPT_DIR/sw-pipeline-vitals.sh" "$@"
304
+ ;;
300
305
  docs)
301
306
  exec "$SCRIPT_DIR/sw-docs.sh" "$@"
302
307
  ;;
@@ -318,6 +323,9 @@ main() {
318
323
  version|--version|-v)
319
324
  show_version
320
325
  ;;
326
+ hello)
327
+ echo "hello world"
328
+ ;;
321
329
  *)
322
330
  error "Unknown command: ${cmd}"
323
331
  echo ""
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="1.9.0"
9
+ VERSION="1.10.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="1.9.0"
9
+ VERSION="1.10.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -8,7 +8,7 @@
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
11
- VERSION="1.9.0"
11
+ VERSION="1.10.0"
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
 
14
14
  # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
@@ -328,6 +328,78 @@ cmd_clear() {
328
328
  fi
329
329
  }
330
330
 
331
+ # ─── Expire ──────────────────────────────────────────────────────────────────
332
+
333
+ cmd_expire() {
334
+ local max_hours=24
335
+
336
+ while [[ $# -gt 0 ]]; do
337
+ case "$1" in
338
+ --hours)
339
+ max_hours="${2:-24}"
340
+ shift 2
341
+ ;;
342
+ --hours=*)
343
+ max_hours="${1#--hours=}"
344
+ shift
345
+ ;;
346
+ --help|-h)
347
+ show_help
348
+ return 0
349
+ ;;
350
+ *)
351
+ error "Unknown option: $1"
352
+ return 1
353
+ ;;
354
+ esac
355
+ done
356
+
357
+ if [[ ! -d "$CHECKPOINT_DIR" ]]; then
358
+ return 0
359
+ fi
360
+
361
+ local max_secs=$((max_hours * 3600))
362
+ local now_e
363
+ now_e=$(date +%s)
364
+ local expired=0
365
+
366
+ local file
367
+ for file in "${CHECKPOINT_DIR}"/*-checkpoint.json; do
368
+ [[ -f "$file" ]] || continue
369
+
370
+ # Check created_at from checkpoint JSON
371
+ local created_at
372
+ created_at=$(jq -r '.created_at // empty' "$file" 2>/dev/null || true)
373
+
374
+ if [[ -n "$created_at" ]]; then
375
+ # Parse ISO date to epoch
376
+ local file_epoch
377
+ file_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$created_at" +%s 2>/dev/null \
378
+ || date -d "$created_at" +%s 2>/dev/null \
379
+ || echo "0")
380
+ if [[ "$file_epoch" -gt 0 && $((now_e - file_epoch)) -gt $max_secs ]]; then
381
+ local stage_name
382
+ stage_name=$(jq -r '.stage // "unknown"' "$file" 2>/dev/null || echo "unknown")
383
+ rm -f "$file"
384
+ expired=$((expired + 1))
385
+ info "Expired: ${stage_name} checkpoint (${max_hours}h+ old)"
386
+ fi
387
+ else
388
+ # Fallback: check file mtime
389
+ local mtime
390
+ mtime=$(stat -f '%m' "$file" 2>/dev/null || stat -c '%Y' "$file" 2>/dev/null || echo "0")
391
+ if [[ "$mtime" -gt 0 && $((now_e - mtime)) -gt $max_secs ]]; then
392
+ rm -f "$file"
393
+ expired=$((expired + 1))
394
+ fi
395
+ fi
396
+ done
397
+
398
+ if [[ "$expired" -gt 0 ]]; then
399
+ success "Expired ${expired} checkpoint(s) older than ${max_hours}h"
400
+ fi
401
+ }
402
+
331
403
  # ─── Help ────────────────────────────────────────────────────────────────────
332
404
 
333
405
  show_help() {
@@ -341,6 +413,7 @@ show_help() {
341
413
  echo -e " ${CYAN}restore${RESET} Restore a checkpoint (prints JSON to stdout)"
342
414
  echo -e " ${CYAN}list${RESET} Show all available checkpoints"
343
415
  echo -e " ${CYAN}clear${RESET} Remove checkpoint(s)"
416
+ echo -e " ${CYAN}expire${RESET} Remove checkpoints older than N hours"
344
417
  echo ""
345
418
  echo -e "${BOLD}SAVE OPTIONS${RESET}"
346
419
  echo -e " ${CYAN}--stage${RESET} <name> Stage name (required)"
@@ -357,6 +430,9 @@ show_help() {
357
430
  echo -e " ${CYAN}--stage${RESET} <name> Stage to clear"
358
431
  echo -e " ${CYAN}--all${RESET} Clear all checkpoints"
359
432
  echo ""
433
+ echo -e "${BOLD}EXPIRE OPTIONS${RESET}"
434
+ echo -e " ${CYAN}--hours${RESET} <n> Max age in hours (default: 24)"
435
+ echo ""
360
436
  echo -e "${BOLD}EXAMPLES${RESET}"
361
437
  echo -e " ${DIM}shipwright checkpoint save --stage build --iteration 5${RESET}"
362
438
  echo -e " ${DIM}shipwright checkpoint save --stage build --iteration 3 --tests-passing --files-modified \"src/auth.ts,src/middleware.ts\"${RESET}"
@@ -364,6 +440,7 @@ show_help() {
364
440
  echo -e " ${DIM}shipwright checkpoint list${RESET}"
365
441
  echo -e " ${DIM}shipwright checkpoint clear --stage build${RESET}"
366
442
  echo -e " ${DIM}shipwright checkpoint clear --all${RESET}"
443
+ echo -e " ${DIM}shipwright checkpoint expire --hours 48${RESET}"
367
444
  }
368
445
 
369
446
  # ─── Command Router ─────────────────────────────────────────────────────────
@@ -377,6 +454,7 @@ main() {
377
454
  restore) cmd_restore "$@" ;;
378
455
  list) cmd_list ;;
379
456
  clear) cmd_clear "$@" ;;
457
+ expire) cmd_expire "$@" ;;
380
458
  help|--help|-h) show_help ;;
381
459
  *)
382
460
  error "Unknown command: ${cmd}"
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env bash
2
2
  # ╔═══════════════════════════════════════════════════════════════════════════╗
3
- # ║ sw-cleanup.sh — Clean up orphaned Claude team sessions
3
+ # ║ sw-cleanup.sh — Clean up orphaned Claude team sessions & artifacts
4
4
  # ║ ║
5
5
  # ║ Default: dry-run (shows what would be cleaned). ║
6
6
  # ║ Use --force to actually kill sessions and remove files. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="1.9.0"
8
+ VERSION="1.10.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -31,7 +31,7 @@ for arg in "$@"; do
31
31
  case "$arg" in
32
32
  --force|-f) FORCE=true ;;
33
33
  --help|-h)
34
- echo -e "${CYAN}${BOLD}shipwright cleanup${RESET} — Clean up orphaned Claude team sessions"
34
+ echo -e "${CYAN}${BOLD}shipwright cleanup${RESET} — Clean up orphaned sessions and artifacts"
35
35
  echo ""
36
36
  echo -e "${BOLD}USAGE${RESET}"
37
37
  echo -e " shipwright cleanup ${DIM}# Dry-run: show what would be cleaned${RESET}"
@@ -53,6 +53,15 @@ TEAM_DIRS_FOUND=0
53
53
  TEAM_DIRS_REMOVED=0
54
54
  TASK_DIRS_FOUND=0
55
55
  TASK_DIRS_REMOVED=0
56
+ ARTIFACTS_FOUND=0
57
+ ARTIFACTS_REMOVED=0
58
+ CHECKPOINTS_FOUND=0
59
+ CHECKPOINTS_REMOVED=0
60
+ HEARTBEATS_FOUND=0
61
+ HEARTBEATS_REMOVED=0
62
+ BRANCHES_FOUND=0
63
+ BRANCHES_REMOVED=0
64
+ STATE_RESET=0
56
65
 
57
66
  # ─── 1. Find orphaned tmux windows ──────────────────────────────────────────
58
67
 
@@ -149,17 +158,193 @@ else
149
158
  echo -e " ${DIM}No task directories found.${RESET}"
150
159
  fi
151
160
 
161
+ # ─── 4. Pipeline Artifacts ──────────────────────────────────────────────────
162
+
163
+ echo ""
164
+ echo -e "${BOLD}Pipeline Artifacts${RESET} ${DIM}.claude/pipeline-artifacts/${RESET}"
165
+ echo -e "${DIM}────────────────────────────────────────${RESET}"
166
+
167
+ PIPELINE_ARTIFACTS=".claude/pipeline-artifacts"
168
+ if [[ -d "$PIPELINE_ARTIFACTS" ]]; then
169
+ artifact_file_count=$(find "$PIPELINE_ARTIFACTS" -type f 2>/dev/null | wc -l | tr -d ' ')
170
+ if [[ "${artifact_file_count:-0}" -gt 0 ]]; then
171
+ ARTIFACTS_FOUND=$((artifact_file_count))
172
+
173
+ # Calculate total size
174
+ artifact_size=$(du -sh "$PIPELINE_ARTIFACTS" 2>/dev/null | cut -f1 || echo "unknown")
175
+
176
+ if $FORCE; then
177
+ rm -rf "$PIPELINE_ARTIFACTS"
178
+ mkdir -p "$PIPELINE_ARTIFACTS"
179
+ ARTIFACTS_REMOVED=$((artifact_file_count))
180
+ echo -e " ${RED}✗${RESET} Cleaned ${artifact_file_count} files (${artifact_size})"
181
+ else
182
+ echo -e " ${YELLOW}○${RESET} Would clean: ${artifact_file_count} files (${artifact_size})"
183
+ fi
184
+ else
185
+ echo -e " ${DIM}No pipeline artifacts found.${RESET}"
186
+ fi
187
+ else
188
+ echo -e " ${DIM}No pipeline artifacts directory.${RESET}"
189
+ fi
190
+
191
+ # ─── 5. Checkpoints ────────────────────────────────────────────────────────
192
+
193
+ echo ""
194
+ echo -e "${BOLD}Checkpoints${RESET} ${DIM}.claude/pipeline-artifacts/checkpoints/${RESET}"
195
+ echo -e "${DIM}────────────────────────────────────────${RESET}"
196
+
197
+ CHECKPOINT_DIR=".claude/pipeline-artifacts/checkpoints"
198
+ if [[ -d "$CHECKPOINT_DIR" ]]; then
199
+ cp_file_count=0
200
+ for cp_file in "${CHECKPOINT_DIR}"/*-checkpoint.json; do
201
+ [[ -f "$cp_file" ]] || continue
202
+ cp_file_count=$((cp_file_count + 1))
203
+ done
204
+
205
+ if [[ "$cp_file_count" -gt 0 ]]; then
206
+ CHECKPOINTS_FOUND=$cp_file_count
207
+
208
+ if $FORCE; then
209
+ rm -f "${CHECKPOINT_DIR}"/*-checkpoint.json
210
+ CHECKPOINTS_REMOVED=$cp_file_count
211
+ echo -e " ${RED}✗${RESET} Removed ${cp_file_count} checkpoint(s)"
212
+ else
213
+ echo -e " ${YELLOW}○${RESET} Would remove: ${cp_file_count} checkpoint(s)"
214
+ fi
215
+ else
216
+ echo -e " ${DIM}No checkpoints found.${RESET}"
217
+ fi
218
+ else
219
+ echo -e " ${DIM}No checkpoint directory.${RESET}"
220
+ fi
221
+
222
+ # ─── 6. Pipeline State ─────────────────────────────────────────────────────
223
+
224
+ echo ""
225
+ echo -e "${BOLD}Pipeline State${RESET} ${DIM}.claude/pipeline-state.md${RESET}"
226
+ echo -e "${DIM}────────────────────────────────────────${RESET}"
227
+
228
+ PIPELINE_STATE=".claude/pipeline-state.md"
229
+ if [[ -f "$PIPELINE_STATE" ]]; then
230
+ state_status=$(sed -n 's/^status: *//p' "$PIPELINE_STATE" | head -1 || true)
231
+ state_issue=$(sed -n 's/^issue: *//p' "$PIPELINE_STATE" | head -1 || true)
232
+
233
+ case "${state_status:-}" in
234
+ complete|failed|idle|"")
235
+ if $FORCE; then
236
+ rm -f "$PIPELINE_STATE"
237
+ STATE_RESET=1
238
+ echo -e " ${RED}✗${RESET} Removed stale state (was: ${state_status:-empty}${state_issue:+, issue #$state_issue})"
239
+ else
240
+ echo -e " ${YELLOW}○${RESET} Would remove: status=${state_status:-empty}${state_issue:+, issue #$state_issue}"
241
+ fi
242
+ ;;
243
+ running|paused|interrupted)
244
+ echo -e " ${CYAN}●${RESET} Active pipeline: status=${state_status}${state_issue:+, issue #$state_issue} ${DIM}(skipping)${RESET}"
245
+ ;;
246
+ *)
247
+ echo -e " ${DIM}Unknown state: ${state_status}${RESET}"
248
+ ;;
249
+ esac
250
+ else
251
+ echo -e " ${DIM}No pipeline state file.${RESET}"
252
+ fi
253
+
254
+ # ─── 7. Stale Heartbeats ───────────────────────────────────────────────────
255
+
256
+ echo ""
257
+ echo -e "${BOLD}Heartbeats${RESET} ${DIM}~/.shipwright/heartbeats/${RESET}"
258
+ echo -e "${DIM}────────────────────────────────────────${RESET}"
259
+
260
+ HEARTBEAT_DIR="${HOME}/.shipwright/heartbeats"
261
+ if [[ -d "$HEARTBEAT_DIR" ]]; then
262
+ now_e=$(date +%s)
263
+ stale_threshold=3600 # 1 hour
264
+
265
+ while IFS= read -r hb_file; do
266
+ [[ -f "$hb_file" ]] || continue
267
+ hb_mtime=$(stat -f '%m' "$hb_file" 2>/dev/null || stat -c '%Y' "$hb_file" 2>/dev/null || echo "0")
268
+ if [[ $((now_e - hb_mtime)) -gt $stale_threshold ]]; then
269
+ HEARTBEATS_FOUND=$((HEARTBEATS_FOUND + 1))
270
+ hb_name=$(basename "$hb_file" .json)
271
+
272
+ if $FORCE; then
273
+ rm -f "$hb_file"
274
+ HEARTBEATS_REMOVED=$((HEARTBEATS_REMOVED + 1))
275
+ echo -e " ${RED}✗${RESET} Removed: ${hb_name} ${DIM}(stale >1h)${RESET}"
276
+ else
277
+ age_min=$(( (now_e - hb_mtime) / 60 ))
278
+ echo -e " ${YELLOW}○${RESET} Would remove: ${hb_name} ${DIM}(${age_min}m old)${RESET}"
279
+ fi
280
+ fi
281
+ done < <(find "$HEARTBEAT_DIR" -name '*.json' -type f 2>/dev/null)
282
+
283
+ if [[ "$HEARTBEATS_FOUND" -eq 0 ]]; then
284
+ echo -e " ${DIM}No stale heartbeats.${RESET}"
285
+ fi
286
+ else
287
+ echo -e " ${DIM}No heartbeat directory.${RESET}"
288
+ fi
289
+
290
+ # ─── 8. Orphaned pipeline/* branches ───────────────────────────────────────
291
+
292
+ echo ""
293
+ echo -e "${BOLD}Orphaned Branches${RESET} ${DIM}pipeline/* and daemon/*${RESET}"
294
+ echo -e "${DIM}────────────────────────────────────────${RESET}"
295
+
296
+ if command -v git &>/dev/null && git rev-parse --is-inside-work-tree &>/dev/null; then
297
+ # Collect active worktree paths
298
+ active_worktrees=""
299
+ while IFS= read -r wt_line; do
300
+ active_worktrees="${active_worktrees} ${wt_line}"
301
+ done < <(git worktree list --porcelain 2>/dev/null | grep '^worktree ' | sed 's/^worktree //')
302
+
303
+ while IFS= read -r branch; do
304
+ [[ -z "$branch" ]] && continue
305
+ branch="${branch## }" # trim leading spaces
306
+ # Check if this branch has an active worktree
307
+ has_worktree=false
308
+ for wt in $active_worktrees; do
309
+ if echo "$wt" | grep -q "${branch##*/}" 2>/dev/null; then
310
+ has_worktree=true
311
+ break
312
+ fi
313
+ done
314
+
315
+ if [[ "$has_worktree" == "false" ]]; then
316
+ BRANCHES_FOUND=$((BRANCHES_FOUND + 1))
317
+ if $FORCE; then
318
+ git branch -D "$branch" 2>/dev/null || true
319
+ BRANCHES_REMOVED=$((BRANCHES_REMOVED + 1))
320
+ echo -e " ${RED}✗${RESET} Deleted: ${branch}"
321
+ else
322
+ echo -e " ${YELLOW}○${RESET} Would delete: ${branch}"
323
+ fi
324
+ fi
325
+ done < <(git branch --list 'pipeline/*' --list 'daemon/*' 2>/dev/null)
326
+
327
+ if [[ "$BRANCHES_FOUND" -eq 0 ]]; then
328
+ echo -e " ${DIM}No orphaned branches.${RESET}"
329
+ fi
330
+ else
331
+ echo -e " ${DIM}Not in a git repository.${RESET}"
332
+ fi
333
+
152
334
  # ─── Summary ─────────────────────────────────────────────────────────────────
153
335
 
154
336
  echo ""
155
337
  echo -e "${DIM}────────────────────────────────────────${RESET}"
156
338
 
157
- TOTAL_FOUND=$((WINDOWS_FOUND + TEAM_DIRS_FOUND + TASK_DIRS_FOUND))
339
+ TOTAL_FOUND=$((WINDOWS_FOUND + TEAM_DIRS_FOUND + TASK_DIRS_FOUND + ARTIFACTS_FOUND + CHECKPOINTS_FOUND + HEARTBEATS_FOUND + BRANCHES_FOUND + STATE_RESET))
158
340
 
159
341
  if $FORCE; then
160
- TOTAL_CLEANED=$((WINDOWS_KILLED + TEAM_DIRS_REMOVED + TASK_DIRS_REMOVED))
342
+ TOTAL_CLEANED=$((WINDOWS_KILLED + TEAM_DIRS_REMOVED + TASK_DIRS_REMOVED + ARTIFACTS_REMOVED + CHECKPOINTS_REMOVED + HEARTBEATS_REMOVED + BRANCHES_REMOVED + STATE_RESET))
161
343
  if [[ $TOTAL_CLEANED -gt 0 ]]; then
162
- success "Cleaned ${TOTAL_CLEANED} items (${WINDOWS_KILLED} windows, ${TEAM_DIRS_REMOVED} team dirs, ${TASK_DIRS_REMOVED} task dirs)"
344
+ success "Cleaned ${TOTAL_CLEANED} items"
345
+ echo -e " ${DIM}windows: ${WINDOWS_KILLED}, teams: ${TEAM_DIRS_REMOVED}, tasks: ${TASK_DIRS_REMOVED}${RESET}"
346
+ echo -e " ${DIM}artifacts: ${ARTIFACTS_REMOVED}, checkpoints: ${CHECKPOINTS_REMOVED}, heartbeats: ${HEARTBEATS_REMOVED}${RESET}"
347
+ echo -e " ${DIM}branches: ${BRANCHES_REMOVED}, state: ${STATE_RESET}${RESET}"
163
348
  else
164
349
  success "Nothing to clean up."
165
350
  fi
@@ -168,7 +353,7 @@ else
168
353
  warn "Found ${TOTAL_FOUND} items to clean. Run with ${BOLD}--force${RESET} to remove them:"
169
354
  echo -e " ${DIM}shipwright cleanup --force${RESET}"
170
355
  else
171
- success "Everything is clean. No orphaned sessions found."
356
+ success "Everything is clean. No orphaned sessions or artifacts found."
172
357
  fi
173
358
  fi
174
359
  echo ""
@@ -8,7 +8,7 @@
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
11
- VERSION="1.9.0"
11
+ VERSION="1.10.0"
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
 
14
14
  # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="1.9.0"
9
+ VERSION="1.10.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12