shipwright-cli 2.4.0 → 3.1.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 (169) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +248 -94
  3. package/completions/shipwright.bash +68 -19
  4. package/completions/shipwright.fish +310 -42
  5. package/config/decision-tiers.json +55 -0
  6. package/config/defaults.json +111 -0
  7. package/config/event-schema.json +218 -0
  8. package/config/policy.json +21 -18
  9. package/dashboard/coverage/coverage-summary.json +14 -0
  10. package/dashboard/public/index.html +1 -1
  11. package/dashboard/server.ts +306 -17
  12. package/dashboard/src/components/charts/bar.test.ts +79 -0
  13. package/dashboard/src/components/charts/donut.test.ts +68 -0
  14. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  15. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  16. package/dashboard/src/core/api.test.ts +309 -0
  17. package/dashboard/src/core/helpers.test.ts +301 -0
  18. package/dashboard/src/core/router.test.ts +307 -0
  19. package/dashboard/src/core/router.ts +7 -0
  20. package/dashboard/src/core/sse.test.ts +144 -0
  21. package/dashboard/src/views/metrics.test.ts +186 -0
  22. package/dashboard/src/views/overview.test.ts +173 -0
  23. package/dashboard/src/views/pipelines.test.ts +183 -0
  24. package/dashboard/src/views/team.test.ts +253 -0
  25. package/dashboard/vitest.config.ts +14 -5
  26. package/docs/TIPS.md +1 -1
  27. package/docs/patterns/README.md +1 -1
  28. package/package.json +7 -9
  29. package/scripts/adapters/docker-deploy.sh +1 -1
  30. package/scripts/adapters/tmux-adapter.sh +11 -1
  31. package/scripts/adapters/wezterm-adapter.sh +1 -1
  32. package/scripts/check-version-consistency.sh +1 -1
  33. package/scripts/lib/architecture.sh +127 -0
  34. package/scripts/lib/bootstrap.sh +75 -0
  35. package/scripts/lib/compat.sh +89 -6
  36. package/scripts/lib/config.sh +91 -0
  37. package/scripts/lib/daemon-adaptive.sh +3 -3
  38. package/scripts/lib/daemon-dispatch.sh +63 -17
  39. package/scripts/lib/daemon-failure.sh +0 -0
  40. package/scripts/lib/daemon-health.sh +1 -1
  41. package/scripts/lib/daemon-patrol.sh +64 -17
  42. package/scripts/lib/daemon-poll.sh +54 -25
  43. package/scripts/lib/daemon-state.sh +125 -23
  44. package/scripts/lib/daemon-triage.sh +31 -9
  45. package/scripts/lib/decide-autonomy.sh +295 -0
  46. package/scripts/lib/decide-scoring.sh +228 -0
  47. package/scripts/lib/decide-signals.sh +462 -0
  48. package/scripts/lib/fleet-failover.sh +63 -0
  49. package/scripts/lib/helpers.sh +29 -6
  50. package/scripts/lib/pipeline-detection.sh +2 -2
  51. package/scripts/lib/pipeline-github.sh +9 -9
  52. package/scripts/lib/pipeline-intelligence.sh +105 -38
  53. package/scripts/lib/pipeline-quality-checks.sh +17 -16
  54. package/scripts/lib/pipeline-quality.sh +1 -1
  55. package/scripts/lib/pipeline-stages.sh +440 -59
  56. package/scripts/lib/pipeline-state.sh +54 -4
  57. package/scripts/lib/policy.sh +0 -0
  58. package/scripts/lib/test-helpers.sh +247 -0
  59. package/scripts/postinstall.mjs +78 -12
  60. package/scripts/signals/example-collector.sh +36 -0
  61. package/scripts/sw +17 -7
  62. package/scripts/sw-activity.sh +1 -11
  63. package/scripts/sw-adaptive.sh +109 -85
  64. package/scripts/sw-adversarial.sh +4 -14
  65. package/scripts/sw-architecture-enforcer.sh +1 -11
  66. package/scripts/sw-auth.sh +8 -17
  67. package/scripts/sw-autonomous.sh +111 -49
  68. package/scripts/sw-changelog.sh +1 -11
  69. package/scripts/sw-checkpoint.sh +144 -20
  70. package/scripts/sw-ci.sh +2 -12
  71. package/scripts/sw-cleanup.sh +13 -17
  72. package/scripts/sw-code-review.sh +16 -36
  73. package/scripts/sw-connect.sh +5 -12
  74. package/scripts/sw-context.sh +9 -26
  75. package/scripts/sw-cost.sh +17 -18
  76. package/scripts/sw-daemon.sh +76 -71
  77. package/scripts/sw-dashboard.sh +57 -17
  78. package/scripts/sw-db.sh +524 -26
  79. package/scripts/sw-decide.sh +685 -0
  80. package/scripts/sw-decompose.sh +1 -11
  81. package/scripts/sw-deps.sh +15 -25
  82. package/scripts/sw-developer-simulation.sh +1 -11
  83. package/scripts/sw-discovery.sh +138 -30
  84. package/scripts/sw-doc-fleet.sh +7 -17
  85. package/scripts/sw-docs-agent.sh +6 -16
  86. package/scripts/sw-docs.sh +4 -12
  87. package/scripts/sw-doctor.sh +134 -43
  88. package/scripts/sw-dora.sh +11 -19
  89. package/scripts/sw-durable.sh +35 -52
  90. package/scripts/sw-e2e-orchestrator.sh +11 -27
  91. package/scripts/sw-eventbus.sh +115 -115
  92. package/scripts/sw-evidence.sh +114 -30
  93. package/scripts/sw-feedback.sh +3 -13
  94. package/scripts/sw-fix.sh +2 -20
  95. package/scripts/sw-fleet-discover.sh +1 -11
  96. package/scripts/sw-fleet-viz.sh +10 -18
  97. package/scripts/sw-fleet.sh +13 -17
  98. package/scripts/sw-github-app.sh +6 -16
  99. package/scripts/sw-github-checks.sh +1 -11
  100. package/scripts/sw-github-deploy.sh +1 -11
  101. package/scripts/sw-github-graphql.sh +2 -12
  102. package/scripts/sw-guild.sh +1 -11
  103. package/scripts/sw-heartbeat.sh +49 -12
  104. package/scripts/sw-hygiene.sh +45 -43
  105. package/scripts/sw-incident.sh +48 -74
  106. package/scripts/sw-init.sh +35 -37
  107. package/scripts/sw-instrument.sh +1 -11
  108. package/scripts/sw-intelligence.sh +368 -53
  109. package/scripts/sw-jira.sh +5 -14
  110. package/scripts/sw-launchd.sh +2 -12
  111. package/scripts/sw-linear.sh +8 -17
  112. package/scripts/sw-logs.sh +4 -12
  113. package/scripts/sw-loop.sh +905 -104
  114. package/scripts/sw-memory.sh +263 -20
  115. package/scripts/sw-mission-control.sh +2 -12
  116. package/scripts/sw-model-router.sh +73 -34
  117. package/scripts/sw-otel.sh +15 -23
  118. package/scripts/sw-oversight.sh +1 -11
  119. package/scripts/sw-patrol-meta.sh +5 -11
  120. package/scripts/sw-pipeline-composer.sh +7 -17
  121. package/scripts/sw-pipeline-vitals.sh +1 -11
  122. package/scripts/sw-pipeline.sh +550 -122
  123. package/scripts/sw-pm.sh +2 -12
  124. package/scripts/sw-pr-lifecycle.sh +33 -28
  125. package/scripts/sw-predictive.sh +16 -22
  126. package/scripts/sw-prep.sh +6 -16
  127. package/scripts/sw-ps.sh +1 -11
  128. package/scripts/sw-public-dashboard.sh +2 -12
  129. package/scripts/sw-quality.sh +85 -14
  130. package/scripts/sw-reaper.sh +1 -11
  131. package/scripts/sw-recruit.sh +15 -25
  132. package/scripts/sw-regression.sh +11 -21
  133. package/scripts/sw-release-manager.sh +19 -28
  134. package/scripts/sw-release.sh +8 -16
  135. package/scripts/sw-remote.sh +1 -11
  136. package/scripts/sw-replay.sh +48 -44
  137. package/scripts/sw-retro.sh +70 -92
  138. package/scripts/sw-review-rerun.sh +1 -1
  139. package/scripts/sw-scale.sh +174 -41
  140. package/scripts/sw-security-audit.sh +12 -22
  141. package/scripts/sw-self-optimize.sh +239 -23
  142. package/scripts/sw-session.sh +5 -15
  143. package/scripts/sw-setup.sh +8 -18
  144. package/scripts/sw-standup.sh +5 -15
  145. package/scripts/sw-status.sh +32 -23
  146. package/scripts/sw-strategic.sh +129 -13
  147. package/scripts/sw-stream.sh +1 -11
  148. package/scripts/sw-swarm.sh +76 -36
  149. package/scripts/sw-team-stages.sh +10 -20
  150. package/scripts/sw-templates.sh +4 -14
  151. package/scripts/sw-testgen.sh +3 -13
  152. package/scripts/sw-tmux-pipeline.sh +1 -19
  153. package/scripts/sw-tmux-role-color.sh +0 -10
  154. package/scripts/sw-tmux-status.sh +3 -11
  155. package/scripts/sw-tmux.sh +2 -20
  156. package/scripts/sw-trace.sh +1 -19
  157. package/scripts/sw-tracker-github.sh +0 -10
  158. package/scripts/sw-tracker-jira.sh +1 -11
  159. package/scripts/sw-tracker-linear.sh +1 -11
  160. package/scripts/sw-tracker.sh +7 -24
  161. package/scripts/sw-triage.sh +29 -39
  162. package/scripts/sw-upgrade.sh +5 -23
  163. package/scripts/sw-ux.sh +1 -19
  164. package/scripts/sw-webhook.sh +18 -32
  165. package/scripts/sw-widgets.sh +3 -21
  166. package/scripts/sw-worktree.sh +11 -27
  167. package/scripts/update-homebrew-sha.sh +73 -0
  168. package/templates/pipelines/tdd.json +72 -0
  169. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -9,7 +9,7 @@ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
