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
@@ -11,7 +11,7 @@ unset CLAUDECODE 2>/dev/null || true
11
11
  # Ignore SIGHUP so tmux attach/detach doesn't kill long-running plan/design/review stages
12
12
  trap '' HUP
13
13
 
14
- VERSION="2.4.0"
14
+ VERSION="3.1.0"
15
15
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
16
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
17
17
 
@@ -21,6 +21,7 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
21
21
  # Canonical helpers (colors, output, events)
22
22
  # shellcheck source=lib/helpers.sh
23
23
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
24
+ [[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
24
25
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
25
26
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
26
27
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -30,23 +31,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
30
31
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
31
32
  now_epoch() { date +%s; }
32
33
  fi
33
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
- emit_event() {
35
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
36
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
37
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
38
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
39
- }
40
- fi
41
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
42
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
43
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
44
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
45
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
46
- RED="${RED:-\033[38;2;248;113;113m}"
47
- DIM="${DIM:-\033[2m}"
48
- BOLD="${BOLD:-\033[1m}"
49
- RESET="${RESET:-\033[0m}"
50
34
  # Policy + pipeline quality thresholds (config/policy.json via lib/pipeline-quality.sh)
51
35
  [[ -f "$SCRIPT_DIR/lib/pipeline-quality.sh" ]] && source "$SCRIPT_DIR/lib/pipeline-quality.sh"
52
36
  # shellcheck source=lib/pipeline-state.sh
@@ -107,6 +91,14 @@ fi
107
91
  if [[ -f "$SCRIPT_DIR/sw-durable.sh" ]]; then
108
92
  source "$SCRIPT_DIR/sw-durable.sh"
109
93
  fi
94
+ # shellcheck source=sw-db.sh — for db_save_checkpoint/db_load_checkpoint (durable workflows)
95
+ [[ -f "$SCRIPT_DIR/sw-db.sh" ]] && source "$SCRIPT_DIR/sw-db.sh"
96
+ # Ensure DB schema exists so emit_event → db_add_event can write rows (CREATE IF NOT EXISTS is idempotent)
97
+ if type init_schema >/dev/null 2>&1 && type check_sqlite3 >/dev/null 2>&1 && check_sqlite3 2>/dev/null; then
98
+ init_schema 2>/dev/null || true
99
+ fi
100
+ # shellcheck source=sw-cost.sh — for cost_record persistence to costs.json + DB
101
+ [[ -f "$SCRIPT_DIR/sw-cost.sh" ]] && source "$SCRIPT_DIR/sw-cost.sh"
110
102
 
111
103
  # ─── GitHub API Modules (optional) ─────────────────────────────────────────
112
104
  # shellcheck source=sw-github-graphql.sh
@@ -151,6 +143,21 @@ format_duration() {
151
143
  fi
152
144
  }
153
145
 
146
+ # Rotate event log if needed (standalone mode — daemon has its own rotation in poll loop)
147
+ rotate_event_log_if_needed() {
148
+ local events_file="${EVENTS_FILE:-$HOME/.shipwright/events.jsonl}"
149
+ local max_lines=10000
150
+ [[ ! -f "$events_file" ]] && return
151
+ local lines
152
+ lines=$(wc -l < "$events_file" 2>/dev/null || echo "0")
153
+ if [[ "$lines" -gt "$max_lines" ]]; then
154
+ local tmp="${events_file}.rotating"
155
+ if tail -5000 "$events_file" > "$tmp" 2>/dev/null && mv "$tmp" "$events_file" 2>/dev/null; then
156
+ info "Rotated events.jsonl: ${lines} -> 5000 lines"
157
+ fi
158
+ fi
159
+ }
160
+
154
161
  _pipeline_compact_goal() {
155
162
  local goal="$1"
156
163
  local plan_file="${2:-}"
@@ -199,33 +206,6 @@ load_composed_pipeline() {
199
206
  return 0
200
207
  }
201
208
 
202
- # ─── Structured Event Log ──────────────────────────────────────────────────
203
- # Appends JSON events to ~/.shipwright/events.jsonl for metrics/traceability
204
-
205
- EVENTS_DIR="${HOME}/.shipwright"
206
- EVENTS_FILE="${EVENTS_DIR}/events.jsonl"
207
-
208
- emit_event() {
209
- local event_type="$1"
210
- shift
211
- # Remaining args are key=value pairs
212
- local json_fields=""
213
- for kv in "$@"; do
214
- local key="${kv%%=*}"
215
- local val="${kv#*=}"
216
- # Numbers: don't quote; strings: quote
217
- if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
218
- json_fields="${json_fields},\"${key}\":${val}"
219
- else
220
- # Escape quotes in value
221
- val="${val//\"/\\\"}"
222
- json_fields="${json_fields},\"${key}\":\"${val}\""
223
- fi
224
- done
225
- mkdir -p "$EVENTS_DIR"
226
- echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
227
- }
228
-
229
209
  # ─── Token / Cost Parsing ─────────────────────────────────────────────────
230
210
  parse_claude_tokens() {
231
211
  local log_file="$1"
@@ -237,6 +217,36 @@ parse_claude_tokens() {
237
217
  TOTAL_OUTPUT_TOKENS=$(( TOTAL_OUTPUT_TOKENS + ${output_tok:-0} ))
238
218
  }
239
219
 
220
+ # Estimate pipeline cost using historical averages from completed pipelines.
221
+ # Falls back to per-stage estimates when no history exists.
222
+ estimate_pipeline_cost() {
223
+ local stages="$1"
224
+ local stage_count
225
+ stage_count=$(echo "$stages" | jq 'length' 2>/dev/null || echo "6")
226
+ [[ ! "$stage_count" =~ ^[0-9]+$ ]] && stage_count=6
227
+
228
+ local events_file="${EVENTS_FILE:-$HOME/.shipwright/events.jsonl}"
229
+ local avg_input=0 avg_output=0
230
+ if [[ -f "$events_file" ]]; then
231
+ local hist
232
+ hist=$(grep '"type":"pipeline.completed"' "$events_file" 2>/dev/null | tail -10)
233
+ if [[ -n "$hist" ]]; then
234
+ avg_input=$(echo "$hist" | jq -s -r '[.[] | .input_tokens // 0 | tonumber] | if length > 0 then (add / length | floor | tostring) else "0" end' 2>/dev/null | head -1)
235
+ avg_output=$(echo "$hist" | jq -s -r '[.[] | .output_tokens // 0 | tonumber] | if length > 0 then (add / length | floor | tostring) else "0" end' 2>/dev/null | head -1)
236
+ fi
237
+ fi
238
+ [[ ! "$avg_input" =~ ^[0-9]+$ ]] && avg_input=0
239
+ [[ ! "$avg_output" =~ ^[0-9]+$ ]] && avg_output=0
240
+
241
+ # Fall back to reasonable per-stage estimates only if no history
242
+ if [[ "$avg_input" -eq 0 ]]; then
243
+ avg_input=$(( stage_count * 8000 )) # More realistic: ~8K input per stage
244
+ avg_output=$(( stage_count * 4000 )) # ~4K output per stage
245
+ fi
246
+
247
+ echo "{\"input_tokens\":${avg_input},\"output_tokens\":${avg_output}}"
248
+ }
249
+
240
250
  # ─── Defaults ───────────────────────────────────────────────────────────────
241
251
  GOAL=""
242
252
  ISSUE_NUMBER=""
@@ -260,6 +270,7 @@ CI_MODE=false
260
270
  DRY_RUN=false
261
271
  IGNORE_BUDGET=false
262
272
  COMPLETED_STAGES=""
273
+ RESUME_FROM_CHECKPOINT=false
263
274
  MAX_ITERATIONS_OVERRIDE=""
264
275
  MAX_RESTARTS_OVERRIDE=""
265
276
  FAST_TEST_CMD_OVERRIDE=""
@@ -285,6 +296,10 @@ GH_AVAILABLE=false
285
296
  # Timing
286
297
  PIPELINE_START_EPOCH=""
287
298
  STAGE_TIMINGS=""
299
+ PIPELINE_STAGES_PASSED=""
300
+ PIPELINE_SLOWEST_STAGE=""
301
+ LAST_STAGE_ERROR_CLASS=""
302
+ LAST_STAGE_ERROR=""
288
303
 
289
304
  PROJECT_ROOT=""
290
305
  STATE_DIR=""
@@ -333,6 +348,7 @@ show_help() {
333
348
  echo -e " ${DIM}--max-iterations <n>${RESET} Override max build loop iterations"
334
349
  echo -e " ${DIM}--max-restarts <n>${RESET} Max session restarts in build loop"
335
350
  echo -e " ${DIM}--fast-test-cmd <cmd>${RESET} Fast/subset test for build loop"
351
+ echo -e " ${DIM}--tdd${RESET} Test-first: generate tests before implementation"
336
352
  echo -e " ${DIM}--completed-stages \"a,b\"${RESET} Skip these stages (CI resume)"
337
353
  echo ""
338
354
  echo -e "${BOLD}STAGES${RESET} ${DIM}(configurable per pipeline template)${RESET}"
@@ -413,6 +429,7 @@ parse_args() {
413
429
  --ignore-budget) IGNORE_BUDGET=true; shift ;;
414
430
  --max-iterations) MAX_ITERATIONS_OVERRIDE="$2"; shift 2 ;;
415
431
  --completed-stages) COMPLETED_STAGES="$2"; shift 2 ;;
432
+ --resume) RESUME_FROM_CHECKPOINT=true; shift ;;
416
433
  --worktree=*) AUTO_WORKTREE=true; WORKTREE_NAME="${1#--worktree=}"; WORKTREE_NAME="${WORKTREE_NAME//[^a-zA-Z0-9_-]/}"; if [[ -z "$WORKTREE_NAME" ]]; then error "Invalid worktree name (alphanumeric, hyphens, underscores only)"; exit 1; fi; shift ;;
417
434
  --worktree) AUTO_WORKTREE=true; shift ;;
