shipwright-cli 1.7.1 → 1.10.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 (115) hide show
  1. package/.claude/agents/code-reviewer.md +90 -0
  2. package/.claude/agents/devops-engineer.md +142 -0
  3. package/.claude/agents/pipeline-agent.md +80 -0
  4. package/.claude/agents/shell-script-specialist.md +150 -0
  5. package/.claude/agents/test-specialist.md +196 -0
  6. package/.claude/hooks/post-tool-use.sh +45 -0
  7. package/.claude/hooks/pre-tool-use.sh +25 -0
  8. package/.claude/hooks/session-started.sh +37 -0
  9. package/README.md +212 -814
  10. package/claude-code/CLAUDE.md.shipwright +54 -0
  11. package/claude-code/hooks/notify-idle.sh +2 -2
  12. package/claude-code/hooks/session-start.sh +24 -0
  13. package/claude-code/hooks/task-completed.sh +6 -2
  14. package/claude-code/settings.json.template +12 -0
  15. package/dashboard/public/app.js +4422 -0
  16. package/dashboard/public/index.html +816 -0
  17. package/dashboard/public/styles.css +4755 -0
  18. package/dashboard/server.ts +4315 -0
  19. package/docs/KNOWN-ISSUES.md +18 -10
  20. package/docs/TIPS.md +38 -26
  21. package/docs/patterns/README.md +33 -23
  22. package/package.json +9 -5
  23. package/scripts/adapters/iterm2-adapter.sh +1 -1
  24. package/scripts/adapters/tmux-adapter.sh +52 -23
  25. package/scripts/adapters/wezterm-adapter.sh +26 -14
  26. package/scripts/lib/compat.sh +200 -0
  27. package/scripts/lib/helpers.sh +72 -0
  28. package/scripts/postinstall.mjs +72 -13
  29. package/scripts/{cct → sw} +118 -22
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +468 -0
  33. package/scripts/sw-cleanup.sh +359 -0
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/sw-daemon.sh +5574 -0
  37. package/scripts/sw-dashboard.sh +477 -0
  38. package/scripts/sw-developer-simulation.sh +252 -0
  39. package/scripts/sw-docs.sh +635 -0
  40. package/scripts/sw-doctor.sh +907 -0
  41. package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
  42. package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
  43. package/scripts/sw-github-checks.sh +521 -0
  44. package/scripts/sw-github-deploy.sh +533 -0
  45. package/scripts/sw-github-graphql.sh +972 -0
  46. package/scripts/sw-heartbeat.sh +293 -0
  47. package/scripts/{cct-init.sh → sw-init.sh} +144 -11
  48. package/scripts/sw-intelligence.sh +1196 -0
  49. package/scripts/sw-jira.sh +643 -0
  50. package/scripts/sw-launchd.sh +364 -0
  51. package/scripts/sw-linear.sh +648 -0
  52. package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
  53. package/scripts/sw-loop.sh +2217 -0
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +514 -36
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/sw-pipeline-vitals.sh +1096 -0
  58. package/scripts/sw-pipeline.sh +7593 -0
  59. package/scripts/sw-predictive.sh +820 -0
  60. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  61. package/scripts/{cct-ps.sh → sw-ps.sh} +9 -6
  62. package/scripts/{cct-reaper.sh → sw-reaper.sh} +10 -6
  63. package/scripts/sw-remote.sh +687 -0
  64. package/scripts/sw-self-optimize.sh +1048 -0
  65. package/scripts/sw-session.sh +541 -0
  66. package/scripts/sw-setup.sh +234 -0
  67. package/scripts/sw-status.sh +796 -0
  68. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  69. package/scripts/sw-tmux.sh +591 -0
  70. package/scripts/sw-tracker-jira.sh +277 -0
  71. package/scripts/sw-tracker-linear.sh +292 -0
  72. package/scripts/sw-tracker.sh +409 -0
  73. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  74. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  75. package/templates/pipelines/autonomous.json +35 -6
  76. package/templates/pipelines/cost-aware.json +21 -0
  77. package/templates/pipelines/deployed.json +40 -6
  78. package/templates/pipelines/enterprise.json +16 -2
  79. package/templates/pipelines/fast.json +19 -0
  80. package/templates/pipelines/full.json +28 -2
  81. package/templates/pipelines/hotfix.json +19 -0
  82. package/templates/pipelines/standard.json +31 -0
  83. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  84. package/tmux/templates/accessibility.json +34 -0
  85. package/tmux/templates/api-design.json +35 -0
  86. package/tmux/templates/architecture.json +1 -0
  87. package/tmux/templates/bug-fix.json +9 -0
  88. package/tmux/templates/code-review.json +1 -0
  89. package/tmux/templates/compliance.json +36 -0
  90. package/tmux/templates/data-pipeline.json +36 -0
  91. package/tmux/templates/debt-paydown.json +34 -0
  92. package/tmux/templates/devops.json +1 -0
  93. package/tmux/templates/documentation.json +1 -0
  94. package/tmux/templates/exploration.json +1 -0
  95. package/tmux/templates/feature-dev.json +1 -0
  96. package/tmux/templates/full-stack.json +8 -0
  97. package/tmux/templates/i18n.json +34 -0
  98. package/tmux/templates/incident-response.json +36 -0
  99. package/tmux/templates/migration.json +1 -0
  100. package/tmux/templates/observability.json +35 -0
  101. package/tmux/templates/onboarding.json +33 -0
  102. package/tmux/templates/performance.json +35 -0
  103. package/tmux/templates/refactor.json +1 -0
  104. package/tmux/templates/release.json +35 -0
  105. package/tmux/templates/security-audit.json +8 -0
  106. package/tmux/templates/spike.json +34 -0
  107. package/tmux/templates/testing.json +1 -0
  108. package/tmux/tmux.conf +98 -9
  109. package/scripts/cct-cleanup.sh +0 -172
  110. package/scripts/cct-daemon.sh +0 -3189
  111. package/scripts/cct-doctor.sh +0 -414
  112. package/scripts/cct-loop.sh +0 -1332
  113. package/scripts/cct-pipeline.sh +0 -3844
  114. package/scripts/cct-session.sh +0 -284
  115. package/scripts/cct-status.sh +0 -169
