shipwright-cli 2.3.0 → 2.4.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 (112) hide show
  1. package/README.md +82 -20
  2. package/config/policy.json +160 -2
  3. package/config/policy.schema.json +162 -1
  4. package/dashboard/public/index.html +1 -1
  5. package/dashboard/src/core/api.test.ts +362 -0
  6. package/dashboard/src/core/router.test.ts +266 -0
  7. package/dashboard/src/core/state.test.ts +235 -0
  8. package/dashboard/src/core/ws.test.ts +216 -0
  9. package/dashboard/src/design/icons.test.ts +105 -0
  10. package/dashboard/src/design/tokens.test.ts +204 -0
  11. package/dashboard/tsconfig.json +1 -1
  12. package/dashboard/vitest.config.ts +27 -0
  13. package/package.json +23 -4
  14. package/scripts/lib/pipeline-stages.sh +59 -0
  15. package/scripts/sw +1 -1
  16. package/scripts/sw-activity.sh +1 -1
  17. package/scripts/sw-adaptive.sh +1 -1
  18. package/scripts/sw-adversarial.sh +1 -1
  19. package/scripts/sw-architecture-enforcer.sh +1 -1
  20. package/scripts/sw-auth.sh +1 -1
  21. package/scripts/sw-autonomous.sh +230 -13
  22. package/scripts/sw-changelog.sh +1 -1
  23. package/scripts/sw-checkpoint.sh +1 -1
  24. package/scripts/sw-ci.sh +1 -1
  25. package/scripts/sw-cleanup.sh +1 -1
  26. package/scripts/sw-code-review.sh +1 -1
  27. package/scripts/sw-connect.sh +1 -1
  28. package/scripts/sw-context.sh +1 -1
  29. package/scripts/sw-cost.sh +1 -1
  30. package/scripts/sw-daemon.sh +1 -1
  31. package/scripts/sw-dashboard.sh +1 -1
  32. package/scripts/sw-db.sh +1 -1
  33. package/scripts/sw-decompose.sh +1 -1
  34. package/scripts/sw-deps.sh +1 -1
  35. package/scripts/sw-developer-simulation.sh +1 -1
  36. package/scripts/sw-discovery.sh +1 -1
  37. package/scripts/sw-doc-fleet.sh +1 -1
  38. package/scripts/sw-docs-agent.sh +1 -1
  39. package/scripts/sw-docs.sh +1 -1
  40. package/scripts/sw-doctor.sh +1 -1
  41. package/scripts/sw-dora.sh +1 -1
  42. package/scripts/sw-durable.sh +1 -1
  43. package/scripts/sw-e2e-orchestrator.sh +1 -1
  44. package/scripts/sw-eventbus.sh +1 -1
  45. package/scripts/sw-evidence.sh +664 -0
  46. package/scripts/sw-feedback.sh +1 -1
  47. package/scripts/sw-fix.sh +1 -1
  48. package/scripts/sw-fleet-discover.sh +1 -1
  49. package/scripts/sw-fleet-viz.sh +1 -1
  50. package/scripts/sw-fleet.sh +1 -1
  51. package/scripts/sw-github-app.sh +1 -1
  52. package/scripts/sw-github-checks.sh +1 -1
  53. package/scripts/sw-github-deploy.sh +1 -1
  54. package/scripts/sw-github-graphql.sh +1 -1
  55. package/scripts/sw-guild.sh +1 -1
  56. package/scripts/sw-heartbeat.sh +1 -1
  57. package/scripts/sw-hygiene.sh +1 -1
  58. package/scripts/sw-incident.sh +244 -1
  59. package/scripts/sw-init.sh +1 -1
  60. package/scripts/sw-instrument.sh +1 -1
  61. package/scripts/sw-intelligence.sh +1 -1
  62. package/scripts/sw-jira.sh +1 -1
  63. package/scripts/sw-launchd.sh +1 -1
  64. package/scripts/sw-linear.sh +1 -1
  65. package/scripts/sw-logs.sh +1 -1
  66. package/scripts/sw-loop.sh +1 -1
  67. package/scripts/sw-memory.sh +1 -1
  68. package/scripts/sw-mission-control.sh +1 -1
  69. package/scripts/sw-model-router.sh +1 -1
  70. package/scripts/sw-otel.sh +1 -1
  71. package/scripts/sw-oversight.sh +1 -1
  72. package/scripts/sw-pipeline-composer.sh +1 -1
  73. package/scripts/sw-pipeline-vitals.sh +1 -1
  74. package/scripts/sw-pipeline.sh +1 -1
  75. package/scripts/sw-pm.sh +1 -1
  76. package/scripts/sw-pr-lifecycle.sh +177 -5
  77. package/scripts/sw-predictive.sh +1 -1
  78. package/scripts/sw-prep.sh +1 -1
  79. package/scripts/sw-ps.sh +1 -1
  80. package/scripts/sw-public-dashboard.sh +1 -1
  81. package/scripts/sw-quality.sh +1 -1
  82. package/scripts/sw-reaper.sh +1 -1
  83. package/scripts/sw-regression.sh +1 -1
  84. package/scripts/sw-release-manager.sh +1 -1
  85. package/scripts/sw-release.sh +1 -1
  86. package/scripts/sw-remote.sh +1 -1
  87. package/scripts/sw-replay.sh +1 -1
  88. package/scripts/sw-retro.sh +4 -1
  89. package/scripts/sw-review-rerun.sh +220 -0
  90. package/scripts/sw-scale.sh +1 -1
  91. package/scripts/sw-security-audit.sh +1 -1
  92. package/scripts/sw-self-optimize.sh +99 -1
  93. package/scripts/sw-session.sh +1 -1
  94. package/scripts/sw-setup.sh +1 -1
  95. package/scripts/sw-standup.sh +1 -1
  96. package/scripts/sw-status.sh +1 -1
  97. package/scripts/sw-strategic.sh +1 -1
  98. package/scripts/sw-stream.sh +1 -1
  99. package/scripts/sw-swarm.sh +1 -1
  100. package/scripts/sw-team-stages.sh +1 -1
  101. package/scripts/sw-templates.sh +1 -1
  102. package/scripts/sw-testgen.sh +1 -1
  103. package/scripts/sw-tmux-pipeline.sh +1 -1
  104. package/scripts/sw-tmux.sh +1 -1
  105. package/scripts/sw-trace.sh +1 -1
  106. package/scripts/sw-tracker.sh +1 -1
  107. package/scripts/sw-triage.sh +198 -11
  108. package/scripts/sw-upgrade.sh +1 -1
  109. package/scripts/sw-ux.sh +1 -1
  110. package/scripts/sw-webhook.sh +1 -1
  111. package/scripts/sw-widgets.sh +1 -1
  112. package/scripts/sw-worktree.sh +1 -1
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -206,6 +206,100 @@ optimize_analyze_outcome() {
206
206
  success "Outcome recorded for issue #${issue_number:-unknown} (${result:-unknown})"
207
207
  }
