shipwright-cli 1.7.0 → 1.9.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 (106) 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 +38 -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} +109 -21
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +390 -0
  33. package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
  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/sw-init.sh +522 -0
  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/{cct-loop.sh → sw-loop.sh} +534 -44
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
  58. package/scripts/sw-predictive.sh +820 -0
  59. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  60. package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
  61. package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
  62. package/scripts/sw-remote.sh +687 -0
  63. package/scripts/sw-self-optimize.sh +947 -0
  64. package/scripts/sw-session.sh +519 -0
  65. package/scripts/sw-setup.sh +234 -0
  66. package/scripts/sw-status.sh +605 -0
  67. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  68. package/scripts/sw-tmux.sh +591 -0
  69. package/scripts/sw-tracker-jira.sh +277 -0
  70. package/scripts/sw-tracker-linear.sh +292 -0
  71. package/scripts/sw-tracker.sh +409 -0
  72. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  73. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  74. package/templates/pipelines/autonomous.json +27 -5
  75. package/templates/pipelines/full.json +12 -0
  76. package/templates/pipelines/standard.json +12 -0
  77. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  78. package/tmux/templates/accessibility.json +34 -0
  79. package/tmux/templates/api-design.json +35 -0
  80. package/tmux/templates/architecture.json +1 -0
  81. package/tmux/templates/bug-fix.json +9 -0
  82. package/tmux/templates/code-review.json +1 -0
  83. package/tmux/templates/compliance.json +36 -0
  84. package/tmux/templates/data-pipeline.json +36 -0
  85. package/tmux/templates/debt-paydown.json +34 -0
  86. package/tmux/templates/devops.json +1 -0
  87. package/tmux/templates/documentation.json +1 -0
  88. package/tmux/templates/exploration.json +1 -0
  89. package/tmux/templates/feature-dev.json +1 -0
  90. package/tmux/templates/full-stack.json +8 -0
  91. package/tmux/templates/i18n.json +34 -0
  92. package/tmux/templates/incident-response.json +36 -0
  93. package/tmux/templates/migration.json +1 -0
  94. package/tmux/templates/observability.json +35 -0
  95. package/tmux/templates/onboarding.json +33 -0
  96. package/tmux/templates/performance.json +35 -0
  97. package/tmux/templates/refactor.json +1 -0
  98. package/tmux/templates/release.json +35 -0
  99. package/tmux/templates/security-audit.json +8 -0
  100. package/tmux/templates/spike.json +34 -0
  101. package/tmux/templates/testing.json +1 -0
  102. package/tmux/tmux.conf +98 -9
  103. package/scripts/cct-doctor.sh +0 -328
  104. package/scripts/cct-init.sh +0 -282
  105. package/scripts/cct-session.sh +0 -284
  106. 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.0"
9
+ VERSION="1.9.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,100 @@ 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"; }
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"
212
248
 
213
- if [[ "$existing_idx" != "-1" && "$existing_idx" != "null" ]]; then
214
- # Update existing entry
215
- jq --argjson idx "$existing_idx" \
216
- --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" \
223
- --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"
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"
266
+
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
233
278
  fi
234
279
 
235
- emit_event "memory.failure" "stage=${stage}" "pattern=${pattern:0:80}"
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"; }
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" \
295
+ --arg ts "$(now_iso)" \
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}"
236
313
  }
237
314
 
238
315
  # memory_analyze_failure <log_file> <stage>
