shipwright-cli 2.3.1 → 3.0.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 (162) hide show
  1. package/README.md +95 -28
  2. package/completions/_shipwright +1 -1
  3. package/completions/shipwright.bash +3 -8
  4. package/completions/shipwright.fish +1 -1
  5. package/config/defaults.json +111 -0
  6. package/config/event-schema.json +81 -0
  7. package/config/policy.json +155 -2
  8. package/config/policy.schema.json +162 -1
  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 +15 -5
  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 +126 -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 +39 -16
  39. package/scripts/lib/daemon-health.sh +1 -1
  40. package/scripts/lib/daemon-patrol.sh +24 -12
  41. package/scripts/lib/daemon-poll.sh +37 -25
  42. package/scripts/lib/daemon-state.sh +115 -23
  43. package/scripts/lib/daemon-triage.sh +30 -8
  44. package/scripts/lib/fleet-failover.sh +63 -0
  45. package/scripts/lib/helpers.sh +30 -6
  46. package/scripts/lib/pipeline-detection.sh +2 -2
  47. package/scripts/lib/pipeline-github.sh +9 -9
  48. package/scripts/lib/pipeline-intelligence.sh +85 -35
  49. package/scripts/lib/pipeline-quality-checks.sh +16 -16
  50. package/scripts/lib/pipeline-quality.sh +1 -1
  51. package/scripts/lib/pipeline-stages.sh +242 -28
  52. package/scripts/lib/pipeline-state.sh +40 -4
  53. package/scripts/lib/test-helpers.sh +247 -0
  54. package/scripts/postinstall.mjs +3 -11
  55. package/scripts/sw +10 -4
  56. package/scripts/sw-activity.sh +1 -11
  57. package/scripts/sw-adaptive.sh +109 -85
  58. package/scripts/sw-adversarial.sh +4 -14
  59. package/scripts/sw-architecture-enforcer.sh +1 -11
  60. package/scripts/sw-auth.sh +8 -17
  61. package/scripts/sw-autonomous.sh +111 -49
  62. package/scripts/sw-changelog.sh +1 -11
  63. package/scripts/sw-checkpoint.sh +144 -20
  64. package/scripts/sw-ci.sh +2 -12
  65. package/scripts/sw-cleanup.sh +13 -17
  66. package/scripts/sw-code-review.sh +16 -36
  67. package/scripts/sw-connect.sh +5 -12
  68. package/scripts/sw-context.sh +9 -26
  69. package/scripts/sw-cost.sh +6 -16
  70. package/scripts/sw-daemon.sh +75 -70
  71. package/scripts/sw-dashboard.sh +57 -17
  72. package/scripts/sw-db.sh +506 -15
  73. package/scripts/sw-decompose.sh +1 -11
  74. package/scripts/sw-deps.sh +15 -25
  75. package/scripts/sw-developer-simulation.sh +1 -11
  76. package/scripts/sw-discovery.sh +112 -30
  77. package/scripts/sw-doc-fleet.sh +7 -17
  78. package/scripts/sw-docs-agent.sh +6 -16
  79. package/scripts/sw-docs.sh +4 -12
  80. package/scripts/sw-doctor.sh +134 -43
  81. package/scripts/sw-dora.sh +11 -19
  82. package/scripts/sw-durable.sh +35 -52
  83. package/scripts/sw-e2e-orchestrator.sh +11 -27
  84. package/scripts/sw-eventbus.sh +115 -115
  85. package/scripts/sw-evidence.sh +748 -0
  86. package/scripts/sw-feedback.sh +3 -13
  87. package/scripts/sw-fix.sh +2 -20
  88. package/scripts/sw-fleet-discover.sh +1 -11
  89. package/scripts/sw-fleet-viz.sh +10 -18
  90. package/scripts/sw-fleet.sh +13 -17
  91. package/scripts/sw-github-app.sh +6 -16
  92. package/scripts/sw-github-checks.sh +1 -11
  93. package/scripts/sw-github-deploy.sh +1 -11
  94. package/scripts/sw-github-graphql.sh +2 -12
  95. package/scripts/sw-guild.sh +1 -11
  96. package/scripts/sw-heartbeat.sh +49 -12
  97. package/scripts/sw-hygiene.sh +45 -43
  98. package/scripts/sw-incident.sh +284 -67
  99. package/scripts/sw-init.sh +35 -37
  100. package/scripts/sw-instrument.sh +1 -11
  101. package/scripts/sw-intelligence.sh +362 -51
  102. package/scripts/sw-jira.sh +5 -14
  103. package/scripts/sw-launchd.sh +2 -12
  104. package/scripts/sw-linear.sh +8 -17
  105. package/scripts/sw-logs.sh +4 -12
  106. package/scripts/sw-loop.sh +641 -90
  107. package/scripts/sw-memory.sh +243 -17
  108. package/scripts/sw-mission-control.sh +2 -12
  109. package/scripts/sw-model-router.sh +73 -34
  110. package/scripts/sw-otel.sh +11 -21
  111. package/scripts/sw-oversight.sh +1 -11
  112. package/scripts/sw-patrol-meta.sh +5 -11
  113. package/scripts/sw-pipeline-composer.sh +7 -17
  114. package/scripts/sw-pipeline-vitals.sh +1 -11
  115. package/scripts/sw-pipeline.sh +478 -122
  116. package/scripts/sw-pm.sh +2 -12
  117. package/scripts/sw-pr-lifecycle.sh +203 -29
  118. package/scripts/sw-predictive.sh +16 -22
  119. package/scripts/sw-prep.sh +6 -16
  120. package/scripts/sw-ps.sh +1 -11
  121. package/scripts/sw-public-dashboard.sh +2 -12
  122. package/scripts/sw-quality.sh +77 -10
  123. package/scripts/sw-reaper.sh +1 -11
  124. package/scripts/sw-recruit.sh +15 -25
  125. package/scripts/sw-regression.sh +11 -21
  126. package/scripts/sw-release-manager.sh +19 -28
  127. package/scripts/sw-release.sh +8 -16
  128. package/scripts/sw-remote.sh +1 -11
  129. package/scripts/sw-replay.sh +48 -44
  130. package/scripts/sw-retro.sh +70 -92
  131. package/scripts/sw-review-rerun.sh +220 -0
  132. package/scripts/sw-scale.sh +109 -32
  133. package/scripts/sw-security-audit.sh +12 -22
  134. package/scripts/sw-self-optimize.sh +239 -23
  135. package/scripts/sw-session.sh +3 -13
  136. package/scripts/sw-setup.sh +8 -18
  137. package/scripts/sw-standup.sh +5 -15
  138. package/scripts/sw-status.sh +32 -23
  139. package/scripts/sw-strategic.sh +129 -13
  140. package/scripts/sw-stream.sh +1 -11
  141. package/scripts/sw-swarm.sh +76 -36
  142. package/scripts/sw-team-stages.sh +10 -20
  143. package/scripts/sw-templates.sh +4 -14
  144. package/scripts/sw-testgen.sh +3 -13
  145. package/scripts/sw-tmux-pipeline.sh +1 -19
  146. package/scripts/sw-tmux-role-color.sh +0 -10
  147. package/scripts/sw-tmux-status.sh +3 -11
  148. package/scripts/sw-tmux.sh +2 -20
  149. package/scripts/sw-trace.sh +1 -19
  150. package/scripts/sw-tracker-github.sh +0 -10
  151. package/scripts/sw-tracker-jira.sh +1 -11
  152. package/scripts/sw-tracker-linear.sh +1 -11
  153. package/scripts/sw-tracker.sh +7 -24
  154. package/scripts/sw-triage.sh +24 -34
  155. package/scripts/sw-upgrade.sh +5 -23
  156. package/scripts/sw-ux.sh +1 -19
  157. package/scripts/sw-webhook.sh +18 -32
  158. package/scripts/sw-widgets.sh +3 -21
  159. package/scripts/sw-worktree.sh +11 -27
  160. package/scripts/update-homebrew-sha.sh +67 -0
  161. package/templates/pipelines/tdd.json +72 -0
  162. 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.3.1"
14
+ VERSION="3.0.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,8 @@ 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"
110
96
 
111
97
  # ─── GitHub API Modules (optional) ─────────────────────────────────────────
112
98
  # shellcheck source=sw-github-graphql.sh
@@ -151,6 +137,21 @@ format_duration() {
151
137
  fi
152
138
  }
153
139
 
