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
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -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
  # ─── Defaults ───────────────────────────────────────────────────────────────
47
29
  DEPS_DIR="${HOME}/.shipwright/deps"
48
30
  TEST_CMD=""
@@ -174,7 +156,7 @@ cmd_classify() {
174
156
  info "Classifying PR #${pr_num}..."
175
157
 
176
158
  local pr_data
177
- pr_data=$(gh pr view "$pr_num" --json number,title,author,changedFiles,isDraft --template '{{json .}}' 2>/dev/null)
159
+ pr_data=$(gh pr view "$pr_num" --json number,title,author,changedFiles,isDraft --template '{{json .}}' 2>/dev/null || echo "")
178
160
 
179
161
  if [[ -z "$pr_data" ]]; then
180
162
  error "PR #${pr_num} not found"
@@ -415,7 +397,7 @@ cmd_batch() {
415
397
  local approved=0
416
398
  local flagged=0
417
399
 
418
- echo "$prs" | jq -r '.[] | .number' | while read -r pr_num; do
400
+ while read -r pr_num; do
419
401
  info "Processing PR #${pr_num}..."
420
402
 
421
403
  local classify_json
@@ -438,7 +420,7 @@ cmd_batch() {
438
420
  ;;
439
421
  esac
440
422
  processed=$((processed + 1))
441
- done
423
+ done < <(echo "$prs" | jq -r '.[] | .number')
442
424
 
443
425
  echo ""
444
426
  echo -e "${CYAN}${BOLD}═══ Batch Summary ═══${RESET}"
@@ -470,7 +452,7 @@ cmd_report() {
470
452
  if [[ -n "$prs" && "$prs" != "[]" ]]; then
471
453
  total=$(echo "$prs" | jq 'length')
472
454
 
473
- echo "$prs" | jq -r '.[] | .title' | while read -r title; do
455
+ while read -r title; do
474
456
  if [[ "$title" =~ from\ ([^ ]+)\ to\ ([^ ]+) ]]; then
475
457
  local from_ver="${BASH_REMATCH[1]}"
476
458
  local to_ver="${BASH_REMATCH[2]}"
@@ -482,7 +464,7 @@ cmd_report() {
482
464
  major) major_count=$((major_count + 1)) ;;
483
465
  esac
484
466
  fi
485
- done
467
+ done < <(echo "$prs" | jq -r '.[] | .title')
486
468
  fi
487
469
 
488
470
  # Find oldest PR
@@ -491,7 +473,15 @@ cmd_report() {
491
473
  local oldest_date
492
474
  oldest_date=$(echo "$prs" | jq -r '.[0].createdAt')
493
475
  if [[ -n "$oldest_date" && "$oldest_date" != "null" ]]; then
494
- oldest_age="$(date -d "$oldest_date" '+%s' 2>/dev/null || echo '?') seconds ago"
476
+ local oldest_epoch
477
+ oldest_epoch=$(date -d "$oldest_date" '+%s' 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "$oldest_date" '+%s' 2>/dev/null || echo "")
478
+ if [[ -n "$oldest_epoch" ]]; then
479
+ local now_e; now_e=$(date +%s)
480
+ local age_days=$(( (now_e - oldest_epoch) / 86400 ))
481
+ oldest_age="${age_days} days ago"
482
+ else
483
+ oldest_age="?"
484
+ fi
495
485
  fi
496
486
  fi
497
487
 
@@ -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
  # ─── Source Intelligence Core ─────────────────────────────────────────────
48
38
  if [[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]]; then
49
39
  source "$SCRIPT_DIR/sw-intelligence.sh"
@@ -7,12 +7,12 @@
7
7
  set -euo pipefail
8
8
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
9
9
 
10
- VERSION="2.4.0"
10
+ VERSION="3.0.0"
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
13
 
14
14
  # ─── Dependency check ─────────────────────────────────────────────────────────
15
- if ! command -v jq &>/dev/null; then
15
+ if ! command -v jq >/dev/null 2>&1; then
16
16
  echo "ERROR: sw-discovery.sh requires 'jq'. Install with: brew install jq (macOS) or apt install jq (Linux)" >&2
17
17
  exit 1
18
18
  fi
@@ -41,16 +41,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
41
41
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
42
42
  }
43
43
  fi
44
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
45
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
46
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
47
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
48
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
49
- RED="${RED:-\033[38;2;248;113;113m}"
50
- DIM="${DIM:-\033[2m}"
51
- BOLD="${BOLD:-\033[1m}"
52
- RESET="${RESET:-\033[0m}"
53
-
54
44
  # ─── Discovery Storage ──────────────────────────────────────────────────────
55
45
  DISCOVERIES_FILE="${HOME}/.shipwright/discoveries.jsonl"
56
46
  DISCOVERIES_DIR="${HOME}/.shipwright/discoveries"
@@ -91,11 +81,12 @@ broadcast_discovery() {
91
81
  '{ts: $ts, ts_epoch: $ts_epoch, pipeline_id: $pipeline_id, category: $category, file_patterns: $file_patterns, discovery: $discovery, resolution: $resolution}')
92
82
 
93
83
  echo "$entry" >> "$DISCOVERIES_FILE"
94
- type rotate_jsonl &>/dev/null 2>&1 && rotate_jsonl "$DISCOVERIES_FILE" 5000
84
+ type rotate_jsonl >/dev/null 2>&1 && rotate_jsonl "$DISCOVERIES_FILE" 5000
95
85
  success "Broadcast discovery: ${category} (${file_patterns})"
96
86
  }
97
87
 
98
88
  # query: find relevant discoveries for given file patterns
89
+ # Uses path overlap + semantic similarity (Jaccard on keywords, domain expansion)
99
90
  query_discoveries() {
100
91
  local file_patterns="$1"
101
92
  local limit="${2:-10}"
@@ -109,30 +100,60 @@ query_discoveries() {
109
100
 
110
101
  local count=0
111
102
  local found=false
103
+ local query_context
104
+ query_context=$(_expand_domain_keywords "$file_patterns")
112
105
 
106
+ # Collect candidates (path or semantic match)
107
+ local candidates
108
+ candidates=()
113
109
  while IFS= read -r line; do
114
110
  [[ -z "$line" ]] && continue
115
111
 
116
- local disc_patterns
112
+ local disc_patterns discovery_desc
117
113
  disc_patterns=$(echo "$line" | jq -r '.file_patterns // ""' 2>/dev/null || echo "")
114
+ discovery_desc=$(echo "$line" | jq -r '.discovery // ""' 2>/dev/null || echo "")
118
115
 
119
- # Check if patterns overlap
116
+ local matched=false
120
117
  if patterns_overlap "$file_patterns" "$disc_patterns"; then
121
- if [[ "$found" == "false" ]]; then
122
- success "Found relevant discoveries:"
123
- found=true
118
+ matched=true
119
+ else
120
+ # Semantic match: discovery about "authentication" matches "session verification"
121
+ local desc_similarity
122
+ desc_similarity=$(_discovery_semantic_match "$query_context" "$discovery_desc")
123
+ if [[ "${desc_similarity:-0}" -gt 30 ]]; then
124
+ matched=true
124
125
  fi
126
+ fi
125
127
 
126
- local category discovery
127
- category=$(echo "$line" | jq -r '.category' 2>/dev/null || echo "?")
128
- discovery=$(echo "$line" | jq -r '.discovery' 2>/dev/null || echo "?")
128
+ if [[ "$matched" == "true" ]]; then
129
+ candidates+=("$line")
130
+ fi
131
+ done < "$DISCOVERIES_FILE"
129
132
 
130
- echo -e " ${DIM}→${RESET} [${category}] ${discovery} [${disc_patterns}]"
133
+ # Optionally use Claude to rank when many candidates
134
+ if [[ "${INTELLIGENCE_ENABLED:-auto}" != "false" ]] && command -v claude &>/dev/null 2>&1 && [[ ${#candidates[@]} -gt 5 ]]; then
135
+ # TODO: batch Claude call to rank by relevance (future enhancement)
136
+ :
137
+ fi
131
138
 
132
- ((count++))
133
- [[ "$count" -ge "$limit" ]] && break
139
+ # Output up to limit
140
+ local line
141
+ for line in "${candidates[@]+"${candidates[@]}"}"; do
142
+ if [[ "$found" == "false" ]]; then
143
+ success "Found relevant discoveries:"
144
+ found=true
134
145
  fi
135
- done < "$DISCOVERIES_FILE"
146
+
147
+ local category discovery disc_patterns
148
+ category=$(echo "$line" | jq -r '.category' 2>/dev/null || echo "?")
149
+ discovery=$(echo "$line" | jq -r '.discovery' 2>/dev/null || echo "?")
150
+ disc_patterns=$(echo "$line" | jq -r '.file_patterns // ""' 2>/dev/null || echo "")
151
+
152
+ echo -e " ${DIM}→${RESET} [${category}] ${discovery} [${disc_patterns}]"
153
+
154
+ count=$((count + 1))
155
+ [[ "$count" -ge "$limit" ]] && break
156
+ done
136
157
 
137
158
  if [[ "$found" == "false" ]]; then
138
159
  info "No relevant discoveries found for patterns: ${file_patterns}"
@@ -153,8 +174,10 @@ inject_discoveries() {
153
174
 
154
175
  local seen_file
155
176
  seen_file=$(get_seen_file "$pipeline_id")
177
+ local query_context
178
+ query_context=$(_expand_domain_keywords "$file_patterns")
156
179
 
157
- # Find relevant discoveries not yet seen
180
+ # Find relevant discoveries not yet seen (path or semantic match)
158
181
  local new_count=0
159
182
  local injected_entries=()
160
183
 
@@ -171,13 +194,25 @@ inject_discoveries() {
171
194
  fi
172
195
  fi
173
196
 
174
- # Check if relevant to current file patterns
175
- local disc_patterns
197
+ # Check if relevant: path overlap OR semantic similarity > 30
198
+ local disc_patterns discovery_desc
176
199
  disc_patterns=$(echo "$line" | jq -r '.file_patterns // ""' 2>/dev/null || echo "")
200
+ discovery_desc=$(echo "$line" | jq -r '.discovery // ""' 2>/dev/null || echo "")
177
201
 
202
+ local matched=false
178
203
  if [[ -n "$disc_patterns" ]] && patterns_overlap "$file_patterns" "$disc_patterns"; then
204
+ matched=true
205
+ else
206
+ local desc_similarity
207
+ desc_similarity=$(_discovery_semantic_match "$query_context" "$discovery_desc")
208
+ if [[ "${desc_similarity:-0}" -gt 30 ]]; then
209
+ matched=true
210
+ fi
211
+ fi
212
+
213
+ if [[ "$matched" == "true" ]]; then
179
214
  injected_entries+=("$line")
180
- ((new_count++))
215
+ new_count=$((new_count + 1))
181
216
  fi
182
217
  done < "$DISCOVERIES_FILE"
183
218
 
@@ -227,6 +262,53 @@ inject_discoveries() {
227
262
  done
228
263
  }
229
264
 
265
+ # ─── Semantic matching helpers ───────────────────────────────────────────────
266
+
267
+ # Domain keyword expansion for related concepts
268
+ _expand_domain_keywords() {
269
+ local text="$1"
270
+ local expanded="$text"
271
+
272
+ # Domain synonym groups (iterate over fixed keys to avoid set -u issues)
273
+ local dom
274
+ for dom in auth api db ui test deploy error perf; do
275
+ case "$dom" in
276
+ auth) [[ "$text" =~ [aA]uth ]] && expanded="$expanded authentication authorization login session token credential permission access" ;;
277
+ api) [[ "$text" =~ [aA]pi ]] && expanded="$expanded endpoint route handler request response rest graphql" ;;
278
+ db) [[ "$text" =~ [dD]b ]] && expanded="$expanded database query migration schema model table sql" ;;
279
+ ui) [[ "$text" =~ [uU]i ]] && expanded="$expanded component view render template layout style css frontend" ;;
280
+ test) [[ "$text" =~ [tT]est ]] && expanded="$expanded testing assertion coverage mock stub fixture spec" ;;
281
+ deploy) [[ "$text" =~ [dD]eploy ]] && expanded="$expanded deployment release publish ship ci cd pipeline" ;;
282
+ error) [[ "$text" =~ [eE]rror ]] && expanded="$expanded exception failure crash bug issue defect" ;;
283
+ perf) [[ "$text" =~ [pP]erf ]] && expanded="$expanded performance optimization speed latency throughput cache" ;;
284
+ esac
285
+ done
286
+
287
+ echo "$expanded"
288
+ }
289
+
290
+ # Semantic similarity between discovery descriptions (Jaccard on keywords)
291
+ _discovery_semantic_match() {
292
+ local query_desc="$1"
293
+ local discovery_desc="$2"
294
+
295
+ # Extract keywords from both descriptions
296
+ local query_words discovery_words
297
+ query_words=$(echo "$query_desc" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '\n' | sort -u | grep -vE '^(the|a|an|is|are|was|were|in|on|at|to|for|of|and|or|but|not|with|this|that|from|by)$')
298
+ discovery_words=$(echo "$discovery_desc" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '\n' | sort -u | grep -vE '^(the|a|an|is|are|was|were|in|on|at|to|for|of|and|or|but|not|with|this|that|from|by)$')
299
+
300
+ # Compute Jaccard similarity
301
+ local intersection union
302
+ intersection=$(comm -12 <(echo "$query_words") <(echo "$discovery_words") 2>/dev/null | wc -l | tr -d ' ')
303
+ union=$(sort -u <(echo "$query_words") <(echo "$discovery_words") 2>/dev/null | wc -l | tr -d ' ')
304
+
305
+ if [[ "$union" -gt 0 ]]; then
306
+ echo "$((intersection * 100 / union))"
307
+ else
308
+ echo "0"
309
+ fi
310
+ }
311
+
230
312
  # patterns_overlap: check if two comma-separated patterns overlap
231
313
  patterns_overlap() {
232
314
  local patterns1="$1"
@@ -292,7 +374,7 @@ clean_discoveries() {
292
374
  if [[ "$ts_epoch" -ge "$cutoff" ]]; then
293
375
  echo "$line" >> "$tmp_file"
294
376
  else
295
- ((removed_count++))
377
+ removed_count=$((removed_count + 1))
296
378
  fi
297
379
  done < "$DISCOVERIES_FILE"
298
380
 
@@ -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
 
@@ -32,16 +32,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
32
32
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
33
33
  }
34
34
  fi
35
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
36
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
37
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
38
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
39
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
40
- RED="${RED:-\033[38;2;248;113;113m}"
41
- DIM="${DIM:-\033[2m}"
42
- BOLD="${BOLD:-\033[1m}"
43
- RESET="${RESET:-\033[0m}"
44
-
45
35
  # ─── Constants ──────────────────────────────────────────────────────────────
46
36
  FLEET_HOME="${HOME}/.shipwright/doc-fleet"
47
37
  FLEET_STATE="${FLEET_HOME}/state.json"
@@ -115,9 +105,9 @@ cmd_audit() {
115
105
  total_checks=$((total_checks + 1))
116
106
  if [[ -f "${REPO_DIR}/.claude/CLAUDE.md" ]]; then
117
107
  local claude_age_days=0
118
- if command -v stat &>/dev/null; then
108
+ if command -v stat >/dev/null 2>&1; then
119
109
  local claude_mtime
120
- claude_mtime=$(stat -f %m "${REPO_DIR}/.claude/CLAUDE.md" 2>/dev/null || stat -c %Y "${REPO_DIR}/.claude/CLAUDE.md" 2>/dev/null || echo "0")
110
+ claude_mtime=$(file_mtime "${REPO_DIR}/.claude/CLAUDE.md")
121
111
  local now_epoch_val
122
112
  now_epoch_val=$(date +%s)
123
113
  claude_age_days=$(( (now_epoch_val - claude_mtime) / 86400 ))
@@ -376,7 +366,7 @@ cmd_launch() {
376
366
  fi
377
367
 
378
368
  # Spawn via tmux if available
379
- if command -v tmux &>/dev/null; then
369
+ if command -v tmux >/dev/null 2>&1; then
380
370
  local session_name="docfleet-${role}"
381
371
 
382
372
  # Kill existing session for this role if present
@@ -482,7 +472,7 @@ cmd_status() {
482
472
  local active=0
483
473
  for role in $FLEET_ROLES; do
484
474
  local session_name="docfleet-${role}"
485
- if command -v tmux &>/dev/null && tmux has-session -t "$session_name" 2>/dev/null; then
475
+ if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session_name" 2>/dev/null; then
486
476
  echo -e " ${GREEN}●${RESET} ${CYAN}${role}${RESET} → tmux session: ${DIM}${session_name}${RESET}"
487
477
  active=$((active + 1))
488
478
  else
@@ -529,7 +519,7 @@ cmd_retire() {
529
519
  local retired=0
530
520
  for role in $roles_to_retire; do
531
521
  local session_name="docfleet-${role}"
532
- if command -v tmux &>/dev/null && tmux has-session -t "$session_name" 2>/dev/null; then
522
+ if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session_name" 2>/dev/null; then
533
523
  tmux kill-session -t "$session_name" 2>/dev/null && \
534
524
  success "Retired: ${CYAN}${role}${RESET}" || \
535
525
  warn "Failed to retire: ${role}"
@@ -566,7 +556,7 @@ cmd_manifest() {
566
556
  # Extract first heading
567
557
  title=$(grep -m1 '^#' "$md_file" 2>/dev/null | sed 's/^#* //' || echo "$rel_path")
568
558
  local mtime
569
- mtime=$(stat -f %m "$md_file" 2>/dev/null || stat -c %Y "$md_file" 2>/dev/null || echo "0")
559
+ mtime=$(file_mtime "$md_file")
570
560
 
571
561
  # Determine audience
572
562
  local audience="contributor"
@@ -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
  # ─── Documentation Agent State ────────────────────────────────────────────
48
38
  AGENT_HOME="${HOME}/.shipwright/docs-agent"
49
39
  FRESHNESS_DB="${AGENT_HOME}/freshness.json"
@@ -321,7 +311,7 @@ show_coverage() {
321
311
  script_name=$(basename "$script" .sh | sed 's/^sw-//')
322
312
 
323
313
  if grep -q "$script_name" "$REPO_DIR/.claude/CLAUDE.md" 2>/dev/null; then
324
- ((documented_count++))
314
+ documented_count=$((documented_count + 1))
325
315
  else
326
316
  undocumented_scripts="${undocumented_scripts}${script_name}\\n"
327
317
  fi
@@ -371,7 +361,7 @@ scan_gaps() {
371
361
 
372
362
  if [[ "$freshness" -lt 70 ]]; then
373
363
  warn "Stale section in README: $section (freshness: ${freshness}%)"
374
- ((gaps_found++))
364
+ gaps_found=$((gaps_found + 1))
375
365
  fi
376
366
  done
377
367
  fi
@@ -387,7 +377,7 @@ scan_gaps() {
387
377
 
388
378
  if [[ "$freshness" -lt 70 ]]; then
389
379
  warn "Stale section in CLAUDE.md: $section (freshness: ${freshness}%)"
390
- ((gaps_found++))
380
+ gaps_found=$((gaps_found + 1))
391
381
  fi
392
382
  done
393
383
  fi
@@ -412,11 +402,11 @@ sync_docs() {
412
402
 
413
403
  # Regenerate API reference
414
404
  generate_api_reference
415
- ((synced_count++))
405
+ synced_count=$((synced_count + 1))
416
406
 
417
407
  # Regenerate wiki
418
408
  generate_wiki_pages
419
- ((synced_count++))
409
+ synced_count=$((synced_count + 1))
420
410
 
421
411
  success "Documentation sync complete ($synced_count updates)"
422
412
  emit_event "docs_sync_complete" "updates=$synced_count"
@@ -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
 
@@ -19,6 +19,8 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
19
19
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
20
20
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
21
21
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
22
+ # Color fallbacks when helpers not loaded
23
+ : "${CYAN:=}" "${BOLD:=}" "${RESET:=}" "${DIM:=}" "${GREEN:=}" "${RED:=}" "${YELLOW:=}" "${PURPLE:=}" "${WHITE:=}" "${BLUE:=}"
22
24
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
23
25
  [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
24
26
  [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
@@ -34,16 +36,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
36
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
37
  }
36
38
  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
39
  # ─── AUTO Section Processing ────────────────────────────────────────────────
48
40
 
49
41
  # Find all files with AUTO markers
@@ -501,7 +493,7 @@ docs_wiki() {
501
493
  fi
502
494
 
503
495
  # Push to GitHub wiki
504
- if [[ "${NO_GITHUB:-}" == "true" ]] || ! command -v gh &>/dev/null; then
496
+ if [[ "${NO_GITHUB:-}" == "true" ]] || ! command -v gh >/dev/null 2>&1; then
505
497
  warn "GitHub not available — wiki pages saved to: $wiki_dir"
506
498
  return 0
507
499
  fi