208
208
 
209
+ # ═════════════════════════════════════════════════════════════════════════════
210
+ # RETRO INGEST
211
+ # ═════════════════════════════════════════════════════════════════════════════
212
+
213
+ # optimize_ingest_retro
214
+ # Read most recent retro JSON, append summary to outcomes, adjust template weights
215
+ optimize_ingest_retro() {
216
+ local retros_dir="${HOME}/.shipwright/retros"
217
+
218
+ if [[ ! -d "$retros_dir" ]]; then
219
+ return 0
220
+ fi
221
+
222
+ local latest_retro
223
+ latest_retro=$(ls -t "$retros_dir"/retro-*.json 2>/dev/null | head -1)
224
+ [[ -z "$latest_retro" || ! -f "$latest_retro" ]] && return 0
225
+
226
+ if ! command -v jq &>/dev/null; then
227
+ warn "jq required for retro ingest — skipping"
228
+ return 0
229
+ fi
230
+
231
+ ensure_optimization_dir
232
+
233
+ # Extract metrics from retro JSON
234
+ local analysis_json
235
+ analysis_json=$(jq -r '.analysis // {}' "$latest_retro" 2>/dev/null || echo "{}")
236
+ [[ -z "$analysis_json" || "$analysis_json" == "null" ]] && return 0
237
+
238
+ local success_rate avg_duration slowest_stage quality_score retries from_date to_date
239
+ success_rate=$(echo "$analysis_json" | jq -r 'if .pipelines > 0 then ((.succeeded // 0) / .pipelines * 100) | floor else 0 end' 2>/dev/null || echo "0")
240
+ avg_duration=$(echo "$analysis_json" | jq -r '.avg_duration // 0' 2>/dev/null || echo "0")
241
+ slowest_stage=$(echo "$analysis_json" | jq -r '.slowest_stage // ""' 2>/dev/null || echo "")
242
+ quality_score=$(echo "$analysis_json" | jq -r '.quality_score // 0' 2>/dev/null || echo "0")
243
+ retries=$(echo "$analysis_json" | jq -r '.retries // 0' 2>/dev/null || echo "0")
244
+ from_date=$(jq -r '.from_date // ""' "$latest_retro" 2>/dev/null || echo "")
245
+ to_date=$(jq -r '.to_date // ""' "$latest_retro" 2>/dev/null || echo "")
246
+
247
+ # Append retro_summary to outcomes.jsonl
248
+ local retro_record
249
+ retro_record=$(jq -c -n \
250
+ --arg ts "$(now_iso)" \
251
+ --argjson success_rate "${success_rate:-0}" \
252
+ --argjson avg_duration "${avg_duration:-0}" \
253
+ --arg slowest_stage "${slowest_stage:-}" \
254
+ --argjson quality_score "${quality_score:-0}" \
255
+ --argjson retries "${retries:-0}" \
256
+ --arg from_date "${from_date:-}" \
257
+ --arg to_date "${to_date:-}" \
258
+ '{
259
+ ts: $ts,
260
+ type: "retro_summary",
261
+ success_rate: $success_rate,
262
+ avg_duration: $avg_duration,
263
+ slowest_stage: $slowest_stage,
264
+ quality_score: $quality_score,
265
+ retries: $retries,
266
+ from_date: $from_date,
267
+ to_date: $to_date
268
+ }')
269
+ echo "$retro_record" >> "$OUTCOMES_FILE"
270
+
271
+ # Adjust template weights when quality is low — boost templates with stronger performance
272
+ # (success_rate in .weights is avg weight; values > 1.0 indicate above-average success)
273
+ if [[ "${quality_score:-0}" -lt 70 ]] && [[ -f "$TEMPLATE_WEIGHTS_FILE" ]]; then
274
+ info "Quality score low (${quality_score}) — boosting templates with stronger quality gates"
275
+ local tmp_weights
276
+ tmp_weights=$(mktemp "${TEMPLATE_WEIGHTS_FILE}.tmp.XXXXXX")
277
+ trap "rm -f '$tmp_weights'" RETURN
278
+ if jq '
279
+ if .weights then
280
+ .weights |= with_entries(
281
+ if (.value.success_rate >= 1.2) and (.value.raw_weights != null) then
282
+ .value.raw_weights |= with_entries(.value = ((.value * 1.15) | if . > 2.0 then 2.0 else . end))
283
+ else . end
284
+ )
285
+ else . end
286
+ ' "$TEMPLATE_WEIGHTS_FILE" > "$tmp_weights" 2>/dev/null && [[ -s "$tmp_weights" ]]; then
287
+ mv "$tmp_weights" "$TEMPLATE_WEIGHTS_FILE"
288
+ else
289
+ rm -f "$tmp_weights"
290
+ fi
291
+ fi
292
+
293
+ type rotate_jsonl &>/dev/null 2>&1 && rotate_jsonl "$OUTCOMES_FILE" 10000
294
+
295
+ emit_event "optimize.retro_ingested" \
296
+ "success_rate=${success_rate:-0}" \
297
+ "quality_score=${quality_score:-0}" \
298
+ "slowest_stage=${slowest_stage:-}"
299
+
300
+ success "Retro ingested from $(basename "$latest_retro")"
301
+ }
302
+
209
303
  # ═════════════════════════════════════════════════════════════════════════════
