shipwright-cli 2.4.0 → 3.0.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 (161) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +1 -1
  3. package/completions/shipwright.bash +3 -8
  4. package/completions/shipwright.fish +1 -1
  5. package/config/defaults.json +111 -0
  6. package/config/event-schema.json +81 -0
  7. package/config/policy.json +13 -18
  8. package/dashboard/coverage/coverage-summary.json +14 -0
  9. package/dashboard/public/index.html +1 -1
  10. package/dashboard/server.ts +306 -17
  11. package/dashboard/src/components/charts/bar.test.ts +79 -0
  12. package/dashboard/src/components/charts/donut.test.ts +68 -0
  13. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  14. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  15. package/dashboard/src/core/api.test.ts +309 -0
  16. package/dashboard/src/core/helpers.test.ts +301 -0
  17. package/dashboard/src/core/router.test.ts +307 -0
  18. package/dashboard/src/core/router.ts +7 -0
  19. package/dashboard/src/core/sse.test.ts +144 -0
  20. package/dashboard/src/views/metrics.test.ts +186 -0
  21. package/dashboard/src/views/overview.test.ts +173 -0
  22. package/dashboard/src/views/pipelines.test.ts +183 -0
  23. package/dashboard/src/views/team.test.ts +253 -0
  24. package/dashboard/vitest.config.ts +14 -5
  25. package/docs/TIPS.md +1 -1
  26. package/docs/patterns/README.md +1 -1
  27. package/package.json +5 -7
  28. package/scripts/adapters/docker-deploy.sh +1 -1
  29. package/scripts/adapters/tmux-adapter.sh +11 -1
  30. package/scripts/adapters/wezterm-adapter.sh +1 -1
  31. package/scripts/check-version-consistency.sh +1 -1
  32. package/scripts/lib/architecture.sh +126 -0
  33. package/scripts/lib/bootstrap.sh +75 -0
  34. package/scripts/lib/compat.sh +89 -6
  35. package/scripts/lib/config.sh +91 -0
  36. package/scripts/lib/daemon-adaptive.sh +3 -3
  37. package/scripts/lib/daemon-dispatch.sh +39 -16
  38. package/scripts/lib/daemon-health.sh +1 -1
  39. package/scripts/lib/daemon-patrol.sh +24 -12
  40. package/scripts/lib/daemon-poll.sh +37 -25
  41. package/scripts/lib/daemon-state.sh +115 -23
  42. package/scripts/lib/daemon-triage.sh +30 -8
  43. package/scripts/lib/fleet-failover.sh +63 -0
  44. package/scripts/lib/helpers.sh +30 -6
  45. package/scripts/lib/pipeline-detection.sh +2 -2
  46. package/scripts/lib/pipeline-github.sh +9 -9
  47. package/scripts/lib/pipeline-intelligence.sh +85 -35
  48. package/scripts/lib/pipeline-quality-checks.sh +16 -16
  49. package/scripts/lib/pipeline-quality.sh +1 -1
  50. package/scripts/lib/pipeline-stages.sh +242 -28
  51. package/scripts/lib/pipeline-state.sh +40 -4
  52. package/scripts/lib/test-helpers.sh +247 -0
  53. package/scripts/postinstall.mjs +3 -11
  54. package/scripts/sw +10 -4
  55. package/scripts/sw-activity.sh +1 -11
  56. package/scripts/sw-adaptive.sh +109 -85
  57. package/scripts/sw-adversarial.sh +4 -14
  58. package/scripts/sw-architecture-enforcer.sh +1 -11
  59. package/scripts/sw-auth.sh +8 -17
  60. package/scripts/sw-autonomous.sh +111 -49
  61. package/scripts/sw-changelog.sh +1 -11
  62. package/scripts/sw-checkpoint.sh +144 -20
  63. package/scripts/sw-ci.sh +2 -12
  64. package/scripts/sw-cleanup.sh +13 -17
  65. package/scripts/sw-code-review.sh +16 -36
  66. package/scripts/sw-connect.sh +5 -12
  67. package/scripts/sw-context.sh +9 -26
  68. package/scripts/sw-cost.sh +6 -16
  69. package/scripts/sw-daemon.sh +75 -70
  70. package/scripts/sw-dashboard.sh +57 -17
  71. package/scripts/sw-db.sh +506 -15
  72. package/scripts/sw-decompose.sh +1 -11
  73. package/scripts/sw-deps.sh +15 -25
  74. package/scripts/sw-developer-simulation.sh +1 -11
  75. package/scripts/sw-discovery.sh +112 -30
  76. package/scripts/sw-doc-fleet.sh +7 -17
  77. package/scripts/sw-docs-agent.sh +6 -16
  78. package/scripts/sw-docs.sh +4 -12
  79. package/scripts/sw-doctor.sh +134 -43
  80. package/scripts/sw-dora.sh +11 -19
  81. package/scripts/sw-durable.sh +35 -52
  82. package/scripts/sw-e2e-orchestrator.sh +11 -27
  83. package/scripts/sw-eventbus.sh +115 -115
  84. package/scripts/sw-evidence.sh +114 -30
  85. package/scripts/sw-feedback.sh +3 -13
  86. package/scripts/sw-fix.sh +2 -20
  87. package/scripts/sw-fleet-discover.sh +1 -11
  88. package/scripts/sw-fleet-viz.sh +10 -18
  89. package/scripts/sw-fleet.sh +13 -17
  90. package/scripts/sw-github-app.sh +6 -16
  91. package/scripts/sw-github-checks.sh +1 -11
  92. package/scripts/sw-github-deploy.sh +1 -11
  93. package/scripts/sw-github-graphql.sh +2 -12
  94. package/scripts/sw-guild.sh +1 -11
  95. package/scripts/sw-heartbeat.sh +49 -12
  96. package/scripts/sw-hygiene.sh +45 -43
  97. package/scripts/sw-incident.sh +48 -74
  98. package/scripts/sw-init.sh +35 -37
  99. package/scripts/sw-instrument.sh +1 -11
  100. package/scripts/sw-intelligence.sh +362 -51
  101. package/scripts/sw-jira.sh +5 -14
  102. package/scripts/sw-launchd.sh +2 -12
  103. package/scripts/sw-linear.sh +8 -17
  104. package/scripts/sw-logs.sh +4 -12
  105. package/scripts/sw-loop.sh +641 -90
  106. package/scripts/sw-memory.sh +243 -17
  107. package/scripts/sw-mission-control.sh +2 -12
  108. package/scripts/sw-model-router.sh +73 -34
  109. package/scripts/sw-otel.sh +11 -21
  110. package/scripts/sw-oversight.sh +1 -11
  111. package/scripts/sw-patrol-meta.sh +5 -11
  112. package/scripts/sw-pipeline-composer.sh +7 -17
  113. package/scripts/sw-pipeline-vitals.sh +1 -11
  114. package/scripts/sw-pipeline.sh +478 -122
  115. package/scripts/sw-pm.sh +2 -12
  116. package/scripts/sw-pr-lifecycle.sh +27 -25
  117. package/scripts/sw-predictive.sh +16 -22
  118. package/scripts/sw-prep.sh +6 -16
  119. package/scripts/sw-ps.sh +1 -11
  120. package/scripts/sw-public-dashboard.sh +2 -12
  121. package/scripts/sw-quality.sh +77 -10
  122. package/scripts/sw-reaper.sh +1 -11
  123. package/scripts/sw-recruit.sh +15 -25
  124. package/scripts/sw-regression.sh +11 -21
  125. package/scripts/sw-release-manager.sh +19 -28
  126. package/scripts/sw-release.sh +8 -16
  127. package/scripts/sw-remote.sh +1 -11
  128. package/scripts/sw-replay.sh +48 -44
  129. package/scripts/sw-retro.sh +70 -92
  130. package/scripts/sw-review-rerun.sh +1 -1
  131. package/scripts/sw-scale.sh +109 -32
  132. package/scripts/sw-security-audit.sh +12 -22
  133. package/scripts/sw-self-optimize.sh +239 -23
  134. package/scripts/sw-session.sh +3 -13
  135. package/scripts/sw-setup.sh +8 -18
  136. package/scripts/sw-standup.sh +5 -15
  137. package/scripts/sw-status.sh +32 -23
  138. package/scripts/sw-strategic.sh +129 -13
  139. package/scripts/sw-stream.sh +1 -11
  140. package/scripts/sw-swarm.sh +76 -36
  141. package/scripts/sw-team-stages.sh +10 -20
  142. package/scripts/sw-templates.sh +4 -14
  143. package/scripts/sw-testgen.sh +3 -13
  144. package/scripts/sw-tmux-pipeline.sh +1 -19
  145. package/scripts/sw-tmux-role-color.sh +0 -10
  146. package/scripts/sw-tmux-status.sh +3 -11
  147. package/scripts/sw-tmux.sh +2 -20
  148. package/scripts/sw-trace.sh +1 -19
  149. package/scripts/sw-tracker-github.sh +0 -10
  150. package/scripts/sw-tracker-jira.sh +1 -11
  151. package/scripts/sw-tracker-linear.sh +1 -11
  152. package/scripts/sw-tracker.sh +7 -24
  153. package/scripts/sw-triage.sh +24 -34
  154. package/scripts/sw-upgrade.sh +5 -23
  155. package/scripts/sw-ux.sh +1 -19
  156. package/scripts/sw-webhook.sh +18 -32
  157. package/scripts/sw-widgets.sh +3 -21
  158. package/scripts/sw-worktree.sh +11 -27
  159. package/scripts/update-homebrew-sha.sh +67 -0
  160. package/templates/pipelines/tdd.json +72 -0
  161. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -9,11 +9,14 @@