@@ -4,8 +4,9 @@
4
4
  # ║ Captures learnings · Injects context · Searches memory · Tracks metrics║
5
5
  # ╚═══════════════════════════════════════════════════════════════════════════╝
6
6
  set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
7
8
 
8
- VERSION="1.7.1"
9
+ VERSION="1.10.0"
9
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
11
12
 
@@ -20,6 +21,14 @@ DIM='\033[2m'
20
21
  BOLD='\033[1m'
21
22
  RESET='\033[0m'
22
23
 
24
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
25
+ # shellcheck source=lib/compat.sh
26
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
27
+
28
+ # ─── Intelligence Engine (optional) ──────────────────────────────────────────
29
+ # shellcheck source=sw-intelligence.sh
30
+ [[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]] && source "$SCRIPT_DIR/sw-intelligence.sh"
31
+
23
32
  # ─── Output Helpers ─────────────────────────────────────────────────────────
24
33
  info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
25
34
  success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
@@ -30,7 +39,7 @@ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
30
39
  now_epoch() { date +%s; }
31
40
 
32
41
  # ─── Structured Event Log ──────────────────────────────────────────────────
33
- EVENTS_FILE="${HOME}/.claude-teams/events.jsonl"
42
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
34
43
 
35
44
  emit_event() {
36
45
  local event_type="$1"
@@ -46,7 +55,7 @@ emit_event() {
46
55
  json_fields="${json_fields},\"${key}\":\"${val}\""
47
56
  fi
48
57
  done
49
- mkdir -p "${HOME}/.claude-teams"
58
+ mkdir -p "${HOME}/.shipwright"
50
59
  echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
51
60
  }
52
61
 
@@ -207,32 +216,272 @@ memory_capture_failure() {
207
216
  '[.failures[]] | to_entries | map(select(.value.pattern == $pat)) | .[0].key // -1' \
208
217
  "$failures_file" 2>/dev/null || echo "-1")
209
218
 
210
- local tmp_file
211
- tmp_file=$(mktemp)
219
+ (
220
+ if command -v flock &>/dev/null; then
221
+ flock -w 10 200 2>/dev/null || { warn "Memory lock timeout"; return 1; }
222
+ fi
223
+ local tmp_file
224
+ tmp_file=$(mktemp "${failures_file}.tmp.XXXXXX")
225
+
226
+ if [[ "$existing_idx" != "-1" && "$existing_idx" != "null" ]]; then
227
+ # Update existing entry
228
+ jq --argjson idx "$existing_idx" \
229
+ --arg ts "$(now_iso)" \
230
+ '.failures[$idx].seen_count += 1 | .failures[$idx].last_seen = $ts' \
231
+ "$failures_file" > "$tmp_file" && mv "$tmp_file" "$failures_file" || rm -f "$tmp_file"
232
+ else
233
+ # Add new failure entry
234
+ jq --arg stage "$stage" \
235
+ --arg pattern "$pattern" \
236
+ --arg ts "$(now_iso)" \
237
+ '.failures += [{
238
+ stage: $stage,
239
+ pattern: $pattern,
240
+ root_cause: "",
241
+ fix: "",
242
+ seen_count: 1,
243
+ last_seen: $ts
244
+ }] | .failures = (.failures | .[-100:])' \
245
+ "$failures_file" > "$tmp_file" && mv "$tmp_file" "$failures_file" || rm -f "$tmp_file"
246
+ fi
247
+ ) 200>"${failures_file}.lock"
248
+
249
+ emit_event "memory.failure" "stage=${stage}" "pattern=${pattern:0:80}"
250
+ }
251
+
252
+ # memory_record_fix_outcome <failure_hash_or_pattern> <fix_applied:bool> <fix_resolved:bool>
253
+ # Tracks whether suggested fixes actually worked. Builds effectiveness data
254
+ # so future memory injection can prioritize high-success-rate fixes.
255
+ memory_record_fix_outcome() {
256
+ local pattern_match="${1:-}"
257
+ local fix_applied="${2:-false}"
258
+ local fix_resolved="${3:-false}"
259
+
260
+ [[ -z "$pattern_match" ]] && return 1
261
+
262
+ ensure_memory_dir
263
+ local mem_dir
264
+ mem_dir="$(repo_memory_dir)"
265
+ local failures_file="$mem_dir/failures.json"
212
266
 
213
- if [[ "$existing_idx" != "-1" && "$existing_idx" != "null" ]]; then
214
- # Update existing entry
215
- jq --argjson idx "$existing_idx" \
267
+ [[ ! -f "$failures_file" ]] && return 1
268
+
269
+ # Find matching failure by pattern substring
270
+ local match_idx
271
+ match_idx=$(jq --arg pat "$pattern_match" \
272
+ '[.failures[]] | to_entries | map(select(.value.pattern | contains($pat))) | .[0].key // -1' \
273
+ "$failures_file" 2>/dev/null || echo "-1")
274
+
275
+ if [[ "$match_idx" == "-1" || "$match_idx" == "null" ]]; then
276
+ warn "No matching failure found for: ${pattern_match:0:60}"
277
+ return 1
278
+ fi
279
+
280
+ # Update fix outcome tracking fields
281
+ local applied_inc=0 resolved_inc=0
282
+ [[ "$fix_applied" == "true" ]] && applied_inc=1
283
+ [[ "$fix_resolved" == "true" ]] && resolved_inc=1
284
+
285
+ (
286
+ if command -v flock &>/dev/null; then
287
+ flock -w 10 200 2>/dev/null || { warn "Memory lock timeout"; return 1; }
288
+ fi
289
+ local tmp_file
290
+ tmp_file=$(mktemp "${failures_file}.tmp.XXXXXX")
291
+
292
+ jq --argjson idx "$match_idx" \
293
+ --argjson app "$applied_inc" \
294
+ --argjson res "$resolved_inc" \
216
295
  --arg ts "$(now_iso)" \
217
- '.failures[$idx].seen_count += 1 | .failures[$idx].last_seen = $ts' \
218
- "$failures_file" > "$tmp_file" && mv "$tmp_file" "$failures_file"
219
- else
220
- # Add new failure entry
221
- jq --arg stage "$stage" \
222
- --arg pattern "$pattern" \
296
+ '.failures[$idx].times_fix_suggested = ((.failures[$idx].times_fix_suggested // 0) + 1) |
297
+ .failures[$idx].times_fix_applied = ((.failures[$idx].times_fix_applied // 0) + $app) |
298
+ .failures[$idx].times_fix_resolved = ((.failures[$idx].times_fix_resolved // 0) + $res) |
299
+ .failures[$idx].fix_effectiveness_rate = (
300
+ if ((.failures[$idx].times_fix_applied // 0) + $app) > 0 then
301
+ (((.failures[$idx].times_fix_resolved // 0) + $res) * 100 /
302
+ ((.failures[$idx].times_fix_applied // 0) + $app))
303
+ else 0 end
304
+ ) |
305
+ .failures[$idx].last_outcome_at = $ts' \
306
+ "$failures_file" > "$tmp_file" && mv "$tmp_file" "$failures_file" || rm -f "$tmp_file"
307
+ ) 200>"${failures_file}.lock"
308
+
309
+ emit_event "memory.fix_outcome" \
310
+ "pattern=${pattern_match:0:60}" \
311
+ "applied=${fix_applied}" \
312
+ "resolved=${fix_resolved}"
313
+ }
314
+
315
+ # memory_track_fix <error_sig> <success_bool>
316
+ # Convenience wrapper for memory_record_fix_outcome
317
+ memory_track_fix() {
318
+ local error_sig="${1:-}"
319
+ local success="${2:-false}"
320
+ [[ -z "$error_sig" ]] && return 0
321
+ memory_record_fix_outcome "$error_sig" "true" "$success" 2>/dev/null || true
322
+ }
323
+
324
+ # memory_query_fix_for_error <error_pattern>
325
+ # Searches failure memory for known fixes matching the given error pattern.
326
+ # Returns JSON with the best fix (highest effectiveness rate) or empty.
327
+ memory_query_fix_for_error() {
328
+ local error_pattern="$1"
329
+ [[ -z "$error_pattern" ]] && return 0
330
+
331
+ ensure_memory_dir
332
+ local mem_dir
333
+ mem_dir="$(repo_memory_dir)"
334
+ local failures_file="$mem_dir/failures.json"
335
+
336
+ [[ ! -f "$failures_file" ]] && return 0
337
+
338
+ # Search for matching failures with successful fixes
339
+ local matches
340
+ matches=$(jq -r --arg pat "$error_pattern" '
341
+ [.failures[]
342
+ | select(.pattern != null and .pattern != "")
343
+ | select(.pattern | test($pat; "i") // false)
344
+ | select(.fix != null and .fix != "")
345
+ | select((.fix_effectiveness_rate // 0) > 30)
346
+ | {fix, fix_effectiveness_rate, seen_count, category, stage, pattern}]
347
+ | sort_by(-.fix_effectiveness_rate)
348
+ | .[0] // null
349
+ ' "$failures_file" 2>/dev/null) || true
350
+
351
+ if [[ -n "$matches" && "$matches" != "null" ]]; then
352
+ echo "$matches"
353
+ fi
354
+ }
355
+
356
+ # memory_closed_loop_inject <error_sig>
357
+ # Combines error → memory → fix into injectable text for build retries.
358
+ # Returns a one-line summary suitable for goal augmentation.
359
+ memory_closed_loop_inject() {
360
+ local error_sig="$1"
361
+ [[ -z "$error_sig" ]] && return 0
362
+
363
+ local fix_json
364
+ fix_json=$(memory_query_fix_for_error "$error_sig") || true
365
+ [[ -z "$fix_json" || "$fix_json" == "null" ]] && return 0
366
+
367
+ local fix_text success_rate category
368
+ fix_text=$(echo "$fix_json" | jq -r '.fix // ""')
369
+ success_rate=$(echo "$fix_json" | jq -r '.fix_effectiveness_rate // 0')
370
+ category=$(echo "$fix_json" | jq -r '.category // "unknown"')
371
+
372
+ [[ -z "$fix_text" ]] && return 0
373
+
374
+ echo "[$category, ${success_rate}% success rate] $fix_text"
375
+ }
376
+
377
+ memory_capture_failure_from_log() {
378
+ local artifacts_dir="${1:-}"
379
+ local error_log="${artifacts_dir}/error-log.jsonl"
380
+ [[ ! -f "$error_log" ]] && return 0
381
+
382
+ ensure_memory_dir
383
+ local mem_dir
384
+ mem_dir="$(repo_memory_dir)"
385
+ local failures_file="$mem_dir/failures.json"
386
+
387
+ # Read last 50 entries
388
+ local entries
389
+ entries=$(tail -50 "$error_log" 2>/dev/null) || return 0
390
+ [[ -z "$entries" ]] && return 0
391
+
392
+ local captured=0
393
+ while IFS= read -r line; do
394
+ [[ -z "$line" ]] && continue
395
+
396
+ local err_type err_text
397
+ err_type=$(echo "$line" | jq -r '.type // "unknown"' 2>/dev/null) || continue
398
+ err_text=$(echo "$line" | jq -r '.error // ""' 2>/dev/null) || continue
399
+ [[ -z "$err_text" ]] && continue
400
+
401
+ # Deduplicate: skip if this exact pattern already exists in failures
402
+ local pattern_short
403
+ pattern_short=$(echo "$err_text" | head -1 | cut -c1-200)
404
+ local already_exists
405
+ already_exists=$(jq --arg pat "$pattern_short" \
406
+ '[.failures[] | select(.pattern == $pat)] | length' \
407
+ "$failures_file" 2>/dev/null || echo "0")
408
+ if [[ "${already_exists:-0}" -gt 0 ]]; then
409
+ continue
410
+ fi
411
+
412
+ # Feed into memory_capture_failure with the error type as stage
413
+ memory_capture_failure "$err_type" "$err_text" 2>/dev/null || true
414
+ captured=$((captured + 1))
415
+ done <<< "$entries"
416
+
417
+ if [[ "$captured" -gt 0 ]]; then
418
+ emit_event "memory.error_log_processed" "captured=$captured"
419
+ fi
420
+ }
421
+
422
+ # _memory_aggregate_global
423
+ # Promotes high-frequency failure patterns to global.json for cross-repo learning
424
+ _memory_aggregate_global() {
425
+ ensure_memory_dir
426
+ local mem_dir
427
+ mem_dir="$(repo_memory_dir)"
428
+ local failures_file="$mem_dir/failures.json"
429
+ [[ ! -f "$failures_file" ]] && return 0
430
+
431
+ local global_file="$GLOBAL_MEMORY"
432
+ [[ ! -f "$global_file" ]] && return 0
433
+
434
+ # Find patterns with seen_count >= 3
435
+ local frequent_patterns
436
+ frequent_patterns=$(jq -r '.failures[] | select(.seen_count >= 3) | .pattern' \
437
+ "$failures_file" 2>/dev/null) || return 0
438
+ [[ -z "$frequent_patterns" ]] && return 0
439
+
440
+ local promoted=0
441
+ while IFS= read -r pattern; do
442
+ [[ -z "$pattern" ]] && continue
443
+
444
+ # Check if already in global
445
+ local exists
446
+ exists=$(jq --arg p "$pattern" \
447
+ '[.common_patterns[] | select(.pattern == $p)] | length' \
448
+ "$global_file" 2>/dev/null || echo "0")
449
+ if [[ "${exists:-0}" -gt 0 ]]; then
450
+ continue
451
+ fi
452
+
453
+ # Add to global, cap at 100 entries
454
+ local tmp_global
455
+ tmp_global=$(mktemp "${global_file}.tmp.XXXXXX")
456
+ jq --arg p "$pattern" \
223
457
  --arg ts "$(now_iso)" \
224
- '.failures += [{
225
- stage: $stage,
226
- pattern: $pattern,
227
- root_cause: "",
228
- fix: "",
229
- seen_count: 1,
230
- last_seen: $ts
231
- }] | .failures = (.failures | .[-100:])' \
232
- "$failures_file" > "$tmp_file" && mv "$tmp_file" "$failures_file"
458
+ --arg cat "general" \
459
+ '.common_patterns += [{pattern: $p, promoted_at: $ts, category: $cat, source: "aggregate"}] |
460
+ .common_patterns = (.common_patterns | .[-100:])' \
461
+ "$global_file" > "$tmp_global" && mv "$tmp_global" "$global_file" || rm -f "$tmp_global"
462
+ promoted=$((promoted + 1))
463
+ done <<< "$frequent_patterns"
464
+
465
+ if [[ "$promoted" -gt 0 ]]; then
466
+ emit_event "memory.global_aggregated" "promoted=$promoted"
233
467
  fi
468
+ }
234
469
 
235
- emit_event "memory.failure" "stage=${stage}" "pattern=${pattern:0:80}"
470
+ # memory_finalize_pipeline <state_file> <artifacts_dir>
471
+ # Single call that closes multiple feedback loops at pipeline completion
472
+ memory_finalize_pipeline() {
473
+ local state_file="${1:-}"
474
+ local artifacts_dir="${2:-}"
475
+ [[ -z "$state_file" || ! -f "$state_file" ]] && return 0
476
+
477
+ # Step 1: Capture pipeline-level learnings
478
+ memory_capture_pipeline "$state_file" "$artifacts_dir" 2>/dev/null || true
479
+
480
+ # Step 2: Process error log into failures.json
481
+ memory_capture_failure_from_log "$artifacts_dir" 2>/dev/null || true
482
+
483
+ # Step 3: Aggregate high-frequency patterns to global memory
484
+ _memory_aggregate_global 2>/dev/null || true
236
485
  }
237
486
 
238
487
  # memory_analyze_failure <log_file> <stage>
@@ -281,16 +530,41 @@ memory_analyze_failure() {
281
530
 
282
531
  info "Analyzing failure in ${CYAN}${stage}${RESET} stage..."
283
532
 
533
+ # Gather past successful analyses for the same stage/category as examples
534
+ local past_examples=""
535
+ if [[ -f "$failures_file" ]]; then
536
+ past_examples=$(jq -r --arg stg "$stage" \
537
+ '[.failures[] | select(.stage == $stg and .root_cause != "" and .fix != "")] |
538
+ sort_by(-.fix_effectiveness_rate // 0) | .[:2][] |
539
+ "- Pattern: \(.pattern[:80])\n Root cause: \(.root_cause)\n Fix: \(.fix)"' \
540
+ "$failures_file" 2>/dev/null || true)
541
+ fi
542
+
543
+ # Build valid categories list (from compat.sh if available, else hardcoded)
544
+ local valid_cats="test_failure, build_error, lint_error, timeout, dependency, flaky, config"
545
+ if [[ -n "${SW_ERROR_CATEGORIES:-}" ]]; then
546
+ valid_cats=$(echo "$SW_ERROR_CATEGORIES" | tr ' ' ', ')
547
+ fi
548
+
284
549
  # Build the analysis prompt
285
550
  local prompt
286
551
  prompt="Analyze this pipeline failure. The stage was: ${stage}.
287
552
  The error pattern is: ${last_pattern}
288
553
 
289
554
  Log output (last 200 lines):
290
- ${log_tail}
555
+ ${log_tail}"
556
+
557
+ if [[ -n "$past_examples" ]]; then
558
+ prompt="${prompt}
559
+
560
+ Here are examples of how similar failures were diagnosed in this repo:
561
+ ${past_examples}"
562
+ fi
563
+
564
+ prompt="${prompt}
291
565
 
292
566
  Return ONLY a JSON object with exactly these fields:
293
- {\"root_cause\": \"one-line root cause\", \"fix\": \"one-line fix suggestion\", \"category\": \"one of: test_failure, build_error, lint_error, timeout, dependency, flaky, config\"}
567
+ {\"root_cause\": \"one-line root cause\", \"fix\": \"one-line fix suggestion\", \"category\": \"one of: ${valid_cats}\"}
294
568
 
295
569
  Return JSON only, no markdown fences, no explanation."
296
570
 
@@ -315,11 +589,17 @@ Return JSON only, no markdown fences, no explanation."
315
589
  return 1
316
590
  fi
317
591
 
318
- # Validate category against allowed values
319
- case "$category" in
320
- test_failure|build_error|lint_error|timeout|dependency|flaky|config) ;;
321
- *) category="unknown" ;;
322
- esac
592
+ # Validate category against shared taxonomy (compat.sh) or built-in list
593
+ if type sw_valid_error_category &>/dev/null 2>&1; then
594
+ if ! sw_valid_error_category "$category"; then
595
+ category="unknown"
596
+ fi
597
+ else
598
+ case "$category" in
599
+ test_failure|build_error|lint_error|timeout|dependency|flaky|config) ;;
600
+ *) category="unknown" ;;
601
+ esac
602
+ fi
323
603
 
324
604
  # Update the most recent failure entry with root_cause, fix, category
325
605
  local tmp_file
@@ -478,9 +758,27 @@ memory_capture_pattern() {
478
758
 
479
759
  # memory_inject_context <stage_id>
480
760
  # Returns a text block of relevant memory for a given pipeline stage.
761
+ # When intelligence engine is available, uses AI-ranked search for better relevance.
481
762
  memory_inject_context() {
482
763
  local stage_id="${1:-}"
483
764
 
765
+ # Try intelligence-ranked search first
766
+ if type intelligence_search_memory &>/dev/null 2>&1; then
767
+ local config="${REPO_DIR:-.}/.claude/daemon-config.json"
768
+ local intel_enabled="false"
769
+ if [[ -f "$config" ]]; then
770
+ intel_enabled=$(jq -r '.intelligence.enabled // false' "$config" 2>/dev/null || echo "false")
771
+ fi
772
+ if [[ "$intel_enabled" == "true" ]]; then
773
+ local ranked_result
774
+ ranked_result=$(intelligence_search_memory "$stage_id stage context" "$(repo_memory_dir)" 5 2>/dev/null || echo "")
775
+ if [[ -n "$ranked_result" ]] && [[ "$ranked_result" != *'"error"'* ]]; then
776
+ echo "$ranked_result"
777
+ return 0
778
+ fi
779
+ fi
780
+ fi
781
+
484
782
  ensure_memory_dir
485
783
  local mem_dir
486
784
  mem_dir="$(repo_memory_dir)"
@@ -537,12 +835,28 @@ memory_inject_context() {
537
835
  ;;
538
836
 
539
837
  build)
540
- # Failure patterns to avoid + code conventions
838
+ # Failure patterns to avoid — ranked by relevance (recency + effectiveness + frequency)
541
839
  echo "## Failure Patterns to Avoid"
542
840
  if [[ -f "$mem_dir/failures.json" ]]; then
543
- jq -r '.failures | sort_by(-.seen_count) | .[:10][] |
841
+ jq -r 'now as $now |
842
+ .failures | map(. +
843
+ { relevance_score:
844
+ ((.seen_count // 1) * 1) +
845
+ (if .fix_effectiveness_rate then (.fix_effectiveness_rate / 10) else 0 end) +
846
+ (if .last_seen then
847
+ (($now - ((.last_seen | sub("\\.[0-9]+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) // 0)) |
848
+ if . < 86400 then 5
849
+ elif . < 604800 then 3
850
+ elif . < 2592000 then 1
851
+ else 0 end)
852
+ else 0 end)
853
+ }
854
+ ) | sort_by(-.relevance_score) | .[:10][] |
544
855
  "- [\(.stage)] \(.pattern) (seen \(.seen_count)x)" +
545
- if .fix != "" then "\n Fix: \(.fix)" else "" end' \
856
+ if .fix != "" then
857
+ "\n Fix: \(.fix)" +
858
+ if .fix_effectiveness_rate then " (effectiveness: \(.fix_effectiveness_rate)%)" else "" end
859
+ else "" end' \
546
860
  "$mem_dir/failures.json" 2>/dev/null || echo "- No failures recorded."
547
861
  fi
548
862
 
@@ -550,7 +864,8 @@ memory_inject_context() {
550
864
  echo "## Known Fixes"
551
865
  if [[ -f "$mem_dir/failures.json" ]]; then
552
866
  jq -r '.failures[] | select(.root_cause != "" and .fix != "" and .stage == "build") |
553
- "- [\(.category // "unknown")] \(.root_cause)\n Fix: \(.fix)"' \
867
+ "- [\(.category // "unknown")] \(.root_cause)\n Fix: \(.fix)" +
868
+ if .fix_effectiveness_rate then " (effectiveness: \(.fix_effectiveness_rate)%)" else "" end' \
554
869
  "$mem_dir/failures.json" 2>/dev/null || echo "- No analyzed fixes yet."
555
870
  else
556
871
  echo "- No analyzed fixes yet."
@@ -618,19 +933,150 @@ memory_inject_context() {
618
933
  ;;
619
934
 
620
935
  *)
621
- # Generic context for any other stage
936
+ # Generic context for any stage — inject top-K most relevant across all categories
622
937
  echo "## Repository Patterns"
623
938
  if [[ -f "$mem_dir/patterns.json" ]]; then
624
939
  jq -r 'to_entries | map(select(.key != "known_issues")) | from_entries' \
625
940
  "$mem_dir/patterns.json" 2>/dev/null || true
626
941
  fi
942
+
943
+ # Inject top failures regardless of category (ranked by relevance)
944
+ echo ""
945
+ echo "## Relevant Failure Patterns"
946
+ if [[ -f "$mem_dir/failures.json" ]]; then
947
+ jq -r --arg stg "$stage_id" \
948
+ '.failures |
949
+ map(. + { stage_match: (if .stage == $stg then 10 else 0 end) }) |
950
+ sort_by(-(.seen_count + .stage_match + (.fix_effectiveness_rate // 0) / 10)) |
951
+ .[:5][] |
952
+ "- [\(.stage)] \(.pattern[:80]) (seen \(.seen_count)x)" +
953
+ if .fix != "" then "\n Fix: \(.fix)" else "" end' \
954
+ "$mem_dir/failures.json" 2>/dev/null || echo "- None recorded."
955
+ fi
956
+
957
+ # Inject recent decisions
958
+ echo ""
959
+ echo "## Recent Decisions"
960
+ if [[ -f "$mem_dir/decisions.json" ]]; then
961
+ jq -r '.decisions[-3:][] |
962
+ "- [\(.type // "decision")] \(.summary // "no description")"' \
963
+ "$mem_dir/decisions.json" 2>/dev/null || echo "- None recorded."
964
+ fi
627
965
  ;;
628
966
  esac
629
967
 
968
+ # ── Cross-repo memory injection (global learnings) ──
969
+ if [[ -f "$GLOBAL_MEMORY" ]]; then
970
+ local global_patterns
971
+ global_patterns=$(jq -r --arg stage "$stage_id" '
972
+ .common_patterns // [] | .[] |
973
+ select(.category == $stage or .category == "general" or .category == null) |
974
+ .summary // .description // empty
975
+ ' "$GLOBAL_MEMORY" 2>/dev/null | head -5 || true)
976
+
977
+ local cross_repo_learnings
978
+ cross_repo_learnings=$(jq -r '
979
+ .cross_repo_learnings // [] | .[-5:][] |
980
+ "- [\(.repo // "unknown")] \(.type // "learning"): bugs=\(.bugs // 0), warnings=\(.warnings // 0)"
981
+ ' "$GLOBAL_MEMORY" 2>/dev/null | head -5 || true)
982
+
983
+ if [[ -n "$global_patterns" || -n "$cross_repo_learnings" ]]; then
984
+ echo ""
985
+ echo "## Cross-Repo Learnings (Global)"
986
+ [[ -n "$global_patterns" ]] && echo "$global_patterns"
987
+ [[ -n "$cross_repo_learnings" ]] && echo "$cross_repo_learnings"
988
+ fi
989
+ fi
990
+
630
991
  echo ""
631
992
  emit_event "memory.inject" "stage=${stage_id}"
632
993
  }
633
994
 
995
+ # memory_get_actionable_failures [threshold]
996
+ # Returns JSON array of failure patterns with seen_count >= threshold.
997
+ # Used by daemon patrol to detect recurring failures worth fixing.
998
+ memory_get_actionable_failures() {
999
+ local threshold="${1:-3}"
1000
+
1001
+ ensure_memory_dir
1002
+ local mem_dir
1003
+ mem_dir="$(repo_memory_dir)"
1004
+ local failures_file="$mem_dir/failures.json"
1005
+
1006
+ if [[ ! -f "$failures_file" ]]; then
1007
+ echo "[]"
1008
+ return 0
1009
+ fi
1010
+
1011
+ jq --argjson t "$threshold" \
1012
+ '[.failures[] | select(.seen_count >= $t)] | sort_by(-.seen_count)' \
1013
+ "$failures_file" 2>/dev/null || echo "[]"
1014
+ }
1015
+
1016
+ # memory_get_dora_baseline [window_days] [offset_days]
1017
+ # Calculates DORA metrics for a time window from events.jsonl.
1018
+ # Returns JSON: {deploy_freq, cycle_time, cfr, mttr, total, grades: {df, ct, cfr, mttr}}
1019
+ memory_get_dora_baseline() {
1020
+ local window_days="${1:-7}"
1021
+ local offset_days="${2:-0}"
1022
+
1023
+ local events_file="${HOME}/.shipwright/events.jsonl"
1024
+ if [[ ! -f "$events_file" ]]; then
1025
+ echo '{"deploy_freq":0,"cycle_time":0,"cfr":0,"mttr":0,"total":0}'
1026
+ return 0
1027
+ fi
1028
+
1029
+ local now_e
1030
+ now_e=$(now_epoch)
1031
+ local window_end=$((now_e - offset_days * 86400))
1032
+ local window_start=$((window_end - window_days * 86400))
1033
+
1034
+ # Extract pipeline events for the window
1035
+ local metrics
1036
+ metrics=$(jq -s --argjson start "$window_start" --argjson end "$window_end" '
1037
+ [.[] | select(.ts_epoch >= $start and .ts_epoch < $end)] as $events |
1038
+ [$events[] | select(.type == "pipeline.completed")] as $completed |
1039
+ ($completed | length) as $total |
1040
+ [$completed[] | select(.result == "success")] as $successes |
1041
+ [$completed[] | select(.result == "failure")] as $failures |
1042
+ ($successes | length) as $success_count |
1043
+ ($failures | length) as $failure_count |
1044
+
1045
+ # Deploy frequency (per week)
1046
+ (if $total > 0 then ($success_count * 7 / '"$window_days"') else 0 end) as $deploy_freq |
1047
+
1048
+ # Cycle time median
1049
+ ([$successes[] | .duration_s] | sort |
1050
+ if length > 0 then .[length/2 | floor] else 0 end) as $cycle_time |
1051
+
1052
+ # Change failure rate
1053
+ (if $total > 0 then ($failure_count / $total * 100) else 0 end) as $cfr |
1054
+
1055
+ # MTTR
1056
+ ($completed | sort_by(.ts_epoch // 0) |
1057
+ [range(length) as $i |
1058
+ if .[$i].result == "failure" then
1059
+ [.[$i+1:][] | select(.result == "success")][0] as $next |
1060
+ if $next and $next.ts_epoch and .[$i].ts_epoch then
1061
+ ($next.ts_epoch - .[$i].ts_epoch)
1062
+ else null end
1063
+ else null end
1064
+ ] | map(select(. != null)) |
1065
+ if length > 0 then (add / length | floor) else 0 end
1066
+ ) as $mttr |
1067
+
1068
+ {
1069
+ deploy_freq: ($deploy_freq * 10 | floor / 10),
1070
+ cycle_time: $cycle_time,
1071
+ cfr: ($cfr * 10 | floor / 10),
1072
+ mttr: $mttr,
1073
+ total: $total
1074
+ }
1075
+ ' "$events_file" 2>/dev/null || echo '{"deploy_freq":0,"cycle_time":0,"cfr":0,"mttr":0,"total":0}')
1076
+
1077
+ echo "$metrics"
1078
+ }
1079
+
634
1080
  # memory_update_metrics <metric_name> <value>
635
1081
  # Track performance baselines and flag regressions.
636
1082
  memory_update_metrics() {
@@ -838,6 +1284,34 @@ memory_search() {
838
1284
 
839
1285
  local found=0
840
1286
 
1287
+ # ── Semantic search via intelligence (if available) ──
1288
+ if type intelligence_search_memory &>/dev/null 2>&1; then
1289
+ local semantic_results
1290
+ semantic_results=$(intelligence_search_memory "$keyword" "$mem_dir" 5 2>/dev/null || echo "")
1291
+ if [[ -n "$semantic_results" ]] && echo "$semantic_results" | jq -e '.results | length > 0' &>/dev/null; then
1292
+ echo -e " ${BOLD}${CYAN}Semantic Results (AI-ranked):${RESET}"
1293
+ local result_count
1294
+ result_count=$(echo "$semantic_results" | jq '.results | length')
1295
+ local i=0
1296
+ while [[ "$i" -lt "$result_count" ]]; do
1297
+ local file rel summary
1298
+ file=$(echo "$semantic_results" | jq -r ".results[$i].file // \"\"")
1299
+ rel=$(echo "$semantic_results" | jq -r ".results[$i].relevance // 0")
1300
+ summary=$(echo "$semantic_results" | jq -r ".results[$i].summary // \"\"")
1301
+ echo -e " ${GREEN}●${RESET} [${rel}%] ${BOLD}${file}${RESET} — ${summary}"
1302
+ i=$((i + 1))
1303
+ done
1304
+ echo ""
1305
+ found=$((found + 1))
1306
+
1307
+ # Also run grep search below for completeness
1308
+ echo -e " ${DIM}Grep results (supplemental):${RESET}"
1309
+ echo ""
1310
+ fi
1311
+ fi
1312
+
1313
+ # ── Grep-based search (fallback / supplemental) ──
1314
+
841
1315
  # Search patterns
842
1316
  if [[ -f "$mem_dir/patterns.json" ]]; then
843
1317
  local pattern_matches
@@ -1083,6 +1557,7 @@ show_help() {
1083
1557
  echo -e " ${CYAN}metric${RESET} <name> <value> Update a performance baseline"
1084
1558
  echo -e " ${CYAN}decision${RESET} <type> <summary> Record a design decision"
1085
1559
  echo -e " ${CYAN}analyze-failure${RESET} <log> <stage> Analyze failure root cause via AI"
1560
+ echo -e " ${CYAN}fix-outcome${RESET} <pattern> <applied> <resolved> Record fix effectiveness"
1086
1561
  echo ""
1087
1562
  echo -e "${BOLD}EXAMPLES${RESET}"
1088
1563
  echo -e " ${DIM}shipwright memory show${RESET} # View repo memory"
@@ -1136,6 +1611,9 @@ case "$SUBCOMMAND" in
1136
1611
  analyze-failure)
1137
1612
  memory_analyze_failure "$@"
1138
1613
  ;;
1614
+ fix-outcome)
1615
+ memory_record_fix_outcome "$@"
1616
+ ;;
1139
1617
  help|--help|-h)
1140
1618
  show_help
1141
1619
  ;;