140
+ # Rotate event log if needed (standalone mode — daemon has its own rotation in poll loop)
141
+ rotate_event_log_if_needed() {
142
+ local events_file="${EVENTS_FILE:-$HOME/.shipwright/events.jsonl}"
143
+ local max_lines=10000
144
+ [[ ! -f "$events_file" ]] && return
145
+ local lines
146
+ lines=$(wc -l < "$events_file" 2>/dev/null || echo "0")
147
+ if [[ "$lines" -gt "$max_lines" ]]; then
148
+ local tmp="${events_file}.rotating"
149
+ if tail -5000 "$events_file" > "$tmp" 2>/dev/null && mv "$tmp" "$events_file" 2>/dev/null; then
150
+ info "Rotated events.jsonl: ${lines} -> 5000 lines"
151
+ fi
152
+ fi
153
+ }
154
+
154
155
  _pipeline_compact_goal() {
155
156
  local goal="$1"
156
157
  local plan_file="${2:-}"
@@ -199,33 +200,6 @@ load_composed_pipeline() {
199
200
  return 0
200
201
  }
201
202
 
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
203
  # ─── Token / Cost Parsing ─────────────────────────────────────────────────
230
204
  parse_claude_tokens() {
231
205
  local log_file="$1"
@@ -237,6 +211,36 @@ parse_claude_tokens() {
237
211
  TOTAL_OUTPUT_TOKENS=$(( TOTAL_OUTPUT_TOKENS + ${output_tok:-0} ))
238
212
  }
239
213
 
214
+ # Estimate pipeline cost using historical averages from completed pipelines.
215
+ # Falls back to per-stage estimates when no history exists.
216
+ estimate_pipeline_cost() {
217
+ local stages="$1"
218
+ local stage_count
219
+ stage_count=$(echo "$stages" | jq 'length' 2>/dev/null || echo "6")
220
+ [[ ! "$stage_count" =~ ^[0-9]+$ ]] && stage_count=6
221
+
222
+ local events_file="${EVENTS_FILE:-$HOME/.shipwright/events.jsonl}"
223
+ local avg_input=0 avg_output=0
224
+ if [[ -f "$events_file" ]]; then
225
+ local hist
226
+ hist=$(grep '"type":"pipeline.completed"' "$events_file" 2>/dev/null | tail -10)
227
+ if [[ -n "$hist" ]]; then
228
+ 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)
229
+ 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)
230
+ fi
231
+ fi
232
+ [[ ! "$avg_input" =~ ^[0-9]+$ ]] && avg_input=0
233
+ [[ ! "$avg_output" =~ ^[0-9]+$ ]] && avg_output=0
234
+
235
+ # Fall back to reasonable per-stage estimates only if no history
236
+ if [[ "$avg_input" -eq 0 ]]; then
237
+ avg_input=$(( stage_count * 8000 )) # More realistic: ~8K input per stage
238
+ avg_output=$(( stage_count * 4000 )) # ~4K output per stage
239
+ fi
240
+
241
+ echo "{\"input_tokens\":${avg_input},\"output_tokens\":${avg_output}}"
242
+ }
243
+
240
244
  # ─── Defaults ───────────────────────────────────────────────────────────────
241
245
  GOAL=""
242
246
  ISSUE_NUMBER=""
@@ -260,6 +264,7 @@ CI_MODE=false
260
264
  DRY_RUN=false
261
265
  IGNORE_BUDGET=false
262
266
  COMPLETED_STAGES=""
267
+ RESUME_FROM_CHECKPOINT=false
263
268
  MAX_ITERATIONS_OVERRIDE=""
264
269
  MAX_RESTARTS_OVERRIDE=""
265
270
  FAST_TEST_CMD_OVERRIDE=""
@@ -285,6 +290,10 @@ GH_AVAILABLE=false
285
290
  # Timing
286
291
  PIPELINE_START_EPOCH=""
287
292
  STAGE_TIMINGS=""
293
+ PIPELINE_STAGES_PASSED=""
294
+ PIPELINE_SLOWEST_STAGE=""
295
+ LAST_STAGE_ERROR_CLASS=""
296
+ LAST_STAGE_ERROR=""
288
297
 
289
298
  PROJECT_ROOT=""
290
299
  STATE_DIR=""
@@ -333,6 +342,7 @@ show_help() {
333
342
  echo -e " ${DIM}--max-iterations <n>${RESET} Override max build loop iterations"
334
343
  echo -e " ${DIM}--max-restarts <n>${RESET} Max session restarts in build loop"
335
344
  echo -e " ${DIM}--fast-test-cmd <cmd>${RESET} Fast/subset test for build loop"
345
+ echo -e " ${DIM}--tdd${RESET} Test-first: generate tests before implementation"
336
346
  echo -e " ${DIM}--completed-stages \"a,b\"${RESET} Skip these stages (CI resume)"
337
347
  echo ""
338
348
  echo -e "${BOLD}STAGES${RESET} ${DIM}(configurable per pipeline template)${RESET}"
@@ -413,6 +423,7 @@ parse_args() {
413
423
  --ignore-budget) IGNORE_BUDGET=true; shift ;;
414
424
  --max-iterations) MAX_ITERATIONS_OVERRIDE="$2"; shift 2 ;;
415
425
  --completed-stages) COMPLETED_STAGES="$2"; shift 2 ;;
426
+ --resume) RESUME_FROM_CHECKPOINT=true; shift ;;
416
427
  --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
428
  --worktree) AUTO_WORKTREE=true; shift ;;
418
429
  --dry-run) DRY_RUN=true; shift ;;
@@ -427,6 +438,7 @@ parse_args() {
427
438
  shift 2 ;;
428
439
 
429
440
  --fast-test-cmd) FAST_TEST_CMD_OVERRIDE="$2"; shift 2 ;;
441
+ --tdd) TDD_ENABLED=true; shift ;;
430
442
  --help|-h) show_help; exit 0 ;;
431
443
  *)
432
444
  if [[ -z "$PIPELINE_NAME_ARG" ]]; then