9
9
  #
10
10
  # Provides:
11
11
  # - NO_COLOR / dumb terminal / non-tty detection (auto-blanks color vars)
12
+ # - _to_lower() / _to_upper() — bash 3.2 compat (${var,,}/${var^^} require bash 4+)
13
+ # - file_mtime() — cross-platform file modification time (epoch)
12
14
  # - sed_i() — cross-platform sed in-place editing
13
15
  # - open_url() — cross-platform browser open
14
16
  # - tmp_dir() — returns best temp directory for platform
15
17
  # - is_wsl() — detect WSL environment
16
18
  # - is_macos() / is_linux() — platform checks
19
+ # - _timeout() — run command with timeout (timeout/gtimeout or no-op on macOS)
17
20
 
18
21
  # ─── NO_COLOR support (https://no-color.org/) ─────────────────────────────
19
22
  # Blanks standard color variables when:
@@ -30,6 +33,11 @@ _COMPAT_UNAME="${_COMPAT_UNAME:-$(uname -s 2>/dev/null || echo "Unknown")}"
30
33
 
31
34
  is_macos() { [[ "$_COMPAT_UNAME" == "Darwin" ]]; }
32
35
  is_linux() { [[ "$_COMPAT_UNAME" == "Linux" ]]; }
36
+
37
+ # ─── Bash 3.2 compat (macOS ships bash 3.2) ───────────────────────────────
38
+ # Case conversion: ${var,,} and ${var^^} require bash 4+. Use these instead:
39
+ _to_lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
40
+ _to_upper() { echo "$1" | tr '[:lower:]' '[:upper:]'; }
33
41
  is_wsl() { is_linux && [[ -n "${WSL_DISTRO_NAME:-}" || -f /proc/version ]] && grep -qi microsoft /proc/version 2>/dev/null; }
