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="$(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
  # Canonical helpers (colors, output, events)
18
18
  # shellcheck source=lib/helpers.sh
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
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
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 $*"; }
@@ -26,24 +27,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
26
27
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
27
28
  now_epoch() { date +%s; }
28
29
  fi
29
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
30
- emit_event() {
31
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
32
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
33
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
34
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
- }
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
30
  # ─── Configuration ─────────────────────────────────────────────────────────
48
31
  CONFIG_DIR="${HOME}/.shipwright"
49
32
  TRACKER_CONFIG="${CONFIG_DIR}/tracker-config.json"
@@ -127,7 +110,7 @@ _dispatch_provider() {
127
110
  local provider_func="provider_${func}"
128
111
 
129
112
  # Provider scripts define provider_* functions
130
- if type "$provider_func" &>/dev/null; then
113
+ if type "$provider_func" >/dev/null 2>&1; then
131
114
  "$provider_func" "$@"
132
115
  return $?
133
116
  else
@@ -137,7 +120,7 @@ _dispatch_provider() {
137
120
  # Try GitHub provider
138
121
  if [[ -f "$SCRIPT_DIR/sw-tracker-github.sh" ]]; then
139
122
  source "$SCRIPT_DIR/sw-tracker-github.sh"
140
- if type "$provider_func" &>/dev/null; then
123
+ if type "$provider_func" >/dev/null 2>&1; then
141
124
  "$provider_func" "$@"
142
125
  return $?
143
126
  fi
@@ -221,7 +204,7 @@ tracker_notify() {
221
204
  fi
222
205
 
223
206
  # Provider scripts define provider_notify()
224
- if type provider_notify &>/dev/null; then
207
+ if type provider_notify >/dev/null 2>&1; then
225
208
  provider_notify "$event" "$gh_issue" "$detail"
226
209
  else
227
210
  warn "Provider '$TRACKER_PROVIDER' loaded but provider_notify() not defined"
@@ -330,7 +313,7 @@ _init_linear() {
330
313
  local payload
331
314
  payload=$(jq -n --arg q 'query { viewer { id name } }' '{query: $q}')
332
315
  local response
333
- response=$(curl -sf -X POST "https://api.linear.app/graphql" \
316
+ response=$(curl -sf --connect-timeout "$(_config_get_int "network.connect_timeout" 10)" --max-time "$(_config_get_int "network.max_time" 30)" -X POST "$(_config_get "urls.linear_api" "https://api.linear.app/graphql")" \
334
317
  -H "Authorization: $api_key" \
335
318
  -H "Content-Type: application/json" \
336
319
  -d "$payload" 2>&1) || {
@@ -405,7 +388,7 @@ _init_jira() {
405
388
  local auth
406
389
  auth=$(printf '%s:%s' "$email" "$api_token" | base64)
407
390
  local response
408
- response=$(curl -sf -X GET "${base_url}/rest/api/3/myself" \
391
+ response=$(curl -sf --connect-timeout "$(_config_get_int "network.connect_timeout" 10)" --max-time "$(_config_get_int "network.max_time" 30)" -X GET "${base_url}/rest/api/3/myself" \
409
392
  -H "Authorization: Basic $auth" \
410
393
  -H "Content-Type: application/json" 2>&1) || {
411
394
  warn "Could not validate connection — check your credentials"
@@ -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
 
@@ -16,7 +16,15 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && 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)
21
+ [[ -z "${CYAN:-}" ]] && { [[ -z "${NO_COLOR:-}" ]] && CYAN='\033[38;2;0;212;255m' || CYAN=''; } || true
22
+ [[ -z "${RESET:-}" ]] && { [[ -z "${NO_COLOR:-}" ]] && RESET='\033[0m' || RESET=''; } || true
23
+ [[ -z "${BOLD:-}" ]] && { [[ -z "${NO_COLOR:-}" ]] && BOLD='\033[1m' || BOLD=''; } || true
24
+ [[ -z "${DIM:-}" ]] && { [[ -z "${NO_COLOR:-}" ]] && DIM='\033[2m' || DIM=''; } || true
25
+ [[ -z "${GREEN:-}" ]] && { [[ -z "${NO_COLOR:-}" ]] && GREEN='\033[38;2;74;222;128m' || GREEN=''; } || true
26
+ [[ -z "${RED:-}" ]] && { [[ -z "${NO_COLOR:-}" ]] && RED='\033[38;2;248;113;113m' || RED=''; } || true
27
+ [[ -z "${YELLOW:-}" ]] && { [[ -z "${NO_COLOR:-}" ]] && YELLOW='\033[38;2;250;204;21m' || YELLOW=''; } || true
20
28
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
21
29
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
22
30
  [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
@@ -25,24 +33,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
25
33
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
26
34
  now_epoch() { date +%s; }
27
35
  fi
28
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
29
- emit_event() {
30
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
31
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
32
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
33
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
- }
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
  # ─── GitHub API (safe when NO_GITHUB set) ──────────────────────────────────
47
37
 
48
38
  check_gh() {
@@ -50,7 +40,7 @@ check_gh() {
50
40
  error "GitHub access disabled (NO_GITHUB=1)"
51
41
  exit 1
52
42
  fi
53
- if ! command -v gh &>/dev/null; then
43
+ if ! command -v gh >/dev/null 2>&1; then
54
44
  error "gh CLI not found. Install: https://cli.github.com"
55
45
  exit 1
56
46
  fi
@@ -178,7 +168,7 @@ analyze_with_ai() {
178
168
  if [[ ! -f "${SCRIPT_DIR}/sw-intelligence.sh" ]]; then
179
169
  return 1
180
170
  fi
181
- if ! command -v claude &>/dev/null; then
171
+ if ! command -v claude >/dev/null 2>&1; then
182
172
  return 1
183
173
  fi
184
174
 
@@ -210,7 +200,7 @@ Return JSON with exactly these fields:
210
200
  local result
211
201
  result=$(_intelligence_call_claude "$prompt" "$cache_key" 2>/dev/null) || true
212
202
 
213
- if [[ -z "$result" ]] || echo "$result" | jq -e '.' &>/dev/null; then
203
+ if [[ -z "$result" ]] || echo "$result" | jq -e '.' >/dev/null 2>&1; then
214
204
  : # result is empty or valid JSON
215
205
  else
216
206
  return 1
@@ -225,7 +215,7 @@ Return JSON with exactly these fields:
225
215
  labels_val=$(echo "$result" | jq -r '.labels // []' 2>/dev/null)
226
216
 
227
217
  # Reject if we got an error object
228
- if echo "$result" | jq -e '.error' &>/dev/null; then
218
+ if echo "$result" | jq -e '.error' >/dev/null 2>&1; then
229
219
  return 1
230
220
  fi
231
221
 
@@ -445,8 +435,8 @@ cmd_label() {
445
435
  labels_str=$(echo "$analysis" | jq -r '.suggested_labels | join(" ")')
446
436
 
447
437
  # Apply labels via gh CLI
448
- local label_array
449
- mapfile -t label_array <<< "$(echo "$labels_str" | tr ' ' '\n')"
438
+ local label_array=()
439
+ while IFS= read -r _l; do [[ -n "$_l" ]] && label_array+=("$_l"); done <<< "$(echo "$labels_str" | tr ' ' '\n')"
450
440
 
451
441
  for label in "${label_array[@]}"; do
452
442
  [[ -z "$label" ]] && continue
@@ -469,7 +459,7 @@ cmd_prioritize() {
469
459
 
470
460
  # Fetch all open issues
471
461
  local issues_json
472
- issues_json=$(gh issue list --state open --json number,title,body,labels,createdAt,reactions --limit 100 2>/dev/null || echo "[]")
462
+ issues_json=$(gh issue list --state open --json number,title,body,labels,createdAt,reactions --limit "$(_config_get_int "limits.triage_issues" 100)" 2>/dev/null || echo "[]")
473
463
 
474
464
  local issue_count
475
465
  issue_count=$(echo "$issues_json" | jq 'length')
@@ -540,13 +530,13 @@ cmd_prioritize() {
540
530
  echo -e "${BOLD}Prioritized Backlog${RESET}"
541
531
  echo "─────────────────────────────────────────────────────────────────"
542
532
  echo ""
543
- echo "$output_json" | jq -r '.[] | "\(.number | tostring | @json) \(.score | tostring): \(.title) [type:\(.type) complexity:\(.complexity) risk:\(.risk)]"' | while IFS= read -r line; do
533
+ echo "$output_json" | jq -r '.[] | "#\(.number) \(.score): \(.title) [type:\(.type) complexity:\(.complexity) risk:\(.risk)]"' | while IFS= read -r line; do
544
534
  local number score rest
545
- number=$(echo "$line" | jq -r 'split(" ")[0]' <<< "$line")
546
- score=$(echo "$line" | cut -d: -f2 | cut -d' ' -f1)
535
+ number=$(echo "$line" | cut -d' ' -f1)
536
+ score=$(echo "$line" | cut -d' ' -f2 | tr -d ':')
547
537
  rest=$(echo "$line" | cut -d' ' -f3-)
548
538
 
549
- echo -e " ${CYAN}#${number}${RESET} ${BOLD}${score}${RESET} ${rest}"
539
+ echo -e " ${CYAN}${number}${RESET} ${BOLD}${score}${RESET} ${rest}"
550
540
  done
551
541
 
552
542
  echo ""
@@ -565,7 +555,7 @@ cmd_team() {
565
555
 
566
556
  # Determine if GitHub is available (don't exit — allow offline fallback)
567
557
  local gh_available=false
568
- if [[ "${NO_GITHUB:-}" != "1" ]] && command -v gh &>/dev/null; then
558
+ if [[ "${NO_GITHUB:-}" != "1" ]] && command -v gh >/dev/null 2>&1; then
569
559
  gh_available=true
570
560
  fi
571
561
 
@@ -595,7 +585,7 @@ cmd_team() {
595
585
 
596
586
  local recruit_result
597
587
  recruit_result=$(bash "$SCRIPT_DIR/sw-recruit.sh" team --json "$issue_title" 2>/dev/null) || true
598
- if [[ -n "$recruit_result" ]] && echo "$recruit_result" | jq -e '.team' &>/dev/null 2>&1; then
588
+ if [[ -n "$recruit_result" ]] && echo "$recruit_result" | jq -e '.team' >/dev/null 2>&1; then
599
589
  model=$(echo "$recruit_result" | jq -r '.model // "sonnet"')
600
590
  agents=$(echo "$recruit_result" | jq -r '.agents // 2')
601
591
  template=$(echo "$recruit_result" | jq -r '.template // ""')
@@ -675,7 +665,7 @@ cmd_batch() {
675
665
 
676
666
  # Fetch unlabeled open issues
677
667
  local issues_json
678
- issues_json=$(gh issue list --state open --search "no:label" --json number --limit 50 2>/dev/null || echo "[]")
668
+ issues_json=$(gh issue list --state open --search "no:label" --json number --limit "$(_config_get_int "limits.triage_unlabeled" 50)" 2>/dev/null || echo "[]")
679
669
 
680
670
  local issue_count
681
671
  issue_count=$(echo "$issues_json" | jq 'length')
@@ -701,7 +691,7 @@ cmd_report() {
701
691
 
702
692
  # Fetch labeled issues
703
693
  local issues_json
704
- issues_json=$(gh issue list --state open --json labels,title,number --limit 100 2>/dev/null || echo "[]")
694
+ issues_json=$(gh issue list --state open --json labels,title,number --limit "$(_config_get_int "limits.triage_issues" 100)" 2>/dev/null || echo "[]")
705
695
 
706
696
  local type_counts complexity_counts priority_counts
707
697
  type_counts='{}'
@@ -2,7 +2,7 @@
2
2
  # ╔═══════════════════════════════════════════════════════════════════════════╗
3
3
  # ║ sw upgrade — Detect and apply updates from the repo ║
4
4
  # ╚═══════════════════════════════════════════════════════════════════════════╝
5
- VERSION="2.4.0"
5
+ VERSION="3.0.0"
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
@@ -25,24 +25,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
25
25
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
26
26
  now_epoch() { date +%s; }
27
27
  fi
28
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
29
- emit_event() {
30
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
31
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
32
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
33
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
- }
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
28
  # ─── Parse flags ───────────────────────────────────────────────────────────
47
29
  APPLY=false
48
30
  REPO_OVERRIDE=""
@@ -70,8 +52,8 @@ find_repo() {
70
52
  fi
71
53
 
72
54
  # 2. Environment variable
73
- if [[ -n "${CCT_REPO_PATH:-}" ]]; then
74
- echo "$CCT_REPO_PATH"
55
+ if [[ -n "${SHIPWRIGHT_REPO_PATH:-}" ]]; then
56
+ echo "$SHIPWRIGHT_REPO_PATH"
75
57
  return
76
58
  fi
77
59
 
@@ -102,7 +84,7 @@ REPO_PATH="$(find_repo)" || {
102
84
  error "Cannot locate the Shipwright repo."
103
85
  echo ""
104
86
  echo -e " Try one of:"
105
- echo -e " ${DIM}export CCT_REPO_PATH=/path/to/shipwright${RESET}"
87
+ echo -e " ${DIM}export SHIPWRIGHT_REPO_PATH=/path/to/shipwright${RESET}"
106
88
  echo -e " ${DIM}shipwright upgrade --repo-path /path/to/shipwright${RESET}"
107
89
  exit 1
108
90
  }
@@ -277,7 +259,7 @@ bootstrap_manifest() {
277
259
  IFS='|' read -r key _ dest _ _ <<< "$entry"
278
260
  if [[ -f "$dest" ]]; then
279
261
  echo -e " ${GREEN}✓${RESET} ${DIM}$key${RESET} → $dest"
280
- ((found++))
262
+ found=$((found + 1))
281
263
  fi
282
264
  done
283
265
 
package/scripts/sw-ux.sh CHANGED
@@ -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 ──────────────────────────────────────────
@@ -26,24 +26,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
26
26
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
27
27
  now_epoch() { date +%s; }
28
28
  fi
29
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
30
- emit_event() {
31
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
32
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
33
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
34
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
- }
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
29
  # ─── Structured Event Log ──────────────────────────────────────────────────
48
30
  EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
49
31
 
@@ -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,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 $*"; }
@@ -25,29 +26,11 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
25
26
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
26
27
  now_epoch() { date +%s; }
27
28
  fi
28
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
29
- emit_event() {
30
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
31
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
32
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
33
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
- }
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
29
  # ─── Constants ──────────────────────────────────────────────────────────────
47
30
  SHIPWRIGHT_DIR="$HOME/.shipwright"
48
31
  WEBHOOK_SECRET_FILE="$SHIPWRIGHT_DIR/webhook-secret"
49
32
  WEBHOOK_EVENTS_FILE="$SHIPWRIGHT_DIR/webhook-events.jsonl"
50
- WEBHOOK_PORT="${WEBHOOK_PORT:-8765}"
33
+ WEBHOOK_PORT="${WEBHOOK_PORT:-$(_config_get_int "webhook.port" 8765 2>/dev/null || echo 8765)}"
51
34
  WEBHOOK_PID_FILE="$SHIPWRIGHT_DIR/webhook.pid"
52
35
  WEBHOOK_LOG="$SHIPWRIGHT_DIR/webhook.log"
53
36
 
@@ -141,7 +124,7 @@ process_webhook_event() {
141
124
 
142
125
  # Check if nc (netcat) is available
143
126
  check_nc() {
144
- if ! command -v nc &>/dev/null; then
127
+ if ! command -v nc >/dev/null 2>&1; then
145
128
  error "netcat (nc) is required but not installed"
146
129
  echo -e " ${DIM}brew install netcat${RESET} (macOS)"
147
130
  echo -e " ${DIM}sudo apt install netcat-openbsd${RESET} (Ubuntu/Debian)"
@@ -254,17 +237,20 @@ webhook_server_bash() {
254
237
  local method path protocol
255
238
  read -r method path protocol <<< "$request_line"
256
239
 
257
- # Read headers until blank line
258
- local -A headers
240
+ # Read headers until blank line (Bash 3.2 compatible — no associative arrays)
259
241
  local header_line content_length=0
242
+ local hdr_signature="" hdr_event_type=""
260
243
  while read -r -u 3 -t 0.1 header_line; do
261
244
  [[ -z "$header_line" || "$header_line" == $'\r' ]] && break
262
245
  local key="${header_line%%:*}"
263
246
  local value="${header_line#*:}"
264
247
  value="${value#[[:space:]]}"
265
248
  value="${value%$'\r'}"
266
- headers["$key"]="$value"
267
- [[ "${key,,}" == "content-length" ]] && content_length="$value"
249
+ local key_lower
250
+ key_lower=$(printf '%s' "$key" | tr '[:upper:]' '[:lower:]')
251
+ [[ "$key_lower" == "content-length" ]] && content_length="$value"
252
+ [[ "$key_lower" == "x-hub-signature-256" ]] && hdr_signature="$value"
253
+ [[ "$key_lower" == "x-github-event" ]] && hdr_event_type="$value"
268
254
  done 2>/dev/null || true
269
255
 
270
256
  # Read body if content-length > 0
@@ -275,8 +261,8 @@ webhook_server_bash() {
275
261
 
276
262
  # Process webhook if method is POST
277
263
  if [[ "$method" == "POST" && "$path" == "/webhook" ]]; then
278
- local signature="${headers[X-Hub-Signature-256]:-}"
279
- local event_type="${headers[X-Github-Event]:-}"
264
+ local signature="$hdr_signature"
265
+ local event_type="$hdr_event_type"
280
266
 
281
267
  if validate_webhook_signature "$body" "$signature"; then
282
268
  if process_webhook_event "$body" "$event_type"; then
@@ -331,7 +317,7 @@ cmd_setup() {
331
317
  info "Webhook endpoint: http://localhost:${WEBHOOK_PORT}/webhook"
332
318
 
333
319
  # Check if gh CLI is available
334
- if ! command -v gh &>/dev/null; then
320
+ if ! command -v gh >/dev/null 2>&1; then
335
321
  error "GitHub CLI (gh) is required but not installed"
336
322
  return 1
337
323
  fi
@@ -345,7 +331,7 @@ cmd_setup() {
345
331
  -f "url=http://localhost:${WEBHOOK_PORT}/webhook" \
346
332
  -F "events=issues" \
347
333
  -f "config[content_type]=json" \
348
- -f "config[secret]=${secret}" 2>&1); then
334
+ -f "config[secret]=${secret}" --timeout 30 2>&1); then
349
335
 
350
336
  local hook_id
351
337
  hook_id=$(echo "$webhook_response" | jq -r '.id // empty' 2>/dev/null || true)
@@ -403,7 +389,7 @@ cmd_test() {
403
389
  return 1
404
390
  fi
405
391
 
406
- if ! command -v gh &>/dev/null; then
392
+ if ! command -v gh >/dev/null 2>&1; then
407
393
  error "GitHub CLI (gh) is required"
408
394
  return 1
409
395
  fi
@@ -440,7 +426,7 @@ cmd_test() {
440
426
  if gh api "repos/${org_repo}/hooks/tests" \
441
427
  -H "Accept: application/vnd.github+json" \
442
428
  -X POST \
443
- 2>&1 | grep -q "Test hook sent"; then
429
+ --timeout 30 2>&1 | grep -q "Test hook sent"; then
444
430
  success "Test ping sent to GitHub"
445
431
  else
446
432
  warn "Could not send test via GitHub API, but payload is valid:"
@@ -534,7 +520,7 @@ cmd_secret() {
534
520
  echo "$new_secret" > "$WEBHOOK_SECRET_FILE"
535
521
  chmod 600 "$WEBHOOK_SECRET_FILE"
536
522
  success "Webhook secret regenerated"
537
- info "New secret: ${new_secret}"
523
+ info "Secret: ${new_secret:0:8}... (full value in ${WEBHOOK_SECRET_FILE})"
538
524
  ;;
539
525
  *)
540
526
  error "Unknown secret action: $action"
@@ -8,7 +8,7 @@
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
11
- VERSION="2.4.0"
11
+ VERSION="3.0.0"
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14
14
 
@@ -29,24 +29,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
29
29
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
30
30
  now_epoch() { date +%s; }
31
31
  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
-
50
32
  # ─── Configuration ─────────────────────────────────────────────────────────
51
33
  CONFIG_DIR="${HOME}/.shipwright"
52
34
  CONFIG_FILE="${CONFIG_DIR}/widgets-config.json"
@@ -301,8 +283,8 @@ cmd_slack() {
301
283
  )
302
284
 
303
285
  # Send to webhook
304
- if command -v curl &>/dev/null; then
305
- response=$(curl -s -X POST "$webhook_url" \
286
+ if command -v curl >/dev/null 2>&1; then
287
+ response=$(curl -s --connect-timeout 10 --max-time 30 -X POST "$webhook_url" \
306
288
  -H 'Content-Type: application/json' \
307
289
  -d "$message_json" 2>&1)
308
290
 
@@ -5,7 +5,7 @@
5
5
  # ║ Each agent gets its own worktree so parallel agents don't clobber ║
6
6
  # ║ each other's files. Worktrees live in .worktrees/ relative to root. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="2.4.0"
8
+ VERSION="3.0.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -23,24 +23,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
23
23
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
24
24
  now_epoch() { date +%s; }
25
25
  fi
26
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
27
- emit_event() {
28
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
29
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
30
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
31
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
32
- }
33
- fi
34
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
35
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
36
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
37
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
38
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
39
- RED="${RED:-\033[38;2;248;113;113m}"
40
- DIM="${DIM:-\033[2m}"
41
- BOLD="${BOLD:-\033[1m}"
42
- RESET="${RESET:-\033[0m}"
43
-
44
26
  # ─── Repo root ─────────────────────────────────────────────────────────────
45
27
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
46
28
  error "Not inside a git repository."
@@ -122,7 +104,7 @@ worktree_list() {
122
104
 
123
105
  printf " ${CYAN}%-16s${RESET} ${PURPLE}%-22s${RESET} %b ${DIM}.worktrees/%s/${RESET}\n" \
124
106
  "$name" "$branch" "$status_str" "$name"
125
- ((found++))
107
+ found=$((found + 1))
126
108
  done
127
109
 
128
110
  if [[ $found -eq 0 ]]; then
@@ -167,8 +149,8 @@ worktree_sync_all() {
167
149
  [[ -d "$dir" ]] || continue
168
150
  local name
169
151
  name="$(basename "$dir")"
170
- worktree_sync "$name" || ((failed++))
171
- ((count++))
152
+ worktree_sync "$name" || failed=$((failed + 1))
153
+ count=$((count + 1))
172
154
  done
173
155
 
174
156
  echo ""
@@ -185,7 +167,7 @@ worktree_merge() {
185
167
  local current_branch
186
168
  current_branch="$(git branch --show-current)"
187
169
 
188
- if ! git rev-parse --verify "$branch" &>/dev/null; then
170
+ if ! git rev-parse --verify "$branch" >/dev/null 2>&1; then
189
171
  error "Branch '$branch' does not exist."
190
172
  return 1
191
173
  fi
@@ -219,7 +201,7 @@ worktree_merge_all() {
219
201
  echo -e " ${DIM}Resolve the conflict, then re-run: shipwright worktree merge-all${RESET}"
220
202
  return 1
221
203
  }
222
- ((count++))
204
+ count=$((count + 1))
223
205
  done
224
206
 
225
207
  echo ""
@@ -234,7 +216,9 @@ worktree_remove() {
234
216
  if [[ -d "$worktree_path" ]]; then
235
217
  git worktree remove "$worktree_path" --force 2>/dev/null || {
236
218
  warn "Could not cleanly remove worktree $name, forcing..."
237
- rm -rf "$worktree_path"
219
+ if [[ -n "$worktree_path" && "$worktree_path" == "$WORKTREE_DIR/"* ]]; then
220
+ rm -rf "$worktree_path"
221
+ fi
238
222
  git worktree prune 2>/dev/null || true
239
223
  }
240
224
  fi
@@ -257,7 +241,7 @@ worktree_cleanup() {
257
241
  local name
258
242
  name="$(basename "$dir")"
259
243
  worktree_remove "$name"
260
- ((count++))
244
+ count=$((count + 1))
261
245
  done
262
246
 
263
247
  # Prune stale worktree references
@@ -302,7 +286,7 @@ worktree_status() {
302
286
 
303
287
  printf " ${CYAN}%-16s${RESET} ${PURPLE}%-22s${RESET} ${GREEN}%s ahead${RESET}, ${YELLOW}%s behind${RESET}%b\n" \
304
288
  "$name" "$branch" "$ahead" "$behind" "$dirty"
305
- ((found++))
289
+ found=$((found + 1))
306
290
  done
307
291
 
308
292
  if [[ $found -eq 0 ]]; then