@@ -487,11 +499,11 @@ find_pipeline_config() {
487
499
  load_pipeline_config() {
488
500
  # Check for intelligence-composed pipeline first
489
501
  local composed_pipeline="${ARTIFACTS_DIR}/composed-pipeline.json"
490
- if [[ -f "$composed_pipeline" ]] && type composer_validate_pipeline &>/dev/null; then
502
+ if [[ -f "$composed_pipeline" ]] && type composer_validate_pipeline >/dev/null 2>&1; then
491
503
  # Use composed pipeline if fresh (< 1 hour old)
492
504
  local composed_age=99999
493
505
  local composed_mtime
494
- composed_mtime=$(stat -f %m "$composed_pipeline" 2>/dev/null || stat -c %Y "$composed_pipeline" 2>/dev/null || echo "0")
506
+ composed_mtime=$(file_mtime "$composed_pipeline")
495
507
  if [[ "$composed_mtime" -gt 0 ]]; then
496
508
  composed_age=$(( $(now_epoch) - composed_mtime ))
497
509
  fi
@@ -513,6 +525,9 @@ load_pipeline_config() {
513
525
  exit 1
514
526
  }
515
527
  info "Pipeline: ${BOLD}$PIPELINE_NAME${RESET} ${DIM}($PIPELINE_CONFIG)${RESET}"
528
+ # TDD from template (overridable by --tdd)
529
+ [[ "$(jq -r '.tdd // false' "$PIPELINE_CONFIG" 2>/dev/null)" == "true" ]] && PIPELINE_TDD=true
530
+ return 0
516
531
  }
517
532
 
518
533
  CURRENT_STAGE_ID=""
@@ -522,7 +537,7 @@ SLACK_WEBHOOK=""
522
537
  NOTIFICATION_ENABLED=false
523
538
 
524
539
  # Self-healing
525
- BUILD_TEST_RETRIES=2
540
+ BUILD_TEST_RETRIES=$(_config_get_int "pipeline.build_test_retries" 3 2>/dev/null || echo 3)
526
541
  STASHED_CHANGES=false
527
542
  SELF_HEAL_COUNT=0
528
543
 
@@ -544,7 +559,7 @@ start_heartbeat() {
544
559
  --stage "${CURRENT_STAGE_ID:-unknown}" \
545
560
  --iteration "0" \
546
561
  --activity "$(get_stage_description "${CURRENT_STAGE_ID:-}" 2>/dev/null || echo "Running pipeline")" 2>/dev/null || true
547
- sleep 30
562
+ sleep "$(_config_get_int "pipeline.heartbeat_interval" 30 2>/dev/null || echo 30)"
548
563
  done
549
564
  ) >/dev/null 2>&1 &
550
565
  HEARTBEAT_PID=$!
@@ -574,7 +589,10 @@ ci_push_partial_work() {
574
589
  fi
575
590
 
576
591
  # Push branch (create if needed, force to overwrite previous WIP)
577
- git push origin "HEAD:refs/heads/$branch" --force 2>/dev/null || true
592
+ if ! git push origin "HEAD:refs/heads/$branch" --force 2>/dev/null; then
593
+ warn "git push failed for $branch — remote may be out of sync"
594
+ emit_event "pipeline.push_failed" "branch=$branch"
595
+ fi
578
596
  }
579
597
 
580
598
  ci_post_stage_event() {
@@ -584,7 +602,7 @@ ci_post_stage_event() {
584
602
 
585
603
  local stage="$1" status="$2" elapsed="${3:-0s}"
586
604
  local comment="<!-- SHIPWRIGHT-STAGE: ${stage}:${status}:${elapsed} -->"
587
- gh issue comment "$ISSUE_NUMBER" --body "$comment" 2>/dev/null || true
605
+ _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
606
  }
589
607
 
590
608
  # ─── Signal Handling ───────────────────────────────────────────────────────
@@ -620,7 +638,10 @@ cleanup_on_exit() {
620
638
 
621
639
  # Update GitHub
622
640
  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
641
+ 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
642
+ warn "gh issue comment failed — status update may not have been posted"
643
+ emit_event "pipeline.comment_failed" "issue=$ISSUE_NUMBER"
644
+ fi
624
645
  fi
625
646
 
626
647
  exit "$exit_code"
@@ -641,7 +662,7 @@ preflight_checks() {
641
662
  local optional_tools=("gh" "claude" "bc" "curl")
642
663
 
643
664
  for tool in "${required_tools[@]}"; do
644
- if command -v "$tool" &>/dev/null; then
665
+ if command -v "$tool" >/dev/null 2>&1; then
645
666
  echo -e " ${GREEN}✓${RESET} $tool"
646
667
  else
647
668
  echo -e " ${RED}✗${RESET} $tool ${RED}(required)${RESET}"
@@ -650,7 +671,7 @@ preflight_checks() {
650
671
  done
651
672
 
652
673
  for tool in "${optional_tools[@]}"; do
653
- if command -v "$tool" &>/dev/null; then
674
+ if command -v "$tool" >/dev/null 2>&1; then
654
675
  echo -e " ${GREEN}✓${RESET} $tool"
655
676
  else
656
677
  echo -e " ${DIM}○${RESET} $tool ${DIM}(optional — some features disabled)${RESET}"
@@ -659,7 +680,7 @@ preflight_checks() {
659
680
 
660
681
  # 2. Git state
661
682
  echo ""
662
- if git rev-parse --is-inside-work-tree &>/dev/null; then
683
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
663
684
  echo -e " ${GREEN}✓${RESET} Inside git repo"
664
685
  else
665
686
  echo -e " ${RED}✗${RESET} Not inside a git repository"
@@ -685,7 +706,7 @@ preflight_checks() {
685
706
  fi
686
707
 
687
708
  # Check if base branch exists
688
- if git rev-parse --verify "$BASE_BRANCH" &>/dev/null; then
709
+ if git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
689
710
  echo -e " ${GREEN}✓${RESET} Base branch: $BASE_BRANCH"
690
711
  else
691
712
  echo -e " ${RED}✗${RESET} Base branch not found: $BASE_BRANCH"
@@ -693,8 +714,8 @@ preflight_checks() {
693
714
  fi
694
715
 
695
716
  # 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
717
+ if [[ "$NO_GITHUB" != "true" ]] && command -v gh >/dev/null 2>&1; then
718
+ if gh auth status >/dev/null 2>&1; then
698
719
  echo -e " ${GREEN}✓${RESET} GitHub authenticated"
699
720
  else
700
721
  echo -e " ${YELLOW}⚠${RESET} GitHub not authenticated (features disabled)"
@@ -702,7 +723,7 @@ preflight_checks() {
702
723
  fi
703
724
 
704
725
  # 4. Claude CLI
705
- if command -v claude &>/dev/null; then
726
+ if command -v claude >/dev/null 2>&1; then
706
727
  echo -e " ${GREEN}✓${RESET} Claude CLI available"
707
728
  else
708
729
  echo -e " ${RED}✗${RESET} Claude CLI not found — plan/build stages will fail"
@@ -754,12 +775,12 @@ notify() {
754
775
  payload=$(jq -n \
755
776
  --arg text "${emoji} *${title}*\n${message}" \
756
777
  '{text: $text}')
757
- curl -sf -X POST -H 'Content-Type: application/json' \
778
+ 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
779
  -d "$payload" "$SLACK_WEBHOOK" >/dev/null 2>&1 || true
759
780
  fi
760
781
 
761
- # Custom webhook (env var SHIPWRIGHT_WEBHOOK_URL, with CCT_WEBHOOK_URL fallback)
762
- local _webhook_url="${SHIPWRIGHT_WEBHOOK_URL:-${CCT_WEBHOOK_URL:-}}"
782
+ # Custom webhook (env var SHIPWRIGHT_WEBHOOK_URL)
783
+ local _webhook_url="${SHIPWRIGHT_WEBHOOK_URL:-}"
763
784
  if [[ -n "$_webhook_url" ]]; then
764
785
  local payload
765
786
  payload=$(jq -n \
@@ -767,7 +788,7 @@ notify() {
767
788
  --arg level "$level" --arg pipeline "${PIPELINE_NAME:-}" \
768
789
  --arg goal "${GOAL:-}" --arg stage "${CURRENT_STAGE_ID:-}" \
769
790
  '{title:$title, message:$message, level:$level, pipeline:$pipeline, goal:$goal, stage:$stage}')
770
- curl -sf -X POST -H 'Content-Type: application/json' \
791
+ curl -sf --connect-timeout 10 --max-time 30 -X POST -H 'Content-Type: application/json' \
771
792
  -d "$payload" "$_webhook_url" >/dev/null 2>&1 || true
772
793
  fi
773
794
  }
@@ -815,7 +836,7 @@ classify_error() {
815
836
  elif echo "$log_tail" | grep -qiE 'error\[E[0-9]+\]|error: aborting|FAILED.*compile|build failed|tsc.*error|eslint.*error'; then
816
837
  classification="logic"
817
838
  # 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
839
+ elif [[ "$classification" == "unknown" ]] && type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
819
840
  local ai_class
820
841
  ai_class=$(claude --print --output-format text -p "Classify this error as exactly one of: infrastructure, configuration, logic, unknown.
821
842
 
@@ -882,14 +903,23 @@ run_stage_with_retry() {
882
903
  return 0
883
904
  fi
884
905
 
906
+ # Capture error_class and error snippet for stage.failed / pipeline.completed events
907
+ local error_class
908
+ error_class=$(classify_error "$stage_id")
909
+ LAST_STAGE_ERROR_CLASS="$error_class"
910
+ LAST_STAGE_ERROR=""
911
+ local _log_file="${ARTIFACTS_DIR}/${stage_id}-results.log"
912
+ [[ ! -f "$_log_file" ]] && _log_file="${ARTIFACTS_DIR}/test-results.log"
913
+ if [[ -f "$_log_file" ]]; then
914
+ 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)
915
+ fi
916
+
885
917
  attempt=$((attempt + 1))
886
918
  if [[ "$attempt" -gt "$max_retries" ]]; then
887
919
  return 1
888
920
  fi
889
921
 
890
- # Classify the error to decide whether retry makes sense
891
- local error_class
892
- error_class=$(classify_error "$stage_id")
922
+ # Classify done above; decide whether retry makes sense
893
923
 
894
924
  emit_event "retry.classified" \
895
925
  "issue=${ISSUE_NUMBER:-0}" \
@@ -926,6 +956,15 @@ run_stage_with_retry() {
926
956
  esac
927
957
  prev_error_class="$error_class"
928
958
 
959
+ if type db_save_reasoning_trace >/dev/null 2>&1; then
960
+ local job_id="${SHIPWRIGHT_PIPELINE_ID:-$$}"
961
+ local error_msg="${LAST_STAGE_ERROR:-$error_class}"
962
+ db_save_reasoning_trace "$job_id" "retry_reasoning" \
963
+ "stage=$stage_id error=$error_msg" \
964
+ "Stage failed, analyzing error pattern before retry" \
965
+ "retry_strategy=self_heal" 0.6 2>/dev/null || true
966
+ fi
967
+
929
968
  warn "Stage $stage_id failed (attempt $attempt/$((max_retries + 1)), class: $error_class) — retrying..."
930
969
  # Exponential backoff with jitter to avoid thundering herd
931
970
  local backoff=$((2 ** attempt))
@@ -951,9 +990,9 @@ self_healing_build_test() {
951
990
  local prev_fail_count=0 zero_convergence_streak=0
952
991
 
953
992
  # Vitals-driven adaptive limit (preferred over static BUILD_TEST_RETRIES)
954
- if type pipeline_adaptive_limit &>/dev/null 2>&1; then
993
+ if type pipeline_adaptive_limit >/dev/null 2>&1; then
955
994
  local _vitals_json=""
956
- if type pipeline_compute_vitals &>/dev/null 2>&1; then
995
+ if type pipeline_compute_vitals >/dev/null 2>&1; then
957
996
  _vitals_json=$(pipeline_compute_vitals "$STATE_FILE" "$ARTIFACTS_DIR" "${ISSUE_NUMBER:-}" 2>/dev/null) || true
958
997
  fi
959
998
  local vitals_limit
@@ -968,7 +1007,7 @@ self_healing_build_test() {
968
1007
  "vitals_limit=$vitals_limit"
969
1008
  fi
970
1009
  # Fallback: intelligence-based adaptive limits
971
- elif type composer_estimate_iterations &>/dev/null 2>&1; then
1010
+ elif type composer_estimate_iterations >/dev/null 2>&1; then
972
1011
  local estimated
973
1012
  estimated=$(composer_estimate_iterations \
974
1013
  "${INTELLIGENCE_ANALYSIS:-{}}" \
@@ -1022,7 +1061,7 @@ self_healing_build_test() {
1022
1061
  if [[ "$cycle" -gt 1 && -n "$last_test_error" ]]; then
1023
1062
  # Query memory for known fixes
1024
1063
  local _memory_fix=""
1025
- if type memory_closed_loop_inject &>/dev/null 2>&1; then
1064
+ if type memory_closed_loop_inject >/dev/null 2>&1; then
1026
1065
  local _error_sig_short
1027
1066
  _error_sig_short=$(echo "$last_test_error" | head -3 || echo "")
1028
1067
  _memory_fix=$(memory_closed_loop_inject "$_error_sig_short" 2>/dev/null) || true
@@ -1053,7 +1092,7 @@ Focus on fixing the failing tests while keeping all passing tests working."
1053
1092
  local timing
1054
1093
  timing=$(get_stage_timing "build")
1055
1094
  success "Stage ${BOLD}build${RESET} complete ${DIM}(${timing})${RESET}"
1056
- if type pipeline_emit_progress_snapshot &>/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1095
+ if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1057
1096
  local _diff_count
1058
1097
  _diff_count=$(git diff --stat HEAD~1 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1) || true
1059
1098
  local _snap_files _snap_error
@@ -1078,7 +1117,7 @@ Focus on fixing the failing tests while keeping all passing tests working."
1078
1117
  local timing
1079
1118
  timing=$(get_stage_timing "build")
1080
1119
  success "Stage ${BOLD}build${RESET} complete ${DIM}(${timing})${RESET}"
1081
- if type pipeline_emit_progress_snapshot &>/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1120
+ if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1082
1121
  local _diff_count
1083
1122
  _diff_count=$(git diff --stat HEAD~1 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1) || true
1084
1123
  local _snap_files _snap_error
@@ -1109,7 +1148,7 @@ Focus on fixing the failing tests while keeping all passing tests working."
1109
1148
  emit_event "convergence.tests_passed" \
1110
1149
  "issue=${ISSUE_NUMBER:-0}" \
1111
1150
  "cycle=$cycle"
1112
- if type pipeline_emit_progress_snapshot &>/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1151
+ if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
1113
1152
  local _diff_count
1114
1153
  _diff_count=$(git diff --stat HEAD~1 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1) || true
1115
1154
  local _snap_files _snap_error
@@ -1235,6 +1274,9 @@ auto_rebase() {
1235
1274
  }
1236
1275
 
1237
1276
  run_pipeline() {
1277
+ # Rotate event log if needed (standalone mode)
1278
+ rotate_event_log_if_needed
1279
+
1238
1280
  local stages
1239
1281
  stages=$(jq -c '.stages[]' "$PIPELINE_CONFIG")
1240
1282
 
@@ -1329,6 +1371,10 @@ run_pipeline() {
1329
1371
 
1330
1372
  # Self-healing build→test loop: when we hit build, run both together
1331
1373
  if [[ "$id" == "build" && "$use_self_healing" == "true" ]]; then
1374
+ # TDD: generate tests before build when enabled
1375
+ if [[ "${TDD_ENABLED:-false}" == "true" || "${PIPELINE_TDD:-}" == "true" ]]; then
1376
+ stage_test_first || true
1377
+ fi
1332
1378
  # Gate check for build
1333
1379
  local build_gate
1334
1380
  build_gate=$(echo "$stage" | jq -r '.gate')
@@ -1362,6 +1408,11 @@ run_pipeline() {
1362
1408
  continue
1363
1409
  fi
1364
1410
 
1411
+ # TDD: generate tests before build when enabled (non-self-healing path)
1412
+ if [[ "$id" == "build" && "$use_self_healing" != "true" ]] && [[ "${TDD_ENABLED:-false}" == "true" || "${PIPELINE_TDD:-}" == "true" ]]; then
1413
+ stage_test_first || true
1414
+ fi
1415
+
1365
1416
  # Skip test if already handled by self-healing loop
1366
1417
  if [[ "$id" == "test" && "$use_self_healing" == "true" ]]; then
1367
1418
  stage_status=$(get_stage_status "test")
@@ -1401,52 +1452,59 @@ run_pipeline() {
1401
1452
  fi
1402
1453
  fi
1403
1454
 
1404
- # Intelligence: per-stage model routing with A/B testing
1405
- if type intelligence_recommend_model &>/dev/null 2>&1; then
1455
+ # Intelligence: per-stage model routing (UCB1 when DB has data, else A/B testing)
1456
+ local recommended_model="" from_ucb1=false
1457
+ if type ucb1_select_model >/dev/null 2>&1; then
1458
+ recommended_model=$(ucb1_select_model "$id" 2>/dev/null || echo "")
1459
+ [[ -n "$recommended_model" ]] && from_ucb1=true
1460
+ fi
1461
+ if [[ -z "$recommended_model" ]] && type intelligence_recommend_model >/dev/null 2>&1; then
1406
1462
  local stage_complexity="${INTELLIGENCE_COMPLEXITY:-5}"
1407
1463
  local budget_remaining=""
1408
1464
  if [[ -x "$SCRIPT_DIR/sw-cost.sh" ]]; then
1409
1465
  budget_remaining=$(bash "$SCRIPT_DIR/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
1410
1466
  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
1467
+ local recommended_json
1468
+ recommended_json=$(intelligence_recommend_model "$id" "$stage_complexity" "$budget_remaining" 2>/dev/null || echo "")
1469
+ recommended_model=$(echo "$recommended_json" | jq -r '.model // empty' 2>/dev/null || echo "")
1470
+ fi
1471
+ if [[ -n "$recommended_model" && "$recommended_model" != "null" ]]; then
1472
+ if [[ "$from_ucb1" == "true" ]]; then
1473
+ # UCB1 already balances exploration/exploitation — use directly
1474
+ export CLAUDE_MODEL="$recommended_model"
1475
+ emit_event "intelligence.model_ucb1" \
1476
+ "issue=${ISSUE_NUMBER:-0}" \
1477
+ "stage=$id" \
1478
+ "model=$recommended_model"
1479
+ else
1480
+ # A/B testing for intelligence recommendation
1481
+ local ab_ratio=20
1416
1482
  local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
1417
1483
  if [[ -f "$daemon_cfg" ]]; then
1418
1484
  local cfg_ratio
1419
1485
  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
1486
  ab_ratio=$(awk -v r="$cfg_ratio" 'BEGIN{printf "%d", r * 100}' 2>/dev/null || echo "20")
1422
1487
  fi
1423
1488
 
1424
- # Check if we have enough data points to graduate from A/B testing
1425
1489
  local routing_file="${HOME}/.shipwright/optimization/model-routing.json"
1426
1490
  local use_recommended=false
1427
1491
  local ab_group="control"
1428
1492
 
1429
1493
  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
1494
+ local stage_samples total_samples
1495
+ stage_samples=$(jq -r --arg s "$id" '.routes[$s].sonnet_samples // .[$s].sonnet_samples // 0' "$routing_file" 2>/dev/null || echo "0")
1496
+ 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")
1497
+ if [[ "${total_samples:-0}" -ge 50 ]]; then
1437
1498
  use_recommended=true
1438
1499
  ab_group="graduated"
1439
1500
  fi
1440
1501
  fi
1441
1502
 
1442
1503
  if [[ "$use_recommended" != "true" ]]; then
1443
- # A/B test: RANDOM % 100 < ab_ratio → use recommended
1444
1504
  local roll=$((RANDOM % 100))
1445
1505
  if [[ "$roll" -lt "$ab_ratio" ]]; then
1446
1506
  use_recommended=true
1447
1507
  ab_group="experiment"
1448
- else
1449
- ab_group="control"
1450
1508
  fi
1451
1509
  fi
1452
1510
 
@@ -1475,7 +1533,7 @@ run_pipeline() {
1475
1533
  emit_event "stage.started" "issue=${ISSUE_NUMBER:-0}" "stage=$id"
1476
1534
 
1477
1535
  # Mark GitHub Check Run as in-progress
1478
- if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update &>/dev/null 2>&1; then
1536
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update >/dev/null 2>&1; then
1479
1537
  gh_checks_stage_update "$id" "in_progress" "" "Stage $id started" 2>/dev/null || true
1480
1538
  fi
1481
1539
 
@@ -1491,7 +1549,9 @@ run_pipeline() {
1491
1549
  timing=$(get_stage_timing "$id")
1492
1550
  stage_dur_s=$(( $(now_epoch) - stage_start_epoch ))
1493
1551
  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"
1552
+ emit_event "stage.completed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s" "result=success"
1553
+ # Record model outcome for UCB1 learning
1554
+ 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
1555
  # Broadcast discovery for cross-pipeline learning
1496
1556
  if [[ -x "$SCRIPT_DIR/sw-discovery.sh" ]]; then
1497
1557
  local _disc_cat _disc_patterns _disc_text
@@ -1514,9 +1574,16 @@ run_pipeline() {
1514
1574
  stage_dur_s=$(( $(now_epoch) - stage_start_epoch ))
1515
1575
  error "Pipeline failed at stage: ${BOLD}$id${RESET}"
1516
1576
  update_status "failed" "$id"
1517
- emit_event "stage.failed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s"
1577
+ emit_event "stage.failed" \
1578
+ "issue=${ISSUE_NUMBER:-0}" \
1579
+ "stage=$id" \
1580
+ "duration_s=$stage_dur_s" \
1581
+ "error=${LAST_STAGE_ERROR:-unknown}" \
1582
+ "error_class=${LAST_STAGE_ERROR_CLASS:-unknown}"
1518
1583
  # Log model used for prediction feedback
1519
1584
  echo "${id}|${stage_model_used}|false" >> "${ARTIFACTS_DIR}/model-routing.log"
1585
+ # Record model outcome for UCB1 learning
1586
+ 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
1587
  # Cancel any remaining in_progress check runs
1521
1588
  pipeline_cancel_check_runs 2>/dev/null || true
1522
1589
  return 1
@@ -1525,6 +1592,11 @@ run_pipeline() {
1525
1592
 
1526
1593
  # Pipeline complete!
1527
1594
  update_status "complete" ""
1595
+ PIPELINE_STAGES_PASSED="$completed"
1596
+ PIPELINE_SLOWEST_STAGE=""
1597
+ if type get_slowest_stage >/dev/null 2>&1; then
1598
+ PIPELINE_SLOWEST_STAGE=$(get_slowest_stage 2>/dev/null || true)
1599
+ fi
1528
1600
  local total_dur=""
1529
1601
  if [[ -n "$PIPELINE_START_EPOCH" ]]; then
1530
1602
  total_dur=$(format_duration $(( $(now_epoch) - PIPELINE_START_EPOCH )))
@@ -1568,7 +1640,7 @@ run_pipeline() {
1568
1640
  pipeline_post_completion_cleanup() {
1569
1641
  local cleaned=0
1570
1642
 
1571
- # 1. Clear checkpoints (they only matter for resume; pipeline is done)
1643
+ # 1. Clear checkpoints and context files (they only matter for resume; pipeline is done)
1572
1644
  if [[ -d "${ARTIFACTS_DIR}/checkpoints" ]]; then
1573
1645
  local cp_count=0
1574
1646
  local cp_file
@@ -1577,6 +1649,11 @@ pipeline_post_completion_cleanup() {
1577
1649
  rm -f "$cp_file"
1578
1650
  cp_count=$((cp_count + 1))
1579
1651
  done
1652
+ for cp_file in "${ARTIFACTS_DIR}/checkpoints"/*-claude-context.json; do
1653
+ [[ -f "$cp_file" ]] || continue
1654
+ rm -f "$cp_file"
1655
+ cp_count=$((cp_count + 1))
1656
+ done
1580
1657
  if [[ "$cp_count" -gt 0 ]]; then
1581
1658
  cleaned=$((cleaned + cp_count))
1582
1659
  fi
@@ -1621,7 +1698,7 @@ pipeline_cancel_check_runs() {
1621
1698
  return
1622
1699
  fi
1623
1700
 
1624
- if ! type gh_checks_stage_update &>/dev/null 2>&1; then
1701
+ if ! type gh_checks_stage_update >/dev/null 2>&1; then
1625
1702
  return
1626
1703
  fi
1627
1704
 
@@ -1673,7 +1750,7 @@ pipeline_setup_worktree() {
1673
1750
 
1674
1751
  # Store original dir for cleanup, then cd into worktree
1675
1752
  ORIGINAL_REPO_DIR="$(pwd)"
1676
- cd "$worktree_path"
1753
+ cd "$worktree_path" || { error "Failed to cd into worktree: $worktree_path"; return 1; }
1677
1754
  CLEANUP_WORKTREE=true
1678
1755
 
1679
1756
  success "Worktree ready: ${CYAN}${worktree_path}${RESET} (branch: ${branch_name})"
@@ -1807,7 +1884,7 @@ run_dry_run() {
1807
1884
  local optional_tools=("gh" "claude" "bc")
1808
1885
 
1809
1886
  for tool in "${required_tools[@]}"; do
1810
- if command -v "$tool" &>/dev/null; then
1887
+ if command -v "$tool" >/dev/null 2>&1; then
1811
1888
  echo -e " ${GREEN}✓${RESET} $tool"
1812
1889
  else
1813
1890
  echo -e " ${RED}✗${RESET} $tool ${RED}(required)${RESET}"
@@ -1816,7 +1893,7 @@ run_dry_run() {
1816
1893
  done
1817
1894
 
1818
1895
  for tool in "${optional_tools[@]}"; do
1819
- if command -v "$tool" &>/dev/null; then
1896
+ if command -v "$tool" >/dev/null 2>&1; then
1820
1897
  echo -e " ${GREEN}✓${RESET} $tool"
1821
1898
  else
1822
1899
  echo -e " ${DIM}○${RESET} $tool"
@@ -1825,15 +1902,17 @@ run_dry_run() {
1825
1902
 
1826
1903
  echo ""
1827
1904
 
1828
- # Cost estimation (rough approximation)
1905
+ # Cost estimation: use historical averages from past pipelines when available
1829
1906
  echo -e "${BLUE}${BOLD}━━━ Estimated Resource Usage ━━━${RESET}"
1830
1907
  echo ""
1831
1908
 
1832
- # Very rough cost estimation: ~2000 input tokens per stage, ~3000 output tokens
1833
- # Adjust based on pipeline complexity
1909
+ local stages_json
1910
+ stages_json=$(jq '[.stages[] | select(.enabled == true)]' "$PIPELINE_CONFIG" 2>/dev/null || echo "[]")
1911
+ local est
1912
+ est=$(estimate_pipeline_cost "$stages_json")
1834
1913
  local input_tokens_estimate output_tokens_estimate
1835
- input_tokens_estimate=$(( enabled_stages * 2000 ))
1836
- output_tokens_estimate=$(( enabled_stages * 3000 ))
1914
+ input_tokens_estimate=$(echo "$est" | jq -r '.input_tokens // 0')
1915
+ output_tokens_estimate=$(echo "$est" | jq -r '.output_tokens // 0')
1837
1916
 
1838
1917
  # Calculate cost based on selected model
1839
1918
  local input_rate output_rate input_cost output_cost total_cost
@@ -1848,11 +1927,11 @@ run_dry_run() {
1848
1927
  echo -e " ${BOLD}Estimated Input Tokens:${RESET} ~$input_tokens_estimate"
1849
1928
  echo -e " ${BOLD}Estimated Output Tokens:${RESET} ~$output_tokens_estimate"
1850
1929
  echo -e " ${BOLD}Model Cost Rate:${RESET} $stage_model"
1851
- echo -e " ${BOLD}Estimated Cost:${RESET} \$$total_cost USD (rough estimate)"
1930
+ echo -e " ${BOLD}Estimated Cost:${RESET} \$$total_cost USD"
1852
1931
  echo ""
1853
1932
 
1854
1933
  # Validate composed pipeline if intelligence is enabled
1855
- if [[ -f "$ARTIFACTS_DIR/composed-pipeline.json" ]] && type composer_validate_pipeline &>/dev/null; then
1934
+ if [[ -f "$ARTIFACTS_DIR/composed-pipeline.json" ]] && type composer_validate_pipeline >/dev/null 2>&1; then
1856
1935
  echo -e "${BLUE}${BOLD}━━━ Intelligence-Composed Pipeline ━━━${RESET}"
1857
1936
  echo ""
1858
1937
 
@@ -1877,6 +1956,100 @@ run_dry_run() {
1877
1956
  return 0
1878
1957
  }
1879
1958
 
1959
+ # ─── Reasoning Trace Generation ──────────────────────────────────────────────
1960
+ # Multi-step autonomous reasoning traces for pipeline start (before stages run)
1961
+
1962
+ generate_reasoning_trace() {
1963
+ local job_id="${SHIPWRIGHT_PIPELINE_ID:-$$}"
1964
+ local issue="${ISSUE_NUMBER:-}"
1965
+ local goal="${GOAL:-}"
1966
+
1967
+ # Step 1: Analyze issue complexity and risk
1968
+ local complexity="medium"
1969
+ local risk_score=50
1970
+ if [[ -n "$issue" ]] && type intelligence_analyze_issue >/dev/null 2>&1; then
1971
+ local issue_json analysis
1972
+ issue_json=$(gh issue view "$issue" --json number,title,body,labels 2>/dev/null || echo "{}")
1973
+ if [[ -n "$issue_json" && "$issue_json" != "{}" ]]; then
1974
+ analysis=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "")
1975
+ if [[ -n "$analysis" ]]; then
1976
+ local comp_num
1977
+ comp_num=$(echo "$analysis" | jq -r '.complexity // 5' 2>/dev/null || echo "5")
1978
+ if [[ "$comp_num" -le 3 ]]; then
1979
+ complexity="low"
1980
+ elif [[ "$comp_num" -le 6 ]]; then
1981
+ complexity="medium"
1982
+ else
1983
+ complexity="high"
1984
+ fi
1985
+ risk_score=$((100 - $(echo "$analysis" | jq -r '.success_probability // 50' 2>/dev/null || echo "50")))
1986
+ fi
1987
+ fi
1988
+ elif [[ -n "$goal" ]]; then
1989
+ issue_json=$(jq -n --arg title "${goal}" --arg body "" '{title: $title, body: $body, labels: []}')
1990
+ if type intelligence_analyze_issue >/dev/null 2>&1; then
1991
+ analysis=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "")
1992
+ if [[ -n "$analysis" ]]; then
1993
+ local comp_num
1994
+ comp_num=$(echo "$analysis" | jq -r '.complexity // 5' 2>/dev/null || echo "5")
1995
+ if [[ "$comp_num" -le 3 ]]; then complexity="low"; elif [[ "$comp_num" -le 6 ]]; then complexity="medium"; else complexity="high"; fi
1996
+ risk_score=$((100 - $(echo "$analysis" | jq -r '.success_probability // 50' 2>/dev/null || echo "50")))
1997
+ fi
1998
+ fi
1999
+ fi
2000
+
2001
+ # Step 2: Query similar past issues
2002
+ local similar_context=""
2003
+ if type memory_semantic_search >/dev/null 2>&1 && [[ -n "$goal" ]]; then
2004
+ similar_context=$(memory_semantic_search "$goal" "" 3 2>/dev/null || echo "")
2005
+ fi
2006
+
2007
+ # Step 3: Select template using Thompson sampling
2008
+ local selected_template="${PIPELINE_TEMPLATE:-}"
2009
+ if [[ -z "$selected_template" ]] && type thompson_select_template >/dev/null 2>&1; then
2010
+ selected_template=$(thompson_select_template "$complexity" 2>/dev/null || echo "standard")
2011
+ fi
2012
+ [[ -z "$selected_template" ]] && selected_template="standard"
2013
+
2014
+ # Step 4: Predict failure modes from memory
2015
+ local failure_predictions=""
2016
+ if type memory_semantic_search >/dev/null 2>&1 && [[ -n "$goal" ]]; then
2017
+ failure_predictions=$(memory_semantic_search "failure error $goal" "" 3 2>/dev/null || echo "")
2018
+ fi
2019
+
2020
+ # Save reasoning traces to DB
2021
+ if type db_save_reasoning_trace >/dev/null 2>&1; then
2022
+ db_save_reasoning_trace "$job_id" "complexity_analysis" \
2023
+ "issue=$issue goal=$goal" \
2024
+ "Analyzed complexity=$complexity risk=$risk_score" \
2025
+ "complexity=$complexity risk_score=$risk_score" 0.7 2>/dev/null || true
2026
+
2027
+ db_save_reasoning_trace "$job_id" "template_selection" \
2028
+ "complexity=$complexity historical_outcomes" \
2029
+ "Thompson sampling over historical success rates" \
2030
+ "template=$selected_template" 0.8 2>/dev/null || true
2031
+
2032
+ if [[ -n "$similar_context" && "$similar_context" != "[]" ]]; then
2033
+ db_save_reasoning_trace "$job_id" "similar_issues" \
2034
+ "$goal" \
2035
+ "Found similar past issues for context injection" \
2036
+ "$similar_context" 0.6 2>/dev/null || true
2037
+ fi
2038
+
2039
+ if [[ -n "$failure_predictions" && "$failure_predictions" != "[]" ]]; then
2040
+ db_save_reasoning_trace "$job_id" "failure_prediction" \
2041
+ "$goal" \
2042
+ "Predicted potential failure modes from history" \
2043
+ "$failure_predictions" 0.5 2>/dev/null || true
2044
+ fi
2045
+ fi
2046
+
2047
+ # Export for use by pipeline stages
2048
+ [[ -n "$selected_template" && -z "${PIPELINE_TEMPLATE:-}" ]] && export PIPELINE_TEMPLATE="$selected_template"
2049
+
2050
+ emit_event "reasoning.trace" "job_id=$job_id" "complexity=$complexity" "risk=$risk_score" "template=${selected_template:-standard}" 2>/dev/null || true
2051
+ }
2052
+
1880
2053
  # ─── Subcommands ────────────────────────────────────────────────────────────
1881
2054
 
1882
2055
  pipeline_start() {
@@ -1898,6 +2071,13 @@ pipeline_start() {
1898
2071
  info "Using repository: $ORIGINAL_REPO_DIR"
1899
2072
  fi
1900
2073
 
2074
+ # Bootstrap optimization & memory if cold start (before first intelligence use)
2075
+ if [[ -f "$SCRIPT_DIR/lib/bootstrap.sh" ]]; then
2076
+ source "$SCRIPT_DIR/lib/bootstrap.sh"
2077
+ [[ ! -f "$HOME/.shipwright/optimization/iteration-model.json" ]] && bootstrap_optimization 2>/dev/null || true
2078
+ [[ ! -f "$HOME/.shipwright/memory/patterns.json" ]] && bootstrap_memory 2>/dev/null || true
2079
+ fi
2080
+
1901
2081
  if [[ -z "$GOAL" && -z "$ISSUE_NUMBER" ]]; then
1902
2082
  error "Must provide --goal or --issue"
1903
2083
  echo -e " Example: ${DIM}shipwright pipeline start --goal \"Add JWT auth\"${RESET}"
@@ -1905,7 +2085,7 @@ pipeline_start() {
1905
2085
  exit 1
1906
2086
  fi
1907
2087
 
1908
- if ! command -v jq &>/dev/null; then
2088
+ if ! command -v jq >/dev/null 2>&1; then
1909
2089
  error "jq is required. Install it: brew install jq"
1910
2090
  exit 1
1911
2091
  fi
@@ -1923,6 +2103,13 @@ pipeline_start() {
1923
2103
 
1924
2104
  setup_dirs
1925
2105
 
2106
+ # Generate reasoning trace (complexity analysis, template selection, failure predictions)
2107
+ local user_specified_pipeline="$PIPELINE_NAME"
2108
+ generate_reasoning_trace 2>/dev/null || true
2109
+ if [[ -n "${PIPELINE_TEMPLATE:-}" && "$user_specified_pipeline" == "standard" ]]; then
2110
+ PIPELINE_NAME="$PIPELINE_TEMPLATE"
2111
+ fi
2112
+
1926
2113
  # Check for existing pipeline
1927
2114
  if [[ -f "$STATE_FILE" ]]; then
1928
2115
  local existing_status
@@ -1942,7 +2129,87 @@ pipeline_start() {
1942
2129
  gh_init
1943
2130
 
1944
2131
  load_pipeline_config
1945
- initialize_state
2132
+
2133
+ # Checkpoint resume: when --resume is passed, try DB first, then file-based
2134
+ checkpoint_stage=""
2135
+ checkpoint_iteration=0
2136
+ if $RESUME_FROM_CHECKPOINT && type db_load_checkpoint >/dev/null 2>&1; then
2137
+ local saved_checkpoint
2138
+ saved_checkpoint=$(db_load_checkpoint "pipeline-${SHIPWRIGHT_PIPELINE_ID:-$$}" 2>/dev/null || echo "")
2139
+ if [[ -n "$saved_checkpoint" ]]; then
2140
+ checkpoint_stage=$(echo "$saved_checkpoint" | jq -r '.stage // ""' 2>/dev/null || echo "")
2141
+ if [[ -n "$checkpoint_stage" ]]; then
2142
+ info "Resuming from DB checkpoint: stage=$checkpoint_stage"
2143
+ checkpoint_iteration=$(echo "$saved_checkpoint" | jq -r '.iteration // 0' 2>/dev/null || echo "0")
2144
+ # Build COMPLETED_STAGES: all enabled stages before checkpoint_stage
2145
+ local enabled_list before_list=""
2146
+ enabled_list=$(jq -r '.stages[] | select(.enabled == true) | .id' "$PIPELINE_CONFIG" 2>/dev/null) || true
2147
+ local s
2148
+ while IFS= read -r s; do
2149
+ [[ -z "$s" ]] && continue
2150
+ if [[ "$s" == "$checkpoint_stage" ]]; then
2151
+ break
2152
+ fi
2153
+ [[ -n "$before_list" ]] && before_list="${before_list},${s}" || before_list="$s"
2154
+ done <<< "$enabled_list"
2155
+ if [[ -n "$before_list" ]]; then
2156
+ COMPLETED_STAGES="${before_list}"
2157
+ SELF_HEAL_COUNT="${checkpoint_iteration}"
2158
+ fi
2159
+ fi
2160
+ fi
2161
+ fi
2162
+ if $RESUME_FROM_CHECKPOINT && [[ -z "$checkpoint_stage" ]] && [[ -d "${ARTIFACTS_DIR}/checkpoints" ]]; then
2163
+ local cp_dir="${ARTIFACTS_DIR}/checkpoints"
2164
+ local latest_cp="" latest_mtime=0
2165
+ local f
2166
+ for f in "$cp_dir"/*-checkpoint.json; do
2167
+ [[ -f "$f" ]] || continue
2168
+ local mtime
2169
+ mtime=$(file_mtime "$f" 2>/dev/null || echo "0")
2170
+ if [[ "${mtime:-0}" -gt "$latest_mtime" ]]; then
2171
+ latest_mtime="${mtime}"
2172
+ latest_cp="$f"
2173
+ fi
2174
+ done
2175
+ if [[ -n "$latest_cp" && -x "$SCRIPT_DIR/sw-checkpoint.sh" ]]; then
2176
+ checkpoint_stage="$(basename "$latest_cp" -checkpoint.json)"
2177
+ local cp_json
2178
+ cp_json="$("$SCRIPT_DIR/sw-checkpoint.sh" restore --stage "$checkpoint_stage" 2>/dev/null)" || true
2179
+ if [[ -n "$cp_json" ]] && command -v jq >/dev/null 2>&1; then
2180
+ checkpoint_iteration="$(echo "$cp_json" | jq -r '.iteration // 0' 2>/dev/null)" || checkpoint_iteration=0
2181
+ info "Checkpoint resume: stage=${checkpoint_stage} iteration=${checkpoint_iteration}"
2182
+ # Build COMPLETED_STAGES: all enabled stages before checkpoint_stage
2183
+ local enabled_list before_list=""
2184
+ enabled_list="$(jq -r '.stages[] | select(.enabled == true) | .id' "$PIPELINE_CONFIG" 2>/dev/null)" || true
2185
+ local s
2186
+ while IFS= read -r s; do
2187
+ [[ -z "$s" ]] && continue
2188
+ if [[ "$s" == "$checkpoint_stage" ]]; then
2189
+ break
2190
+ fi
2191
+ [[ -n "$before_list" ]] && before_list="${before_list},${s}" || before_list="$s"
2192
+ done <<< "$enabled_list"
2193
+ if [[ -n "$before_list" ]]; then
2194
+ COMPLETED_STAGES="${before_list}"
2195
+ SELF_HEAL_COUNT="${checkpoint_iteration}"
2196
+ fi
2197
+ fi
2198
+ fi
2199
+ fi
2200
+
2201
+ # Restore from state file if resuming (failed/interrupted pipeline); else initialize fresh
2202
+ if $RESUME_FROM_CHECKPOINT && [[ -f "$STATE_FILE" ]]; then
2203
+ local existing_status
2204
+ existing_status="$(sed -n 's/^status: *//p' "$STATE_FILE" | head -1)"
2205
+ if [[ "$existing_status" == "failed" || "$existing_status" == "interrupted" ]]; then
2206
+ resume_state
2207
+ else
2208
+ initialize_state
2209
+ fi
2210
+ else
2211
+ initialize_state
2212
+ fi
1946
2213
 
1947
2214
  # CI resume: restore branch + goal context when intake is skipped
1948
2215
  if [[ -n "${COMPLETED_STAGES:-}" ]] && echo "$COMPLETED_STAGES" | tr ',' '\n' | grep -qx "intake"; then
@@ -1951,7 +2218,7 @@ pipeline_start() {
1951
2218
 
1952
2219
  # Restore GOAL from issue if not already set
1953
2220
  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}")
2221
+ 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
2222
  info "CI resume: goal from issue — ${GOAL}"
1956
2223
  fi
1957
2224
 
@@ -2018,11 +2285,38 @@ pipeline_start() {
2018
2285
  return $?
2019
2286
  fi
2020
2287
 
2288
+ # Capture predictions for feedback loop (intelligence → actuals → learning)
2289
+ 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
2290
+ local issue_json="${INTELLIGENCE_ANALYSIS:-}"
2291
+ if [[ -z "$issue_json" || "$issue_json" == "{}" ]]; then
2292
+ if [[ -n "$ISSUE_NUMBER" ]]; then
2293
+ issue_json=$(gh issue view "$ISSUE_NUMBER" --json number,title,body,labels 2>/dev/null || echo "{}")
2294
+ else
2295
+ issue_json=$(jq -n --arg title "${GOAL:-untitled}" --arg body "" '{title: $title, body: $body, labels: []}')
2296
+ fi
2297
+ if [[ -n "$issue_json" && "$issue_json" != "{}" ]]; then
2298
+ issue_json=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "{}")
2299
+ fi
2300
+ fi
2301
+ if [[ -n "$issue_json" && "$issue_json" != "{}" ]]; then
2302
+ if type intelligence_estimate_iterations >/dev/null 2>&1; then
2303
+ PREDICTED_ITERATIONS=$(intelligence_estimate_iterations "$issue_json" "" 2>/dev/null || echo "")
2304
+ export PREDICTED_ITERATIONS
2305
+ fi
2306
+ if type intelligence_predict_cost >/dev/null 2>&1; then
2307
+ local cost_json
2308
+ cost_json=$(intelligence_predict_cost "$issue_json" "{}" 2>/dev/null || echo "{}")
2309
+ PREDICTED_COST=$(echo "$cost_json" | jq -r '.estimated_cost_usd // empty' 2>/dev/null || echo "")
2310
+ export PREDICTED_COST
2311
+ fi
2312
+ fi
2313
+ fi
2314
+
2021
2315
  # Start background heartbeat writer
2022
2316
  start_heartbeat
2023
2317
 
2024
2318
  # Initialize GitHub Check Runs for all pipeline stages
2025
- if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_pipeline_start &>/dev/null 2>&1; then
2319
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_pipeline_start >/dev/null 2>&1; then
2026
2320
  local head_sha
2027
2321
  head_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
2028
2322
  if [[ -n "$head_sha" && -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
@@ -2038,12 +2332,15 @@ pipeline_start() {
2038
2332
 
2039
2333
  emit_event "pipeline.started" \
2040
2334
  "issue=${ISSUE_NUMBER:-0}" \
2335
+ "template=${PIPELINE_NAME}" \
2336
+ "complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
2337
+ "machine=$(hostname 2>/dev/null || echo "unknown")" \
2041
2338
  "pipeline=${PIPELINE_NAME}" \
2042
2339
  "model=${MODEL:-opus}" \
2043
2340
  "goal=${GOAL}"
2044
2341
 
2045
2342
  # Durable WAL: publish pipeline start event
2046
- if type publish_event &>/dev/null 2>&1; then
2343
+ if type publish_event >/dev/null 2>&1; then
2047
2344
  publish_event "pipeline.started" "{\"issue\":\"${ISSUE_NUMBER:-0}\",\"pipeline\":\"${PIPELINE_NAME}\",\"goal\":\"${GOAL:0:200}\"}" 2>/dev/null || true
2048
2345
  fi
2049
2346
 
@@ -2051,6 +2348,18 @@ pipeline_start() {
2051
2348
  local exit_code=$?
2052
2349
  PIPELINE_EXIT_CODE="$exit_code"
2053
2350
 
2351
+ # Compute total cost for pipeline.completed (prefer actual from Claude when available)
2352
+ local model_key="${MODEL:-sonnet}"
2353
+ local total_cost
2354
+ if [[ -n "${TOTAL_COST_USD:-}" && "${TOTAL_COST_USD}" != "0" && "${TOTAL_COST_USD}" != "null" ]]; then
2355
+ total_cost="${TOTAL_COST_USD}"
2356
+ else
2357
+ local input_cost output_cost
2358
+ 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}')
2359
+ 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}')
2360
+ total_cost=$(awk -v i="$input_cost" -v o="$output_cost" 'BEGIN{printf "%.4f", i + o}')
2361
+ fi
2362
+
2054
2363
  # Send completion notification + event
2055
2364
  local total_dur_s=""
2056
2365
  [[ -n "$PIPELINE_START_EPOCH" ]] && total_dur_s=$(( $(now_epoch) - PIPELINE_START_EPOCH ))
@@ -2064,10 +2373,16 @@ pipeline_start() {
2064
2373
  "issue=${ISSUE_NUMBER:-0}" \
2065
2374
  "result=success" \
2066
2375
  "duration_s=${total_dur_s:-0}" \
2376
+ "iterations=$((SELF_HEAL_COUNT + 1))" \
2377
+ "template=${PIPELINE_NAME}" \
2378
+ "complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
2379
+ "stages_passed=${PIPELINE_STAGES_PASSED:-0}" \
2380
+ "slowest_stage=${PIPELINE_SLOWEST_STAGE:-}" \
2067
2381
  "pr_url=${pr_url:-}" \
2068
2382
  "agent_id=${PIPELINE_AGENT_ID}" \
2069
2383
  "input_tokens=$TOTAL_INPUT_TOKENS" \
2070
2384
  "output_tokens=$TOTAL_OUTPUT_TOKENS" \
2385
+ "total_cost=$total_cost" \
2071
2386
  "self_heal_count=$SELF_HEAL_COUNT"
2072
2387
 
2073
2388
  # Auto-ingest pipeline outcome into recruit profiles
@@ -2080,10 +2395,15 @@ pipeline_start() {
2080
2395
  "issue=${ISSUE_NUMBER:-0}" \
2081
2396
  "result=failure" \
2082
2397
  "duration_s=${total_dur_s:-0}" \
2398
+ "iterations=$((SELF_HEAL_COUNT + 1))" \
2399
+ "template=${PIPELINE_NAME}" \
2400
+ "complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
2083
2401
  "failed_stage=${CURRENT_STAGE_ID:-unknown}" \
2402
+ "error_class=${LAST_STAGE_ERROR_CLASS:-unknown}" \
2084
2403
  "agent_id=${PIPELINE_AGENT_ID}" \
2085
2404
  "input_tokens=$TOTAL_INPUT_TOKENS" \
2086
2405
  "output_tokens=$TOTAL_OUTPUT_TOKENS" \
2406
+ "total_cost=$total_cost" \
2087
2407
  "self_heal_count=$SELF_HEAL_COUNT"
2088
2408
 
2089
2409
  # Auto-ingest pipeline outcome into recruit profiles
@@ -2121,7 +2441,7 @@ pipeline_start() {
2121
2441
  "success=$pipeline_success"
2122
2442
 
2123
2443
  # Close intelligence prediction feedback loop — validate predicted vs actual
2124
- if type intelligence_validate_prediction &>/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
2444
+ if type intelligence_validate_prediction >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
2125
2445
  intelligence_validate_prediction \
2126
2446
  "$ISSUE_NUMBER" \
2127
2447
  "${INTELLIGENCE_COMPLEXITY:-0}" \
@@ -2129,6 +2449,12 @@ pipeline_start() {
2129
2449
  "$pipeline_success" 2>/dev/null || true
2130
2450
  fi
2131
2451
 
2452
+ # Validate iterations prediction against actuals (cost validation moved below after total_cost is computed)
2453
+ local ACTUAL_ITERATIONS=$((SELF_HEAL_COUNT + 1))
2454
+ if [[ -n "${PREDICTED_ITERATIONS:-}" ]] && type intelligence_validate_prediction >/dev/null 2>&1; then
2455
+ intelligence_validate_prediction "iterations" "$PREDICTED_ITERATIONS" "$ACTUAL_ITERATIONS" 2>/dev/null || true
2456
+ fi
2457
+
2132
2458
  # Close predictive anomaly feedback loop — confirm whether flagged anomalies were real
2133
2459
  if [[ -x "$SCRIPT_DIR/sw-predictive.sh" ]]; then
2134
2460
  local _actual_failure="false"
@@ -2144,7 +2470,8 @@ pipeline_start() {
2144
2470
  "issue=${ISSUE_NUMBER:-0}" \
2145
2471
  "template=${PIPELINE_NAME}" \
2146
2472
  "success=$pipeline_success" \
2147
- "duration_s=${total_dur_s:-0}"
2473
+ "duration_s=${total_dur_s:-0}" \
2474
+ "complexity=${INTELLIGENCE_COMPLEXITY:-0}"
2148
2475
 
2149
2476
  # Risk prediction vs actual failure
2150
2477
  local predicted_risk="${INTELLIGENCE_RISK_SCORE:-0}"
@@ -2167,20 +2494,26 @@ pipeline_start() {
2167
2494
  fi
2168
2495
 
2169
2496
  # Record pipeline outcome for model routing feedback loop
2170
- if type optimize_analyze_outcome &>/dev/null 2>&1; then
2497
+ if type optimize_analyze_outcome >/dev/null 2>&1; then
2171
2498
  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
2499
  fi
2177
2500
 
2178
- if type memory_finalize_pipeline &>/dev/null 2>&1; then
2501
+ # Auto-learn after pipeline completion (non-blocking)
2502
+ if type optimize_tune_templates &>/dev/null; then
2503
+ (
2504
+ optimize_tune_templates 2>/dev/null
2505
+ optimize_learn_iterations 2>/dev/null
2506
+ optimize_route_models 2>/dev/null
2507
+ optimize_learn_risk_keywords 2>/dev/null
2508
+ ) &
2509
+ fi
2510
+
2511
+ if type memory_finalize_pipeline >/dev/null 2>&1; then
2179
2512
  memory_finalize_pipeline "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
2180
2513
  fi
2181
2514
 
2182
2515
  # Broadcast discovery for cross-pipeline learning
2183
- if type broadcast_discovery &>/dev/null 2>&1; then
2516
+ if type broadcast_discovery >/dev/null 2>&1; then
2184
2517
  local _disc_result="failure"
2185
2518
  [[ "$exit_code" -eq 0 ]] && _disc_result="success"
2186
2519
  local _disc_files=""
@@ -2209,6 +2542,29 @@ pipeline_start() {
2209
2542
  "model=$model_key" \
2210
2543
  "cost_usd=$total_cost"
2211
2544
 
2545
+ # Record pipeline outcome for Thompson sampling / outcome-based learning
2546
+ if type db_record_outcome >/dev/null 2>&1; then
2547
+ local _outcome_success=0
2548
+ [[ "$exit_code" -eq 0 ]] && _outcome_success=1
2549
+ local _outcome_complexity="medium"
2550
+ [[ "${INTELLIGENCE_COMPLEXITY:-5}" -le 3 ]] && _outcome_complexity="low"
2551
+ [[ "${INTELLIGENCE_COMPLEXITY:-5}" -ge 7 ]] && _outcome_complexity="high"
2552
+ db_record_outcome \
2553
+ "${SHIPWRIGHT_PIPELINE_ID:-pipeline-$$-${ISSUE_NUMBER:-0}}" \
2554
+ "${ISSUE_NUMBER:-}" \
2555
+ "${PIPELINE_NAME:-standard}" \
2556
+ "$_outcome_success" \
2557
+ "${total_dur_s:-0}" \
2558
+ "${SELF_HEAL_COUNT:-0}" \
2559
+ "${total_cost:-0}" \
2560
+ "$_outcome_complexity" 2>/dev/null || true
2561
+ fi
2562
+
2563
+ # Validate cost prediction against actual (after total_cost is computed)
2564
+ if [[ -n "${PREDICTED_COST:-}" ]] && type intelligence_validate_prediction >/dev/null 2>&1; then
2565
+ intelligence_validate_prediction "cost" "$PREDICTED_COST" "$total_cost" 2>/dev/null || true
2566
+ fi
2567
+
2212
2568
  return $exit_code
2213
2569
  }
2214
2570