418
435
  --dry-run) DRY_RUN=true; shift ;;
@@ -427,6 +444,7 @@ parse_args() {
427
444
  shift 2 ;;
428
445
 
429
446
  --fast-test-cmd) FAST_TEST_CMD_OVERRIDE="$2"; shift 2 ;;
447
+ --tdd) TDD_ENABLED=true; shift ;;
430
448
  --help|-h) show_help; exit 0 ;;
431
449
  *)
432
450
  if [[ -z "$PIPELINE_NAME_ARG" ]]; then
@@ -487,11 +505,11 @@ find_pipeline_config() {
487
505
  load_pipeline_config() {
488
506
  # Check for intelligence-composed pipeline first
489
507
  local composed_pipeline="${ARTIFACTS_DIR}/composed-pipeline.json"
490
- if [[ -f "$composed_pipeline" ]] && type composer_validate_pipeline &>/dev/null; then
508
+ if [[ -f "$composed_pipeline" ]] && type composer_validate_pipeline >/dev/null 2>&1; then
491
509
  # Use composed pipeline if fresh (< 1 hour old)
492
510
  local composed_age=99999
493
511
  local composed_mtime
494
- composed_mtime=$(stat -f %m "$composed_pipeline" 2>/dev/null || stat -c %Y "$composed_pipeline" 2>/dev/null || echo "0")
512
+ composed_mtime=$(file_mtime "$composed_pipeline")
495
513
  if [[ "$composed_mtime" -gt 0 ]]; then
496
514
  composed_age=$(( $(now_epoch) - composed_mtime ))
497
515
  fi
@@ -513,6 +531,9 @@ load_pipeline_config() {
513
531
  exit 1
514
532
  }
515
533
  info "Pipeline: ${BOLD}$PIPELINE_NAME${RESET} ${DIM}($PIPELINE_CONFIG)${RESET}"
534
+ # TDD from template (overridable by --tdd)
535
+ [[ "$(jq -r '.tdd // false' "$PIPELINE_CONFIG" 2>/dev/null)" == "true" ]] && PIPELINE_TDD=true
536
+ return 0
516
537
  }
517
538
 
518
539
  CURRENT_STAGE_ID=""
@@ -522,7 +543,7 @@ SLACK_WEBHOOK=""
522
543
  NOTIFICATION_ENABLED=false
523
544
 
524
545
  # Self-healing
525
- BUILD_TEST_RETRIES=2
546
+ BUILD_TEST_RETRIES=$(_config_get_int "pipeline.build_test_retries" 3 2>/dev/null || echo 3)
526
547
  STASHED_CHANGES=false
527
548
  SELF_HEAL_COUNT=0
528
549
 
@@ -544,7 +565,7 @@ start_heartbeat() {
544
565
  --stage "${CURRENT_STAGE_ID:-unknown}" \
545
566
  --iteration "0" \
546
567
  --activity "$(get_stage_description "${CURRENT_STAGE_ID:-}" 2>/dev/null || echo "Running pipeline")" 2>/dev/null || true
547
- sleep 30
568
+ sleep "$(_config_get_int "pipeline.heartbeat_interval" 30 2>/dev/null || echo 30)"
548
569
  done
549
570
  ) >/dev/null 2>&1 &
550
571
  HEARTBEAT_PID=$!
@@ -574,7 +595,10 @@ ci_push_partial_work() {
574
595
  fi
575
596
 
576
597
  # Push branch (create if needed, force to overwrite previous WIP)
577
- git push origin "HEAD:refs/heads/$branch" --force 2>/dev/null || true
598
+ if ! git push origin "HEAD:refs/heads/$branch" --force 2>/dev/null; then
599
+ warn "git push failed for $branch — remote may be out of sync"
600
+ emit_event "pipeline.push_failed" "branch=$branch"
601
+ fi
578
602
  }
579
603
 
580
604
  ci_post_stage_event() {
@@ -584,7 +608,7 @@ ci_post_stage_event() {
584
608
 
585
609
  local stage="$1" status="$2" elapsed="${3:-0s}"
586
610
  local comment="<!-- SHIPWRIGHT-STAGE: ${stage}:${status}:${elapsed} -->"
587
- gh issue comment "$ISSUE_NUMBER" --body "$comment" 2>/dev/null || true
611
+ _timeout "$(_config_get_int "network.gh_timeout" 30 2>/dev/null || echo 30)" gh issue comment "$ISSUE_NUMBER" --body "$comment" 2>/dev/null || true
588
612
  }
589
613
 
590
614
  # ─── Signal Handling ───────────────────────────────────────────────────────
@@ -615,12 +639,20 @@ cleanup_on_exit() {
615
639
  git stash pop --quiet 2>/dev/null || true
616
640
  fi
617
641
 
642
+ # Release durable pipeline lock
643
+ if [[ -n "${_PIPELINE_LOCK_ID:-}" ]] && type release_lock >/dev/null 2>&1; then
644
+ release_lock "$_PIPELINE_LOCK_ID" 2>/dev/null || true
645
+ fi
646
+
618
647
  # Cancel lingering in_progress GitHub Check Runs
619
648
  pipeline_cancel_check_runs 2>/dev/null || true
620
649
 
621
650
  # Update GitHub
622
651
  if [[ -n "${ISSUE_NUMBER:-}" && "${GH_AVAILABLE:-false}" == "true" ]]; then
623
- gh_comment_issue "$ISSUE_NUMBER" "⏸️ **Pipeline interrupted** at stage: ${CURRENT_STAGE_ID:-unknown}" 2>/dev/null || true
652
+ if ! _timeout "$(_config_get_int "network.gh_timeout" 30 2>/dev/null || echo 30)" gh issue comment "$ISSUE_NUMBER" --body "⏸️ **Pipeline interrupted** at stage: ${CURRENT_STAGE_ID:-unknown}" 2>/dev/null; then
653
+ warn "gh issue comment failed — status update may not have been posted"
654
+ emit_event "pipeline.comment_failed" "issue=$ISSUE_NUMBER"
655
+ fi
624
656
  fi
625
657
 
626
658
  exit "$exit_code"
@@ -641,7 +673,7 @@ preflight_checks() {
641
673
  local optional_tools=("gh" "claude" "bc" "curl")
642
674
 
643
675
  for tool in "${required_tools[@]}"; do
644
- if command -v "$tool" &>/dev/null; then
676
+ if command -v "$tool" >/dev/null 2>&1; then
645
677
  echo -e " ${GREEN}✓${RESET} $tool"
646
678
  else
647
679
  echo -e " ${RED}✗${RESET} $tool ${RED}(required)${RESET}"
@@ -650,7 +682,7 @@ preflight_checks() {
650
682
  done
651
683
 
652
684
  for tool in "${optional_tools[@]}"; do
653
- if command -v "$tool" &>/dev/null; then
685
+ if command -v "$tool" >/dev/null 2>&1; then
654
686
  echo -e " ${GREEN}✓${RESET} $tool"
655
687
  else
656
688
  echo -e " ${DIM}○${RESET} $tool ${DIM}(optional — some features disabled)${RESET}"
@@ -659,7 +691,7 @@ preflight_checks() {
659
691
 
660
692
  # 2. Git state
661
693
  echo ""
662
- if git rev-parse --is-inside-work-tree &>/dev/null; then
694
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
663
695
  echo -e " ${GREEN}✓${RESET} Inside git repo"
664
696
  else
665
697
  echo -e " ${RED}✗${RESET} Not inside a git repository"
@@ -685,7 +717,7 @@ preflight_checks() {
685
717
  fi
686
718
 
687
719
  # Check if base branch exists
688
- if git rev-parse --verify "$BASE_BRANCH" &>/dev/null; then
720
+ if git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
689
721
  echo -e " ${GREEN}✓${RESET} Base branch: $BASE_BRANCH"
690
722
  else
691
723
  echo -e " ${RED}✗${RESET} Base branch not found: $BASE_BRANCH"
@@ -693,8 +725,8 @@ preflight_checks() {
693
725
  fi
694
726
 
695
727
  # 3. GitHub auth (if gh available and not disabled)
696
- if [[ "$NO_GITHUB" != "true" ]] && command -v gh &>/dev/null; then
697
- if gh auth status &>/dev/null 2>&1; then
728
+ if [[ "$NO_GITHUB" != "true" ]] && command -v gh >/dev/null 2>&1; then
729
+ if gh auth status >/dev/null 2>&1; then
698
730
  echo -e " ${GREEN}✓${RESET} GitHub authenticated"
699
731
  else
700
732
  echo -e " ${YELLOW}⚠${RESET} GitHub not authenticated (features disabled)"
@@ -702,7 +734,7 @@ preflight_checks() {
702
734
  fi
703
735
 
704
736
  # 4. Claude CLI
705
- if command -v claude &>/dev/null; then
737
+ if command -v claude >/dev/null 2>&1; then
706
738
  echo -e " ${GREEN}✓${RESET} Claude CLI available"
707
739
  else
708
740
  echo -e " ${RED}✗${RESET} Claude CLI not found — plan/build stages will fail"
@@ -754,12 +786,12 @@ notify() {
754
786
  payload=$(jq -n \
755
787
  --arg text "${emoji} *${title}*\n${message}" \
756
788
  '{text: $text}')
757
- curl -sf -X POST -H 'Content-Type: application/json' \
789
+ curl -sf --connect-timeout "$(_config_get_int "network.connect_timeout" 10 2>/dev/null || echo 10)" --max-time "$(_config_get_int "network.max_time" 60 2>/dev/null || echo 60)" -X POST -H 'Content-Type: application/json' \
758
790
  -d "$payload" "$SLACK_WEBHOOK" >/dev/null 2>&1 || true
759
791
  fi
760
792
 
761
- # Custom webhook (env var SHIPWRIGHT_WEBHOOK_URL, with CCT_WEBHOOK_URL fallback)
762
- local _webhook_url="${SHIPWRIGHT_WEBHOOK_URL:-${CCT_WEBHOOK_URL:-}}"
793
+ # Custom webhook (env var SHIPWRIGHT_WEBHOOK_URL)
794
+ local _webhook_url="${SHIPWRIGHT_WEBHOOK_URL:-}"
763
795
  if [[ -n "$_webhook_url" ]]; then
764
796
  local payload
765
797
  payload=$(jq -n \
@@ -767,7 +799,7 @@ notify() {
767
799
  --arg level "$level" --arg pipeline "${PIPELINE_NAME:-}" \
768
800
  --arg goal "${GOAL:-}" --arg stage "${CURRENT_STAGE_ID:-}" \
769
801
  '{title:$title, message:$message, level:$level, pipeline:$pipeline, goal:$goal, stage:$stage}')
770
- curl -sf -X POST -H 'Content-Type: application/json' \
802
+ curl -sf --connect-timeout 10 --max-time 30 -X POST -H 'Content-Type: application/json' \
771
803
  -d "$payload" "$_webhook_url" >/dev/null 2>&1 || true
772
804
  fi
773
805
  }
@@ -815,7 +847,7 @@ classify_error() {
815
847
  elif echo "$log_tail" | grep -qiE 'error\[E[0-9]+\]|error: aborting|FAILED.*compile|build failed|tsc.*error|eslint.*error'; then
816
848
  classification="logic"
817
849
  # Intelligence fallback: Claude classification for unknown errors
818
- elif [[ "$classification" == "unknown" ]] && type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
850
+ elif [[ "$classification" == "unknown" ]] && type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
819
851
  local ai_class
820
852
  ai_class=$(claude --print --output-format text -p "Classify this error as exactly one of: infrastructure, configuration, logic, unknown.
821
853
 
@@ -882,14 +914,23 @@ run_stage_with_retry() {
882
914
  return 0
883
915
  fi
884
916
 
917
+ # Capture error_class and error snippet for stage.failed / pipeline.completed events
918
+ local error_class
919
+ error_class=$(classify_error "$stage_id")
920
+ LAST_STAGE_ERROR_CLASS="$error_class"
921
+ LAST_STAGE_ERROR=""
922
+ local _log_file="${ARTIFACTS_DIR}/${stage_id}-results.log"
923
+ [[ ! -f "$_log_file" ]] && _log_file="${ARTIFACTS_DIR}/test-results.log"
924
+ if [[ -f "$_log_file" ]]; then
925
+ LAST_STAGE_ERROR=$(tail -20 "$_log_file" 2>/dev/null | grep -iE 'error|fail|exception|fatal' 2>/dev/null | head -1 | cut -c1-200 || true)
926
+ fi
927
+
885
928
  attempt=$((attempt + 1))
886
929
  if [[ "$attempt" -gt "$max_retries" ]]; then
887
930
  return 1
888
931
  fi
889
932
 
890
- # Classify the error to decide whether retry makes sense
891
- local error_class
892
- error_class=$(classify_error "$stage_id")
933
+ # Classify done above; decide whether retry makes sense
893
934
 
894
935
  emit_event "retry.classified" \
895
936
  "issue=${ISSUE_NUMBER:-0}" \
@@ -926,6 +967,15 @@ run_stage_with_retry() {
926
967
  esac
927
968
  prev_error_class="$error_class"
928
969
 
970
+ if type db_save_reasoning_trace >/dev/null 2>&1; then
971
+ local job_id="${SHIPWRIGHT_PIPELINE_ID:-$$}"
972
+ local error_msg="${LAST_STAGE_ERROR:-$error_class}"
973
+ db_save_reasoning_trace "$job_id" "retry_reasoning" \
974
+ "stage=$stage_id error=$error_msg" \
975
+ "Stage failed, analyzing error pattern before retry" \
976
+ "retry_strategy=self_heal" 0.6 2>/dev/null || true
977
+ fi
978
+
929
979
  warn "Stage $stage_id failed (attempt $attempt/$((max_retries + 1)), class: $error_class) — retrying..."
930
980
  # Exponential backoff with jitter to avoid thundering herd
931
981
  local backoff=$((2 ** attempt))
@@ -951,9 +1001,9 @@ self_healing_build_test() {
951
1001
  local prev_fail_count=0 zero_convergence_streak=0
952
1002
 
953
1003
  # Vitals-driven adaptive limit (preferred over static BUILD_TEST_RETRIES)
954
- if type pipeline_adaptive_limit &>/dev/null 2>&1; then
1004
+ if type pipeline_adaptive_limit >/dev/null 2>&1; then
955
1005
  local _vitals_json=""
956
- if type pipeline_compute_vitals &>/dev/null 2>&1; then
1006
+ if type pipeline_compute_vitals >/dev/null 2>&1; then
957
1007
  _vitals_json=$(pipeline_compute_vitals "$STATE_FILE" "$ARTIFACTS_DIR" "${ISSUE_NUMBER:-}" 2>/dev/null) || true
958
1008
  fi
959
1009
  local vitals_limit
@@ -968,7 +1018,7 @@ self_healing_build_test() {
968
1018
  "vitals_limit=$vitals_limit"
969
1019
  fi
970
1020
  # Fallback: intelligence-based adaptive limits
971
- elif type composer_estimate_iterations &>/dev/null 2>&1; then
1021
+ elif type composer_estimate_iterations >/dev/null 2>&1; then
972
1022
  local estimated
973
1023
  estimated=$(composer_estimate_iterations \
974
1024
  "${INTELLIGENCE_ANALYSIS:-{}}" \
@@ -1022,7 +1072,7 @@ self_healing_build_test() {
1022
1072
  if [[ "$cycle" -gt 1 && -n "$last_test_error" ]]; then
1023
1073
  # Query memory for known fixes
1024
1074
  local _memory_fix=""
1025
- if type memory_closed_loop_inject &>/dev/null 2>&1; then
1075
+ if type memory_closed_loop_inject >/dev/null 2>&1; then
1026
1076
  local _error_sig_short
1027
1077
  _error_sig_short=$(echo "$last_test_error" | head -3 || echo "")
1028
1078
  _memory_fix=$(memory_closed_loop_inject "$_error_sig_short" 2>/dev/null) || true
@@ -1053,7 +1103,7 @@ Focus on fixing the failing tests while keeping all passing tests working."
1053
1103
  local timing
1054
1104
  timing=$(get_stage_timing "build")
1055
1105
  success "Stage ${BOLD}build${RESET} complete ${DIM}(${timing})${RESET}"
1056
- if type pipeline_emit_progress_snapshot &>/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1106
+ if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1057
1107
  local _diff_count
1058
1108
  _diff_count=$(git diff --stat HEAD~1 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1) || true
1059
1109
  local _snap_files _snap_error
@@ -1078,7 +1128,7 @@ Focus on fixing the failing tests while keeping all passing tests working."
1078
1128
  local timing
1079
1129
  timing=$(get_stage_timing "build")
1080
1130
  success "Stage ${BOLD}build${RESET} complete ${DIM}(${timing})${RESET}"
1081
- if type pipeline_emit_progress_snapshot &>/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1131
+ if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1082
1132
  local _diff_count
1083
1133
  _diff_count=$(git diff --stat HEAD~1 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1) || true
1084
1134
  local _snap_files _snap_error
@@ -1109,7 +1159,7 @@ Focus on fixing the failing tests while keeping all passing tests working."
1109
1159
  emit_event "convergence.tests_passed" \
1110
1160
  "issue=${ISSUE_NUMBER:-0}" \
1111
1161
  "cycle=$cycle"
1112
- if type pipeline_emit_progress_snapshot &>/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1162
+ if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1113
1163
  local _diff_count
1114
1164
  _diff_count=$(git diff --stat HEAD~1 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1) || true
1115
1165
  local _snap_files _snap_error
@@ -1235,6 +1285,9 @@ auto_rebase() {
1235
1285
  }
1236
1286
 
1237
1287
  run_pipeline() {
1288
+ # Rotate event log if needed (standalone mode)
1289
+ rotate_event_log_if_needed
1290
+
1238
1291
  local stages
1239
1292
  stages=$(jq -c '.stages[]' "$PIPELINE_CONFIG")
1240
1293
 
@@ -1329,6 +1382,10 @@ run_pipeline() {
1329
1382
 
1330
1383
  # Self-healing build→test loop: when we hit build, run both together
1331
1384
  if [[ "$id" == "build" && "$use_self_healing" == "true" ]]; then
1385
+ # TDD: generate tests before build when enabled
1386
+ if [[ "${TDD_ENABLED:-false}" == "true" || "${PIPELINE_TDD:-}" == "true" ]]; then
1387
+ stage_test_first || true
1388
+ fi
1332
1389
  # Gate check for build
1333
1390
  local build_gate
1334
1391
  build_gate=$(echo "$stage" | jq -r '.gate')
@@ -1362,6 +1419,11 @@ run_pipeline() {
1362
1419
  continue
1363
1420
  fi
1364
1421
 
1422
+ # TDD: generate tests before build when enabled (non-self-healing path)
1423
+ if [[ "$id" == "build" && "$use_self_healing" != "true" ]] && [[ "${TDD_ENABLED:-false}" == "true" || "${PIPELINE_TDD:-}" == "true" ]]; then
1424
+ stage_test_first || true
1425
+ fi
1426
+
1365
1427
  # Skip test if already handled by self-healing loop
1366
1428
  if [[ "$id" == "test" && "$use_self_healing" == "true" ]]; then
1367
1429
  stage_status=$(get_stage_status "test")
@@ -1401,52 +1463,59 @@ run_pipeline() {
1401
1463
  fi
1402
1464
  fi
1403
1465
 
1404
- # Intelligence: per-stage model routing with A/B testing
1405
- if type intelligence_recommend_model &>/dev/null 2>&1; then
1466
+ # Intelligence: per-stage model routing (UCB1 when DB has data, else A/B testing)
1467
+ local recommended_model="" from_ucb1=false
1468
+ if type ucb1_select_model >/dev/null 2>&1; then
1469
+ recommended_model=$(ucb1_select_model "$id" 2>/dev/null || echo "")
1470
+ [[ -n "$recommended_model" ]] && from_ucb1=true
1471
+ fi
1472
+ if [[ -z "$recommended_model" ]] && type intelligence_recommend_model >/dev/null 2>&1; then
1406
1473
  local stage_complexity="${INTELLIGENCE_COMPLEXITY:-5}"
1407
1474
  local budget_remaining=""
1408
1475
  if [[ -x "$SCRIPT_DIR/sw-cost.sh" ]]; then
1409
1476
  budget_remaining=$(bash "$SCRIPT_DIR/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
1410
1477
  fi
1411
- local recommended_model
1412
- recommended_model=$(intelligence_recommend_model "$id" "$stage_complexity" "$budget_remaining" 2>/dev/null || echo "")
1413
- if [[ -n "$recommended_model" && "$recommended_model" != "null" ]]; then
1414
- # A/B testing: decide whether to use the recommended model
1415
- local ab_ratio=20 # default 20% use recommended model
1478
+ local recommended_json
1479
+ recommended_json=$(intelligence_recommend_model "$id" "$stage_complexity" "$budget_remaining" 2>/dev/null || echo "")
1480
+ recommended_model=$(echo "$recommended_json" | jq -r '.model // empty' 2>/dev/null || echo "")
1481
+ fi
1482
+ if [[ -n "$recommended_model" && "$recommended_model" != "null" ]]; then
1483
+ if [[ "$from_ucb1" == "true" ]]; then
1484
+ # UCB1 already balances exploration/exploitation — use directly
1485
+ export CLAUDE_MODEL="$recommended_model"
1486
+ emit_event "intelligence.model_ucb1" \
1487
+ "issue=${ISSUE_NUMBER:-0}" \
1488
+ "stage=$id" \
1489
+ "model=$recommended_model"
1490
+ else
1491
+ # A/B testing for intelligence recommendation
1492
+ local ab_ratio=20
1416
1493
  local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
1417
1494
  if [[ -f "$daemon_cfg" ]]; then
1418
1495
  local cfg_ratio
1419
1496
  cfg_ratio=$(jq -r '.intelligence.ab_test_ratio // 0.2' "$daemon_cfg" 2>/dev/null || echo "0.2")
1420
- # Convert ratio (0.0-1.0) to percentage (0-100)
1421
1497
  ab_ratio=$(awk -v r="$cfg_ratio" 'BEGIN{printf "%d", r * 100}' 2>/dev/null || echo "20")
1422
1498
  fi
1423
1499
 
1424
- # Check if we have enough data points to graduate from A/B testing
1425
1500
  local routing_file="${HOME}/.shipwright/optimization/model-routing.json"
1426
1501
  local use_recommended=false
1427
1502
  local ab_group="control"
1428
1503
 
1429
1504
  if [[ -f "$routing_file" ]]; then
1430
- local stage_samples
1431
- stage_samples=$(jq -r --arg s "$id" '.[$s].sonnet_samples // 0' "$routing_file" 2>/dev/null || echo "0")
1432
- local total_samples
1433
- total_samples=$(jq -r --arg s "$id" '((.[$s].sonnet_samples // 0) + (.[$s].opus_samples // 0))' "$routing_file" 2>/dev/null || echo "0")
1434
-
1435
- if [[ "$total_samples" -ge 50 ]]; then
1436
- # Enough data — use optimizer's recommendation as default
1505
+ local stage_samples total_samples
1506
+ stage_samples=$(jq -r --arg s "$id" '.routes[$s].sonnet_samples // .[$s].sonnet_samples // 0' "$routing_file" 2>/dev/null || echo "0")
1507
+ total_samples=$(jq -r --arg s "$id" '((.routes[$s].sonnet_samples // .[$s].sonnet_samples // 0) + (.routes[$s].opus_samples // .[$s].opus_samples // 0))' "$routing_file" 2>/dev/null || echo "0")
1508
+ if [[ "${total_samples:-0}" -ge 50 ]]; then
1437
1509
  use_recommended=true
1438
1510
  ab_group="graduated"
1439
1511
  fi
1440
1512
  fi
1441
1513
 
1442
1514
  if [[ "$use_recommended" != "true" ]]; then
1443
- # A/B test: RANDOM % 100 < ab_ratio → use recommended
1444
1515
  local roll=$((RANDOM % 100))
1445
1516
  if [[ "$roll" -lt "$ab_ratio" ]]; then
1446
1517
  use_recommended=true
1447
1518
  ab_group="experiment"
1448
- else
1449
- ab_group="control"
1450
1519
  fi
1451
1520
  fi
1452
1521
 
@@ -1475,7 +1544,7 @@ run_pipeline() {
1475
1544
  emit_event "stage.started" "issue=${ISSUE_NUMBER:-0}" "stage=$id"
1476
1545
 
1477
1546
  # Mark GitHub Check Run as in-progress
1478
- if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update &>/dev/null 2>&1; then
1547
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update >/dev/null 2>&1; then
1479
1548
  gh_checks_stage_update "$id" "in_progress" "" "Stage $id started" 2>/dev/null || true
1480
1549
  fi
1481
1550
 
@@ -1491,7 +1560,13 @@ run_pipeline() {
1491
1560
  timing=$(get_stage_timing "$id")
1492
1561
  stage_dur_s=$(( $(now_epoch) - stage_start_epoch ))
1493
1562
  success "Stage ${BOLD}$id${RESET} complete ${DIM}(${timing})${RESET}"
1494
- emit_event "stage.completed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s"
1563
+ emit_event "stage.completed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s" "result=success"
1564
+ # Emit vitals snapshot on every stage transition (not just build/test)
1565
+ if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1566
+ pipeline_emit_progress_snapshot "${ISSUE_NUMBER}" "$id" "0" "0" "0" "" 2>/dev/null || true
1567
+ fi
1568
+ # Record model outcome for UCB1 learning
1569
+ type record_model_outcome >/dev/null 2>&1 && record_model_outcome "$stage_model_used" "$id" 1 "$stage_dur_s" 0 2>/dev/null || true
1495
1570
  # Broadcast discovery for cross-pipeline learning
1496
1571
  if [[ -x "$SCRIPT_DIR/sw-discovery.sh" ]]; then
1497
1572
  local _disc_cat _disc_patterns _disc_text
@@ -1514,9 +1589,20 @@ run_pipeline() {
1514
1589
  stage_dur_s=$(( $(now_epoch) - stage_start_epoch ))
1515
1590
  error "Pipeline failed at stage: ${BOLD}$id${RESET}"
1516
1591
  update_status "failed" "$id"
1517
- emit_event "stage.failed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s"
1592
+ emit_event "stage.failed" \
1593
+ "issue=${ISSUE_NUMBER:-0}" \
1594
+ "stage=$id" \
1595
+ "duration_s=$stage_dur_s" \
1596
+ "error=${LAST_STAGE_ERROR:-unknown}" \
1597
+ "error_class=${LAST_STAGE_ERROR_CLASS:-unknown}"
1598
+ # Emit vitals snapshot on failure too
1599
+ if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1600
+ pipeline_emit_progress_snapshot "${ISSUE_NUMBER}" "$id" "0" "0" "0" "${LAST_STAGE_ERROR:-unknown}" 2>/dev/null || true
1601
+ fi
1518
1602
  # Log model used for prediction feedback
1519
1603
  echo "${id}|${stage_model_used}|false" >> "${ARTIFACTS_DIR}/model-routing.log"
1604
+ # Record model outcome for UCB1 learning
1605
+ type record_model_outcome >/dev/null 2>&1 && record_model_outcome "$stage_model_used" "$id" 0 "$stage_dur_s" 0 2>/dev/null || true
1520
1606
  # Cancel any remaining in_progress check runs
1521
1607
  pipeline_cancel_check_runs 2>/dev/null || true
1522
1608
  return 1
@@ -1525,6 +1611,11 @@ run_pipeline() {
1525
1611
 
1526
1612
  # Pipeline complete!
1527
1613
  update_status "complete" ""
1614
+ PIPELINE_STAGES_PASSED="$completed"
1615
+ PIPELINE_SLOWEST_STAGE=""
1616
+ if type get_slowest_stage >/dev/null 2>&1; then
1617
+ PIPELINE_SLOWEST_STAGE=$(get_slowest_stage 2>/dev/null || true)
1618
+ fi
1528
1619
  local total_dur=""
1529
1620
  if [[ -n "$PIPELINE_START_EPOCH" ]]; then
1530
1621
  total_dur=$(format_duration $(( $(now_epoch) - PIPELINE_START_EPOCH )))
@@ -1568,7 +1659,7 @@ run_pipeline() {
1568
1659
  pipeline_post_completion_cleanup() {
1569
1660
  local cleaned=0
1570
1661
 
1571
- # 1. Clear checkpoints (they only matter for resume; pipeline is done)
1662
+ # 1. Clear checkpoints and context files (they only matter for resume; pipeline is done)
1572
1663
  if [[ -d "${ARTIFACTS_DIR}/checkpoints" ]]; then
1573
1664
  local cp_count=0
1574
1665
  local cp_file
@@ -1577,6 +1668,11 @@ pipeline_post_completion_cleanup() {
1577
1668
  rm -f "$cp_file"
1578
1669
  cp_count=$((cp_count + 1))
1579
1670
  done
1671
+ for cp_file in "${ARTIFACTS_DIR}/checkpoints"/*-claude-context.json; do
1672
+ [[ -f "$cp_file" ]] || continue
1673
+ rm -f "$cp_file"
1674
+ cp_count=$((cp_count + 1))
1675
+ done
1580
1676
  if [[ "$cp_count" -gt 0 ]]; then
1581
1677
  cleaned=$((cleaned + cp_count))
1582
1678
  fi
@@ -1621,7 +1717,7 @@ pipeline_cancel_check_runs() {
1621
1717
  return
1622
1718
  fi
1623
1719
 
1624
- if ! type gh_checks_stage_update &>/dev/null 2>&1; then
1720
+ if ! type gh_checks_stage_update >/dev/null 2>&1; then
1625
1721
  return
1626
1722
  fi
1627
1723
 
@@ -1673,7 +1769,7 @@ pipeline_setup_worktree() {
1673
1769
 
1674
1770
  # Store original dir for cleanup, then cd into worktree
1675
1771
  ORIGINAL_REPO_DIR="$(pwd)"
1676
- cd "$worktree_path"
1772
+ cd "$worktree_path" || { error "Failed to cd into worktree: $worktree_path"; return 1; }
1677
1773
  CLEANUP_WORKTREE=true
1678
1774
 
1679
1775
  success "Worktree ready: ${CYAN}${worktree_path}${RESET} (branch: ${branch_name})"
@@ -1807,7 +1903,7 @@ run_dry_run() {
1807
1903
  local optional_tools=("gh" "claude" "bc")
1808
1904
 
1809
1905
  for tool in "${required_tools[@]}"; do
1810
- if command -v "$tool" &>/dev/null; then
1906
+ if command -v "$tool" >/dev/null 2>&1; then
1811
1907
  echo -e " ${GREEN}✓${RESET} $tool"
1812
1908
  else
1813
1909
  echo -e " ${RED}✗${RESET} $tool ${RED}(required)${RESET}"
@@ -1816,7 +1912,7 @@ run_dry_run() {
1816
1912
  done
1817
1913
 
1818
1914
  for tool in "${optional_tools[@]}"; do
1819
- if command -v "$tool" &>/dev/null; then
1915
+ if command -v "$tool" >/dev/null 2>&1; then
1820
1916
  echo -e " ${GREEN}✓${RESET} $tool"
1821
1917
  else
1822
1918
  echo -e " ${DIM}○${RESET} $tool"
@@ -1825,15 +1921,17 @@ run_dry_run() {
1825
1921
 
1826
1922
  echo ""
1827
1923
 
1828
- # Cost estimation (rough approximation)
1924
+ # Cost estimation: use historical averages from past pipelines when available
1829
1925
  echo -e "${BLUE}${BOLD}━━━ Estimated Resource Usage ━━━${RESET}"
1830
1926
  echo ""
1831
1927
 
1832
- # Very rough cost estimation: ~2000 input tokens per stage, ~3000 output tokens
1833
- # Adjust based on pipeline complexity
1928
+ local stages_json
1929
+ stages_json=$(jq '[.stages[] | select(.enabled == true)]' "$PIPELINE_CONFIG" 2>/dev/null || echo "[]")
1930
+ local est
1931
+ est=$(estimate_pipeline_cost "$stages_json")
1834
1932
  local input_tokens_estimate output_tokens_estimate
1835
- input_tokens_estimate=$(( enabled_stages * 2000 ))
1836
- output_tokens_estimate=$(( enabled_stages * 3000 ))
1933
+ input_tokens_estimate=$(echo "$est" | jq -r '.input_tokens // 0')
1934
+ output_tokens_estimate=$(echo "$est" | jq -r '.output_tokens // 0')
1837
1935
 
1838
1936
  # Calculate cost based on selected model
1839
1937
  local input_rate output_rate input_cost output_cost total_cost
@@ -1848,11 +1946,11 @@ run_dry_run() {
1848
1946
  echo -e " ${BOLD}Estimated Input Tokens:${RESET} ~$input_tokens_estimate"
1849
1947
  echo -e " ${BOLD}Estimated Output Tokens:${RESET} ~$output_tokens_estimate"
1850
1948
  echo -e " ${BOLD}Model Cost Rate:${RESET} $stage_model"
1851
- echo -e " ${BOLD}Estimated Cost:${RESET} \$$total_cost USD (rough estimate)"
1949
+ echo -e " ${BOLD}Estimated Cost:${RESET} \$$total_cost USD"
1852
1950
  echo ""
1853
1951
 
1854
1952
  # Validate composed pipeline if intelligence is enabled
1855
- if [[ -f "$ARTIFACTS_DIR/composed-pipeline.json" ]] && type composer_validate_pipeline &>/dev/null; then
1953
+ if [[ -f "$ARTIFACTS_DIR/composed-pipeline.json" ]] && type composer_validate_pipeline >/dev/null 2>&1; then
1856
1954
  echo -e "${BLUE}${BOLD}━━━ Intelligence-Composed Pipeline ━━━${RESET}"
1857
1955
  echo ""
1858
1956
 
@@ -1877,6 +1975,100 @@ run_dry_run() {
1877
1975
  return 0
1878
1976
  }
1879
1977
 
1978
+ # ─── Reasoning Trace Generation ──────────────────────────────────────────────
1979
+ # Multi-step autonomous reasoning traces for pipeline start (before stages run)
1980
+
1981
+ generate_reasoning_trace() {
1982
+ local job_id="${SHIPWRIGHT_PIPELINE_ID:-$$}"
1983
+ local issue="${ISSUE_NUMBER:-}"
1984
+ local goal="${GOAL:-}"
1985
+
1986
+ # Step 1: Analyze issue complexity and risk
1987
+ local complexity="medium"
1988
+ local risk_score=50
1989
+ if [[ -n "$issue" ]] && type intelligence_analyze_issue >/dev/null 2>&1; then
1990
+ local issue_json analysis
1991
+ issue_json=$(gh issue view "$issue" --json number,title,body,labels 2>/dev/null || echo "{}")
1992
+ if [[ -n "$issue_json" && "$issue_json" != "{}" ]]; then
1993
+ analysis=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "")
1994
+ if [[ -n "$analysis" ]]; then
1995
+ local comp_num
1996
+ comp_num=$(echo "$analysis" | jq -r '.complexity // 5' 2>/dev/null || echo "5")
1997
+ if [[ "$comp_num" -le 3 ]]; then
1998
+ complexity="low"
1999
+ elif [[ "$comp_num" -le 6 ]]; then
2000
+ complexity="medium"
2001
+ else
2002
+ complexity="high"
2003
+ fi
2004
+ risk_score=$((100 - $(echo "$analysis" | jq -r '.success_probability // 50' 2>/dev/null || echo "50")))
2005
+ fi
2006
+ fi
2007
+ elif [[ -n "$goal" ]]; then
2008
+ issue_json=$(jq -n --arg title "${goal}" --arg body "" '{title: $title, body: $body, labels: []}')
2009
+ if type intelligence_analyze_issue >/dev/null 2>&1; then
2010
+ analysis=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "")
2011
+ if [[ -n "$analysis" ]]; then
2012
+ local comp_num
2013
+ comp_num=$(echo "$analysis" | jq -r '.complexity // 5' 2>/dev/null || echo "5")
2014
+ if [[ "$comp_num" -le 3 ]]; then complexity="low"; elif [[ "$comp_num" -le 6 ]]; then complexity="medium"; else complexity="high"; fi
2015
+ risk_score=$((100 - $(echo "$analysis" | jq -r '.success_probability // 50' 2>/dev/null || echo "50")))
2016
+ fi
2017
+ fi
2018
+ fi
2019
+
2020
+ # Step 2: Query similar past issues
2021
+ local similar_context=""
2022
+ if type memory_semantic_search >/dev/null 2>&1 && [[ -n "$goal" ]]; then
2023
+ similar_context=$(memory_semantic_search "$goal" "" 3 2>/dev/null || echo "")
2024
+ fi
2025
+
2026
+ # Step 3: Select template using Thompson sampling
2027
+ local selected_template="${PIPELINE_TEMPLATE:-}"
2028
+ if [[ -z "$selected_template" ]] && type thompson_select_template >/dev/null 2>&1; then
2029
+ selected_template=$(thompson_select_template "$complexity" 2>/dev/null || echo "standard")
2030
+ fi
2031
+ [[ -z "$selected_template" ]] && selected_template="standard"
2032
+
2033
+ # Step 4: Predict failure modes from memory
2034
+ local failure_predictions=""
2035
+ if type memory_semantic_search >/dev/null 2>&1 && [[ -n "$goal" ]]; then
2036
+ failure_predictions=$(memory_semantic_search "failure error $goal" "" 3 2>/dev/null || echo "")
2037
+ fi
2038
+
2039
+ # Save reasoning traces to DB
2040
+ if type db_save_reasoning_trace >/dev/null 2>&1; then
2041
+ db_save_reasoning_trace "$job_id" "complexity_analysis" \
2042
+ "issue=$issue goal=$goal" \
2043
+ "Analyzed complexity=$complexity risk=$risk_score" \
2044
+ "complexity=$complexity risk_score=$risk_score" 0.7 2>/dev/null || true
2045
+
2046
+ db_save_reasoning_trace "$job_id" "template_selection" \
2047
+ "complexity=$complexity historical_outcomes" \
2048
+ "Thompson sampling over historical success rates" \
2049
+ "template=$selected_template" 0.8 2>/dev/null || true
2050
+
2051
+ if [[ -n "$similar_context" && "$similar_context" != "[]" ]]; then
2052
+ db_save_reasoning_trace "$job_id" "similar_issues" \
2053
+ "$goal" \
2054
+ "Found similar past issues for context injection" \
2055
+ "$similar_context" 0.6 2>/dev/null || true
2056
+ fi
2057
+
2058
+ if [[ -n "$failure_predictions" && "$failure_predictions" != "[]" ]]; then
2059
+ db_save_reasoning_trace "$job_id" "failure_prediction" \
2060
+ "$goal" \
2061
+ "Predicted potential failure modes from history" \
2062
+ "$failure_predictions" 0.5 2>/dev/null || true
2063
+ fi
2064
+ fi
2065
+
2066
+ # Export for use by pipeline stages
2067
+ [[ -n "$selected_template" && -z "${PIPELINE_TEMPLATE:-}" ]] && export PIPELINE_TEMPLATE="$selected_template"
2068
+
2069
+ emit_event "reasoning.trace" "job_id=$job_id" "complexity=$complexity" "risk=$risk_score" "template=${selected_template:-standard}" 2>/dev/null || true
2070
+ }
2071
+
1880
2072
  # ─── Subcommands ────────────────────────────────────────────────────────────
1881
2073
 
1882
2074
  pipeline_start() {
@@ -1898,6 +2090,13 @@ pipeline_start() {
1898
2090
  info "Using repository: $ORIGINAL_REPO_DIR"
1899
2091
  fi
1900
2092
 
2093
+ # Bootstrap optimization & memory if cold start (before first intelligence use)
2094
+ if [[ -f "$SCRIPT_DIR/lib/bootstrap.sh" ]]; then
2095
+ source "$SCRIPT_DIR/lib/bootstrap.sh"
2096
+ [[ ! -f "$HOME/.shipwright/optimization/iteration-model.json" ]] && bootstrap_optimization 2>/dev/null || true
2097
+ [[ ! -f "$HOME/.shipwright/memory/patterns.json" ]] && bootstrap_memory 2>/dev/null || true
2098
+ fi
2099
+
1901
2100
  if [[ -z "$GOAL" && -z "$ISSUE_NUMBER" ]]; then
1902
2101
  error "Must provide --goal or --issue"
1903
2102
  echo -e " Example: ${DIM}shipwright pipeline start --goal \"Add JWT auth\"${RESET}"
@@ -1905,7 +2104,7 @@ pipeline_start() {
1905
2104
  exit 1
1906
2105
  fi
1907
2106
 
1908
- if ! command -v jq &>/dev/null; then
2107
+ if ! command -v jq >/dev/null 2>&1; then
1909
2108
  error "jq is required. Install it: brew install jq"
1910
2109
  exit 1
1911
2110
  fi
@@ -1923,6 +2122,26 @@ pipeline_start() {
1923
2122
 
1924
2123
  setup_dirs
1925
2124
 
2125
+ # Acquire durable lock to prevent concurrent pipelines on the same issue/goal
2126
+ _PIPELINE_LOCK_ID=""
2127
+ if type acquire_lock >/dev/null 2>&1; then
2128
+ _PIPELINE_LOCK_ID="pipeline-${ISSUE_NUMBER:-goal-$$}"
2129
+ if ! acquire_lock "$_PIPELINE_LOCK_ID" 5 2>/dev/null; then
2130
+ error "Another pipeline is already running for this issue/goal"
2131
+ echo -e " Wait for it to finish, or remove stale lock:"
2132
+ echo -e " ${DIM}rm -rf ~/.shipwright/durable/locks/${_PIPELINE_LOCK_ID}.lock${RESET}"
2133
+ _PIPELINE_LOCK_ID=""
2134
+ exit 1
2135
+ fi
2136
+ fi
2137
+
2138
+ # Generate reasoning trace (complexity analysis, template selection, failure predictions)
2139
+ local user_specified_pipeline="$PIPELINE_NAME"
2140
+ generate_reasoning_trace 2>/dev/null || true
2141
+ if [[ -n "${PIPELINE_TEMPLATE:-}" && "$user_specified_pipeline" == "standard" ]]; then
2142
+ PIPELINE_NAME="$PIPELINE_TEMPLATE"
2143
+ fi
2144
+
1926
2145
  # Check for existing pipeline
1927
2146
  if [[ -f "$STATE_FILE" ]]; then
1928
2147
  local existing_status
@@ -1942,7 +2161,87 @@ pipeline_start() {
1942
2161
  gh_init
1943
2162
 
1944
2163
  load_pipeline_config
1945
- initialize_state
2164
+
2165
+ # Checkpoint resume: when --resume is passed, try DB first, then file-based
2166
+ checkpoint_stage=""
2167
+ checkpoint_iteration=0
2168
+ if $RESUME_FROM_CHECKPOINT && type db_load_checkpoint >/dev/null 2>&1; then
2169
+ local saved_checkpoint
2170
+ saved_checkpoint=$(db_load_checkpoint "pipeline-${SHIPWRIGHT_PIPELINE_ID:-$$}" 2>/dev/null || echo "")
2171
+ if [[ -n "$saved_checkpoint" ]]; then
2172
+ checkpoint_stage=$(echo "$saved_checkpoint" | jq -r '.stage // ""' 2>/dev/null || echo "")
2173
+ if [[ -n "$checkpoint_stage" ]]; then
2174
+ info "Resuming from DB checkpoint: stage=$checkpoint_stage"
2175
+ checkpoint_iteration=$(echo "$saved_checkpoint" | jq -r '.iteration // 0' 2>/dev/null || echo "0")
2176
+ # Build COMPLETED_STAGES: all enabled stages before checkpoint_stage
2177
+ local enabled_list before_list=""
2178
+ enabled_list=$(jq -r '.stages[] | select(.enabled == true) | .id' "$PIPELINE_CONFIG" 2>/dev/null) || true
2179
+ local s
2180
+ while IFS= read -r s; do
2181
+ [[ -z "$s" ]] && continue
2182
+ if [[ "$s" == "$checkpoint_stage" ]]; then
2183
+ break
2184
+ fi
2185
+ [[ -n "$before_list" ]] && before_list="${before_list},${s}" || before_list="$s"
2186
+ done <<< "$enabled_list"
2187
+ if [[ -n "$before_list" ]]; then
2188
+ COMPLETED_STAGES="${before_list}"
2189
+ SELF_HEAL_COUNT="${checkpoint_iteration}"
2190
+ fi
2191
+ fi
2192
+ fi
2193
+ fi
2194
+ if $RESUME_FROM_CHECKPOINT && [[ -z "$checkpoint_stage" ]] && [[ -d "${ARTIFACTS_DIR}/checkpoints" ]]; then
2195
+ local cp_dir="${ARTIFACTS_DIR}/checkpoints"
2196
+ local latest_cp="" latest_mtime=0
2197
+ local f
2198
+ for f in "$cp_dir"/*-checkpoint.json; do
2199
+ [[ -f "$f" ]] || continue
2200
+ local mtime
2201
+ mtime=$(file_mtime "$f" 2>/dev/null || echo "0")
2202
+ if [[ "${mtime:-0}" -gt "$latest_mtime" ]]; then
2203
+ latest_mtime="${mtime}"
2204
+ latest_cp="$f"
2205
+ fi
2206
+ done
2207
+ if [[ -n "$latest_cp" && -x "$SCRIPT_DIR/sw-checkpoint.sh" ]]; then
2208
+ checkpoint_stage="$(basename "$latest_cp" -checkpoint.json)"
2209
+ local cp_json
2210
+ cp_json="$("$SCRIPT_DIR/sw-checkpoint.sh" restore --stage "$checkpoint_stage" 2>/dev/null)" || true
2211
+ if [[ -n "$cp_json" ]] && command -v jq >/dev/null 2>&1; then
2212
+ checkpoint_iteration="$(echo "$cp_json" | jq -r '.iteration // 0' 2>/dev/null)" || checkpoint_iteration=0
2213
+ info "Checkpoint resume: stage=${checkpoint_stage} iteration=${checkpoint_iteration}"
2214
+ # Build COMPLETED_STAGES: all enabled stages before checkpoint_stage
2215
+ local enabled_list before_list=""
2216
+ enabled_list="$(jq -r '.stages[] | select(.enabled == true) | .id' "$PIPELINE_CONFIG" 2>/dev/null)" || true
2217
+ local s
2218
+ while IFS= read -r s; do
2219
+ [[ -z "$s" ]] && continue
2220
+ if [[ "$s" == "$checkpoint_stage" ]]; then
2221
+ break
2222
+ fi
2223
+ [[ -n "$before_list" ]] && before_list="${before_list},${s}" || before_list="$s"
2224
+ done <<< "$enabled_list"
2225
+ if [[ -n "$before_list" ]]; then
2226
+ COMPLETED_STAGES="${before_list}"
2227
+ SELF_HEAL_COUNT="${checkpoint_iteration}"
2228
+ fi
2229
+ fi
2230
+ fi
2231
+ fi
2232
+
2233
+ # Restore from state file if resuming (failed/interrupted pipeline); else initialize fresh
2234
+ if $RESUME_FROM_CHECKPOINT && [[ -f "$STATE_FILE" ]]; then
2235
+ local existing_status
2236
+ existing_status="$(sed -n 's/^status: *//p' "$STATE_FILE" | head -1)"
2237
+ if [[ "$existing_status" == "failed" || "$existing_status" == "interrupted" ]]; then
2238
+ resume_state
2239
+ else
2240
+ initialize_state
2241
+ fi
2242
+ else
2243
+ initialize_state
2244
+ fi
1946
2245
 
1947
2246
  # CI resume: restore branch + goal context when intake is skipped
1948
2247
  if [[ -n "${COMPLETED_STAGES:-}" ]] && echo "$COMPLETED_STAGES" | tr ',' '\n' | grep -qx "intake"; then
@@ -1951,7 +2250,7 @@ pipeline_start() {
1951
2250
 
1952
2251
  # Restore GOAL from issue if not already set
1953
2252
  if [[ -z "$GOAL" && -n "$ISSUE_NUMBER" ]]; then
1954
- GOAL=$(gh issue view "$ISSUE_NUMBER" --json title -q .title 2>/dev/null || echo "Issue #${ISSUE_NUMBER}")
2253
+ GOAL=$(_timeout "$(_config_get_int "network.gh_timeout" 30 2>/dev/null || echo 30)" gh issue view "$ISSUE_NUMBER" --json title -q .title 2>/dev/null || echo "Issue #${ISSUE_NUMBER}")
1955
2254
  info "CI resume: goal from issue — ${GOAL}"
1956
2255
  fi
1957
2256
 
@@ -2018,11 +2317,38 @@ pipeline_start() {
2018
2317
  return $?
2019
2318
  fi
2020
2319
 
2320
+ # Capture predictions for feedback loop (intelligence → actuals → learning)
2321
+ if type intelligence_analyze_issue >/dev/null 2>&1 && (type intelligence_estimate_iterations >/dev/null 2>&1 || type intelligence_predict_cost >/dev/null 2>&1); then
2322
+ local issue_json="${INTELLIGENCE_ANALYSIS:-}"
2323
+ if [[ -z "$issue_json" || "$issue_json" == "{}" ]]; then
2324
+ if [[ -n "$ISSUE_NUMBER" ]]; then
2325
+ issue_json=$(gh issue view "$ISSUE_NUMBER" --json number,title,body,labels 2>/dev/null || echo "{}")
2326
+ else
2327
+ issue_json=$(jq -n --arg title "${GOAL:-untitled}" --arg body "" '{title: $title, body: $body, labels: []}')
2328
+ fi
2329
+ if [[ -n "$issue_json" && "$issue_json" != "{}" ]]; then
2330
+ issue_json=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "{}")
2331
+ fi
2332
+ fi
2333
+ if [[ -n "$issue_json" && "$issue_json" != "{}" ]]; then
2334
+ if type intelligence_estimate_iterations >/dev/null 2>&1; then
2335
+ PREDICTED_ITERATIONS=$(intelligence_estimate_iterations "$issue_json" "" 2>/dev/null || echo "")
2336
+ export PREDICTED_ITERATIONS
2337
+ fi
2338
+ if type intelligence_predict_cost >/dev/null 2>&1; then
2339
+ local cost_json
2340
+ cost_json=$(intelligence_predict_cost "$issue_json" "{}" 2>/dev/null || echo "{}")
2341
+ PREDICTED_COST=$(echo "$cost_json" | jq -r '.estimated_cost_usd // empty' 2>/dev/null || echo "")
2342
+ export PREDICTED_COST
2343
+ fi
2344
+ fi
2345
+ fi
2346
+
2021
2347
  # Start background heartbeat writer
2022
2348
  start_heartbeat
2023
2349
 
2024
2350
  # Initialize GitHub Check Runs for all pipeline stages
2025
- if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_pipeline_start &>/dev/null 2>&1; then
2351
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_pipeline_start >/dev/null 2>&1; then
2026
2352
  local head_sha
2027
2353
  head_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
2028
2354
  if [[ -n "$head_sha" && -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
@@ -2038,12 +2364,20 @@ pipeline_start() {
2038
2364
 
2039
2365
  emit_event "pipeline.started" \
2040
2366
  "issue=${ISSUE_NUMBER:-0}" \
2367
+ "template=${PIPELINE_NAME}" \
2368
+ "complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
2369
+ "machine=$(hostname 2>/dev/null || echo "unknown")" \
2041
2370
  "pipeline=${PIPELINE_NAME}" \
2042
2371
  "model=${MODEL:-opus}" \
2043
2372
  "goal=${GOAL}"
2044
2373
 
2374
+ # Record pipeline run in SQLite for dashboard visibility
2375
+ if type add_pipeline_run >/dev/null 2>&1; then
2376
+ add_pipeline_run "${SHIPWRIGHT_PIPELINE_ID}" "${ISSUE_NUMBER:-0}" "${GOAL}" "${BRANCH:-}" "${PIPELINE_NAME}" 2>/dev/null || true
2377
+ fi
2378
+
2045
2379
  # Durable WAL: publish pipeline start event
2046
- if type publish_event &>/dev/null 2>&1; then
2380
+ if type publish_event >/dev/null 2>&1; then
2047
2381
  publish_event "pipeline.started" "{\"issue\":\"${ISSUE_NUMBER:-0}\",\"pipeline\":\"${PIPELINE_NAME}\",\"goal\":\"${GOAL:0:200}\"}" 2>/dev/null || true
2048
2382
  fi
2049
2383
 
@@ -2051,6 +2385,18 @@ pipeline_start() {
2051
2385
  local exit_code=$?
2052
2386
  PIPELINE_EXIT_CODE="$exit_code"
2053
2387
 
2388
+ # Compute total cost for pipeline.completed (prefer actual from Claude when available)
2389
+ local model_key="${MODEL:-sonnet}"
2390
+ local total_cost
2391
+ if [[ -n "${TOTAL_COST_USD:-}" && "${TOTAL_COST_USD}" != "0" && "${TOTAL_COST_USD}" != "null" ]]; then
2392
+ total_cost="${TOTAL_COST_USD}"
2393
+ else
2394
+ local input_cost output_cost
2395
+ input_cost=$(awk -v tokens="$TOTAL_INPUT_TOKENS" -v rate="$(echo "$COST_MODEL_RATES" | jq -r ".${model_key}.input // 3")" 'BEGIN{printf "%.4f", (tokens / 1000000) * rate}')
2396
+ output_cost=$(awk -v tokens="$TOTAL_OUTPUT_TOKENS" -v rate="$(echo "$COST_MODEL_RATES" | jq -r ".${model_key}.output // 15")" 'BEGIN{printf "%.4f", (tokens / 1000000) * rate}')
2397
+ total_cost=$(awk -v i="$input_cost" -v o="$output_cost" 'BEGIN{printf "%.4f", i + o}')
2398
+ fi
2399
+
2054
2400
  # Send completion notification + event
2055
2401
  local total_dur_s=""
2056
2402
  [[ -n "$PIPELINE_START_EPOCH" ]] && total_dur_s=$(( $(now_epoch) - PIPELINE_START_EPOCH ))
@@ -2064,28 +2410,69 @@ pipeline_start() {
2064
2410
  "issue=${ISSUE_NUMBER:-0}" \
2065
2411
  "result=success" \
2066
2412
  "duration_s=${total_dur_s:-0}" \
2413
+ "iterations=$((SELF_HEAL_COUNT + 1))" \
2414
+ "template=${PIPELINE_NAME}" \
2415
+ "complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
2416
+ "stages_passed=${PIPELINE_STAGES_PASSED:-0}" \
2417
+ "slowest_stage=${PIPELINE_SLOWEST_STAGE:-}" \
2067
2418
  "pr_url=${pr_url:-}" \
2068
2419
  "agent_id=${PIPELINE_AGENT_ID}" \
2069
2420
  "input_tokens=$TOTAL_INPUT_TOKENS" \
2070
2421
  "output_tokens=$TOTAL_OUTPUT_TOKENS" \
2422
+ "total_cost=$total_cost" \
2071
2423
  "self_heal_count=$SELF_HEAL_COUNT"
2072
2424
 
2425
+ # Update pipeline run status in SQLite
2426
+ if type update_pipeline_status >/dev/null 2>&1; then
2427
+ update_pipeline_status "${SHIPWRIGHT_PIPELINE_ID}" "completed" "${PIPELINE_SLOWEST_STAGE:-}" "complete" "${total_dur_s:-0}" 2>/dev/null || true
2428
+ fi
2429
+
2073
2430
  # Auto-ingest pipeline outcome into recruit profiles
2074
2431
  if [[ -x "$SCRIPT_DIR/sw-recruit.sh" ]]; then
2075
2432
  bash "$SCRIPT_DIR/sw-recruit.sh" ingest-pipeline 1 2>/dev/null || true
2076
2433
  fi
2434
+
2435
+ # Capture success patterns to memory (learn what works — parallel the failure path)
2436
+ if [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
2437
+ bash "$SCRIPT_DIR/sw-memory.sh" capture "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
2438
+ fi
2439
+ # Update memory baselines with successful run metrics
2440
+ if type memory_update_metrics >/dev/null 2>&1; then
2441
+ memory_update_metrics "build_duration_s" "${total_dur_s:-0}" 2>/dev/null || true
2442
+ memory_update_metrics "total_cost_usd" "${total_cost:-0}" 2>/dev/null || true
2443
+ memory_update_metrics "iterations" "$((SELF_HEAL_COUNT + 1))" 2>/dev/null || true
2444
+ fi
2445
+
2446
+ # Record positive fix outcome if self-healing succeeded
2447
+ if [[ "$SELF_HEAL_COUNT" -gt 0 && -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
2448
+ local _success_sig
2449
+ _success_sig=$(tail -30 "$ARTIFACTS_DIR/test-results.log" 2>/dev/null | head -3 | tr '\n' ' ' | sed 's/^ *//;s/ *$//' || true)
2450
+ if [[ -n "$_success_sig" ]]; then
2451
+ bash "$SCRIPT_DIR/sw-memory.sh" fix-outcome "$_success_sig" "true" "true" 2>/dev/null || true
2452
+ fi
2453
+ fi
2077
2454
  else
2078
2455
  notify "Pipeline Failed" "Goal: ${GOAL}\nFailed at: ${CURRENT_STAGE_ID:-unknown}" "error"
2079
2456
  emit_event "pipeline.completed" \
2080
2457
  "issue=${ISSUE_NUMBER:-0}" \
2081
2458
  "result=failure" \
2082
2459
  "duration_s=${total_dur_s:-0}" \
2460
+ "iterations=$((SELF_HEAL_COUNT + 1))" \
2461
+ "template=${PIPELINE_NAME}" \
2462
+ "complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
2083
2463
  "failed_stage=${CURRENT_STAGE_ID:-unknown}" \
2464
+ "error_class=${LAST_STAGE_ERROR_CLASS:-unknown}" \
2084
2465
  "agent_id=${PIPELINE_AGENT_ID}" \
2085
2466
  "input_tokens=$TOTAL_INPUT_TOKENS" \
2086
2467
  "output_tokens=$TOTAL_OUTPUT_TOKENS" \
2468
+ "total_cost=$total_cost" \
2087
2469
  "self_heal_count=$SELF_HEAL_COUNT"
2088
2470
 
2471
+ # Update pipeline run status in SQLite
2472
+ if type update_pipeline_status >/dev/null 2>&1; then
2473
+ update_pipeline_status "${SHIPWRIGHT_PIPELINE_ID}" "failed" "${CURRENT_STAGE_ID:-unknown}" "failed" "${total_dur_s:-0}" 2>/dev/null || true
2474
+ fi
2475
+
2089
2476
  # Auto-ingest pipeline outcome into recruit profiles
2090
2477
  if [[ -x "$SCRIPT_DIR/sw-recruit.sh" ]]; then
2091
2478
  bash "$SCRIPT_DIR/sw-recruit.sh" ingest-pipeline 1 2>/dev/null || true
@@ -2121,7 +2508,7 @@ pipeline_start() {
2121
2508
  "success=$pipeline_success"
2122
2509
 
2123
2510
  # Close intelligence prediction feedback loop — validate predicted vs actual
2124
- if type intelligence_validate_prediction &>/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
2511
+ if type intelligence_validate_prediction >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
2125
2512
  intelligence_validate_prediction \
2126
2513
  "$ISSUE_NUMBER" \
2127
2514
  "${INTELLIGENCE_COMPLEXITY:-0}" \
@@ -2129,6 +2516,12 @@ pipeline_start() {
2129
2516
  "$pipeline_success" 2>/dev/null || true
2130
2517
  fi
2131
2518
 
2519
+ # Validate iterations prediction against actuals (cost validation moved below after total_cost is computed)
2520
+ local ACTUAL_ITERATIONS=$((SELF_HEAL_COUNT + 1))
2521
+ if [[ -n "${PREDICTED_ITERATIONS:-}" ]] && type intelligence_validate_prediction >/dev/null 2>&1; then
2522
+ intelligence_validate_prediction "iterations" "$PREDICTED_ITERATIONS" "$ACTUAL_ITERATIONS" 2>/dev/null || true
2523
+ fi
2524
+
2132
2525
  # Close predictive anomaly feedback loop — confirm whether flagged anomalies were real
2133
2526
  if [[ -x "$SCRIPT_DIR/sw-predictive.sh" ]]; then
2134
2527
  local _actual_failure="false"
@@ -2144,7 +2537,8 @@ pipeline_start() {
2144
2537
  "issue=${ISSUE_NUMBER:-0}" \
2145
2538
  "template=${PIPELINE_NAME}" \
2146
2539
  "success=$pipeline_success" \
2147
- "duration_s=${total_dur_s:-0}"
2540
+ "duration_s=${total_dur_s:-0}" \
2541
+ "complexity=${INTELLIGENCE_COMPLEXITY:-0}"
2148
2542
 
2149
2543
  # Risk prediction vs actual failure
2150
2544
  local predicted_risk="${INTELLIGENCE_RISK_SCORE:-0}"
@@ -2167,20 +2561,26 @@ pipeline_start() {
2167
2561
  fi
2168
2562
 
2169
2563
  # Record pipeline outcome for model routing feedback loop
2170
- if type optimize_analyze_outcome &>/dev/null 2>&1; then
2564
+ if type optimize_analyze_outcome >/dev/null 2>&1; then
2171
2565
  optimize_analyze_outcome "$STATE_FILE" 2>/dev/null || true
2172
- # Tune template weights based on accumulated outcomes
2173
- if type optimize_tune_templates &>/dev/null 2>&1; then
2174
- optimize_tune_templates 2>/dev/null || true
2175
- fi
2176
2566
  fi
2177
2567
 
2178
- if type memory_finalize_pipeline &>/dev/null 2>&1; then
2568
+ # Auto-learn after pipeline completion (non-blocking)
2569
+ if type optimize_tune_templates &>/dev/null; then
2570
+ (
2571
+ optimize_tune_templates 2>/dev/null
2572
+ optimize_learn_iterations 2>/dev/null
2573
+ optimize_route_models 2>/dev/null
2574
+ optimize_learn_risk_keywords 2>/dev/null
2575
+ ) &
2576
+ fi
2577
+
2578
+ if type memory_finalize_pipeline >/dev/null 2>&1; then
2179
2579
  memory_finalize_pipeline "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
2180
2580
  fi
2181
2581
 
2182
2582
  # Broadcast discovery for cross-pipeline learning
2183
- if type broadcast_discovery &>/dev/null 2>&1; then
2583
+ if type broadcast_discovery >/dev/null 2>&1; then
2184
2584
  local _disc_result="failure"
2185
2585
  [[ "$exit_code" -eq 0 ]] && _disc_result="success"
2186
2586
  local _disc_files=""
@@ -2209,6 +2609,34 @@ pipeline_start() {
2209
2609
  "model=$model_key" \
2210
2610
  "cost_usd=$total_cost"
2211
2611
 
2612
+ # Persist cost entry to costs.json + SQLite (was missing — tokens accumulated but never written)
2613
+ if type cost_record >/dev/null 2>&1; then
2614
+ cost_record "$TOTAL_INPUT_TOKENS" "$TOTAL_OUTPUT_TOKENS" "$model_key" "pipeline" "${ISSUE_NUMBER:-}" 2>/dev/null || true
2615
+ fi
2616
+
2617
+ # Record pipeline outcome for Thompson sampling / outcome-based learning
2618
+ if type db_record_outcome >/dev/null 2>&1; then
2619
+ local _outcome_success=0
2620
+ [[ "$exit_code" -eq 0 ]] && _outcome_success=1
2621
+ local _outcome_complexity="medium"
2622
+ [[ "${INTELLIGENCE_COMPLEXITY:-5}" -le 3 ]] && _outcome_complexity="low"
2623
+ [[ "${INTELLIGENCE_COMPLEXITY:-5}" -ge 7 ]] && _outcome_complexity="high"
2624
+ db_record_outcome \
2625
+ "${SHIPWRIGHT_PIPELINE_ID:-pipeline-$$-${ISSUE_NUMBER:-0}}" \
2626
+ "${ISSUE_NUMBER:-}" \
2627
+ "${PIPELINE_NAME:-standard}" \
2628
+ "$_outcome_success" \
2629
+ "${total_dur_s:-0}" \
2630
+ "${SELF_HEAL_COUNT:-0}" \
2631
+ "${total_cost:-0}" \
2632
+ "$_outcome_complexity" 2>/dev/null || true
2633
+ fi
2634
+
2635
+ # Validate cost prediction against actual (after total_cost is computed)
2636
+ if [[ -n "${PREDICTED_COST:-}" ]] && type intelligence_validate_prediction >/dev/null 2>&1; then
2637
+ intelligence_validate_prediction "cost" "$PREDICTED_COST" "$total_cost" 2>/dev/null || true
2638
+ fi
2639
+
2212
2640
  return $exit_code
2213
2641
  }
2214
2642