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
@@ -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.0.0"
10
10
  SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
11
11
  REPO_DIR="${REPO_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
12
12
 
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
35
  }
36
36
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
37
  # ─── Artifacts Directory ─────────────────────────────────────────────────────
48
38
  ARTIFACTS_DIR="${REPO_DIR}/.claude/pipeline-artifacts"
49
39
 
@@ -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.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
35
  }
36
36
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
37
  # ─── Artifacts Directory ─────────────────────────────────────────────────────
48
38
  ARTIFACTS_DIR="${REPO_DIR}/.claude/pipeline-artifacts"
49
39
 
@@ -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.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
35
  }
36
36
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
37
  # ─── Structured Event Log ──────────────────────────────────────────────────
48
38
  EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
49
39
 
@@ -87,7 +77,7 @@ _gh_cache_get() {
87
77
  # Check file age
88
78
  local file_epoch now
89
79
  if [[ "$(uname)" == "Darwin" ]]; then
90
- file_epoch=$(stat -f '%m' "$cache_file" 2>/dev/null || echo "0")
80
+ file_epoch=$(file_mtime "$cache_file")
91
81
  else
92
82
  file_epoch=$(stat -c '%Y' "$cache_file" 2>/dev/null || echo "0")
93
83
  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.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -33,16 +33,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
33
33
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
34
  }
35
35
  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
-
46
36
  # ─── Guild Storage Paths ───────────────────────────────────────────────────
47
37
  GUILD_ROOT="${HOME}/.shipwright/guilds"
48
38
  GUILD_CONFIG="${GUILD_ROOT}/config.json"
@@ -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.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -16,6 +16,10 @@ 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
+
20
+ # SQLite persistence (DB as primary read path)
21
+ # shellcheck source=sw-db.sh
22
+ [[ -f "$SCRIPT_DIR/sw-db.sh" ]] && source "$SCRIPT_DIR/sw-db.sh"
19
23
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
20
24
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
21
25
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -33,16 +37,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
33
37
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
38
  }
35
39
  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
-
46
40
  # ─── Constants ──────────────────────────────────────────────────────────────
47
41
  HEARTBEAT_DIR="$HOME/.shipwright/heartbeats"
48
42
 
@@ -167,6 +161,11 @@ cmd_write() {
167
161
 
168
162
  # Atomic write
169
163
  mv "$tmp_file" "${HEARTBEAT_DIR}/${job_id}.json"
164
+
165
+ # Dual-write to DB when available
166
+ if type db_record_heartbeat >/dev/null 2>&1 && db_available 2>/dev/null; then
167
+ db_record_heartbeat "$job_id" "$pid" "${issue:-0}" "$stage" "${iteration:-0}" "$activity" "$memory_mb" 2>/dev/null || true
168
+ fi
170
169
  }
171
170
 
172
171
  # ─── Check Heartbeat ───────────────────────────────────────────────────────
@@ -190,6 +189,30 @@ cmd_check() {
190
189
  esac
191
190
  done
192
191
 
192
+ # Try DB first when available
193
+ if type db_list_heartbeats >/dev/null 2>&1 && db_available 2>/dev/null; then
194
+ local hb_json
195
+ hb_json=$(db_list_heartbeats 2>/dev/null | jq -r --arg id "$job_id" '.[] | select(.job_id == $id) | .updated_at' 2>/dev/null || true)
196
+ if [[ -n "$hb_json" ]]; then
197
+ local updated_at="$hb_json"
198
+ local hb_epoch now_epoch age_secs
199
+ if TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$updated_at" +%s >/dev/null 2>&1; then
200
+ hb_epoch="$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$updated_at" +%s 2>/dev/null)"
201
+ else
202
+ hb_epoch="$(date -d "$updated_at" +%s 2>/dev/null || echo 0)"
203
+ fi
204
+ now_epoch="$(date +%s)"
205
+ age_secs=$((now_epoch - hb_epoch))
206
+ if [[ "$age_secs" -le "$timeout" ]]; then
207
+ success "Job ${job_id} alive (${age_secs}s ago)"
208
+ return 0
209
+ else
210
+ warn "Job ${job_id} stale (${age_secs}s ago, timeout: ${timeout}s)"
211
+ return 1
212
+ fi
213
+ fi
214
+ fi
215
+
193
216
  local hb_file="${HEARTBEAT_DIR}/${job_id}.json"
194
217
 
