shipwright-cli 2.4.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +248 -94
  3. package/completions/shipwright.bash +68 -19
  4. package/completions/shipwright.fish +310 -42
  5. package/config/decision-tiers.json +55 -0
  6. package/config/defaults.json +111 -0
  7. package/config/event-schema.json +218 -0
  8. package/config/policy.json +21 -18
  9. package/dashboard/coverage/coverage-summary.json +14 -0
  10. package/dashboard/public/index.html +1 -1
  11. package/dashboard/server.ts +306 -17
  12. package/dashboard/src/components/charts/bar.test.ts +79 -0
  13. package/dashboard/src/components/charts/donut.test.ts +68 -0
  14. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  15. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  16. package/dashboard/src/core/api.test.ts +309 -0
  17. package/dashboard/src/core/helpers.test.ts +301 -0
  18. package/dashboard/src/core/router.test.ts +307 -0
  19. package/dashboard/src/core/router.ts +7 -0
  20. package/dashboard/src/core/sse.test.ts +144 -0
  21. package/dashboard/src/views/metrics.test.ts +186 -0
  22. package/dashboard/src/views/overview.test.ts +173 -0
  23. package/dashboard/src/views/pipelines.test.ts +183 -0
  24. package/dashboard/src/views/team.test.ts +253 -0
  25. package/dashboard/vitest.config.ts +14 -5
  26. package/docs/TIPS.md +1 -1
  27. package/docs/patterns/README.md +1 -1
  28. package/package.json +7 -9
  29. package/scripts/adapters/docker-deploy.sh +1 -1
  30. package/scripts/adapters/tmux-adapter.sh +11 -1
  31. package/scripts/adapters/wezterm-adapter.sh +1 -1
  32. package/scripts/check-version-consistency.sh +1 -1
  33. package/scripts/lib/architecture.sh +127 -0
  34. package/scripts/lib/bootstrap.sh +75 -0
  35. package/scripts/lib/compat.sh +89 -6
  36. package/scripts/lib/config.sh +91 -0
  37. package/scripts/lib/daemon-adaptive.sh +3 -3
  38. package/scripts/lib/daemon-dispatch.sh +63 -17
  39. package/scripts/lib/daemon-failure.sh +0 -0
  40. package/scripts/lib/daemon-health.sh +1 -1
  41. package/scripts/lib/daemon-patrol.sh +64 -17
  42. package/scripts/lib/daemon-poll.sh +54 -25
  43. package/scripts/lib/daemon-state.sh +125 -23
  44. package/scripts/lib/daemon-triage.sh +31 -9
  45. package/scripts/lib/decide-autonomy.sh +295 -0
  46. package/scripts/lib/decide-scoring.sh +228 -0
  47. package/scripts/lib/decide-signals.sh +462 -0
  48. package/scripts/lib/fleet-failover.sh +63 -0
  49. package/scripts/lib/helpers.sh +29 -6
  50. package/scripts/lib/pipeline-detection.sh +2 -2
  51. package/scripts/lib/pipeline-github.sh +9 -9
  52. package/scripts/lib/pipeline-intelligence.sh +105 -38
  53. package/scripts/lib/pipeline-quality-checks.sh +17 -16
  54. package/scripts/lib/pipeline-quality.sh +1 -1
  55. package/scripts/lib/pipeline-stages.sh +440 -59
  56. package/scripts/lib/pipeline-state.sh +54 -4
  57. package/scripts/lib/policy.sh +0 -0
  58. package/scripts/lib/test-helpers.sh +247 -0
  59. package/scripts/postinstall.mjs +78 -12
  60. package/scripts/signals/example-collector.sh +36 -0
  61. package/scripts/sw +17 -7
  62. package/scripts/sw-activity.sh +1 -11
  63. package/scripts/sw-adaptive.sh +109 -85
  64. package/scripts/sw-adversarial.sh +4 -14
  65. package/scripts/sw-architecture-enforcer.sh +1 -11
  66. package/scripts/sw-auth.sh +8 -17
  67. package/scripts/sw-autonomous.sh +111 -49
  68. package/scripts/sw-changelog.sh +1 -11
  69. package/scripts/sw-checkpoint.sh +144 -20
  70. package/scripts/sw-ci.sh +2 -12
  71. package/scripts/sw-cleanup.sh +13 -17
  72. package/scripts/sw-code-review.sh +16 -36
  73. package/scripts/sw-connect.sh +5 -12
  74. package/scripts/sw-context.sh +9 -26
  75. package/scripts/sw-cost.sh +17 -18
  76. package/scripts/sw-daemon.sh +76 -71
  77. package/scripts/sw-dashboard.sh +57 -17
  78. package/scripts/sw-db.sh +524 -26
  79. package/scripts/sw-decide.sh +685 -0
  80. package/scripts/sw-decompose.sh +1 -11
  81. package/scripts/sw-deps.sh +15 -25
  82. package/scripts/sw-developer-simulation.sh +1 -11
  83. package/scripts/sw-discovery.sh +138 -30
  84. package/scripts/sw-doc-fleet.sh +7 -17
  85. package/scripts/sw-docs-agent.sh +6 -16
  86. package/scripts/sw-docs.sh +4 -12
  87. package/scripts/sw-doctor.sh +134 -43
  88. package/scripts/sw-dora.sh +11 -19
  89. package/scripts/sw-durable.sh +35 -52
  90. package/scripts/sw-e2e-orchestrator.sh +11 -27
  91. package/scripts/sw-eventbus.sh +115 -115
  92. package/scripts/sw-evidence.sh +114 -30
  93. package/scripts/sw-feedback.sh +3 -13
  94. package/scripts/sw-fix.sh +2 -20
  95. package/scripts/sw-fleet-discover.sh +1 -11
  96. package/scripts/sw-fleet-viz.sh +10 -18
  97. package/scripts/sw-fleet.sh +13 -17
  98. package/scripts/sw-github-app.sh +6 -16
  99. package/scripts/sw-github-checks.sh +1 -11
  100. package/scripts/sw-github-deploy.sh +1 -11
  101. package/scripts/sw-github-graphql.sh +2 -12
  102. package/scripts/sw-guild.sh +1 -11
  103. package/scripts/sw-heartbeat.sh +49 -12
  104. package/scripts/sw-hygiene.sh +45 -43
  105. package/scripts/sw-incident.sh +48 -74
  106. package/scripts/sw-init.sh +35 -37
  107. package/scripts/sw-instrument.sh +1 -11
  108. package/scripts/sw-intelligence.sh +368 -53
  109. package/scripts/sw-jira.sh +5 -14
  110. package/scripts/sw-launchd.sh +2 -12
  111. package/scripts/sw-linear.sh +8 -17
  112. package/scripts/sw-logs.sh +4 -12
  113. package/scripts/sw-loop.sh +905 -104
  114. package/scripts/sw-memory.sh +263 -20
  115. package/scripts/sw-mission-control.sh +2 -12
  116. package/scripts/sw-model-router.sh +73 -34
  117. package/scripts/sw-otel.sh +15 -23
  118. package/scripts/sw-oversight.sh +1 -11
  119. package/scripts/sw-patrol-meta.sh +5 -11
  120. package/scripts/sw-pipeline-composer.sh +7 -17
  121. package/scripts/sw-pipeline-vitals.sh +1 -11
  122. package/scripts/sw-pipeline.sh +550 -122
  123. package/scripts/sw-pm.sh +2 -12
  124. package/scripts/sw-pr-lifecycle.sh +33 -28
  125. package/scripts/sw-predictive.sh +16 -22
  126. package/scripts/sw-prep.sh +6 -16
  127. package/scripts/sw-ps.sh +1 -11
  128. package/scripts/sw-public-dashboard.sh +2 -12
  129. package/scripts/sw-quality.sh +85 -14
  130. package/scripts/sw-reaper.sh +1 -11
  131. package/scripts/sw-recruit.sh +15 -25
  132. package/scripts/sw-regression.sh +11 -21
  133. package/scripts/sw-release-manager.sh +19 -28
  134. package/scripts/sw-release.sh +8 -16
  135. package/scripts/sw-remote.sh +1 -11
  136. package/scripts/sw-replay.sh +48 -44
  137. package/scripts/sw-retro.sh +70 -92
  138. package/scripts/sw-review-rerun.sh +1 -1
  139. package/scripts/sw-scale.sh +174 -41
  140. package/scripts/sw-security-audit.sh +12 -22
  141. package/scripts/sw-self-optimize.sh +239 -23
  142. package/scripts/sw-session.sh +5 -15
  143. package/scripts/sw-setup.sh +8 -18
  144. package/scripts/sw-standup.sh +5 -15
  145. package/scripts/sw-status.sh +32 -23
  146. package/scripts/sw-strategic.sh +129 -13
  147. package/scripts/sw-stream.sh +1 -11
  148. package/scripts/sw-swarm.sh +76 -36
  149. package/scripts/sw-team-stages.sh +10 -20
  150. package/scripts/sw-templates.sh +4 -14
  151. package/scripts/sw-testgen.sh +3 -13
  152. package/scripts/sw-tmux-pipeline.sh +1 -19
  153. package/scripts/sw-tmux-role-color.sh +0 -10
  154. package/scripts/sw-tmux-status.sh +3 -11
  155. package/scripts/sw-tmux.sh +2 -20
  156. package/scripts/sw-trace.sh +1 -19
  157. package/scripts/sw-tracker-github.sh +0 -10
  158. package/scripts/sw-tracker-jira.sh +1 -11
  159. package/scripts/sw-tracker-linear.sh +1 -11
  160. package/scripts/sw-tracker.sh +7 -24
  161. package/scripts/sw-triage.sh +29 -39
  162. package/scripts/sw-upgrade.sh +5 -23
  163. package/scripts/sw-ux.sh +1 -19
  164. package/scripts/sw-webhook.sh +18 -32
  165. package/scripts/sw-widgets.sh +3 -21
  166. package/scripts/sw-worktree.sh +11 -27
  167. package/scripts/update-homebrew-sha.sh +73 -0
  168. package/templates/pipelines/tdd.json +72 -0
  169. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.4.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  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
 
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.4.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -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.1.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.1.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,21 +41,18 @@ 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"
57
47
  DISCOVERY_TTL_SECS=$((24 * 60 * 60)) # 24 hours default