210
304
  # TEMPLATE TUNING
211
305
  # ═════════════════════════════════════════════════════════════════════════════
@@ -233,6 +327,8 @@ optimize_tune_templates() {
233
327
 
234
328
  # Extract template, labels, result from each outcome line
235
329
  while IFS= read -r line; do
330
+ # Skip retro_summary and other non-pipeline outcome lines
331
+ [[ "$(echo "$line" | jq -r '.type // ""' 2>/dev/null)" == "retro_summary" ]] && continue
236
332
  local template result labels_str
237
333
  template=$(echo "$line" | jq -r '.template // "unknown"' 2>/dev/null) || continue
238
334
  result=$(echo "$line" | jq -r '.result // "unknown"' 2>/dev/null) || continue
@@ -1176,6 +1272,7 @@ show_help() {
1176
1272
  echo " analyze-outcome <state-file> Analyze a completed pipeline outcome"
1177
1273
  echo " tune Run full optimization analysis"
1178
1274
  echo " report Show optimization report (last 7 days)"
1275
+ echo " ingest-retro Ingest most recent retro into optimization loop"
1179
1276
  echo " evolve-memory Prune/strengthen/promote memory patterns"
1180
1277
  echo " help Show this help"
1181
1278
  echo ""
@@ -1197,6 +1294,7 @@ main() {
1197
1294
  case "$cmd" in
1198
1295
  analyze-outcome) optimize_analyze_outcome "$@" ;;
1199
1296
  tune) optimize_full_analysis ;;
1297
+ ingest-retro) optimize_ingest_retro ;;
1200
1298
  report) optimize_report ;;