@@ -281,16 +358,41 @@ memory_analyze_failure() {
281
358
 
282
359
  info "Analyzing failure in ${CYAN}${stage}${RESET} stage..."
283
360
 
361
+ # Gather past successful analyses for the same stage/category as examples
362
+ local past_examples=""
363
+ if [[ -f "$failures_file" ]]; then
364
+ past_examples=$(jq -r --arg stg "$stage" \
365
+ '[.failures[] | select(.stage == $stg and .root_cause != "" and .fix != "")] |
366
+ sort_by(-.fix_effectiveness_rate // 0) | .[:2][] |
367
+ "- Pattern: \(.pattern[:80])\n Root cause: \(.root_cause)\n Fix: \(.fix)"' \
368
+ "$failures_file" 2>/dev/null || true)
369
+ fi
370
+
371
+ # Build valid categories list (from compat.sh if available, else hardcoded)
372
+ local valid_cats="test_failure, build_error, lint_error, timeout, dependency, flaky, config"
373
+ if [[ -n "${SW_ERROR_CATEGORIES:-}" ]]; then
374
+ valid_cats=$(echo "$SW_ERROR_CATEGORIES" | tr ' ' ', ')
375
+ fi
376
+
284
377
  # Build the analysis prompt
285
378
  local prompt
286
379
  prompt="Analyze this pipeline failure. The stage was: ${stage}.
287
380
  The error pattern is: ${last_pattern}
288
381
 
289
382
  Log output (last 200 lines):
290
- ${log_tail}
383
+ ${log_tail}"
384
+
385
+ if [[ -n "$past_examples" ]]; then
386
+ prompt="${prompt}
387
+
388
+ Here are examples of how similar failures were diagnosed in this repo:
389
+ ${past_examples}"
390
+ fi
391
+
392
+ prompt="${prompt}
291
393
 
292
394
  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\"}
395
+ {\"root_cause\": \"one-line root cause\", \"fix\": \"one-line fix suggestion\", \"category\": \"one of: ${valid_cats}\"}
294
396
 
295
397
  Return JSON only, no markdown fences, no explanation."
296
398
 
@@ -315,11 +417,17 @@ Return JSON only, no markdown fences, no explanation."
315
417
  return 1
316
418
  fi
317
419
 
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
420
+ # Validate category against shared taxonomy (compat.sh) or built-in list
421
+ if type sw_valid_error_category &>/dev/null 2>&1; then
422
+ if ! sw_valid_error_category "$category"; then
423
+ category="unknown"
424
+ fi
425
+ else
426
+ case "$category" in
427
+ test_failure|build_error|lint_error|timeout|dependency|flaky|config) ;;
428
+ *) category="unknown" ;;
429
+ esac
430
+ fi
323
431
 
324
432
  # Update the most recent failure entry with root_cause, fix, category
325
433
  local tmp_file
@@ -478,9 +586,27 @@ memory_capture_pattern() {
478
586
 
479
587
  # memory_inject_context <stage_id>
480
588
  # Returns a text block of relevant memory for a given pipeline stage.
589
+ # When intelligence engine is available, uses AI-ranked search for better relevance.
481
590
  memory_inject_context() {
482
591
  local stage_id="${1:-}"
483
592
 
593
+ # Try intelligence-ranked search first
594
+ if type intelligence_search_memory &>/dev/null 2>&1; then
595
+ local config="${REPO_DIR:-.}/.claude/daemon-config.json"
596
+ local intel_enabled="false"
597
+ if [[ -f "$config" ]]; then
598
+ intel_enabled=$(jq -r '.intelligence.enabled // false' "$config" 2>/dev/null || echo "false")
599
+ fi
600
+ if [[ "$intel_enabled" == "true" ]]; then
601
+ local ranked_result
602
+ ranked_result=$(intelligence_search_memory "$stage_id stage context" "$(repo_memory_dir)" 5 2>/dev/null || echo "")
603
+ if [[ -n "$ranked_result" ]] && [[ "$ranked_result" != *'"error"'* ]]; then
604
+ echo "$ranked_result"
605
+ return 0
606
+ fi
607
+ fi
608
+ fi
609
+
484
610
  ensure_memory_dir
485
611
  local mem_dir
486
612
  mem_dir="$(repo_memory_dir)"
@@ -537,12 +663,28 @@ memory_inject_context() {
537
663
  ;;
538
664
 
539
665
  build)
540
- # Failure patterns to avoid + code conventions
666
+ # Failure patterns to avoid — ranked by relevance (recency + effectiveness + frequency)
541
667
  echo "## Failure Patterns to Avoid"
542
668
  if [[ -f "$mem_dir/failures.json" ]]; then
543
- jq -r '.failures | sort_by(-.seen_count) | .[:10][] |
669
+ jq -r 'now as $now |
670
+ .failures | map(. +
671
+ { relevance_score:
672
+ ((.seen_count // 1) * 1) +
673
+ (if .fix_effectiveness_rate then (.fix_effectiveness_rate / 10) else 0 end) +
674
+ (if .last_seen then
675
+ (($now - ((.last_seen | sub("\\.[0-9]+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) // 0)) |
676
+ if . < 86400 then 5
677
+ elif . < 604800 then 3
678
+ elif . < 2592000 then 1
679
+ else 0 end)
680
+ else 0 end)
681
+ }
682
+ ) | sort_by(-.relevance_score) | .[:10][] |
544
683
  "- [\(.stage)] \(.pattern) (seen \(.seen_count)x)" +
545
- if .fix != "" then "\n Fix: \(.fix)" else "" end' \
684
+ if .fix != "" then
685
+ "\n Fix: \(.fix)" +
686
+ if .fix_effectiveness_rate then " (effectiveness: \(.fix_effectiveness_rate)%)" else "" end
687
+ else "" end' \
546
688
  "$mem_dir/failures.json" 2>/dev/null || echo "- No failures recorded."
547
689
  fi
548
690
 
@@ -550,7 +692,8 @@ memory_inject_context() {
550
692
  echo "## Known Fixes"
551
693
  if [[ -f "$mem_dir/failures.json" ]]; then
552
694
  jq -r '.failures[] | select(.root_cause != "" and .fix != "" and .stage == "build") |
553
- "- [\(.category // "unknown")] \(.root_cause)\n Fix: \(.fix)"' \
695
+ "- [\(.category // "unknown")] \(.root_cause)\n Fix: \(.fix)" +
696
+ if .fix_effectiveness_rate then " (effectiveness: \(.fix_effectiveness_rate)%)" else "" end' \
554
697
  "$mem_dir/failures.json" 2>/dev/null || echo "- No analyzed fixes yet."
555
698
  else
556
699
  echo "- No analyzed fixes yet."
@@ -618,12 +761,35 @@ memory_inject_context() {
618
761
  ;;
619
762
 
620
763
  *)
621
- # Generic context for any other stage
764
+ # Generic context for any stage — inject top-K most relevant across all categories
622
765
  echo "## Repository Patterns"
623
766
  if [[ -f "$mem_dir/patterns.json" ]]; then
624
767
  jq -r 'to_entries | map(select(.key != "known_issues")) | from_entries' \
625
768
  "$mem_dir/patterns.json" 2>/dev/null || true
626
769
  fi
770
+
771
+ # Inject top failures regardless of category (ranked by relevance)
772
+ echo ""
773
+ echo "## Relevant Failure Patterns"
774
+ if [[ -f "$mem_dir/failures.json" ]]; then
775
+ jq -r --arg stg "$stage_id" \
776
+ '.failures |
777
+ map(. + { stage_match: (if .stage == $stg then 10 else 0 end) }) |
778
+ sort_by(-(.seen_count + .stage_match + (.fix_effectiveness_rate // 0) / 10)) |
779
+ .[:5][] |
780
+ "- [\(.stage)] \(.pattern[:80]) (seen \(.seen_count)x)" +
781
+ if .fix != "" then "\n Fix: \(.fix)" else "" end' \
782
+ "$mem_dir/failures.json" 2>/dev/null || echo "- None recorded."
783
+ fi
784
+
785
+ # Inject recent decisions
786
+ echo ""
787
+ echo "## Recent Decisions"
788
+ if [[ -f "$mem_dir/decisions.json" ]]; then
789
+ jq -r '.decisions[-3:][] |
790
+ "- [\(.type // "decision")] \(.summary // "no description")"' \
791
+ "$mem_dir/decisions.json" 2>/dev/null || echo "- None recorded."
792
+ fi
627
793
  ;;
628
794
  esac
629
795
 
@@ -631,6 +797,91 @@ memory_inject_context() {
631
797
  emit_event "memory.inject" "stage=${stage_id}"
632
798
  }
633
799
 
800
+ # memory_get_actionable_failures [threshold]
801
+ # Returns JSON array of failure patterns with seen_count >= threshold.
802
+ # Used by daemon patrol to detect recurring failures worth fixing.
803
+ memory_get_actionable_failures() {
804
+ local threshold="${1:-3}"
805
+
806
+ ensure_memory_dir
807
+ local mem_dir
808
+ mem_dir="$(repo_memory_dir)"
809
+ local failures_file="$mem_dir/failures.json"
810
+
811
+ if [[ ! -f "$failures_file" ]]; then
812
+ echo "[]"
813
+ return 0
814
+ fi
815
+
816
+ jq --argjson t "$threshold" \
817
+ '[.failures[] | select(.seen_count >= $t)] | sort_by(-.seen_count)' \
818
+ "$failures_file" 2>/dev/null || echo "[]"
819
+ }
820
+
821
+ # memory_get_dora_baseline [window_days] [offset_days]
822
+ # Calculates DORA metrics for a time window from events.jsonl.
823
+ # Returns JSON: {deploy_freq, cycle_time, cfr, mttr, total, grades: {df, ct, cfr, mttr}}
824
+ memory_get_dora_baseline() {
825
+ local window_days="${1:-7}"
826
+ local offset_days="${2:-0}"
827
+
828
+ local events_file="${HOME}/.shipwright/events.jsonl"
829
+ if [[ ! -f "$events_file" ]]; then
830
+ echo '{"deploy_freq":0,"cycle_time":0,"cfr":0,"mttr":0,"total":0}'
831
+ return 0
832
+ fi
833
+
834
+ local now_e
835
+ now_e=$(now_epoch)
836
+ local window_end=$((now_e - offset_days * 86400))
837
+ local window_start=$((window_end - window_days * 86400))
838
+
839
+ # Extract pipeline events for the window
840
+ local metrics
841
+ metrics=$(jq -s --argjson start "$window_start" --argjson end "$window_end" '
842
+ [.[] | select(.ts_epoch >= $start and .ts_epoch < $end)] as $events |
843
+ [$events[] | select(.type == "pipeline.completed")] as $completed |
844
+ ($completed | length) as $total |
845
+ [$completed[] | select(.result == "success")] as $successes |
846
+ [$completed[] | select(.result == "failure")] as $failures |
847
+ ($successes | length) as $success_count |
848
+ ($failures | length) as $failure_count |
849
+
850
+ # Deploy frequency (per week)
851
+ (if $total > 0 then ($success_count * 7 / '"$window_days"') else 0 end) as $deploy_freq |
852
+
853
+ # Cycle time median
854
+ ([$successes[] | .duration_s] | sort |
855
+ if length > 0 then .[length/2 | floor] else 0 end) as $cycle_time |
856
+
857
+ # Change failure rate
858
+ (if $total > 0 then ($failure_count / $total * 100) else 0 end) as $cfr |
859
+
860
+ # MTTR
861
+ ($completed | sort_by(.ts_epoch // 0) |
862
+ [range(length) as $i |
863
+ if .[$i].result == "failure" then
864
+ [.[$i+1:][] | select(.result == "success")][0] as $next |
865
+ if $next and $next.ts_epoch and .[$i].ts_epoch then
866
+ ($next.ts_epoch - .[$i].ts_epoch)
867
+ else null end
868
+ else null end
869
+ ] | map(select(. != null)) |
870
+ if length > 0 then (add / length | floor) else 0 end
871
+ ) as $mttr |
872
+
873
+ {
874
+ deploy_freq: ($deploy_freq * 10 | floor / 10),
875
+ cycle_time: $cycle_time,
876
+ cfr: ($cfr * 10 | floor / 10),
877
+ mttr: $mttr,
878
+ total: $total
879
+ }
880
+ ' "$events_file" 2>/dev/null || echo '{"deploy_freq":0,"cycle_time":0,"cfr":0,"mttr":0,"total":0}')
881
+
882
+ echo "$metrics"
883
+ }
884
+
634
885
  # memory_update_metrics <metric_name> <value>
635
886
  # Track performance baselines and flag regressions.
636
887
  memory_update_metrics() {
@@ -838,6 +1089,34 @@ memory_search() {
838
1089
 
839
1090
  local found=0
840
1091
 
1092
+ # ── Semantic search via intelligence (if available) ──
1093
+ if type intelligence_search_memory &>/dev/null 2>&1; then
1094
+ local semantic_results
1095
+ semantic_results=$(intelligence_search_memory "$keyword" "$mem_dir" 5 2>/dev/null || echo "")
1096
+ if [[ -n "$semantic_results" ]] && echo "$semantic_results" | jq -e '.results | length > 0' &>/dev/null; then
1097
+ echo -e " ${BOLD}${CYAN}Semantic Results (AI-ranked):${RESET}"
1098
+ local result_count
1099
+ result_count=$(echo "$semantic_results" | jq '.results | length')
1100
+ local i=0
1101
+ while [[ "$i" -lt "$result_count" ]]; do
1102
+ local file rel summary
1103
+ file=$(echo "$semantic_results" | jq -r ".results[$i].file // \"\"")
1104
+ rel=$(echo "$semantic_results" | jq -r ".results[$i].relevance // 0")
1105
+ summary=$(echo "$semantic_results" | jq -r ".results[$i].summary // \"\"")
1106
+ echo -e " ${GREEN}●${RESET} [${rel}%] ${BOLD}${file}${RESET} — ${summary}"
1107
+ i=$((i + 1))
1108
+ done
1109
+ echo ""
1110
+ found=$((found + 1))
1111
+
1112
+ # Also run grep search below for completeness
1113
+ echo -e " ${DIM}Grep results (supplemental):${RESET}"
1114
+ echo ""
1115
+ fi
1116
+ fi
1117
+
1118
+ # ── Grep-based search (fallback / supplemental) ──
1119
+
841
1120
  # Search patterns
842
1121
  if [[ -f "$mem_dir/patterns.json" ]]; then
843
1122
  local pattern_matches
@@ -1083,6 +1362,7 @@ show_help() {
1083
1362
  echo -e " ${CYAN}metric${RESET} <name> <value> Update a performance baseline"
1084
1363
  echo -e " ${CYAN}decision${RESET} <type> <summary> Record a design decision"
1085
1364
  echo -e " ${CYAN}analyze-failure${RESET} <log> <stage> Analyze failure root cause via AI"
1365
+ echo -e " ${CYAN}fix-outcome${RESET} <pattern> <applied> <resolved> Record fix effectiveness"
1086
1366
  echo ""
1087
1367
  echo -e "${BOLD}EXAMPLES${RESET}"
1088
1368
  echo -e " ${DIM}shipwright memory show${RESET} # View repo memory"
@@ -1136,6 +1416,9 @@ case "$SUBCOMMAND" in
1136
1416
  analyze-failure)
1137
1417
  memory_analyze_failure "$@"
1138
1418
  ;;
1419
+ fix-outcome)
1420
+ memory_record_fix_outcome "$@"
1421
+ ;;
1139
1422
  help|--help|-h)
1140
1423
  show_help
1141
1424
  ;;