58
48
 
49
+ # ─── Remote Discovery Server (optional) ─────────────────────────────────────
50
+ # Set via env var or daemon-config.json: "discovery_server_url"
51
+ DISCOVERY_SERVER_URL="${DISCOVERY_SERVER_URL:-}"
52
+ if [[ -z "$DISCOVERY_SERVER_URL" && -f ".claude/daemon-config.json" ]]; then
53
+ DISCOVERY_SERVER_URL=$(jq -r '.discovery_server_url // ""' ".claude/daemon-config.json" 2>/dev/null || true)
54
+ fi
55
+
59
56
  ensure_discoveries_dir() {
60
57
  mkdir -p "$DISCOVERIES_DIR"
61
58
  }
@@ -91,17 +88,37 @@ broadcast_discovery() {
91
88
  '{ts: $ts, ts_epoch: $ts_epoch, pipeline_id: $pipeline_id, category: $category, file_patterns: $file_patterns, discovery: $discovery, resolution: $resolution}')
92
89
 
93
90
  echo "$entry" >> "$DISCOVERIES_FILE"
94
- type rotate_jsonl &>/dev/null 2>&1 && rotate_jsonl "$DISCOVERIES_FILE" 5000
91
+ type rotate_jsonl >/dev/null 2>&1 && rotate_jsonl "$DISCOVERIES_FILE" 5000
92
+
93
+ # Fire-and-forget POST to remote discovery server if configured
94
+ if [[ -n "${DISCOVERY_SERVER_URL:-}" ]]; then
95
+ curl -sS -X POST "${DISCOVERY_SERVER_URL}/api/discoveries" \
96
+ -H "Content-Type: application/json" \
97
+ -d "$entry" \
98
+ --max-time 5 >/dev/null 2>&1 &
99
+ fi
100
+
95
101
  success "Broadcast discovery: ${category} (${file_patterns})"