34
42
 
35
43
  # ─── sed -i (macOS vs GNU) ────────────────────────────────────────────────
@@ -49,14 +57,14 @@ open_url() {
49
57
  open "$url"
50
58
  elif is_wsl; then
51
59
  # WSL: use wslview (from wslu) or powershell
52
- if command -v wslview &>/dev/null; then
60
+ if command -v wslview >/dev/null 2>&1; then
53
61
  wslview "$url"
54
- elif command -v powershell.exe &>/dev/null; then
62
+ elif command -v powershell.exe >/dev/null 2>&1; then
55
63
  powershell.exe -Command "Start-Process '$url'" 2>/dev/null
56
64
  else
57
65
  return 1
58
66
  fi
59
- elif command -v xdg-open &>/dev/null; then
67
+ elif command -v xdg-open >/dev/null 2>&1; then
60
68
  xdg-open "$url"
61
69
  else
62
70
  return 1
@@ -83,7 +91,7 @@ sw_valid_error_category() {
83
91
  local category="${1:-}"
84
92
  local custom_file="$HOME/.shipwright/optimization/error-taxonomy.json"
85
93
  # Check custom taxonomy first
86
- if [[ -f "$custom_file" ]] && command -v jq &>/dev/null; then
94
+ if [[ -f "$custom_file" ]] && command -v jq >/dev/null 2>&1; then
87
95
  local custom_cats
88
96
  custom_cats=$(jq -r '.categories[]? // empty' "$custom_file" 2>/dev/null || true)
89
97
  if [[ -n "$custom_cats" ]]; then
@@ -113,7 +121,7 @@ complexity_bucket() {
113
121
  local config_file="$HOME/.shipwright/optimization/complexity-clusters.json"
114
122
  local low_boundary=3
115
123
  local high_boundary=6
116
- if [[ -f "$config_file" ]] && command -v jq &>/dev/null; then
124
+ if [[ -f "$config_file" ]] && command -v jq >/dev/null 2>&1; then
117
125
  local lb hb
118
126
  lb=$(jq -r '.low_boundary // 3' "$config_file" 2>/dev/null || echo "3")
119
127
  hb=$(jq -r '.high_boundary // 6' "$config_file" 2>/dev/null || echo "6")
@@ -156,7 +164,7 @@ detect_primary_language() {
156
164
 
157
165
  detect_test_framework() {
158
166
  local dir="${1:-.}"
159
- if [[ -f "$dir/package.json" ]] && command -v jq &>/dev/null; then
167
+ if [[ -f "$dir/package.json" ]] && command -v jq >/dev/null 2>&1; then
160
168
  local runner
161
169
  runner=$(jq -r '
162
170
  if .devDependencies.vitest then "vitest"
@@ -184,6 +192,81 @@ detect_test_framework() {
184
192
  fi
185
193
  }
186
194
 
195
+ # ─── Cross-platform file modification time (epoch) ────────────────────────
196
+ # macOS/BSD: stat -f %m; Linux: stat -c '%Y'
197
+ file_mtime() {
198
+ local file="$1"
199
+ stat -f %m "$file" 2>/dev/null || stat -c '%Y' "$file" 2>/dev/null || echo "0"
200
+ }
201
+
202
+ # ─── Timeout command (macOS may lack timeout; gtimeout from coreutils) ─────
203
+ # Usage: _timeout <seconds> <command> [args...]
204
+ _timeout() {
205
+ local secs="$1"
206
+ shift
207
+ if command -v timeout >/dev/null 2>&1; then
208
+ timeout "$secs" "$@"
209
+ elif command -v gtimeout >/dev/null 2>&1; then
210
+ gtimeout "$secs" "$@"
211
+ else
212
+ # Fallback: run without timeout (e.g. on older macOS)
213
+ "$@"
214
+ fi
215
+ }
216
+
217
+ # ─── Cross-platform date helpers (GNU date -d vs BSD date -j/-v) ──────────
218
+ # date_to_epoch: convert date string to Unix epoch
219
+ # date_days_ago: YYYY-MM-DD for N days ago
220
+ # date_add_days: YYYY-MM-DD for base_date + N days
221
+ # epoch_to_iso: convert epoch to ISO 8601
222
+ date_to_epoch() {
223
+ local datestr="$1"
224
+ local fmt=""
225
+ if [[ "$datestr" == *"T"* ]]; then
226
+ fmt="%Y-%m-%dT%H:%M:%SZ"
227
+ else
228
+ fmt="%Y-%m-%d"
229
+ fi
230
+ if date -u -d "$datestr" +%s 2>/dev/null; then
231
+ return
232
+ fi
233
+ # BSD date: -j = don't set date, -f = format
234
+ date -u -j -f "$fmt" "$datestr" +%s 2>/dev/null || echo "0"
235
+ }
236
+
237
+ date_days_ago() {
238
+ local days="$1"
239
+ if date -u -d "$days days ago" +%Y-%m-%d 2>/dev/null; then
240
+ return
241
+ fi
242
+ date -u -v-${days}d +%Y-%m-%d 2>/dev/null || echo "1970-01-01"
243
+ }
244
+
245
+ date_add_days() {
246
+ local base_date="$1"
247
+ local days="$2"
248
+ if date -u -d "${base_date} + ${days} days" +%Y-%m-%d 2>/dev/null; then
249
+ return
250
+ fi
251
+ # BSD: compute via epoch arithmetic
252
+ local base_epoch
253
+ base_epoch=$(date_to_epoch "$base_date")
254
+ if [[ -n "$base_epoch" && "$base_epoch" != "0" ]]; then
255
+ local result_epoch=$((base_epoch + (days * 86400)))
256
+ date -u -r "$result_epoch" +%Y-%m-%d 2>/dev/null || date -u -d "@$result_epoch" +%Y-%m-%d 2>/dev/null || echo "1970-01-01"
257
+ else
258
+ echo "1970-01-01"
259
+ fi
260
+ }
261
+
262
+ epoch_to_iso() {
263
+ local epoch="$1"
264
+ date -u -r "$epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
265
+ date -u -d "@$epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
266
+ python3 -c "import datetime; print(datetime.datetime.utcfromtimestamp($epoch).strftime('%Y-%m-%dT%H:%M:%SZ'))" 2>/dev/null || \
267
+ echo "1970-01-01T00:00:00Z"
268
+ }
269
+
187
270
  # ─── Cross-platform MD5 ──────────────────────────────────────────────────
188
271
  # Usage:
189
272
  # compute_md5 --string "some text" → md5 hash of string
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # config.sh — Centralized configuration reader for Shipwright
3
+ # Precedence: SHIPWRIGHT_* env var > daemon-config.json > policy.json > defaults.json
4
+ # Usage: source "$SCRIPT_DIR/lib/config.sh"
5
+ # val=$(_config_get "daemon.poll_interval")
6
+ [[ -n "${_SW_CONFIG_LOADED:-}" ]] && return 0
7
+ _SW_CONFIG_LOADED=1
8
+
9
+ _CONFIG_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ _CONFIG_REPO_DIR="$(cd "$_CONFIG_SCRIPT_DIR/../.." 2>/dev/null && pwd || echo "")"
11
+
12
+ _DEFAULTS_FILE="${_CONFIG_REPO_DIR}/config/defaults.json"
13
+ _POLICY_FILE="${_CONFIG_REPO_DIR}/config/policy.json"
14
+ _DAEMON_CONFIG_FILE=".claude/daemon-config.json"
15
+
16
+ # Resolve daemon config relative to git root or cwd
17
+ if [[ ! -f "$_DAEMON_CONFIG_FILE" ]]; then
18
+ local_root="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
19
+ _DAEMON_CONFIG_FILE="${local_root}/.claude/daemon-config.json"
20
+ fi
21
+
22
+ # _config_get "section.key" [default]
23
+ # Reads config with full precedence chain
24
+ _config_get() {
25
+ local dotpath="$1"
26
+ local fallback="${2:-}"
27
+
28
+ # 1. Check env var: daemon.poll_interval -> SHIPWRIGHT_DAEMON_POLL_INTERVAL
29
+ local env_name="SHIPWRIGHT_$(echo "$dotpath" | tr '[:lower:].' '[:upper:]_')"
30
+ local env_val="${!env_name:-}"
31
+ if [[ -n "$env_val" ]]; then
32
+ echo "$env_val"
33
+ return 0
34
+ fi
35
+
36
+ # Convert dotpath to jq path: "daemon.poll_interval" -> ".daemon.poll_interval"
37
+ local jq_path=".${dotpath}"
38
+
39
+ # 2. Check daemon-config.json
40
+ if [[ -f "$_DAEMON_CONFIG_FILE" ]]; then
41
+ local val
42
+ val=$(jq -r "${jq_path} // \"\"" "$_DAEMON_CONFIG_FILE" 2>/dev/null || echo "")
43
+ if [[ -n "$val" && "$val" != "null" ]]; then
44
+ echo "$val"
45
+ return 0
46
+ fi
47
+ fi
48
+
49
+ # 3. Check policy.json
50
+ if [[ -f "$_POLICY_FILE" ]]; then
51
+ local val
52
+ val=$(jq -r "${jq_path} // \"\"" "$_POLICY_FILE" 2>/dev/null || echo "")
53
+ if [[ -n "$val" && "$val" != "null" ]]; then
54
+ echo "$val"
55
+ return 0
56
+ fi
57
+ fi
58
+
59
+ # 4. Check defaults.json
60
+ if [[ -f "$_DEFAULTS_FILE" ]]; then
61
+ local val
62
+ val=$(jq -r "${jq_path} // \"\"" "$_DEFAULTS_FILE" 2>/dev/null || echo "")
63
+ if [[ -n "$val" && "$val" != "null" ]]; then
64
+ echo "$val"
65
+ return 0
66
+ fi
67
+ fi
68
+
69
+ # 5. Return fallback
70
+ echo "$fallback"
71
+ }
72
+
73
+ # _config_get_int "section.key" [default]
74
+ # Same as _config_get but ensures integer output
75
+ _config_get_int() {
76
+ local val
77
+ val=$(_config_get "$1" "${2:-0}")
78
+ # Strip non-numeric
79
+ echo "${val//[!0-9-]/}"
80
+ }
81
+
82
+ # _config_get_bool "section.key" [default]
83
+ # Returns 0 (true) or 1 (false) for use in conditionals
84
+ _config_get_bool() {
85
+ local val
86
+ val=$(_config_get "$1" "${2:-false}")
87
+ case "$val" in
88
+ true|1|yes|on) return 0 ;;
89
+ *) return 1 ;;
90
+ esac
91
+ }
@@ -85,9 +85,9 @@ get_adaptive_heartbeat_timeout() {
85
85
 
86
86
  # Stage-specific defaults (daemon-health.sh when sourced, else policy_get, else literal)
87
87
  local default_timeout="${HEALTH_HEARTBEAT_TIMEOUT:-120}"
88
- if type daemon_health_timeout_for_stage &>/dev/null 2>&1; then
88
+ if type daemon_health_timeout_for_stage >/dev/null 2>&1; then
89
89
  default_timeout=$(daemon_health_timeout_for_stage "$stage" "$default_timeout")
90
- elif type policy_get &>/dev/null 2>&1; then
90
+ elif type policy_get >/dev/null 2>&1; then
91
91
  local policy_stage
92
92
  policy_stage=$(policy_get ".daemon.stage_timeouts.$stage" "")
93
93
  [[ -n "$policy_stage" && "$policy_stage" =~ ^[0-9]+$ ]] && default_timeout="$policy_stage"
@@ -385,7 +385,7 @@ daemon_assess_progress() {
385
385
  ' "$progress_file" > "$tmp_progress" 2>/dev/null && mv "$tmp_progress" "$progress_file"
386
386
 
387
387
  # ── Vitals-based verdict (preferred over static thresholds) ──
388
- if type pipeline_compute_vitals &>/dev/null 2>&1 && type pipeline_health_verdict &>/dev/null 2>&1; then
388
+ if type pipeline_compute_vitals >/dev/null 2>&1 && type pipeline_health_verdict >/dev/null 2>&1; then
389
389
  # Compute vitals using the worktree's pipeline state if available
390
390
  local _worktree_state=""
391
391
  local _worktree_artifacts=""
@@ -37,6 +37,22 @@ daemon_spawn_pipeline() {
37
37
 
38
38
  daemon_log INFO "Spawning pipeline for issue #${issue_num}: ${issue_title}"
39
39
 
40
+ # ── Budget gate: hard-stop if daily budget exhausted ──
41
+ if [[ -x "${SCRIPT_DIR}/sw-cost.sh" ]]; then
42
+ local remaining
43
+ remaining=$("${SCRIPT_DIR}/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
44
+ if [[ -n "$remaining" && "$remaining" != "unlimited" ]]; then
45
+ if awk -v r="$remaining" 'BEGIN { exit !(r <= 0) }' 2>/dev/null; then
46
+ daemon_log WARN "Budget exhausted (remaining: \$${remaining}) — skipping issue #${issue_num}"
47
+ emit_event "daemon.budget_exhausted" "remaining=$remaining" "issue=$issue_num"
48
+ return 1
49
+ fi
50
+ if awk -v r="$remaining" 'BEGIN { exit !(r < 1.0) }' 2>/dev/null; then
51
+ daemon_log WARN "Budget low: \$${remaining} remaining"
52
+ fi
53
+ fi
54
+ fi
55
+
40
56
  # ── Issue decomposition (if decomposer available) ──
41
57
  local decompose_script="${SCRIPT_DIR}/sw-decompose.sh"
42
58
  if [[ -x "$decompose_script" && "$NO_GITHUB" != "true" ]]; then
@@ -45,7 +61,7 @@ daemon_spawn_pipeline() {
45
61
  if [[ "$decompose_result" == *"decomposed"* ]]; then
46
62
  daemon_log INFO "Issue #${issue_num} decomposed into subtasks — skipping pipeline"
47
63
  # Remove the shipwright label so decomposed parent doesn't re-queue
48
- gh issue edit "$issue_num" --remove-label "shipwright" 2>/dev/null || true
64
+ _timeout 30 gh issue edit "$issue_num" --remove-label "shipwright" 2>/dev/null || true
49
65
  return 0
50
66
  fi
51
67
  fi
@@ -54,14 +70,14 @@ daemon_spawn_pipeline() {
54
70
  local issue_goal="$issue_title"
55
71
  if [[ "$NO_GITHUB" != "true" ]]; then
56
72
  local issue_body_first
57
- issue_body_first=$(gh issue view "$issue_num" --json body --jq '.body' 2>/dev/null | head -3 | tr '\n' ' ' | cut -c1-200 || true)
73
+ issue_body_first=$(_timeout 30 gh issue view "$issue_num" --json body --jq '.body' 2>/dev/null | head -3 | tr '\n' ' ' | cut -c1-200 || true)
58
74
  if [[ -n "$issue_body_first" ]]; then
59
75
  issue_goal="${issue_title}: ${issue_body_first}"
60
76
  fi
61
77
  fi
62
78
 
63
79
  # ── Predictive risk assessment (if enabled) ──
64
- if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type predict_pipeline_risk &>/dev/null 2>&1; then
80
+ if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type predict_pipeline_risk >/dev/null 2>&1; then
65
81
  local issue_json_for_pred=""
66
82
  if [[ "$NO_GITHUB" != "true" ]]; then
67
83
  issue_json_for_pred=$(gh issue view "$issue_num" --json number,title,body,labels 2>/dev/null || echo "")
@@ -214,7 +230,7 @@ daemon_track_job() {
214
230
  local issue_num="$1" pid="$2" worktree="$3" title="${4:-}" repo="${5:-}" goal="${6:-}"
215
231
 
216
232
  # Write to SQLite (non-blocking, best-effort)
217
- if type db_save_job &>/dev/null; then
233
+ if type db_save_job >/dev/null 2>&1; then
218
234
  local job_id="daemon-${issue_num}-$(now_epoch)"
219
235
  db_save_job "$job_id" "$issue_num" "$title" "$pid" "$worktree" "" "${PIPELINE_TEMPLATE:-autonomous}" "$goal" 2>/dev/null || true
220
236
  fi
@@ -309,7 +325,7 @@ daemon_reap_completed() {
309
325
  emit_event "daemon.reap" "issue=$issue_num" "result=$result_str" "duration_s=$dur_s"
310
326
 
311
327
  # Update SQLite (mark job complete/failed)
312
- if type db_complete_job &>/dev/null && type db_fail_job &>/dev/null; then
328
+ if type db_complete_job >/dev/null 2>&1 && type db_fail_job >/dev/null 2>&1; then
313
329
  local _db_job_id="daemon-${issue_num}-${start_epoch}"
314
330
  if [[ "$exit_code" -eq 0 ]]; then
315
331
  db_complete_job "$_db_job_id" "$result_str" 2>/dev/null || true
@@ -343,7 +359,7 @@ daemon_reap_completed() {
343
359
  --method PATCH \
344
360
  --field status=completed \
345
361
  --field conclusion=cancelled \
346
- --silent 2>/dev/null || true
362
+ --silent --timeout 30 2>/dev/null || true
347
363
  fi
348
364
  fi
349
365
  done < <(jq -r 'keys[]' "$check_ids_file" 2>/dev/null || true)
@@ -352,13 +368,18 @@ daemon_reap_completed() {
352
368
  fi
353
369
 
354
370
  # Finalize memory (capture failure patterns for future runs)
355
- if type memory_finalize_pipeline &>/dev/null 2>&1; then
371
+ if type memory_finalize_pipeline >/dev/null 2>&1; then
356
372
  local _job_state _job_artifacts
357
373
  _job_state="${worktree:-.}/.claude/pipeline-state.md"
358
374
  _job_artifacts="${worktree:-.}/.claude/pipeline-artifacts"
359
375
  memory_finalize_pipeline "$_job_state" "$_job_artifacts" 2>/dev/null || true
360
376
  fi
361
377
 
378
+ # Trigger learning after pipeline reap
379
+ if type optimize_full_analysis &>/dev/null; then
380
+ optimize_full_analysis &>/dev/null &
381
+ fi
382
+
362
383
  # Clean up progress tracking for this job
363
384
  daemon_clear_progress "$issue_num"
364
385
 
@@ -400,13 +421,15 @@ daemon_reap_completed() {
400
421
  local current_active
401
422
  current_active=$(locked_get_active_count)
402
423
  if [[ "$current_active" -lt "$MAX_PARALLEL" ]]; then
403
- local next_issue
404
- next_issue=$(dequeue_next)
405
- if [[ -n "$next_issue" ]]; then
424
+ local next_issue_key
425
+ next_issue_key=$(dequeue_next)
426
+ if [[ -n "$next_issue_key" ]]; then
427
+ local next_issue_num="$next_issue_key" next_repo=""
428
+ [[ "$next_issue_key" == *:* ]] && next_repo="${next_issue_key%%:*}" && next_issue_num="${next_issue_key##*:}"
406
429
  local next_title
407
- next_title=$(jq -r --arg n "$next_issue" '.titles[$n] // ""' "$STATE_FILE" 2>/dev/null || true)
408
- daemon_log INFO "Dequeuing issue #${next_issue}: ${next_title}"
409
- daemon_spawn_pipeline "$next_issue" "$next_title"
430
+ next_title=$(jq -r --arg n "$next_issue_key" '.titles[$n] // ""' "$STATE_FILE" 2>/dev/null || true)
431
+ daemon_log INFO "Dequeuing issue #${next_issue_num}${next_repo:+, repo=${next_repo}}: ${next_title}"
432
+ daemon_spawn_pipeline "$next_issue_num" "$next_title" "$next_repo"
410
433
  fi
411
434
  fi
412
435
  done <<< "$jobs"
@@ -453,12 +476,12 @@ daemon_on_success() {
453
476
 
454
477
  if [[ "$NO_GITHUB" != "true" ]]; then
455
478
  # Remove watch label, add success label
456
- gh issue edit "$issue_num" \
479
+ _timeout 30 gh issue edit "$issue_num" \
457
480
  --remove-label "$ON_SUCCESS_REMOVE_LABEL" \
458
481
  --add-label "$ON_SUCCESS_ADD_LABEL" 2>/dev/null || true
459
482
 
460
483
  # Comment on issue
461
- gh issue comment "$issue_num" --body "## ✅ Pipeline Complete
484
+ _timeout 30 gh issue comment "$issue_num" --body "## ✅ Pipeline Complete
462
485
 
463
486
  The autonomous pipeline finished successfully.
464
487
 
@@ -471,7 +494,7 @@ Check the associated PR for the implementation." 2>/dev/null || true
471
494
 
472
495
  # Optionally close the issue
473
496
  if [[ "$ON_SUCCESS_CLOSE_ISSUE" == "true" ]]; then
474
- gh issue close "$issue_num" 2>/dev/null || true
497
+ _timeout 30 gh issue close "$issue_num" 2>/dev/null || true
475
498
  fi
476
499
  fi
477
500
 
@@ -11,7 +11,7 @@ _DAEMON_HEALTH_LOADED=1
11
11
  daemon_health_timeout_for_stage() {
12
12
  local stage="${1:-unknown}"
13
13
  local fallback="${2:-120}"
14
- if type policy_get &>/dev/null 2>&1; then
14
+ if type policy_get >/dev/null 2>&1; then
15
15
  local policy_val
16
16
  policy_val=$(policy_get ".daemon.stage_timeouts.$stage" "")
17
17
  if [[ -n "$policy_val" && "$policy_val" =~ ^[0-9]+$ ]]; then
@@ -45,10 +45,22 @@ daemon_patrol() {
45
45
  local findings=0
46
46
 
47
47
  # npm audit
48
- if [[ -f "package.json" ]] && command -v npm &>/dev/null; then
48
+ if [[ -f "package.json" ]] && command -v npm >/dev/null 2>&1; then
49
49
  local audit_json
50
- audit_json=$(npm audit --json 2>/dev/null || true)
51
- if [[ -n "$audit_json" ]]; then
50
+ audit_json=$(npm audit --json 2>/dev/null || echo '{}')
51
+ local audit_version
52
+ audit_version=$(echo "$audit_json" | jq -r '.auditReportVersion // 1')
53
+
54
+ local vuln_list
55
+ if [[ "$audit_version" == "2" ]]; then
56
+ # npm 7+ format: .vulnerabilities is an object keyed by package name
57
+ vuln_list=$(echo "$audit_json" | jq -c '[.vulnerabilities | to_entries[] | .value | {name: .name, severity: .severity, url: (.via[0].url // "N/A"), title: (.via[0].title // .name)}]' 2>/dev/null || echo '[]')
58
+ else
59
+ # npm 6 format: .advisories is an object keyed by advisory ID
60
+ vuln_list=$(echo "$audit_json" | jq -c '[.advisories | to_entries[] | .value | {name: .module_name, severity: .severity, url: .url, title: .title}]' 2>/dev/null || echo '[]')
61
+ fi
62
+
63
+ if [[ -n "$vuln_list" && "$vuln_list" != "[]" ]]; then
52
64
  while IFS= read -r vuln; do
53
65
  local severity name advisory_url title
54
66
  severity=$(echo "$vuln" | jq -r '.severity // "unknown"')
@@ -90,12 +102,12 @@ Auto-detected by \`shipwright daemon patrol\`." \
90
102
  else
91
103
  echo -e " ${RED}●${RESET} ${BOLD}${severity}${RESET}: ${title} in ${CYAN}${name}${RESET}"
92
104
  fi
93
- done < <(echo "$audit_json" | jq -c '.vulnerabilities | to_entries[] | .value' 2>/dev/null)
105
+ done < <(echo "$vuln_list" | jq -c '.[]' 2>/dev/null)
94
106
  fi
95
107
  fi
96
108
 
97
109
  # pip-audit
98
- if [[ -f "requirements.txt" ]] && command -v pip-audit &>/dev/null; then
110
+ if [[ -f "requirements.txt" ]] && command -v pip-audit >/dev/null 2>&1; then
99
111
  local pip_json
100
112
  pip_json=$(pip-audit --format=json 2>/dev/null || true)
101
113
  if [[ -n "$pip_json" ]]; then
@@ -106,7 +118,7 @@ Auto-detected by \`shipwright daemon patrol\`." \
106
118
  fi
107
119
 
108
120
  # cargo audit
109
- if [[ -f "Cargo.toml" ]] && command -v cargo-audit &>/dev/null; then
121
+ if [[ -f "Cargo.toml" ]] && command -v cargo-audit >/dev/null 2>&1; then
110
122
  local cargo_json
111
123
  cargo_json=$(cargo audit --json 2>/dev/null || true)
112
124
  if [[ -n "$cargo_json" ]]; then
@@ -117,8 +129,8 @@ Auto-detected by \`shipwright daemon patrol\`." \
117
129
  fi
118
130
 
119
131
  # Enrich with GitHub security alerts
120
- if type gh_security_alerts &>/dev/null 2>&1 && [[ "${NO_GITHUB:-false}" != "true" ]]; then
121
- if type _gh_detect_repo &>/dev/null 2>&1; then
132
+ if type gh_security_alerts >/dev/null 2>&1 && [[ "${NO_GITHUB:-false}" != "true" ]]; then
133
+ if type _gh_detect_repo >/dev/null 2>&1; then
122
134
  _gh_detect_repo 2>/dev/null || true
123
135
  fi
124
136
  local gh_owner="${GH_OWNER:-}" gh_repo="${GH_REPO:-}"
@@ -135,7 +147,7 @@ Auto-detected by \`shipwright daemon patrol\`." \
135
147
  fi
136
148
 
137
149
  # Enrich with GitHub Dependabot alerts
138
- if type gh_dependabot_alerts &>/dev/null 2>&1 && [[ "${NO_GITHUB:-false}" != "true" ]]; then
150
+ if type gh_dependabot_alerts >/dev/null 2>&1 && [[ "${NO_GITHUB:-false}" != "true" ]]; then
139
151
  local gh_owner="${GH_OWNER:-}" gh_repo="${GH_REPO:-}"
140
152
  if [[ -n "$gh_owner" && -n "$gh_repo" ]]; then
141
153
  local dep_alerts
@@ -162,7 +174,7 @@ Auto-detected by \`shipwright daemon patrol\`." \
162
174
  daemon_log INFO "Patrol: checking for stale dependencies"
163
175
  local findings=0
164
176
 
165
- if [[ -f "package.json" ]] && command -v npm &>/dev/null; then
177
+ if [[ -f "package.json" ]] && command -v npm >/dev/null 2>&1; then
166
178
  local outdated_json
167
179
  outdated_json=$(npm outdated --json 2>/dev/null || true)
168
180
  if [[ -n "$outdated_json" ]] && [[ "$outdated_json" != "{}" ]]; then
@@ -534,7 +546,7 @@ Auto-detected by \`shipwright daemon patrol\` on $(now_iso)." \
534
546
  failures_json=$(
535
547
  (
536
548
  source "$memory_script" > /dev/null 2>&1 || true
537
- if command -v memory_get_actionable_failures &>/dev/null; then
549
+ if command -v memory_get_actionable_failures >/dev/null 2>&1; then
538
550
  memory_get_actionable_failures "$PATROL_FAILURES_THRESHOLD"
539
551
  else
540
552
  echo "[]"
@@ -1046,7 +1058,7 @@ Auto-detected by \`shipwright daemon patrol\` on $(now_iso)." \
1046
1058
  echo ""
1047
1059
 
1048
1060
  # ── Stage 2: AI-Powered Confirmation (if enabled) ──
1049
- if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type patrol_ai_analyze &>/dev/null 2>&1; then
1061
+ if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type patrol_ai_analyze >/dev/null 2>&1; then
1050
1062
  daemon_log INFO "Intelligence: using AI patrol analysis (prediction enabled)"
1051
1063
  echo -e " ${BOLD}AI Deep Analysis${RESET}"
1052
1064
  # Sample recent source files for AI analysis