9
9
  # Allow spawning Claude CLI from within a Claude Code session (daemon, fleet, etc.)
10
10
  unset CLAUDECODE 2>/dev/null || true
11
11
 
12
- VERSION="2.4.0"
12
+ VERSION="3.1.0"
13
13
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
14
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15
15
 
@@ -20,6 +20,7 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
20
20
  # Canonical helpers (colors, output, events)
21
21
  # shellcheck source=lib/helpers.sh
22
22
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
23
+ [[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
23
24
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
24
25
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
25
26
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -29,23 +30,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
29
30
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
30
31
  now_epoch() { date +%s; }
31
32
  fi
32
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
33
- emit_event() {
34
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
35
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
36
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
37
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
38
- }
39
- fi
40
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
41
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
42
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
43
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
44
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
45
- RED="${RED:-\033[38;2;248;113;113m}"
46
- DIM="${DIM:-\033[2m}"
47
- BOLD="${BOLD:-\033[1m}"
48
- RESET="${RESET:-\033[0m}"
49
33
 
50
34
  # Policy (config/policy.json) — daemon defaults when daemon-config.json missing or silent
51
35
  [[ -f "$SCRIPT_DIR/lib/policy.sh" ]] && source "$SCRIPT_DIR/lib/policy.sh"
@@ -109,28 +93,6 @@ format_duration() {
109
93
  fi
110
94
  }
111
95
 
112
- # ─── Structured Event Log ──────────────────────────────────────────────────
113
- EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
114
-
115
- emit_event() {
116
- local event_type="$1"
117
- shift
118
- local json_fields=""
119
- for kv in "$@"; do
120
- local key="${kv%%=*}"
121
- local val="${kv#*=}"
122
- if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
123
- json_fields="${json_fields},\"${key}\":${val}"
124
- else
125
- local escaped_val
126
- escaped_val=$(printf '%s' "$val" | jq -Rs '.' 2>/dev/null || printf '"%s"' "${val//\"/\\\"}")
127
- json_fields="${json_fields},\"${key}\":${escaped_val}"
128
- fi
129
- done
130
- mkdir -p "${HOME}/.shipwright"
131
- echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
132
- }
133
-
134
96
  # ─── Event Log Rotation ─────────────────────────────────────────────────────
135
97
  rotate_event_log() {
136
98
  local max_size=$((50 * 1024 * 1024)) # 50MB
@@ -167,8 +129,8 @@ rotate_event_log() {
167
129
  daemon_github_context() {
168
130
  # Skip if no GitHub
169
131
  [[ "${NO_GITHUB:-false}" == "true" ]] && return 0
170
- type gh_repo_context &>/dev/null 2>&1 || return 0
171
- type _gh_detect_repo &>/dev/null 2>&1 || return 0
132
+ type gh_repo_context >/dev/null 2>&1 || return 0
133
+ type _gh_detect_repo >/dev/null 2>&1 || return 0
172
134
 
173
135
  _gh_detect_repo 2>/dev/null || return 0
174
136
  local owner="${GH_OWNER:-}" repo="${GH_REPO:-}"
@@ -192,8 +154,8 @@ gh_retry() {
192
154
 
193
155
  while [[ $attempt -lt $max_retries ]]; do
194
156
  attempt=$((attempt + 1))
195
- # Run the gh command; capture exit code
196
- if output=$("$@" 2>&1); then
157
+ # Run the gh command with per-call timeout; capture exit code
158
+ if output=$(_timeout "$(_config_get_int "network.gh_timeout" 30 2>/dev/null || echo 30)" "$@" 2>&1); then
197
159
  echo "$output"
198
160
  return 0
199
161
  fi
@@ -227,17 +189,18 @@ LOG_DIR=""
227
189
  WORKTREE_DIR=""
228
190
 
229
191
  # Config defaults (overridden by daemon-config.json; policy overrides when present)
230
- WATCH_LABEL="ready-to-build"
231
- POLL_INTERVAL=60
232
- if type policy_get &>/dev/null 2>&1; then
192
+ WATCH_LABEL="$(_config_get "labels.watch" "shipwright" 2>/dev/null || echo "shipwright")"
193
+ POLL_INTERVAL=$(_config_get_int "daemon.poll_interval" 60 2>/dev/null || echo 60)
194
+ if type policy_get >/dev/null 2>&1; then
233
195
  POLL_INTERVAL=$(policy_get ".daemon.poll_interval_seconds" "60")
234
196
  fi
235
- MAX_PARALLEL=2
197
+ MAX_PARALLEL=$(_config_get_int "daemon.max_parallel" 4 2>/dev/null || echo 4)
198
+ ISSUE_LIMIT=$(_config_get_int "daemon.issue_limit" 100 2>/dev/null || echo 100)
236
199
  PIPELINE_TEMPLATE="autonomous"
237
200
  SKIP_GATES=true
238
201
  MODEL="opus"
239
202
  BASE_BRANCH="main"
240
- ON_SUCCESS_REMOVE_LABEL="ready-to-build"
203
+ ON_SUCCESS_REMOVE_LABEL="shipwright"
241
204
  ON_SUCCESS_ADD_LABEL="pipeline/complete"
242
205
  ON_SUCCESS_CLOSE_ISSUE=false
243
206
  ON_FAILURE_ADD_LABEL="pipeline/failed"
@@ -257,7 +220,7 @@ REPO_FILTER=""
257
220
  # Auto-scaling defaults (policy overrides when present)
258
221
  AUTO_SCALE=false
259
222
  AUTO_SCALE_INTERVAL=5
260
- if type policy_get &>/dev/null 2>&1; then
223
+ if type policy_get >/dev/null 2>&1; then
261
224
  AUTO_SCALE_INTERVAL=$(policy_get ".daemon.auto_scale_interval_cycles" "5")
262
225
  fi
263
226
  MAX_WORKERS=8
@@ -280,7 +243,7 @@ PATROL_RETRY_THRESHOLD=2
280
243
  LAST_PATROL_EPOCH=0
281
244
 
282
245
  # Team dashboard coordination
283
- DASHBOARD_URL="${DASHBOARD_URL:-http://localhost:8767}"
246
+ DASHBOARD_URL="${DASHBOARD_URL:-$(_config_get "dashboard.url" "http://localhost:8767" 2>/dev/null || echo "http://localhost:8767")}"
284
247
 
285
248
  # Runtime
286
249
  NO_GITHUB=false
@@ -370,7 +333,7 @@ show_help() {
370
333
  echo -e " ${DIM}shipwright daemon patrol --once${RESET} # Run patrol once and exit"
371
334
  echo ""
372
335
  echo -e "${BOLD}CONFIG FILE${RESET} ${DIM}(.claude/daemon-config.json)${RESET}"
373
- echo -e " ${DIM}watch_label${RESET} GitHub label to watch for ${DIM}(default: ready-to-build)${RESET}"
336
+ echo -e " ${DIM}watch_label${RESET} GitHub label to watch for ${DIM}(default: shipwright)${RESET}"
374
337
  echo -e " ${DIM}poll_interval${RESET} Seconds between polls ${DIM}(default: 60)${RESET}"
375
338
  echo -e " ${DIM}max_parallel${RESET} Max concurrent pipeline jobs ${DIM}(default: 2)${RESET}"
376
339
  echo -e " ${DIM}pipeline_template${RESET} Pipeline template to use ${DIM}(default: autonomous)${RESET}"
@@ -411,16 +374,17 @@ load_config() {
411
374
 
412
375
  info "Loading config: ${DIM}${config_file}${RESET}"
413
376
 
414
- WATCH_LABEL=$(jq -r '.watch_label // "ready-to-build"' "$config_file")
415
- POLL_INTERVAL=$(jq -r '.poll_interval // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.poll_interval_seconds" "60" || echo "60")"'' "$config_file")
377
+ WATCH_LABEL=$(jq -r '.watch_label // "shipwright"' "$config_file")
378
+ POLL_INTERVAL=$(jq -r '.poll_interval // '"$(type policy_get >/dev/null 2>&1 && policy_get ".daemon.poll_interval_seconds" "60" || echo "60")"'' "$config_file")
416
379
  MAX_PARALLEL=$(jq -r '.max_parallel // 2' "$config_file")
380
+ ISSUE_LIMIT=$(jq -r '.issue_limit // 100' "$config_file")
417
381
  PIPELINE_TEMPLATE=$(jq -r '.pipeline_template // "autonomous"' "$config_file")
418
382
  SKIP_GATES=$(jq -r '.skip_gates // true' "$config_file")
419
383
  MODEL=$(jq -r '.model // "opus"' "$config_file")
420
384
  BASE_BRANCH=$(jq -r '.base_branch // "main"' "$config_file")
421
385
 
422
386
  # on_success settings
423
- ON_SUCCESS_REMOVE_LABEL=$(jq -r '.on_success.remove_label // "ready-to-build"' "$config_file")
387
+ ON_SUCCESS_REMOVE_LABEL=$(jq -r '.on_success.remove_label // "shipwright"' "$config_file")
424
388
  ON_SUCCESS_ADD_LABEL=$(jq -r '.on_success.add_label // "pipeline/complete"' "$config_file")
425
389
  ON_SUCCESS_CLOSE_ISSUE=$(jq -r '.on_success.close_issue // false' "$config_file")
426
390
 
@@ -472,12 +436,41 @@ load_config() {
472
436
 
473
437
  # self-optimization
474
438
  SELF_OPTIMIZE=$(jq -r '.self_optimize // false' "$config_file")
475
- OPTIMIZE_INTERVAL=$(jq -r '.optimize_interval // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.optimize_interval_cycles" "10" || echo "10")"'' "$config_file")
439
+ OPTIMIZE_INTERVAL=$(jq -r '.optimize_interval // '"$(type policy_get >/dev/null 2>&1 && policy_get ".daemon.optimize_interval_cycles" "10" || echo "10")"'' "$config_file")
476
440
 
477
- # intelligence engine settings
478
- INTELLIGENCE_ENABLED=$(jq -r '.intelligence.enabled // false' "$config_file")
441
+ # intelligence engine settings (default "auto" = enable when Claude CLI available)
442
+ INTELLIGENCE_ENABLED=$(jq -r '.intelligence.enabled // "auto"' "$config_file")
479
443
  INTELLIGENCE_CACHE_TTL=$(jq -r '.intelligence.cache_ttl_seconds // 3600' "$config_file")
480
- COMPOSER_ENABLED=$(jq -r '.intelligence.composer_enabled // false' "$config_file")
444
+ COMPOSER_ENABLED=$(jq -r '.intelligence.composer_enabled // "auto"' "$config_file")
445
+
446
+ # Auto-enable intelligence when Claude is available (unless explicitly disabled)
447
+ if [[ "${INTELLIGENCE_ENABLED}" == "false" && "${INTELLIGENCE_EXPLICIT_DISABLE:-false}" != "true" ]]; then
448
+ if command -v claude &>/dev/null; then
449
+ INTELLIGENCE_ENABLED=true
450
+ type daemon_log &>/dev/null && daemon_log INFO "Intelligence auto-enabled (Claude CLI detected). Disable with intelligence.enabled=false in daemon-config.json"
451
+ fi
452
+ elif [[ "${INTELLIGENCE_ENABLED}" == "auto" ]]; then
453
+ if command -v claude &>/dev/null; then
454
+ INTELLIGENCE_ENABLED=true
455
+ type daemon_log &>/dev/null && daemon_log INFO "Intelligence enabled (auto: Claude CLI detected)"
456
+ else
457
+ INTELLIGENCE_ENABLED=false
458
+ fi
459
+ fi
460
+
461
+ # Auto-enable composer when Claude is available (unless explicitly disabled)
462
+ if [[ "${COMPOSER_ENABLED}" == "false" && "${COMPOSER_EXPLICIT_DISABLE:-false}" != "true" ]]; then
463
+ if command -v claude &>/dev/null; then
464
+ COMPOSER_ENABLED=true
465
+ type daemon_log &>/dev/null && daemon_log INFO "Composer auto-enabled (Claude CLI detected). Disable with intelligence.composer_enabled=false in daemon-config.json"
466
+ fi
467
+ elif [[ "${COMPOSER_ENABLED}" == "auto" ]]; then
468
+ if command -v claude &>/dev/null; then
469
+ COMPOSER_ENABLED=true
470
+ else
471
+ COMPOSER_ENABLED=false
472
+ fi
473
+ fi
481
474
  OPTIMIZATION_ENABLED=$(jq -r '.intelligence.optimization_enabled // false' "$config_file")
482
475
  PREDICTION_ENABLED=$(jq -r '.intelligence.prediction_enabled // false' "$config_file")
483
476
  ANOMALY_THRESHOLD=$(jq -r '.intelligence.anomaly_threshold // 3.0' "$config_file")
@@ -491,7 +484,7 @@ load_config() {
491
484
 
492
485
  # stale state reaper: clean old worktrees, artifacts, state entries
493
486
  STALE_REAPER_ENABLED=$(jq -r '.stale_reaper // true' "$config_file")
494
- STALE_REAPER_INTERVAL=$(jq -r '.stale_reaper_interval // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.stale_reaper_interval_cycles" "10" || echo "10")"'' "$config_file")
487
+ STALE_REAPER_INTERVAL=$(jq -r '.stale_reaper_interval // '"$(type policy_get >/dev/null 2>&1 && policy_get ".daemon.stale_reaper_interval_cycles" "10" || echo "10")"'' "$config_file")
495
488
  STALE_REAPER_AGE_DAYS=$(jq -r '.stale_reaper_age_days // 7' "$config_file")
496
489
 
497
490
  # priority lane settings
@@ -508,21 +501,21 @@ load_config() {
508
501
 
509
502
  # auto-scaling
510
503
  AUTO_SCALE=$(jq -r '.auto_scale // false' "$config_file")
511
- AUTO_SCALE_INTERVAL=$(jq -r '.auto_scale_interval // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.auto_scale_interval_cycles" "5" || echo "5")"'' "$config_file")
504
+ AUTO_SCALE_INTERVAL=$(jq -r '.auto_scale_interval // '"$(type policy_get >/dev/null 2>&1 && policy_get ".daemon.auto_scale_interval_cycles" "5" || echo "5")"'' "$config_file")
512
505
  MAX_WORKERS=$(jq -r '.max_workers // 8' "$config_file")
513
506
  MIN_WORKERS=$(jq -r '.min_workers // 1' "$config_file")
514
507
  WORKER_MEM_GB=$(jq -r '.worker_mem_gb // 4' "$config_file")
515
508
  EST_COST_PER_JOB=$(jq -r '.estimated_cost_per_job_usd // 5.0' "$config_file")
516
509
 
517
510
  # heartbeat + checkpoint recovery (policy fallback when config silent)
518
- HEALTH_HEARTBEAT_TIMEOUT=$(jq -r '.health.heartbeat_timeout_s // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.health_heartbeat_timeout" "120" || echo "120")"'' "$config_file")
511
+ HEALTH_HEARTBEAT_TIMEOUT=$(jq -r '.health.heartbeat_timeout_s // '"$(type policy_get >/dev/null 2>&1 && policy_get ".daemon.health_heartbeat_timeout" "120" || echo "120")"'' "$config_file")
519
512
  CHECKPOINT_ENABLED=$(jq -r '.health.checkpoint_enabled // true' "$config_file")
520
513
 
521
514
  # progress-based health monitoring (replaces static timeouts)
522
515
  PROGRESS_MONITORING=$(jq -r '.health.progress_based // true' "$config_file")
523
516
  PROGRESS_CHECKS_BEFORE_WARN=$(jq -r '.health.stale_checks_before_warn // 20' "$config_file")
524
517
  PROGRESS_CHECKS_BEFORE_KILL=$(jq -r '.health.stale_checks_before_kill // 120' "$config_file")
525
- PROGRESS_HARD_LIMIT_S=$(jq -r '.health.hard_limit_s // 0' "$config_file") # 0 = disabled (no hard kill)
518
+ PROGRESS_HARD_LIMIT_S=$(jq -r '.health.hard_limit_s // 21600' "$config_file") # 21600s = 6h default; 0 = disabled
526
519
  NUDGE_ENABLED=$(jq -r '.health.nudge_enabled // true' "$config_file")
527
520
  NUDGE_AFTER_CHECKS=$(jq -r '.health.nudge_after_checks // 40' "$config_file")
528
521
 
@@ -603,6 +596,18 @@ cleanup_on_exit() {
603
596
  done <<< "$child_pids"
604
597
  fi
605
598
  fi
599
+
600
+ # Release claims on active issues before exit
601
+ local active_issues
602
+ active_issues=$(jq -r '.active_jobs[].issue // empty' "$STATE_FILE" 2>/dev/null || true)
603
+ if [[ -n "$active_issues" ]]; then
604
+ local machine_name
605
+ machine_name=$(jq -r '.machines[] | select(.role == "primary") | .name' "$HOME/.shipwright/machines.json" 2>/dev/null || hostname -s)
606
+ for issue_num in $active_issues; do
607
+ [[ -z "$issue_num" ]] && continue
608
+ release_claim "$issue_num" "$machine_name" 2>/dev/null || true
609
+ done
610
+ fi
606
611
  fi
607
612
 
608
613
  rm -f "$PID_FILE" "$SHUTDOWN_FLAG"
@@ -647,7 +652,7 @@ daemon_start() {
647
652
 
648
653
  # Detach mode: re-exec in a tmux session
649
654
  if [[ "$DETACH" == "true" ]]; then
650
- if ! command -v tmux &>/dev/null; then
655
+ if ! command -v tmux >/dev/null 2>&1; then
651
656
  error "tmux required for --detach mode"
652
657
  exit 1
653
658
  fi
@@ -689,7 +694,7 @@ daemon_start() {
689
694
  rm -f "$SHUTDOWN_FLAG"
690
695
 
691
696
  # Initialize SQLite database (if available)
692
- if type init_schema &>/dev/null; then
697
+ if type init_schema >/dev/null 2>&1; then
693
698
  init_schema 2>/dev/null || true
694
699
  fi
695
700
 
@@ -761,7 +766,7 @@ daemon_start() {
761
766
  if [[ -f "$STATE_FILE" ]]; then
762
767
  if ! jq '.' "$STATE_FILE" >/dev/null 2>&1; then
763
768
  daemon_log WARN "Watchdog: state file corrupt — recovering from backup"
764
- type validate_json &>/dev/null 2>&1 && validate_json "$STATE_FILE" || true
769
+ type validate_json >/dev/null 2>&1 && validate_json "$STATE_FILE" || true
765
770
  fi
766
771
  fi
767
772
  done
@@ -931,7 +936,7 @@ daemon_init() {
931
936
 
932
937
  cat > "$config_file" << 'CONFIGEOF'
933
938
  {
934
- "watch_label": "ready-to-build",
939
+ "watch_label": "shipwright",
935
940
  "poll_interval": 60,
936
941
  "max_parallel": 2,
937
942
  "pipeline_template": "autonomous",
@@ -939,7 +944,7 @@ daemon_init() {
939
944
  "model": "opus",
940
945
  "base_branch": "main",
941
946
  "on_success": {
942
- "remove_label": "ready-to-build",
947
+ "remove_label": "shipwright",
943
948
  "add_label": "pipeline/complete",
944
949
  "close_issue": false
945
950
  },
@@ -993,9 +998,9 @@ daemon_init() {
993
998
  "worker_mem_gb": 4,
994
999
  "estimated_cost_per_job_usd": 5.0,
995
1000
  "intelligence": {
996
- "enabled": true,
1001
+ "enabled": "auto",
997
1002
  "cache_ttl_seconds": 3600,
998
- "composer_enabled": true,
1003
+ "composer_enabled": "auto",
999
1004
  "optimization_enabled": true,
1000
1005
  "prediction_enabled": true,
1001
1006
  "adversarial_enabled": false,
@@ -1052,7 +1057,7 @@ daemon_metrics() {
1052
1057
  exit 1
1053
1058
  fi
1054
1059
 
1055
- if ! command -v jq &>/dev/null; then
1060
+ if ! command -v jq >/dev/null 2>&1; then
1056
1061
  error "jq is required for metrics. Install: brew install jq"
1057
1062
  exit 1
1058
1063
  fi
@@ -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="2.4.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -16,6 +16,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
16
  # Canonical helpers (colors, output, events)
17
17
  # shellcheck source=lib/helpers.sh
18
18
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
19
+ [[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
19
20
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
20
21
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
21
22
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -33,15 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
33
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
35
  }
35
36
  fi
36
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
37
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
38
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
39
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
40
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
41
- RED="${RED:-\033[38;2;248;113;113m}"
42
- DIM="${DIM:-\033[2m}"
43
- BOLD="${BOLD:-\033[1m}"
44
- RESET="${RESET:-\033[0m}"
45
37
  UNDERLINE='\033[4m'
46
38
 
47
39
  # ─── Paths ──────────────────────────────────────────────────────────────────
@@ -50,7 +42,7 @@ PID_FILE="${TEAMS_DIR}/dashboard.pid"
50
42
  LOG_DIR="${TEAMS_DIR}/logs"
51
43
  LOG_FILE="${LOG_DIR}/dashboard.log"
52
44
  EVENTS_FILE="${TEAMS_DIR}/events.jsonl"
53
- DEFAULT_PORT=8767
45
+ DEFAULT_PORT=$(_config_get_int "dashboard.port" 8767 2>/dev/null || echo 8767)
54
46
 
55
47
  # ─── Header ────────────────────────────────────────────────────────────────
56
48
  dashboard_header() {
@@ -78,6 +70,7 @@ show_help() {
78
70
  echo -e "${BOLD}OPTIONS${RESET}"
79
71
  echo -e " ${CYAN}--port${RESET} <N> Port to run on ${DIM}(default: ${DEFAULT_PORT})${RESET}"
80
72
  echo -e " ${CYAN}--foreground${RESET} Run in foreground ${DIM}(don't daemonize)${RESET}"
73
+ echo -e " ${CYAN}--no-build${RESET} Skip build check; start immediately"
81
74
  echo ""
82
75
  echo -e "${BOLD}EXAMPLES${RESET}"
83
76
  echo -e " ${DIM}shipwright dashboard${RESET} # Start in foreground"
@@ -91,13 +84,47 @@ show_help() {
91
84
 
92
85
  # ─── Prerequisite Check ────────────────────────────────────────────────────
93
86
  check_bun() {
94
- if ! command -v bun &>/dev/null; then
87
+ if ! command -v bun >/dev/null 2>&1; then
95
88
  error "Bun is required but not installed"
96
89
  info "Install Bun: ${UNDERLINE}https://bun.sh${RESET}"
97
90
  exit 1
98
91
  fi
99
92
  }
100
93
 
94
+ # ─── Build Check (before serve) ─────────────────────────────────────────────
95
+ ensure_dashboard_built() {
96
+ local server_ts="$1"
97
+ local repo_root
98
+ repo_root=$(dirname "$(dirname "$server_ts")")
99
+ local dist_dir="$repo_root/dashboard/public/dist"
100
+ local src_dir="$repo_root/dashboard/src"
101
+
102
+ # Check if dist exists and has main.js
103
+ if [[ ! -d "$dist_dir" ]] || [[ ! -f "$dist_dir/main.js" ]]; then
104
+ echo "Dashboard not built. Building..."
105
+ if command -v bun &>/dev/null; then
106
+ (cd "$repo_root" && bun run dashboard:build)
107
+ elif command -v npx &>/dev/null; then
108
+ (cd "$repo_root" && npx esbuild dashboard/src/main.ts --bundle --outdir=dashboard/public/dist --target=esnext)
109
+ else
110
+ warn "Cannot build dashboard: neither bun nor npx available"
111
+ return 1
112
+ fi
113
+ else
114
+ # Check if source is newer than build
115
+ local src_mtime
116
+ src_mtime=$(find "$src_dir" -name "*.ts" -newer "$dist_dir/main.js" 2>/dev/null | head -1)
117
+ if [[ -n "$src_mtime" ]]; then
118
+ echo "Dashboard source changed. Rebuilding..."
119
+ if command -v bun &>/dev/null; then
120
+ (cd "$repo_root" && bun run dashboard:build)
121
+ elif command -v npx &>/dev/null; then
122
+ (cd "$repo_root" && npx esbuild dashboard/src/main.ts --bundle --outdir=dashboard/public/dist --target=esnext)
123
+ fi
124
+ fi
125
+ fi
126
+ }
127
+
101
128
  # ─── Find Server ───────────────────────────────────────────────────────────
102
129
  find_server() {
103
130
  # Look for dashboard/server.ts relative to the script's repo location
@@ -161,6 +188,10 @@ dashboard_start_bg() {
161
188
  local server_ts
162
189
  server_ts=$(find_server)
163
190
 
191
+ if [[ "$NO_BUILD" != "true" ]]; then
192
+ ensure_dashboard_built "$server_ts" || exit 1
193
+ fi
194
+
164
195
  # Ensure directories exist
165
196
  mkdir -p "$LOG_DIR" "$TEAMS_DIR"
166
197
 
@@ -217,6 +248,10 @@ dashboard_start_fg() {
217
248
  local server_ts
218
249
  server_ts=$(find_server)
219
250
 
251
+ if [[ "$NO_BUILD" != "true" ]]; then
252
+ ensure_dashboard_built "$server_ts" || exit 1
253
+ fi
254
+
220
255
  mkdir -p "$TEAMS_DIR"
221
256
 
222
257
  info "Starting dashboard server on port ${CYAN}${port}${RESET} ${DIM}(foreground)${RESET}"
@@ -294,7 +329,7 @@ dashboard_status() {
294
329
 
295
330
  # Try to get port from /proc or lsof
296
331
  local port=""
297
- if command -v lsof &>/dev/null; then
332
+ if command -v lsof >/dev/null 2>&1; then
298
333
  port=$(lsof -nP -iTCP -sTCP:LISTEN -a -p "$pid" 2>/dev/null | grep -oE ':\d+' | head -1 | tr -d ':' || true)
299
334
  fi
300
335
 
@@ -309,7 +344,7 @@ dashboard_status() {
309
344
  if [[ -f "$PID_FILE" ]]; then
310
345
  local pid_mtime
311
346
  if [[ "$(uname)" == "Darwin" ]]; then
312
- pid_mtime=$(stat -f %m "$PID_FILE" 2>/dev/null || echo "0")
347
+ pid_mtime=$(file_mtime "$PID_FILE")
313
348
  else
314
349
  pid_mtime=$(stat -c %Y "$PID_FILE" 2>/dev/null || echo "0")
315
350
  fi
@@ -331,7 +366,7 @@ dashboard_status() {
331
366
  if [[ -n "$port" ]]; then
332
367
  local health
333
368
  health=$(curl -s --max-time 2 "http://localhost:${port}/api/health" 2>/dev/null || true)
334
- if [[ -n "$health" ]] && command -v jq &>/dev/null; then
369
+ if [[ -n "$health" ]] && command -v jq >/dev/null 2>&1; then
335
370
  local connections
336
371
  connections=$(echo "$health" | jq -r '.connections // empty' 2>/dev/null || true)
337
372
  if [[ -n "$connections" ]]; then
@@ -361,7 +396,7 @@ dashboard_open() {
361
396
  if is_running; then
362
397
  local pid
363
398
  pid=$(get_pid)
364
- if command -v lsof &>/dev/null; then
399
+ if command -v lsof >/dev/null 2>&1; then
365
400
  local detected_port
366
401
  detected_port=$(lsof -nP -iTCP -sTCP:LISTEN -a -p "$pid" 2>/dev/null | grep -oE ':\d+' | head -1 | tr -d ':' || true)
367
402
  if [[ -n "$detected_port" ]]; then
@@ -379,7 +414,7 @@ dashboard_open() {
379
414
 
380
415
  if open_url "$url" 2>/dev/null; then
381
416
  : # opened via compat.sh
382
- elif command -v powershell.exe &>/dev/null; then
417
+ elif command -v powershell.exe >/dev/null 2>&1; then
383
418
  powershell.exe -Command "Start-Process '$url'" 2>/dev/null
384
419
  else
385
420
  error "No browser opener found"
@@ -392,6 +427,7 @@ dashboard_open() {
392
427
  SUBCOMMAND=""
393
428
  PORT="$DEFAULT_PORT"
394
429
  FOREGROUND=false
430
+ NO_BUILD=false
395
431
 
396
432
  while [[ $# -gt 0 ]]; do
397
433
  case "$1" in
@@ -427,6 +463,10 @@ while [[ $# -gt 0 ]]; do
427
463
  FOREGROUND=true
428
464
  shift
429
465
  ;;
466
+ --no-build)
467
+ NO_BUILD=true
468
+ shift
469
+ ;;
430
470
  --version|-v)
431
471
  echo "shipwright dashboard v${VERSION}"
432
472
  exit 0