195
218
  if [[ ! -f "$hb_file" ]]; then
@@ -209,7 +232,7 @@ cmd_check() {
209
232
  local hb_epoch now_epoch age_secs
210
233
 
211
234
  # macOS date -j -f vs GNU date -d (TZ=UTC since timestamps are UTC)
212
- if TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$updated_at" +%s &>/dev/null; then
235
+ if TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$updated_at" +%s >/dev/null 2>&1; then
213
236
  hb_epoch="$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$updated_at" +%s 2>/dev/null)"
214
237
  else
215
238
  hb_epoch="$(date -d "$updated_at" +%s 2>/dev/null || echo 0)"
@@ -231,6 +254,16 @@ cmd_check() {
231
254
  cmd_list() {
232
255
  ensure_dir
233
256
 
257
+ # Try DB first when available
258
+ if type db_list_heartbeats >/dev/null 2>&1 && db_available 2>/dev/null; then
259
+ local db_result
260
+ db_result=$(db_list_heartbeats 2>/dev/null || echo "[]")
261
+ if [[ -n "$db_result" ]] && echo "$db_result" | jq -e 'length > 0' >/dev/null 2>&1; then
262
+ echo "$db_result" | jq -c '[.[] | . + {cpu_pct: 0}]' 2>/dev/null || echo "[]"
263
+ return 0
264
+ fi
265
+ fi
266
+
234
267
  local result="["
235
268
  local first=true
236
269
 
@@ -272,6 +305,10 @@ cmd_clear() {
272
305
 
273
306
  local hb_file="${HEARTBEAT_DIR}/${job_id}.json"
274
307
 
308
+ if type db_clear_heartbeat >/dev/null 2>&1 && db_available 2>/dev/null; then
309
+ db_clear_heartbeat "$job_id" 2>/dev/null || true
310
+ fi
311
+
275
312
  if [[ -f "$hb_file" ]]; then
276
313
  rm -f "$hb_file"
277
314
  success "Cleared heartbeat for job: ${job_id}"
@@ -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.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -17,6 +17,7 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
17
17
  [[ -f "$SCRIPT_DIR/lib/policy.sh" ]] && source "$SCRIPT_DIR/lib/policy.sh"
18
18
  # Canonical helpers (colors, output, events)
19
19
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
20
+ [[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
20
21
  # Fallback when helpers.sh not loaded
21
22
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
22
23
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -31,24 +32,12 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
31
32
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
32
33
  }
33
34
  fi
34
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
35
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
36
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
37
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
38
- RED="${RED:-\033[38;2;248;113;113m}"
39
- DIM="${DIM:-\033[2m}"
40
- BOLD="${BOLD:-\033[1m}"
41
- RESET="${RESET:-\033[0m}"
42
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
43
35
 
44
36
  # ─── Default Settings (policy overrides when config/policy.json exists) ──────
45
37
  SUBCOMMAND="${1:-help}"
46
38
  AUTO_FIX=false
47
39
  VERBOSE=false
48
- ARTIFACT_AGE_DAYS=7
49
- if type policy_get &>/dev/null 2>&1; then
50
- ARTIFACT_AGE_DAYS=$(policy_get ".hygiene.artifact_age_days" "7")
51
- fi
40
+ ARTIFACT_AGE_DAYS=$(_config_get_int "cleanup.artifact_age_days" 7)
52
41
  JSON_OUTPUT=false
53
42
 
54
43
  # ─── Help ───────────────────────────────────────────────────────────────────
@@ -95,15 +84,22 @@ detect_dead_code() {
95
84
  local unused_functions=0
96
85
  local unused_scripts=0
97
86
  local orphaned_tests=0
87
+ local func_limit func_count=0
88
+ func_limit=$(_config_get_int "limits.function_scan_limit" 0)
98
89
 
99
90
  # Find unused bash functions (simplified for Bash 3.2)
100
91
  while IFS= read -r func_file; do
92
+ [[ "$func_limit" -gt 0 && "$func_count" -ge "$func_limit" ]] && break
101
93
  # Extract function names
102
94
  local funcs
103
95
  funcs=$(grep -E '^[a-z_][a-z0-9_]*\(\)' "$func_file" 2>/dev/null | sed 's/()$//' | sed 's/^ *//' || true)
96
+ if [[ "$func_limit" -gt 0 ]]; then
97
+ funcs=$(echo "$funcs" | head -"$((func_limit - func_count))")
98
+ fi
104
99
 
105
100
  while IFS= read -r func; do
106
101
  [[ -z "$func" ]] && continue
102
+ [[ "$func_limit" -gt 0 && "$func_count" -ge "$func_limit" ]] && break
107
103
 
108
104
  # Check if function is used in other files (count lines with this function name)
109
105
  local usage_count
@@ -112,10 +108,11 @@ detect_dead_code() {
112
108
 
113
109
  # Function definition counts as 1 usage; if only 1, it's unused
114
110
  case "$usage_count" in
115
- 0|1) [[ $VERBOSE == true ]] && warn "Unused function: $func (in $(basename "$func_file"))"; ((unused_functions++)) ;;
111
+ 0|1) [[ $VERBOSE == true ]] && warn "Unused function: $func (in $(basename "$func_file"))"; unused_functions=$((unused_functions + 1)) ;;
116
112
  esac
113
+ func_count=$((func_count + 1))
117
114
  done <<< "$funcs"
118
- done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | head -20)
115
+ done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null)
119
116
 
120
117
  # Find scripts referenced nowhere
121
118
  local script_count=0
@@ -133,9 +130,9 @@ detect_dead_code() {
133
130
  ref_count=$(printf '%s' "$ref_count" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
134
131
 
135
132
  case "$ref_count" in
136
- 0|1) [[ $VERBOSE == true ]] && warn "Potentially unused script: $basename_script"; ((unused_scripts++)) ;;
133
+ 0|1) [[ $VERBOSE == true ]] && warn "Potentially unused script: $basename_script"; unused_scripts=$((unused_scripts + 1)) ;;
137
134
  esac
138
- ((script_count++))
135
+ script_count=$((script_count + 1))
139
136
  done < <(find "$REPO_DIR/scripts" -maxdepth 1 -name "sw-*.sh" -type f 2>/dev/null)
140
137
 
141
138
  # Find test fixtures without corresponding tests
@@ -145,7 +142,7 @@ detect_dead_code() {
145
142
 
146
143
  if ! grep -r "$test_name" "$REPO_DIR/scripts" --include="*-test.sh" 2>/dev/null | grep -q .; then
147
144
  warn "Orphaned test fixture: $(basename "$fixture")"
148
- ((orphaned_tests++))
145
+ orphaned_tests=$((orphaned_tests + 1))
149
146
  fi
150
147
  done < <(find "$REPO_DIR" -name "*.fixture" -type f 2>/dev/null)
151
148
 
@@ -170,7 +167,7 @@ validate_structure() {
170
167
 
171
168
  if [[ "$dir" != "$REPO_DIR/scripts" ]]; then
172
169
  warn "Script outside scripts/ directory: $script"
173
- ((structure_issues++))
170
+ structure_issues=$((structure_issues + 1))
174
171
  fi
175
172
  done < <(find "$REPO_DIR" -name "*.sh" -type f ! -path "*/node_modules/*" ! -path "*/.git/*" 2>/dev/null | grep -E '(sw-|shipwright)' || true)
176
173
 
@@ -181,19 +178,19 @@ validate_structure() {
181
178
 
182
179
  if [[ ! "$basename_test" =~ -test\.sh$ ]]; then
183
180
  warn "Test file not named *-test.sh: $basename_test"
184
- ((structure_issues++))
181
+ structure_issues=$((structure_issues + 1))
185
182
  fi
186
183
  done < <(find "$REPO_DIR/scripts" -path "*test*" -name "*.sh" -type f 2>/dev/null)
187
184
 
188
185
  # Check directory organization
189
186
  if [[ ! -d "$REPO_DIR/scripts" ]]; then
190
187
  error "scripts/ directory missing"
191
- ((structure_issues++))
188
+ structure_issues=$((structure_issues + 1))
192
189
  fi
193
190
 
194
191
  if [[ ! -d "$REPO_DIR/.claude" ]]; then
195
192
  error ".claude/ directory missing"
196
- ((structure_issues++))
193
+ structure_issues=$((structure_issues + 1))
197
194
  fi
198
195
 
199
196
  [[ $VERBOSE == true ]] && {
@@ -210,11 +207,16 @@ audit_dependencies() {
210
207
 
211
208
  local unused_deps=0
212
209
  local circular_deps=0
210
+ local dep_limit
211
+ dep_limit=$(_config_get_int "limits.dependency_scan_limit" 0)
213
212
 
214
213
  # Check for unused npm/yarn dependencies
215
214
  if [[ -f "$REPO_DIR/package.json" ]]; then
216
215
  local deps
217
216
  deps=$(jq -r '.dependencies, .devDependencies | keys[]?' "$REPO_DIR/package.json" 2>/dev/null || true)
217
+ if [[ "$dep_limit" -gt 0 ]]; then
218
+ deps=$(echo "$deps" | head -"$dep_limit")
219
+ fi
218
220
 
219
221
  while IFS= read -r dep; do
220
222
  [[ -z "$dep" ]] && continue
@@ -222,7 +224,7 @@ audit_dependencies() {
222
224
  # Simple check: does the dep appear in source files?
223
225
  if ! grep -r "$dep" "$REPO_DIR/scripts" --include="*.sh" 2>/dev/null | grep -q .; then
224
226
  [[ $VERBOSE == true ]] && warn "Potentially unused npm dependency: $dep"
225
- ((unused_deps++))
227
+ unused_deps=$((unused_deps + 1))
226
228
  fi
227
229
  done <<< "$deps"
228
230
  fi
@@ -238,10 +240,10 @@ audit_dependencies() {
238
240
  # Check if sourced_by also sources the original script
239
241
  if grep -q "source.*$(basename "$source_script")" "$script" 2>/dev/null; then
240
242
  warn "Circular dependency: $(basename "$script") ←→ $(basename "$source_script")"
241
- ((circular_deps++))
243
+ circular_deps=$((circular_deps + 1))
242
244
  fi
243
245
  done <<< "$sourced_by"
244
- done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | head -10)
246
+ done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null)
245
247
 
246
248
  [[ $VERBOSE == true ]] && {
247
249
  info "Dependency audit: $unused_deps unused, $circular_deps circular"
@@ -264,7 +266,7 @@ check_naming() {
264
266
 
265
267
  if ! [[ "$basename_script" =~ ^sw-[a-z0-9-]+\.sh$ ]] && ! [[ "$basename_script" == "sw" ]]; then
266
268
  [[ $VERBOSE == true ]] && warn "Script not following naming convention: $basename_script"
267
- ((naming_issues++))
269
+ naming_issues=$((naming_issues + 1))
268
270
  fi
269
271
  done < <(find "$REPO_DIR/scripts" -maxdepth 1 -name "*.sh" -type f 2>/dev/null)
270
272
 
@@ -276,9 +278,9 @@ check_naming() {
276
278
  while IFS= read -r func; do
277
279
  [[ -z "$func" ]] && continue
278
280
  [[ $VERBOSE == true ]] && warn "Function not using snake_case: $func (in $(basename "$script"))"
279
- ((naming_issues++))
281
+ naming_issues=$((naming_issues + 1))
280
282
  done <<< "$bad_functions"
281
- done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | head -20)
283
+ done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null)
282
284
 
283
285
  [[ $VERBOSE == true ]] && {
284
286
  info "Naming validation: $naming_issues issues found"
@@ -292,7 +294,7 @@ check_naming() {
292
294
  list_stale_branches() {
293
295
  info "Scanning for stale branches..."
294
296
 
295
- if ! git rev-parse --git-dir &>/dev/null; then
297
+ if ! git rev-parse --git-dir >/dev/null 2>&1; then
296
298
  error "Not in a git repository"
297
299
  return 1
298
300
  fi
@@ -311,7 +313,7 @@ list_stale_branches() {
311
313
  while IFS= read -r branch; do
312
314
  [[ -z "$branch" ]] && continue
313
315
  echo -e " ${DIM}$branch${RESET}"
314
- ((stale_count++))
316
+ stale_count=$((stale_count + 1))
315
317
  done <<< "$merged_branches"
316
318
  fi
317
319
 
@@ -348,13 +350,13 @@ analyze_size() {
348
350
  awk '{print $5, $9}' | \
349
351
  sort -h | \
350
352
  tail -10 | \
351
- while read size file; do
353
+ while read -r size file; do
352
354
  echo -e " ${DIM}$size${RESET} $(basename "$file")"
353
355
  done
354
356
 
355
357
  # Find bloated directories
356
358
  info "Largest directories:"
357
- du -sh "$REPO_DIR"/* 2>/dev/null | sort -h | tail -10 | while read size dir; do
359
+ du -sh "$REPO_DIR"/* 2>/dev/null | sort -h | tail -10 | while read -r size dir; do
358
360
  echo -e " ${DIM}$size${RESET} $(basename "$dir")"
359
361
  done
360
362
 
@@ -364,7 +366,7 @@ analyze_size() {
364
366
  while IFS= read -r file; do
365
367
  [[ -z "$file" ]] && continue
366
368
  warn "Binary file in repo: $(basename "$file")"
367
- ((binary_count++))
369
+ binary_count=$((binary_count + 1))
368
370
  done < <(find "$REPO_DIR" -type f ! -path '*/.git/*' ! -path '*/node_modules/*' ! -path '*/.claude/*' \
369
371
  -exec file {} \; 2>/dev/null | grep -i "executable\|binary" | cut -d: -f1 || true)
370
372
 
@@ -398,7 +400,7 @@ scan_platform_refactor() {
398
400
  local findings_file findings_raw
399
401
  findings_file=$(mktemp)
400
402
  findings_raw=$(mktemp)
401
- grep -rnE "hardcoded|Hardcoded|Fallback:|fallback:|TODO|FIXME|HACK|KLUDGE" "$scripts_dir" --include="*.sh" 2>/dev/null | head -25 > "$findings_raw" || true
403
+ grep -rnE "hardcoded|Hardcoded|Fallback:|fallback:|TODO|FIXME|HACK|KLUDGE" "$scripts_dir" --include="*.sh" 2>/dev/null > "$findings_raw" || true
402
404
  while IFS= read -r line; do
403
405
  [[ -z "$line" ]] && continue
404
406
  local f="${line%%:*}" rest="${line#*:}" ln="${rest%%:*}"
@@ -464,7 +466,7 @@ auto_fix_issues() {
464
466
  if ! [[ -x "$script" ]]; then
465
467
  chmod +x "$script"
466
468
  success "Made executable: $(basename "$script")"
467
- ((fixed_count++))
469
+ fixed_count=$((fixed_count + 1))
468
470
  fi
469
471
  done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null)
470
472
 
@@ -475,31 +477,31 @@ auto_fix_issues() {
475
477
  sed -i.bak 's/[[:space:]]*$//' "$file" 2>/dev/null || sed -i '' 's/[[:space:]]*$//' "$file"
476
478
  rm -f "${file}.bak" 2>/dev/null || true
477
479
  success "Cleaned whitespace: $(basename "$file")"
478
- ((fixed_count++))
480
+ fixed_count=$((fixed_count + 1))
479
481
  fi
480
- done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | head -20)
482
+ done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null)
481
483
 
482
484
  # Clean up temp files
483
485
  info "Removing temporary files..."
484
- find "$REPO_DIR" -name "*.tmp" -o -name "*.bak" -o -name "*~" 2>/dev/null | while read tmpfile; do
486
+ find "$REPO_DIR" -name "*.tmp" -o -name "*.bak" -o -name "*~" 2>/dev/null | while read -r tmpfile; do
485
487
  rm -f "$tmpfile"
486
488
  success "Removed: $(basename "$tmpfile")"
487
- ((fixed_count++))
489
+ fixed_count=$((fixed_count + 1))
488
490
  done
489
491
 
490
492
  # Remove old build artifacts
491
493
  info "Removing old build artifacts (>$ARTIFACT_AGE_DAYS days)..."
492
494
  find "$REPO_DIR" -type f \( -name "*.o" -o -name "*.a" -o -name "*.out" \) \
493
- -mtime "+$ARTIFACT_AGE_DAYS" 2>/dev/null | while read artifact; do
495
+ -mtime "+$ARTIFACT_AGE_DAYS" 2>/dev/null | while read -r artifact; do
494
496
  rm -f "$artifact"
495
497
  success "Removed: $(basename "$artifact")"
496
- ((fixed_count++))
498
+ fixed_count=$((fixed_count + 1))
497
499
  done
498
500
 
499
501
  success "Auto-fixed $fixed_count issues"
500
502
 
501
503
  # Create a commit if changes were made
502
- if [[ $fixed_count -gt 0 ]] && git rev-parse --git-dir &>/dev/null; then
504
+ if [[ $fixed_count -gt 0 ]] && git rev-parse --git-dir >/dev/null 2>&1; then
503
505
  git add -A
504
506
  git commit -m "chore: hygiene auto-fix ($fixed_count items)
505
507