1201
1299
  evolve-memory) optimize_evolve_memory ;;
1202
1300
  help|--help|-h) show_help ;;
@@ -8,7 +8,7 @@
8
8
  # ║ Supports --template to scaffold from a team template and --terminal ║
9
9
  # ║ to select a terminal adapter (tmux, iterm2, wezterm). ║
10
10
  # ╚═══════════════════════════════════════════════════════════════════════════╝
11
- VERSION="2.3.0"
11
+ VERSION="2.4.0"
12
12
  set -euo pipefail
13
13
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
14
14
 
@@ -10,7 +10,7 @@
10
10
  set -euo pipefail
11
11
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
12
12
 
13
- VERSION="2.3.0"
13
+ VERSION="2.4.0"
14
14
 
15
15
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
16
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -4,7 +4,7 @@
4
4
  # ║ ║
5
5
  # ║ Shows running teams, agent windows, and task progress. ║
6
6
  # ╚═══════════════════════════════════════════════════════════════════════════╝
7
- VERSION="2.3.0"
7
+ VERSION="2.4.0"
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
@@ -7,7 +7,7 @@
7
7
  # When sourced, do NOT add set -euo pipefail — the parent handles that.
8
8
  # When run directly, main() sets up the error handling.
9
9
 
10
- VERSION="2.3.0"
10
+ VERSION="2.4.0"
11
11
 
12
12
  # ─── Paths (set defaults if not provided by parent) ──────────────────────────
13
13
  SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
@@ -5,7 +5,7 @@
5
5
  # ║ Streams tmux pane output in real-time to the dashboard or CLI. ║
6
6
  # ║ Captures output periodically, tags by agent/team, supports replay. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="2.3.0"
8
+ VERSION="2.4.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -5,7 +5,7 @@
5
5
  # ║ Templates define reusable agent team configurations (roles, layout, ║
6
6
  # ║ focus areas) that shipwright session --template can use to scaffold teams. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="2.3.0"
8
+ VERSION="2.4.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Handle subcommands ───────────────────────────────────────────────────────
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -11,7 +11,7 @@
11
11
  # ║ shipwright tmux fix — Auto-fix common issues ║
12
12
  # ║ shipwright tmux reload — Reload tmux config ║
13
13
  # ╚═══════════════════════════════════════════════════════════════════════════╝
14
- VERSION="2.3.0"
14
+ VERSION="2.4.0"
15
15
  set -euo pipefail
16
16
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
17
17
 
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -165,6 +165,125 @@ analyze_effort() {
165
165
  esac
166
166
  }
167
167
 