96
102
  }
97
103
 
98
104
  # query: find relevant discoveries for given file patterns
105
+ # Uses path overlap + semantic similarity (Jaccard on keywords, domain expansion)
99
106
  query_discoveries() {
100
107
  local file_patterns="$1"
101
108
  local limit="${2:-10}"
102
109
 
103
110
  ensure_discoveries_dir
104
111
 
112
+ # Merge remote discoveries if server configured (best-effort)
113
+ if [[ -n "${DISCOVERY_SERVER_URL:-}" ]]; then
114
+ local remote_results
115
+ remote_results=$(curl -sS "${DISCOVERY_SERVER_URL}/api/discoveries?patterns=${file_patterns}" \
116
+ --max-time 5 2>/dev/null || true)
117
+ if [[ -n "$remote_results" ]] && echo "$remote_results" | jq -e '.' >/dev/null 2>&1; then
118
+ echo "$remote_results" | jq -cr '.[]' >> "$DISCOVERIES_FILE" 2>/dev/null || true
119
+ fi
120
+ fi
121
+
105
122
  [[ ! -f "$DISCOVERIES_FILE" ]] && {
106
123
  info "No discoveries yet"
107
124
  return 0
@@ -109,30 +126,60 @@ query_discoveries() {
109
126
 
110
127
  local count=0
111
128
  local found=false
129
+ local query_context
130
+ query_context=$(_expand_domain_keywords "$file_patterns")
112
131
 
132
+ # Collect candidates (path or semantic match)
133
+ local candidates
134
+ candidates=()
113
135
  while IFS= read -r line; do
114
136
  [[ -z "$line" ]] && continue
115
137
 
116
- local disc_patterns
138
+ local disc_patterns discovery_desc
117
139
  disc_patterns=$(echo "$line" | jq -r '.file_patterns // ""' 2>/dev/null || echo "")
140
+ discovery_desc=$(echo "$line" | jq -r '.discovery // ""' 2>/dev/null || echo "")
118
141
 
119
- # Check if patterns overlap
142
+ local matched=false
120
143
  if patterns_overlap "$file_patterns" "$disc_patterns"; then
121
- if [[ "$found" == "false" ]]; then
122
- success "Found relevant discoveries:"
123
- found=true
144
+ matched=true
145
+ else
146
+ # Semantic match: discovery about "authentication" matches "session verification"
147
+ local desc_similarity
148
+ desc_similarity=$(_discovery_semantic_match "$query_context" "$discovery_desc")
149
+ if [[ "${desc_similarity:-0}" -gt 30 ]]; then
150
+ matched=true
124
151
  fi
152
+ fi
125
153
 
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 "?")
154
+ if [[ "$matched" == "true" ]]; then
155
+ candidates+=("$line")
156
+ fi
157
+ done < "$DISCOVERIES_FILE"
129
158
 
130
- echo -e " ${DIM}→${RESET} [${category}] ${discovery} [${disc_patterns}]"
159
+ # Optionally use Claude to rank when many candidates
160
+ if [[ "${INTELLIGENCE_ENABLED:-auto}" != "false" ]] && command -v claude &>/dev/null 2>&1 && [[ ${#candidates[@]} -gt 5 ]]; then
161
+ # TODO: batch Claude call to rank by relevance (future enhancement)
162
+ :
163
+ fi
131
164
 
132
- ((count++))
133
- [[ "$count" -ge "$limit" ]] && break
165
+ # Output up to limit
166
+ local line
167
+ for line in "${candidates[@]+"${candidates[@]}"}"; do
168
+ if [[ "$found" == "false" ]]; then
169
+ success "Found relevant discoveries:"
170
+ found=true
134
171
  fi
135
- done < "$DISCOVERIES_FILE"
172
+
173
+ local category discovery disc_patterns
174
+ category=$(echo "$line" | jq -r '.category' 2>/dev/null || echo "?")
175
+ discovery=$(echo "$line" | jq -r '.discovery' 2>/dev/null || echo "?")
176
+ disc_patterns=$(echo "$line" | jq -r '.file_patterns // ""' 2>/dev/null || echo "")
177
+
178
+ echo -e " ${DIM}→${RESET} [${category}] ${discovery} [${disc_patterns}]"
179
+
180
+ count=$((count + 1))
181
+ [[ "$count" -ge "$limit" ]] && break
182
+ done
136
183
 
137
184
  if [[ "$found" == "false" ]]; then
138
185
  info "No relevant discoveries found for patterns: ${file_patterns}"
@@ -153,8 +200,10 @@ inject_discoveries() {
153
200
 
154
201
  local seen_file
155
202
  seen_file=$(get_seen_file "$pipeline_id")
203
+ local query_context
204
+ query_context=$(_expand_domain_keywords "$file_patterns")
156
205
 
157
- # Find relevant discoveries not yet seen
206
+ # Find relevant discoveries not yet seen (path or semantic match)
158
207
  local new_count=0
159
208
  local injected_entries=()
160
209
 
@@ -171,13 +220,25 @@ inject_discoveries() {
171
220
  fi
172
221
  fi
173
222
 
174
- # Check if relevant to current file patterns
175
- local disc_patterns
223
+ # Check if relevant: path overlap OR semantic similarity > 30
224
+ local disc_patterns discovery_desc
176
225
  disc_patterns=$(echo "$line" | jq -r '.file_patterns // ""' 2>/dev/null || echo "")
226
+ discovery_desc=$(echo "$line" | jq -r '.discovery // ""' 2>/dev/null || echo "")
177
227
 
228
+ local matched=false
178
229
  if [[ -n "$disc_patterns" ]] && patterns_overlap "$file_patterns" "$disc_patterns"; then
230
+ matched=true
231
+ else
232
+ local desc_similarity
233
+ desc_similarity=$(_discovery_semantic_match "$query_context" "$discovery_desc")
234
+ if [[ "${desc_similarity:-0}" -gt 30 ]]; then
235
+ matched=true
236
+ fi
237
+ fi
238
+
239
+ if [[ "$matched" == "true" ]]; then
179
240
  injected_entries+=("$line")
180
- ((new_count++))
241
+ new_count=$((new_count + 1))
181
242
  fi
182
243
  done < "$DISCOVERIES_FILE"
183
244
 
@@ -227,6 +288,53 @@ inject_discoveries() {
227
288
  done
228
289
  }
229
290
 
291
+ # ─── Semantic matching helpers ───────────────────────────────────────────────
292
+
293
+ # Domain keyword expansion for related concepts
294
+ _expand_domain_keywords() {
295
+ local text="$1"
296
+ local expanded="$text"
297
+
298
+ # Domain synonym groups (iterate over fixed keys to avoid set -u issues)
299
+ local dom
300
+ for dom in auth api db ui test deploy error perf; do
301
+ case "$dom" in
302
+ auth) [[ "$text" =~ [aA]uth ]] && expanded="$expanded authentication authorization login session token credential permission access" ;;
303
+ api) [[ "$text" =~ [aA]pi ]] && expanded="$expanded endpoint route handler request response rest graphql" ;;
304
+ db) [[ "$text" =~ [dD]b ]] && expanded="$expanded database query migration schema model table sql" ;;
305
+ ui) [[ "$text" =~ [uU]i ]] && expanded="$expanded component view render template layout style css frontend" ;;
306
+ test) [[ "$text" =~ [tT]est ]] && expanded="$expanded testing assertion coverage mock stub fixture spec" ;;
307
+ deploy) [[ "$text" =~ [dD]eploy ]] && expanded="$expanded deployment release publish ship ci cd pipeline" ;;
308
+ error) [[ "$text" =~ [eE]rror ]] && expanded="$expanded exception failure crash bug issue defect" ;;
309
+ perf) [[ "$text" =~ [pP]erf ]] && expanded="$expanded performance optimization speed latency throughput cache" ;;
310
+ esac
311
+ done
312
+
313
+ echo "$expanded"
314
+ }
315
+
316
+ # Semantic similarity between discovery descriptions (Jaccard on keywords)
317
+ _discovery_semantic_match() {
318
+ local query_desc="$1"
319
+ local discovery_desc="$2"
320
+
321
+ # Extract keywords from both descriptions
322
+ local query_words discovery_words
323
+ 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)$')
324
+ 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)$')
325
+
326
+ # Compute Jaccard similarity
327
+ local intersection union
328
+ intersection=$(comm -12 <(echo "$query_words") <(echo "$discovery_words") 2>/dev/null | wc -l | tr -d ' ')
329
+ union=$(sort -u <(echo "$query_words") <(echo "$discovery_words") 2>/dev/null | wc -l | tr -d ' ')
330
+
331
+ if [[ "$union" -gt 0 ]]; then
332
+ echo "$((intersection * 100 / union))"
333
+ else
334
+ echo "0"
335
+ fi
336
+ }
337
+
230
338
  # patterns_overlap: check if two comma-separated patterns overlap
231
339
  patterns_overlap() {
232
340
  local patterns1="$1"
@@ -292,7 +400,7 @@ clean_discoveries() {
292
400
  if [[ "$ts_epoch" -ge "$cutoff" ]]; then
293
401
  echo "$line" >> "$tmp_file"
294
402
  else
295
- ((removed_count++))
403
+ removed_count=$((removed_count + 1))
296
404
  fi
297
405
  done < "$DISCOVERIES_FILE"
298
406
 
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.4.0"
9
+ VERSION="3.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  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.1.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.1.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