168
+ # analyze_with_ai <title> <body>
169
+ # AI-driven triage via intelligence engine. Returns JSON or empty on failure.
170
+ # Schema: {type, complexity, risk, effort, labels[], summary}
171
+ # Always falls back to keyword-based analysis when AI unavailable.
172
+ analyze_with_ai() {
173
+ local title="$1"
174
+ local body="$2"
175
+ local combined="${title} ${body}"
176
+
177
+ # Check sw-intelligence.sh is sourceable and claude CLI exists
178
+ if [[ ! -f "${SCRIPT_DIR}/sw-intelligence.sh" ]]; then
179
+ return 1
180
+ fi
181
+ if ! command -v claude &>/dev/null; then
182
+ return 1
183
+ fi
184
+
185
+ # Source intelligence (provides _intelligence_call_claude, compute_md5)
186
+ if ! source "${SCRIPT_DIR}/sw-intelligence.sh" 2>/dev/null; then
187
+ return 1
188
+ fi
189
+
190
+ local prompt
191
+ prompt="Analyze this GitHub issue and return ONLY a valid JSON object (no markdown, no explanation).
192
+
193
+ Title: ${title}
194
+
195
+ Body: ${body}
196
+
197
+ Return JSON with exactly these fields:
198
+ {
199
+ \"type\": \"<bug|feature|security|performance|refactor|docs|chore>\",
200
+ \"complexity\": \"<trivial|simple|moderate|complex|epic>\",
201
+ \"risk\": \"<low|medium|high|critical>\",
202
+ \"effort\": \"<xs|s|m|l|xl>\",
203
+ \"labels\": [\"type:X\", \"complexity:X\", \"risk:X\", \"priority:X\", \"effort:X\"],
204
+ \"summary\": \"<brief one-line summary>\"
205
+ }"
206
+
207
+ local cache_key
208
+ cache_key="triage_analyze_$(compute_md5 --string "$combined" 2>/dev/null || echo "$(echo "$combined" | md5 2>/dev/null | cut -c1-16)")"
209
+
210
+ local result
211
+ result=$(_intelligence_call_claude "$prompt" "$cache_key" 2>/dev/null) || true
212
+
213
+ if [[ -z "$result" ]] || echo "$result" | jq -e '.' &>/dev/null; then
214
+ : # result is empty or valid JSON
215
+ else
216
+ return 1
217
+ fi
218
+
219
+ # Validate required fields and normalize
220
+ local type_val complexity_val risk_val effort_val labels_val
221
+ type_val=$(echo "$result" | jq -r '.type // empty' 2>/dev/null)
222
+ complexity_val=$(echo "$result" | jq -r '.complexity // empty' 2>/dev/null)
223
+ risk_val=$(echo "$result" | jq -r '.risk // empty' 2>/dev/null)
224
+ effort_val=$(echo "$result" | jq -r '.effort // empty' 2>/dev/null)
225
+ labels_val=$(echo "$result" | jq -r '.labels // []' 2>/dev/null)
226
+
227
+ # Reject if we got an error object
228
+ if echo "$result" | jq -e '.error' &>/dev/null; then
229
+ return 1
230
+ fi
231
+
232
+ # Need at least type or complexity to consider AI result useful
233
+ if [[ -z "$type_val" && -z "$complexity_val" && -z "$risk_val" ]]; then
234
+ return 1
235
+ fi
236
+
237
+ # Normalize to valid triage schema values
238
+ local valid_type
239
+ case "$(echo "$type_val" | tr '[:upper:]' '[:lower:]')" in
240
+ bug) valid_type="bug" ;;
241
+ feature) valid_type="feature" ;;
242
+ security) valid_type="security" ;;
243
+ performance) valid_type="performance" ;;
244
+ refactor) valid_type="refactor" ;;
245
+ docs) valid_type="docs" ;;
246
+ chore) valid_type="chore" ;;
247
+ *) valid_type="" ;;
248
+ esac
249
+ local valid_complexity
250
+ case "$(echo "$complexity_val" | tr '[:upper:]' '[:lower:]')" in
251
+ trivial) valid_complexity="trivial" ;;
252
+ simple) valid_complexity="simple" ;;
253
+ moderate) valid_complexity="moderate" ;;
254
+ complex) valid_complexity="complex" ;;
255
+ epic) valid_complexity="epic" ;;
256
+ *) valid_complexity="" ;;
257
+ esac
258
+ local valid_risk
259
+ case "$(echo "$risk_val" | tr '[:upper:]' '[:lower:]')" in
260
+ low) valid_risk="low" ;;
261
+ medium) valid_risk="medium" ;;
262
+ high) valid_risk="high" ;;
263
+ critical) valid_risk="critical" ;;
264
+ *) valid_risk="" ;;
265
+ esac
266
+ local valid_effort
267
+ case "$(echo "$effort_val" | tr '[:upper:]' '[:lower:]')" in
268
+ xs) valid_effort="xs" ;;
269
+ s) valid_effort="s" ;;
270
+ m) valid_effort="m" ;;
271
+ l) valid_effort="l" ;;
272
+ xl) valid_effort="xl" ;;
273
+ *) valid_effort="" ;;
274
+ esac
275
+
276
+ jq -n \
277
+ --arg type "${valid_type:-}" \
278
+ --arg complexity "${valid_complexity:-}" \
279
+ --arg risk "${valid_risk:-}" \
280
+ --arg effort "${valid_effort:-}" \
281
+ --argjson labels "${labels_val:-[]}" \
282
+ --arg summary "$(echo "$result" | jq -r '.summary // ""' 2>/dev/null)" \
283
+ '{type: $type, complexity: $complexity, risk: $risk, effort: $effort, labels: $labels, summary: $summary}'
284
+ return 0
285
+ }
286
+
168
287
  # suggest_labels <type> <complexity> <risk> <effort>
169
288
  # Generates label recommendations
170
289
  suggest_labels() {
@@ -199,9 +318,37 @@ suggest_labels() {
199
318
 
200
319
  # ─── Subcommand: analyze ──────────────────────────────────────────────────
201
320
 
321
+ # Check if AI triage should be used (TRIAGE_AI env, --ai flag, or daemon-config)
322
+ _triage_use_ai() {
323
+ if [[ "${TRIAGE_AI:-}" == "1" || "${TRIAGE_AI:-}" == "true" ]]; then
324
+ return 0
325
+ fi
326
+ local config="${REPO_DIR}/.claude/daemon-config.json"
327
+ if [[ -f "$config" ]]; then
328
+ local enabled
329
+ enabled=$(jq -r '.intelligence.enabled // false' "$config" 2>/dev/null || echo "false")
330
+ [[ "$enabled" == "true" ]]
331
+ else
332
+ return 1
333
+ fi
334
+ }
335
+
202
336
  cmd_analyze() {
203
- local issue="${1:-}"
204
- [[ -z "$issue" ]] && { error "Usage: triage analyze <issue>"; exit 1; }
337
+ local issue=""
338
+ local use_ai=false
339
+
340
+ # Parse args for --ai and issue number
341
+ while [[ $# -gt 0 ]]; do
342
+ case "$1" in
343
+ --ai) use_ai=true; shift ;;
344
+ *) [[ -z "$issue" ]] && issue="$1"; shift ;;
345
+ esac
346
+ done
347
+
348
+ [[ -z "$issue" ]] && { error "Usage: triage analyze [--ai] <issue>"; exit 1; }
349
+
350
+ # Enable AI if --ai flag or config
351
+ _triage_use_ai && use_ai=true
205
352
 
206
353
  check_gh
207
354
 
@@ -223,13 +370,45 @@ cmd_analyze() {
223
370
 
224
371
  local combined_text="${title} ${body}"
225
372
 
226
- # Analyze
227
- local type complexity risk effort labels
228
- type=$(analyze_type "$combined_text")
229
- complexity=$(analyze_complexity "$combined_text")
230
- risk=$(analyze_risk "$combined_text")
231
- effort=$(analyze_effort "$complexity" "$risk")
232
- labels=$(suggest_labels "$type" "$complexity" "$risk" "$effort")
373
+ # Keyword-based analysis (always run for fallback)
374
+ local kw_type kw_complexity kw_risk kw_effort kw_labels
375
+ kw_type=$(analyze_type "$combined_text")
376
+ kw_complexity=$(analyze_complexity "$combined_text")
377
+ kw_risk=$(analyze_risk "$combined_text")
378
+ kw_effort=$(analyze_effort "$kw_complexity" "$kw_risk")
379
+ kw_labels=$(suggest_labels "$kw_type" "$kw_complexity" "$kw_risk" "$kw_effort")
380
+
381
+ # Try AI analysis first when enabled
382
+ local type="$kw_type" complexity="$kw_complexity" risk="$kw_risk" effort="$kw_effort" labels="$kw_labels"
383
+ if $use_ai; then
384
+ local ai_result
385
+ if ai_result=$(analyze_with_ai "$title" "$body" 2>/dev/null); then
386
+ local ai_type ai_complexity ai_risk ai_effort ai_labels
387
+ ai_type=$(echo "$ai_result" | jq -r '.type // empty')
388
+ ai_complexity=$(echo "$ai_result" | jq -r '.complexity // empty')
389
+ ai_risk=$(echo "$ai_result" | jq -r '.risk // empty')
390
+ ai_effort=$(echo "$ai_result" | jq -r '.effort // empty')
391
+ ai_labels=$(echo "$ai_result" | jq -r '.labels // []' 2>/dev/null)
392
+
393
+ # Merge: AI takes precedence where available
394
+ [[ -n "$ai_type" ]] && type="$ai_type"
395
+ [[ -n "$ai_complexity" ]] && complexity="$ai_complexity"
396
+ [[ -n "$ai_risk" ]] && risk="$ai_risk"
397
+ if [[ -n "$ai_effort" ]]; then
398
+ effort="$ai_effort"
399
+ else
400
+ effort=$(analyze_effort "$complexity" "$risk")
401
+ fi
402
+ if [[ -n "$ai_labels" && "$ai_labels" != "[]" ]]; then
403
+ labels=$(echo "$ai_labels" | jq -r 'join(" ")')
404
+ else
405
+ labels=$(suggest_labels "$type" "$complexity" "$risk" "$effort")
406
+ fi
407
+ info "AI triage applied"
408
+ else
409
+ warn "AI triage unavailable, using keyword analysis"
410
+ fi
411
+ fi
233
412
 
234
413
  # Output as structured JSON
235
414
  cat << EOF
@@ -575,7 +754,7 @@ cmd_help() {
575
754
  echo -e " ${CYAN}shipwright triage${RESET} <subcommand> [options]"
576
755
  echo ""
577
756
  echo -e "${BOLD}SUBCOMMANDS${RESET}"
578
- echo -e " ${CYAN}analyze <issue>${RESET} Analyze issue and suggest labels (outputs JSON)"
757
+ echo -e " ${CYAN}analyze [--ai] <issue>${RESET} Analyze issue and suggest labels (outputs JSON)"
579
758
  echo -e " ${CYAN}label <issue>${RESET} Apply suggested labels to issue"
580
759
  echo -e " ${CYAN}prioritize${RESET} Score and rank all open issues by priority"
581
760
  echo -e " ${CYAN}team <issue>${RESET} Recommend team size & pipeline template"
@@ -585,6 +764,8 @@ cmd_help() {
585
764
  echo ""
586
765
  echo -e "${BOLD}EXAMPLES${RESET}"
587
766
  echo -e " ${DIM}shipwright triage analyze 42${RESET}"
767
+ echo -e " ${DIM}shipwright triage analyze --ai 42${RESET}"
768
+ echo -e " ${DIM}shipwright triage --ai analyze 42${RESET}"
588
769
  echo -e " ${DIM}shipwright triage label 42${RESET}"
589
770
  echo -e " ${DIM}shipwright triage prioritize${RESET}"
590
771
  echo -e " ${DIM}shipwright triage team 42${RESET}"
@@ -596,6 +777,12 @@ cmd_help() {
596
777
  # ─── Main ──────────────────────────────────────────────────────────────────
597
778
 
598
779
  main() {
780
+ # Parse global --ai flag (enables AI triage for this invocation)
781
+ if [[ "${1:-}" == "--ai" ]]; then
782
+ export TRIAGE_AI=1
783
+ shift
784
+ fi
785
+
599
786
  local cmd="${1:-help}"
600
787
  shift 2>/dev/null || true
601
788
 
@@ -2,7 +2,7 @@
2
2
  # ╔═══════════════════════════════════════════════════════════════════════════╗
3
3
  # ║ sw upgrade — Detect and apply updates from the repo ║
4
4
  # ╚═══════════════════════════════════════════════════════════════════════════╝
5
- VERSION="2.3.0"
5
+ VERSION="2.4.0"
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -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.3.0"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -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.3.0"
11
+ VERSION="2.4.0"
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14
14
 
@@ -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.3.0"
8
+ VERSION="2.4.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11