shipwright-cli 1.7.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.claude/agents/code-reviewer.md +90 -0
  2. package/.claude/agents/devops-engineer.md +142 -0
  3. package/.claude/agents/pipeline-agent.md +80 -0
  4. package/.claude/agents/shell-script-specialist.md +150 -0
  5. package/.claude/agents/test-specialist.md +196 -0
  6. package/.claude/hooks/post-tool-use.sh +38 -0
  7. package/.claude/hooks/pre-tool-use.sh +25 -0
  8. package/.claude/hooks/session-started.sh +37 -0
  9. package/README.md +212 -814
  10. package/claude-code/CLAUDE.md.shipwright +54 -0
  11. package/claude-code/hooks/notify-idle.sh +2 -2
  12. package/claude-code/hooks/session-start.sh +24 -0
  13. package/claude-code/hooks/task-completed.sh +6 -2
  14. package/claude-code/settings.json.template +12 -0
  15. package/dashboard/public/app.js +4422 -0
  16. package/dashboard/public/index.html +816 -0
  17. package/dashboard/public/styles.css +4755 -0
  18. package/dashboard/server.ts +4315 -0
  19. package/docs/KNOWN-ISSUES.md +18 -10
  20. package/docs/TIPS.md +38 -26
  21. package/docs/patterns/README.md +33 -23
  22. package/package.json +9 -5
  23. package/scripts/adapters/iterm2-adapter.sh +1 -1
  24. package/scripts/adapters/tmux-adapter.sh +52 -23
  25. package/scripts/adapters/wezterm-adapter.sh +26 -14
  26. package/scripts/lib/compat.sh +200 -0
  27. package/scripts/lib/helpers.sh +72 -0
  28. package/scripts/postinstall.mjs +72 -13
  29. package/scripts/{cct → sw} +109 -21
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +390 -0
  33. package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
  37. package/scripts/sw-dashboard.sh +477 -0
  38. package/scripts/sw-developer-simulation.sh +252 -0
  39. package/scripts/sw-docs.sh +635 -0
  40. package/scripts/sw-doctor.sh +907 -0
  41. package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
  42. package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
  43. package/scripts/sw-github-checks.sh +521 -0
  44. package/scripts/sw-github-deploy.sh +533 -0
  45. package/scripts/sw-github-graphql.sh +972 -0
  46. package/scripts/sw-heartbeat.sh +293 -0
  47. package/scripts/{cct-init.sh → sw-init.sh} +144 -11
  48. package/scripts/sw-intelligence.sh +1196 -0
  49. package/scripts/sw-jira.sh +643 -0
  50. package/scripts/sw-launchd.sh +364 -0
  51. package/scripts/sw-linear.sh +648 -0
  52. package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
  53. package/scripts/{cct-loop.sh → sw-loop.sh} +534 -44
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
  58. package/scripts/sw-predictive.sh +820 -0
  59. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  60. package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
  61. package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
  62. package/scripts/sw-remote.sh +687 -0
  63. package/scripts/sw-self-optimize.sh +947 -0
  64. package/scripts/sw-session.sh +519 -0
  65. package/scripts/sw-setup.sh +234 -0
  66. package/scripts/sw-status.sh +605 -0
  67. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  68. package/scripts/sw-tmux.sh +591 -0
  69. package/scripts/sw-tracker-jira.sh +277 -0
  70. package/scripts/sw-tracker-linear.sh +292 -0
  71. package/scripts/sw-tracker.sh +409 -0
  72. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  73. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  74. package/templates/pipelines/autonomous.json +27 -5
  75. package/templates/pipelines/full.json +12 -0
  76. package/templates/pipelines/standard.json +12 -0
  77. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  78. package/tmux/templates/accessibility.json +34 -0
  79. package/tmux/templates/api-design.json +35 -0
  80. package/tmux/templates/architecture.json +1 -0
  81. package/tmux/templates/bug-fix.json +9 -0
  82. package/tmux/templates/code-review.json +1 -0
  83. package/tmux/templates/compliance.json +36 -0
  84. package/tmux/templates/data-pipeline.json +36 -0
  85. package/tmux/templates/debt-paydown.json +34 -0
  86. package/tmux/templates/devops.json +1 -0
  87. package/tmux/templates/documentation.json +1 -0
  88. package/tmux/templates/exploration.json +1 -0
  89. package/tmux/templates/feature-dev.json +1 -0
  90. package/tmux/templates/full-stack.json +8 -0
  91. package/tmux/templates/i18n.json +34 -0
  92. package/tmux/templates/incident-response.json +36 -0
  93. package/tmux/templates/migration.json +1 -0
  94. package/tmux/templates/observability.json +35 -0
  95. package/tmux/templates/onboarding.json +33 -0
  96. package/tmux/templates/performance.json +35 -0
  97. package/tmux/templates/refactor.json +1 -0
  98. package/tmux/templates/release.json +35 -0
  99. package/tmux/templates/security-audit.json +8 -0
  100. package/tmux/templates/spike.json +34 -0
  101. package/tmux/templates/testing.json +1 -0
  102. package/tmux/tmux.conf +98 -9
  103. package/scripts/cct-doctor.sh +0 -414
  104. package/scripts/cct-session.sh +0 -284
  105. package/scripts/cct-status.sh +0 -169
@@ -4,8 +4,9 @@
4
4
  # ║ Full GitHub integration · Auto-detection · Task tracking · Metrics ║
5
5
  # ╚═══════════════════════════════════════════════════════════════════════════╝
6
6
  set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
7
8
 
8
- VERSION="1.7.1"
9
+ VERSION="1.9.0"
9
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
11
12
 
@@ -20,6 +21,40 @@ DIM='\033[2m'
20
21
  BOLD='\033[1m'
21
22
  RESET='\033[0m'
22
23
 
24
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
25
+ # shellcheck source=lib/compat.sh
26
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
27
+
28
+ # ─── Intelligence Engine (optional) ──────────────────────────────────────────
29
+ # shellcheck source=sw-intelligence.sh
30
+ if [[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]]; then
31
+ source "$SCRIPT_DIR/sw-intelligence.sh"
32
+ fi
33
+ # shellcheck source=sw-pipeline-composer.sh
34
+ if [[ -f "$SCRIPT_DIR/sw-pipeline-composer.sh" ]]; then
35
+ source "$SCRIPT_DIR/sw-pipeline-composer.sh"
36
+ fi
37
+ # shellcheck source=sw-developer-simulation.sh
38
+ if [[ -f "$SCRIPT_DIR/sw-developer-simulation.sh" ]]; then
39
+ source "$SCRIPT_DIR/sw-developer-simulation.sh"
40
+ fi
41
+ # shellcheck source=sw-architecture-enforcer.sh
42
+ if [[ -f "$SCRIPT_DIR/sw-architecture-enforcer.sh" ]]; then
43
+ source "$SCRIPT_DIR/sw-architecture-enforcer.sh"
44
+ fi
45
+ # shellcheck source=sw-adversarial.sh
46
+ if [[ -f "$SCRIPT_DIR/sw-adversarial.sh" ]]; then
47
+ source "$SCRIPT_DIR/sw-adversarial.sh"
48
+ fi
49
+
50
+ # ─── GitHub API Modules (optional) ─────────────────────────────────────────
51
+ # shellcheck source=sw-github-graphql.sh
52
+ [[ -f "$SCRIPT_DIR/sw-github-graphql.sh" ]] && source "$SCRIPT_DIR/sw-github-graphql.sh"
53
+ # shellcheck source=sw-github-checks.sh
54
+ [[ -f "$SCRIPT_DIR/sw-github-checks.sh" ]] && source "$SCRIPT_DIR/sw-github-checks.sh"
55
+ # shellcheck source=sw-github-deploy.sh
56
+ [[ -f "$SCRIPT_DIR/sw-github-deploy.sh" ]] && source "$SCRIPT_DIR/sw-github-deploy.sh"
57
+
23
58
  # ─── Output Helpers ─────────────────────────────────────────────────────────
24
59
  info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
25
60
  success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
@@ -29,6 +64,30 @@ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
29
64
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
30
65
  now_epoch() { date +%s; }
31
66
 
67
+ # Parse coverage percentage from test output — multi-framework patterns
68
+ # Usage: parse_coverage_from_output <log_file>
69
+ # Outputs coverage percentage or empty string
70
+ parse_coverage_from_output() {
71
+ local log_file="$1"
72
+ [[ ! -f "$log_file" ]] && return
73
+ local cov=""
74
+ # Jest/Istanbul: "Statements : 85.5%"
75
+ cov=$(grep -oE 'Statements\s*:\s*[0-9.]+' "$log_file" 2>/dev/null | grep -oE '[0-9.]+$' || true)
76
+ # Istanbul table: "All files | 85.5"
77
+ [[ -z "$cov" ]] && cov=$(grep -oE 'All files\s*\|\s*[0-9.]+' "$log_file" 2>/dev/null | grep -oE '[0-9.]+$' || true)
78
+ # pytest-cov: "TOTAL 500 75 85%"
79
+ [[ -z "$cov" ]] && cov=$(grep -oE 'TOTAL\s+[0-9]+\s+[0-9]+\s+[0-9]+%' "$log_file" 2>/dev/null | grep -oE '[0-9]+%' | tr -d '%' | tail -1 || true)
80
+ # Vitest: "All files | 85.5 |"
81
+ [[ -z "$cov" ]] && cov=$(grep -oE 'All files\s*\|\s*[0-9.]+\s*\|' "$log_file" 2>/dev/null | grep -oE '[0-9.]+' | head -1 || true)
82
+ # Go coverage: "coverage: 85.5% of statements"
83
+ [[ -z "$cov" ]] && cov=$(grep -oE 'coverage:\s*[0-9.]+%' "$log_file" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
84
+ # Cargo tarpaulin: "85.50% coverage"
85
+ [[ -z "$cov" ]] && cov=$(grep -oE '[0-9.]+%\s*coverage' "$log_file" 2>/dev/null | grep -oE '[0-9.]+' | head -1 || true)
86
+ # Generic: "Coverage: 85.5%"
87
+ [[ -z "$cov" ]] && cov=$(grep -oiE 'coverage:?\s*[0-9.]+%' "$log_file" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
88
+ echo "$cov"
89
+ }
90
+
32
91
  format_duration() {
33
92
  local secs="$1"
34
93
  if [[ "$secs" -ge 3600 ]]; then
@@ -41,9 +100,9 @@ format_duration() {
41
100
  }
42
101
 
43
102
  # ─── Structured Event Log ──────────────────────────────────────────────────
44
- # Appends JSON events to ~/.claude-teams/events.jsonl for metrics/traceability
103
+ # Appends JSON events to ~/.shipwright/events.jsonl for metrics/traceability
45
104
 
46
- EVENTS_DIR="${HOME}/.claude-teams"
105
+ EVENTS_DIR="${HOME}/.shipwright"
47
106
  EVENTS_FILE="${EVENTS_DIR}/events.jsonl"
48
107
 
49
108
  emit_event() {
@@ -94,13 +153,18 @@ REVIEWERS=""
94
153
  LABELS=""
95
154
  BASE_BRANCH="main"
96
155
  NO_GITHUB=false
156
+ NO_GITHUB_LABEL=false
157
+ CI_MODE=false
97
158
  DRY_RUN=false
98
159
  IGNORE_BUDGET=false
160
+ COMPLETED_STAGES=""
161
+ MAX_ITERATIONS_OVERRIDE=""
99
162
  PR_NUMBER=""
100
163
  AUTO_WORKTREE=false
101
164
  WORKTREE_NAME=""
102
165
  CLEANUP_WORKTREE=false
103
166
  ORIGINAL_REPO_DIR=""
167
+ _cleanup_done=""
104
168
 
105
169
  # GitHub metadata (populated during intake)
106
170
  ISSUE_LABELS=""
@@ -150,11 +214,15 @@ show_help() {
150
214
  echo -e " ${DIM}--reviewers \"a,b\"${RESET} Request PR reviewers (auto-detected if omitted)"
151
215
  echo -e " ${DIM}--labels \"a,b\"${RESET} Add labels to PR (inherited from issue if omitted)"
152
216
  echo -e " ${DIM}--no-github${RESET} Disable GitHub integration"
217
+ echo -e " ${DIM}--no-github-label${RESET} Don't modify issue labels"
218
+ echo -e " ${DIM}--ci${RESET} CI mode (skip gates, non-interactive)"
153
219
  echo -e " ${DIM}--ignore-budget${RESET} Skip budget enforcement checks"
154
220
  echo -e " ${DIM}--worktree [=name]${RESET} Run in isolated git worktree (parallel-safe)"
155
221
  echo -e " ${DIM}--dry-run${RESET} Show what would happen without executing"
156
222
  echo -e " ${DIM}--slack-webhook <url>${RESET} Send notifications to Slack"
157
223
  echo -e " ${DIM}--self-heal <n>${RESET} Build→test retry cycles on failure (default: 2)"
224
+ echo -e " ${DIM}--max-iterations <n>${RESET} Override max build loop iterations"
225
+ echo -e " ${DIM}--completed-stages \"a,b\"${RESET} Skip these stages (CI resume)"
158
226
  echo ""
159
227
  echo -e "${BOLD}STAGES${RESET} ${DIM}(configurable per pipeline template)${RESET}"
160
228
  echo -e " intake → plan → design → build → test → review → pr → deploy → validate → monitor"
@@ -217,7 +285,7 @@ parse_args() {
217
285
  case "$1" in
218
286
  --goal) GOAL="$2"; shift 2 ;;
219
287
  --issue) ISSUE_NUMBER="$2"; shift 2 ;;
220
- --pipeline) PIPELINE_NAME="$2"; shift 2 ;;
288
+ --pipeline|--template) PIPELINE_NAME="$2"; shift 2 ;;
221
289
  --test-cmd) TEST_CMD="$2"; shift 2 ;;
222
290
  --model) MODEL="$2"; shift 2 ;;
223
291
  --agents) AGENTS="$2"; shift 2 ;;
@@ -226,7 +294,11 @@ parse_args() {
226
294
  --reviewers) REVIEWERS="$2"; shift 2 ;;
227
295
  --labels) LABELS="$2"; shift 2 ;;
228
296
  --no-github) NO_GITHUB=true; shift ;;
297
+ --no-github-label) NO_GITHUB_LABEL=true; shift ;;
298
+ --ci) CI_MODE=true; SKIP_GATES=true; shift ;;
229
299
  --ignore-budget) IGNORE_BUDGET=true; shift ;;
300
+ --max-iterations) MAX_ITERATIONS_OVERRIDE="$2"; shift 2 ;;
301
+ --completed-stages) COMPLETED_STAGES="$2"; shift 2 ;;
230
302
  --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 ;;
231
303
  --worktree) AUTO_WORKTREE=true; shift ;;
232
304
  --dry-run) DRY_RUN=true; shift ;;
@@ -262,7 +334,7 @@ find_pipeline_config() {
262
334
  local name="$1"
263
335
  local locations=(
264
336
  "$REPO_DIR/templates/pipelines/${name}.json"
265
- "$HOME/.claude-teams/pipelines/${name}.json"
337
+ "$HOME/.shipwright/pipelines/${name}.json"
266
338
  )
267
339
  for loc in "${locations[@]}"; do
268
340
  if [[ -f "$loc" ]]; then
@@ -274,6 +346,28 @@ find_pipeline_config() {
274
346
  }
275
347
 
276
348
  load_pipeline_config() {
349
+ # Check for intelligence-composed pipeline first
350
+ local composed_pipeline="${ARTIFACTS_DIR}/composed-pipeline.json"
351
+ if [[ -f "$composed_pipeline" ]] && type composer_validate_pipeline &>/dev/null; then
352
+ # Use composed pipeline if fresh (< 1 hour old)
353
+ local composed_age=99999
354
+ local composed_mtime
355
+ composed_mtime=$(stat -f %m "$composed_pipeline" 2>/dev/null || stat -c %Y "$composed_pipeline" 2>/dev/null || echo "0")
356
+ if [[ "$composed_mtime" -gt 0 ]]; then
357
+ composed_age=$(( $(now_epoch) - composed_mtime ))
358
+ fi
359
+ if [[ "$composed_age" -lt 3600 ]]; then
360
+ local validate_json
361
+ validate_json=$(cat "$composed_pipeline" 2>/dev/null || echo "")
362
+ if [[ -n "$validate_json" ]] && composer_validate_pipeline "$validate_json" 2>/dev/null; then
363
+ PIPELINE_CONFIG="$composed_pipeline"
364
+ info "Pipeline: ${BOLD}composed${RESET} ${DIM}(intelligence-driven)${RESET}"
365
+ emit_event "pipeline.composed_loaded" "issue=${ISSUE_NUMBER:-0}"
366
+ return
367
+ fi
368
+ fi
369
+ fi
370
+
277
371
  PIPELINE_CONFIG=$(find_pipeline_config "$PIPELINE_NAME") || {
278
372
  error "Pipeline template not found: $PIPELINE_NAME"
279
373
  echo -e " Available templates: ${DIM}shipwright pipeline list${RESET}"
@@ -298,11 +392,72 @@ TOTAL_INPUT_TOKENS=0
298
392
  TOTAL_OUTPUT_TOKENS=0
299
393
  COST_MODEL_RATES='{"opus":{"input":15,"output":75},"sonnet":{"input":3,"output":15},"haiku":{"input":0.25,"output":1.25}}'
300
394
 
395
+ # ─── Heartbeat ────────────────────────────────────────────────────────────────
396
+ HEARTBEAT_PID=""
397
+
398
+ start_heartbeat() {
399
+ local job_id="${PIPELINE_NAME:-pipeline-$$}"
400
+ (
401
+ while true; do
402
+ "$SCRIPT_DIR/sw-heartbeat.sh" write "$job_id" \
403
+ --pid $$ \
404
+ --issue "${ISSUE_NUMBER:-0}" \
405
+ --stage "${CURRENT_STAGE_ID:-unknown}" \
406
+ --iteration "0" \
407
+ --activity "$(get_stage_description "${CURRENT_STAGE_ID:-}" 2>/dev/null || echo "Running pipeline")" 2>/dev/null || true
408
+ sleep 30
409
+ done
410
+ ) >/dev/null 2>&1 &
411
+ HEARTBEAT_PID=$!
412
+ }
413
+
414
+ stop_heartbeat() {
415
+ if [[ -n "${HEARTBEAT_PID:-}" ]]; then
416
+ kill "$HEARTBEAT_PID" 2>/dev/null || true
417
+ wait "$HEARTBEAT_PID" 2>/dev/null || true
418
+ "$SCRIPT_DIR/sw-heartbeat.sh" clear "${PIPELINE_NAME:-pipeline-$$}" 2>/dev/null || true
419
+ HEARTBEAT_PID=""
420
+ fi
421
+ }
422
+
423
+ # ─── CI Helpers ───────────────────────────────────────────────────────────
424
+
425
+ ci_push_partial_work() {
426
+ [[ "${CI_MODE:-false}" != "true" ]] && return 0
427
+ [[ -z "${ISSUE_NUMBER:-}" ]] && return 0
428
+
429
+ local branch="shipwright/issue-${ISSUE_NUMBER}"
430
+
431
+ # Only push if we have uncommitted changes
432
+ if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
433
+ git add -A 2>/dev/null || true
434
+ git commit -m "WIP: partial pipeline progress for #${ISSUE_NUMBER}" --no-verify 2>/dev/null || true
435
+ fi
436
+
437
+ # Push branch (create if needed, force to overwrite previous WIP)
438
+ git push origin "HEAD:refs/heads/$branch" --force 2>/dev/null || true
439
+ }
440
+
441
+ ci_post_stage_event() {
442
+ [[ "${CI_MODE:-false}" != "true" ]] && return 0
443
+ [[ -z "${ISSUE_NUMBER:-}" ]] && return 0
444
+ [[ "${GH_AVAILABLE:-false}" != "true" ]] && return 0
445
+
446
+ local stage="$1" status="$2" elapsed="${3:-0s}"
447
+ local comment="<!-- SHIPWRIGHT-STAGE: ${stage}:${status}:${elapsed} -->"
448
+ gh issue comment "$ISSUE_NUMBER" --body "$comment" 2>/dev/null || true
449
+ }
450
+
301
451
  # ─── Signal Handling ───────────────────────────────────────────────────────
302
452
 
303
453
  cleanup_on_exit() {
454
+ [[ "${_cleanup_done:-}" == "true" ]] && return 0
455
+ _cleanup_done=true
304
456
  local exit_code=$?
305
457
 
458
+ # Stop heartbeat writer
459
+ stop_heartbeat
460
+
306
461
  # Save state if we were running
307
462
  if [[ "$PIPELINE_STATUS" == "running" && -n "$STATE_FILE" ]]; then
308
463
  PIPELINE_STATUS="interrupted"
@@ -311,6 +466,9 @@ cleanup_on_exit() {
311
466
  echo ""
312
467
  warn "Pipeline interrupted — state saved."
313
468
  echo -e " Resume: ${DIM}shipwright pipeline resume${RESET}"
469
+
470
+ # Push partial work in CI mode so retries can pick it up
471
+ ci_push_partial_work
314
472
  fi
315
473
 
316
474
  # Restore stashed changes
@@ -373,7 +531,7 @@ preflight_checks() {
373
531
  echo -e " ${YELLOW}⚠${RESET} $dirty_files uncommitted change(s)"
374
532
  if [[ "$SKIP_GATES" == "true" ]]; then
375
533
  info "Auto-stashing uncommitted changes..."
376
- git stash push -m "cct-pipeline: auto-stash before pipeline" --quiet 2>/dev/null && STASHED_CHANGES=true
534
+ git stash push -m "sw-pipeline: auto-stash before pipeline" --quiet 2>/dev/null && STASHED_CHANGES=true
377
535
  if [[ "$STASHED_CHANGES" == "true" ]]; then
378
536
  echo -e " ${GREEN}✓${RESET} Changes stashed (will restore on exit)"
379
537
  fi
@@ -409,11 +567,11 @@ preflight_checks() {
409
567
  errors=$((errors + 1))
410
568
  fi
411
569
 
412
- # 5. cct loop (needed for build stage)
413
- if [[ -x "$SCRIPT_DIR/cct-loop.sh" ]]; then
570
+ # 5. sw loop (needed for build stage)
571
+ if [[ -x "$SCRIPT_DIR/sw-loop.sh" ]]; then
414
572
  echo -e " ${GREEN}✓${RESET} shipwright loop available"
415
573
  else
416
- echo -e " ${RED}✗${RESET} cct-loop.sh not found at $SCRIPT_DIR"
574
+ echo -e " ${RED}✗${RESET} sw-loop.sh not found at $SCRIPT_DIR"
417
575
  errors=$((errors + 1))
418
576
  fi
419
577
 
@@ -580,19 +738,21 @@ gh_get_issue_meta() {
580
738
  gh_build_progress_body() {
581
739
  local body="## 🤖 Pipeline Progress — \`${PIPELINE_NAME}\`
582
740
 
583
- | Stage | Status | Duration |
584
- |-------|--------|----------|"
741
+ **Delivering:** ${GOAL}
742
+
743
+ | Stage | Status | Duration | |
744
+ |-------|--------|----------|-|"
585
745
 
586
746
  local stages
587
747
  stages=$(jq -c '.stages[]' "$PIPELINE_CONFIG" 2>/dev/null)
588
- while IFS= read -r stage; do
748
+ while IFS= read -r -u 3 stage; do
589
749
  local id enabled
590
750
  id=$(echo "$stage" | jq -r '.id')
591
751
  enabled=$(echo "$stage" | jq -r '.enabled')
592
752
 
593
753
  if [[ "$enabled" != "true" ]]; then
594
754
  body="${body}
595
- | ${id} | ⏭️ skipped | — |"
755
+ | ${id} | ⏭️ skipped | — | |"
596
756
  continue
597
757
  fi
598
758
 
@@ -601,21 +761,20 @@ gh_build_progress_body() {
601
761
  local duration
602
762
  duration=$(get_stage_timing "$id")
603
763
 
604
- local icon
764
+ local icon detail_col
605
765
  case "$sstatus" in
606
- complete) icon="✅" ;;
607
- running) icon="🔄" ;;
608
- failed) icon="❌" ;;
609
- *) icon="⬜" ;;
766
+ complete) icon="✅"; detail_col="" ;;
767
+ running) icon="🔄"; detail_col=$(get_stage_description "$id") ;;
768
+ failed) icon="❌"; detail_col="" ;;
769
+ *) icon="⬜"; detail_col=$(get_stage_description "$id") ;;
610
770
  esac
611
771
 
612
772
  body="${body}
613
- | ${id} | ${icon} ${sstatus:-pending} | ${duration:-—} |"
614
- done <<< "$stages"
773
+ | ${id} | ${icon} ${sstatus:-pending} | ${duration:-—} | ${detail_col} |"
774
+ done 3<<< "$stages"
615
775
 
616
776
  body="${body}
617
777
 
618
- **Goal:** ${GOAL}
619
778
  **Branch:** \`${GIT_BRANCH}\`"
620
779
 
621
780
  [[ -n "${GITHUB_ISSUE:-}" ]] && body="${body}
@@ -628,10 +787,19 @@ gh_build_progress_body() {
628
787
  **Elapsed:** ${total_dur}"
629
788
  fi
630
789
 
790
+ # Artifacts section
791
+ local artifacts=""
792
+ [[ -f "$ARTIFACTS_DIR/plan.md" ]] && artifacts="${artifacts}[Plan](.claude/pipeline-artifacts/plan.md)"
793
+ [[ -f "$ARTIFACTS_DIR/design.md" ]] && { [[ -n "$artifacts" ]] && artifacts="${artifacts} · "; artifacts="${artifacts}[Design](.claude/pipeline-artifacts/design.md)"; }
794
+ [[ -n "${PR_NUMBER:-}" ]] && { [[ -n "$artifacts" ]] && artifacts="${artifacts} · "; artifacts="${artifacts}PR #${PR_NUMBER}"; }
795
+ [[ -n "$artifacts" ]] && body="${body}
796
+
797
+ 📎 **Artifacts:** ${artifacts}"
798
+
631
799
  body="${body}
632
800
 
633
801
  ---
634
- _Updated: $(now_iso) · Generated by \`shipwright pipeline\`_"
802
+ _Updated: $(now_iso) · shipwright pipeline_"
635
803
  echo "$body"
636
804
  }
637
805
 
@@ -725,36 +893,58 @@ detect_test_cmd() {
725
893
  # Detect project language/framework
726
894
  detect_project_lang() {
727
895
  local root="$PROJECT_ROOT"
896
+ local detected=""
897
+
898
+ # Fast heuristic detection (grep-based)
728
899
  if [[ -f "$root/package.json" ]]; then
729
900
  if grep -q "typescript" "$root/package.json" 2>/dev/null; then
730
- echo "typescript"
901
+ detected="typescript"
731
902
  elif grep -q "\"next\"" "$root/package.json" 2>/dev/null; then
732
- echo "nextjs"
903
+ detected="nextjs"
733
904
  elif grep -q "\"react\"" "$root/package.json" 2>/dev/null; then
734
- echo "react"
905
+ detected="react"
735
906
  else
736
- echo "nodejs"
907
+ detected="nodejs"
737
908
  fi
738
909
  elif [[ -f "$root/Cargo.toml" ]]; then
739
- echo "rust"
910
+ detected="rust"
740
911
  elif [[ -f "$root/go.mod" ]]; then
741
- echo "go"
912
+ detected="go"
742
913
  elif [[ -f "$root/pyproject.toml" || -f "$root/setup.py" || -f "$root/requirements.txt" ]]; then
743
- echo "python"
914
+ detected="python"
744
915
  elif [[ -f "$root/Gemfile" ]]; then
745
- echo "ruby"
916
+ detected="ruby"
746
917
  elif [[ -f "$root/pom.xml" || -f "$root/build.gradle" ]]; then
747
- echo "java"
918
+ detected="java"
748
919
  else
749
- echo "unknown"
920
+ detected="unknown"
921
+ fi
922
+
923
+ # Intelligence: holistic analysis for polyglot/monorepo detection
924
+ if [[ "$detected" == "unknown" ]] && type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
925
+ local config_files
926
+ config_files=$(ls "$root" 2>/dev/null | grep -E '\.(json|toml|yaml|yml|xml|gradle|lock|mod)$' | head -15)
927
+ if [[ -n "$config_files" ]]; then
928
+ local ai_lang
929
+ ai_lang=$(claude --print --output-format text -p "Based on these config files in a project root, what is the primary language/framework? Reply with ONE word (e.g., typescript, python, rust, go, java, ruby, nodejs):
930
+
931
+ Files: ${config_files}" --model haiku < /dev/null 2>/dev/null || true)
932
+ ai_lang=$(echo "$ai_lang" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
933
+ case "$ai_lang" in
934
+ typescript|python|rust|go|java|ruby|nodejs|react|nextjs|kotlin|swift|elixir|scala)
935
+ detected="$ai_lang" ;;
936
+ esac
937
+ fi
750
938
  fi
939
+
940
+ echo "$detected"
751
941
  }
752
942
 
753
943
  # Detect likely reviewers from CODEOWNERS or git log
754
944
  detect_reviewers() {
755
945
  local root="$PROJECT_ROOT"
756
946
 
757
- # Check CODEOWNERS
947
+ # Check CODEOWNERS — common paths first, then broader search
758
948
  local codeowners=""
759
949
  for f in "$root/.github/CODEOWNERS" "$root/CODEOWNERS" "$root/docs/CODEOWNERS"; do
760
950
  if [[ -f "$f" ]]; then
@@ -762,6 +952,10 @@ detect_reviewers() {
762
952
  break
763
953
  fi
764
954
  done
955
+ # Broader search if not found at common locations
956
+ if [[ -z "$codeowners" ]]; then
957
+ codeowners=$(find "$root" -maxdepth 3 -name "CODEOWNERS" -type f 2>/dev/null | head -1 || true)
958
+ fi
765
959
 
766
960
  if [[ -n "$codeowners" ]]; then
767
961
  # Extract GitHub usernames from CODEOWNERS (lines like: * @user1 @user2)
@@ -774,22 +968,54 @@ detect_reviewers() {
774
968
  fi
775
969
  fi
776
970
 
777
- # Fallback: top contributors from recent git log (excluding self)
971
+ # Fallback: try to extract GitHub usernames from recent commit emails
972
+ # Format: user@users.noreply.github.com → user, or noreply+user@... → user
778
973
  local current_user
779
- current_user=$(gh api user --jq '.login' 2>/dev/null || git config user.name 2>/dev/null || true)
974
+ current_user=$(gh api user --jq '.login' 2>/dev/null || true)
780
975
  local contributors
781
- contributors=$(git log --format='%aN' -100 2>/dev/null | \
976
+ contributors=$(git log --format='%aE' -100 2>/dev/null | \
977
+ grep -oE '[a-zA-Z0-9_-]+@users\.noreply\.github\.com' | \
978
+ sed 's/@users\.noreply\.github\.com//' | sed 's/^[0-9]*+//' | \
782
979
  sort | uniq -c | sort -rn | \
783
980
  awk '{print $NF}' | \
784
- grep -v "^${current_user}$" 2>/dev/null | \
981
+ grep -v "^${current_user:-___}$" 2>/dev/null | \
785
982
  head -2 | tr '\n' ',')
786
983
  contributors="${contributors%,}"
787
984
  echo "$contributors"
788
985
  }
789
986
 
790
- # Get branch prefix from task type
987
+ # Get branch prefix from task type — checks git history for conventions first
791
988
  branch_prefix_for_type() {
792
- case "$1" in
989
+ local task_type="$1"
990
+
991
+ # Analyze recent branches for naming conventions
992
+ local branch_prefixes
993
+ branch_prefixes=$(git branch -r 2>/dev/null | sed 's#origin/##' | grep -oE '^[a-z]+/' | sort | uniq -c | sort -rn | head -5 || true)
994
+ if [[ -n "$branch_prefixes" ]]; then
995
+ local total_branches dominant_prefix dominant_count
996
+ total_branches=$(echo "$branch_prefixes" | awk '{s+=$1} END {print s}' || echo "0")
997
+ dominant_prefix=$(echo "$branch_prefixes" | head -1 | awk '{print $2}' | tr -d '/' || true)
998
+ dominant_count=$(echo "$branch_prefixes" | head -1 | awk '{print $1}' || echo "0")
999
+ # If >80% of branches use a pattern, adopt it for the matching type
1000
+ if [[ "$total_branches" -gt 5 ]] && [[ "$dominant_count" -gt 0 ]]; then
1001
+ local pct=$(( (dominant_count * 100) / total_branches ))
1002
+ if [[ "$pct" -gt 80 && -n "$dominant_prefix" ]]; then
1003
+ # Map task type to the repo's convention
1004
+ local mapped=""
1005
+ case "$task_type" in
1006
+ bug) mapped=$(echo "$branch_prefixes" | awk '{print $2}' | tr -d '/' | grep -E '^(fix|bug|hotfix)$' | head -1 || true) ;;
1007
+ feature) mapped=$(echo "$branch_prefixes" | awk '{print $2}' | tr -d '/' | grep -E '^(feat|feature)$' | head -1 || true) ;;
1008
+ esac
1009
+ if [[ -n "$mapped" ]]; then
1010
+ echo "$mapped"
1011
+ return
1012
+ fi
1013
+ fi
1014
+ fi
1015
+ fi
1016
+
1017
+ # Fallback: hardcoded mapping
1018
+ case "$task_type" in
793
1019
  bug) echo "fix" ;;
794
1020
  refactor) echo "refactor" ;;
795
1021
  testing) echo "test" ;;
@@ -855,6 +1081,84 @@ get_stage_timing() {
855
1081
  fi
856
1082
  }
857
1083
 
1084
+ get_stage_description() {
1085
+ local stage_id="$1"
1086
+
1087
+ # Try to generate dynamic description from pipeline config
1088
+ if [[ -n "${PIPELINE_CONFIG:-}" && -f "${PIPELINE_CONFIG:-/dev/null}" ]]; then
1089
+ local stage_cfg
1090
+ stage_cfg=$(jq -c --arg id "$stage_id" '.stages[] | select(.id == $id) | .config // {}' "$PIPELINE_CONFIG" 2>/dev/null || echo "{}")
1091
+ case "$stage_id" in
1092
+ test)
1093
+ local cfg_test_cmd cfg_cov_min
1094
+ cfg_test_cmd=$(echo "$stage_cfg" | jq -r '.test_cmd // empty' 2>/dev/null || true)
1095
+ cfg_cov_min=$(echo "$stage_cfg" | jq -r '.coverage_min // empty' 2>/dev/null || true)
1096
+ if [[ -n "$cfg_test_cmd" ]]; then
1097
+ echo "Running ${cfg_test_cmd}${cfg_cov_min:+ with ${cfg_cov_min}% coverage gate}"
1098
+ return
1099
+ fi
1100
+ ;;
1101
+ build)
1102
+ local cfg_max_iter cfg_model
1103
+ cfg_max_iter=$(echo "$stage_cfg" | jq -r '.max_iterations // empty' 2>/dev/null || true)
1104
+ cfg_model=$(jq -r '.defaults.model // empty' "$PIPELINE_CONFIG" 2>/dev/null || true)
1105
+ if [[ -n "$cfg_max_iter" ]]; then
1106
+ echo "Building with ${cfg_max_iter} max iterations${cfg_model:+ using ${cfg_model}}"
1107
+ return
1108
+ fi
1109
+ ;;
1110
+ monitor)
1111
+ local cfg_dur cfg_thresh
1112
+ cfg_dur=$(echo "$stage_cfg" | jq -r '.duration_minutes // empty' 2>/dev/null || true)
1113
+ cfg_thresh=$(echo "$stage_cfg" | jq -r '.error_threshold // empty' 2>/dev/null || true)
1114
+ if [[ -n "$cfg_dur" ]]; then
1115
+ echo "Monitoring for ${cfg_dur}m${cfg_thresh:+ (threshold: ${cfg_thresh} errors)}"
1116
+ return
1117
+ fi
1118
+ ;;
1119
+ esac
1120
+ fi
1121
+
1122
+ # Static fallback descriptions
1123
+ case "$stage_id" in
1124
+ intake) echo "Extracting requirements and auto-detecting project setup" ;;
1125
+ plan) echo "Creating implementation plan with architecture decisions" ;;
1126
+ design) echo "Designing interfaces, data models, and API contracts" ;;
1127
+ build) echo "Writing production code with self-healing iteration" ;;
1128
+ test) echo "Running test suite and validating coverage" ;;
1129
+ review) echo "Code quality, security audit, performance review" ;;
1130
+ compound_quality) echo "Adversarial testing, E2E validation, DoD checklist" ;;
1131
+ pr) echo "Creating pull request with CI integration" ;;
1132
+ merge) echo "Merging PR with branch cleanup" ;;
1133
+ deploy) echo "Deploying to staging/production" ;;
1134
+ validate) echo "Smoke tests and health checks post-deploy" ;;
1135
+ monitor) echo "Production monitoring with auto-rollback" ;;
1136
+ *) echo "" ;;
1137
+ esac
1138
+ }
1139
+
1140
+ # Build inline stage progress string (e.g. "intake:complete plan:running test:pending")
1141
+ build_stage_progress() {
1142
+ local progress=""
1143
+ local stages
1144
+ stages=$(jq -c '.stages[]' "$PIPELINE_CONFIG" 2>/dev/null) || return 0
1145
+ while IFS= read -r -u 3 stage; do
1146
+ local id enabled
1147
+ id=$(echo "$stage" | jq -r '.id')
1148
+ enabled=$(echo "$stage" | jq -r '.enabled')
1149
+ [[ "$enabled" != "true" ]] && continue
1150
+ local sstatus
1151
+ sstatus=$(get_stage_status "$id")
1152
+ sstatus="${sstatus:-pending}"
1153
+ if [[ -n "$progress" ]]; then
1154
+ progress="${progress} ${id}:${sstatus}"
1155
+ else
1156
+ progress="${id}:${sstatus}"
1157
+ fi
1158
+ done 3<<< "$stages"
1159
+ echo "$progress"
1160
+ }
1161
+
858
1162
  update_status() {
859
1163
  local status="$1" stage="$2"
860
1164
  PIPELINE_STATUS="$status"
@@ -877,6 +1181,20 @@ mark_stage_complete() {
877
1181
  local body
878
1182
  body=$(gh_build_progress_body)
879
1183
  gh_update_progress "$body"
1184
+
1185
+ # Notify tracker (Linear/Jira) of stage completion
1186
+ local stage_desc
1187
+ stage_desc=$(get_stage_description "$stage_id")
1188
+ "$SCRIPT_DIR/sw-tracker.sh" notify "stage_complete" "$ISSUE_NUMBER" \
1189
+ "${stage_id}|${timing}|${stage_desc}" 2>/dev/null || true
1190
+
1191
+ # Post structured stage event for CI sweep/retry intelligence
1192
+ ci_post_stage_event "$stage_id" "complete" "$timing"
1193
+ fi
1194
+
1195
+ # Update GitHub Check Run for this stage
1196
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update &>/dev/null 2>&1; then
1197
+ gh_checks_stage_update "$stage_id" "completed" "success" "Stage $stage_id: ${timing}" 2>/dev/null || true
880
1198
  fi
881
1199
  }
882
1200
 
@@ -899,6 +1217,22 @@ mark_stage_failed() {
899
1217
  \`\`\`
900
1218
  $(tail -5 "$ARTIFACTS_DIR/${stage_id}"*.log 2>/dev/null || echo 'No log available')
901
1219
  \`\`\`"
1220
+
1221
+ # Notify tracker (Linear/Jira) of stage failure
1222
+ local error_context
1223
+ error_context=$(tail -5 "$ARTIFACTS_DIR/${stage_id}"*.log 2>/dev/null || echo "No log")
1224
+ "$SCRIPT_DIR/sw-tracker.sh" notify "stage_failed" "$ISSUE_NUMBER" \
1225
+ "${stage_id}|${error_context}" 2>/dev/null || true
1226
+
1227
+ # Post structured stage event for CI sweep/retry intelligence
1228
+ ci_post_stage_event "$stage_id" "failed" "$timing"
1229
+ fi
1230
+
1231
+ # Update GitHub Check Run for this stage
1232
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update &>/dev/null 2>&1; then
1233
+ local fail_summary
1234
+ fail_summary=$(tail -3 "$ARTIFACTS_DIR/${stage_id}"*.log 2>/dev/null | head -c 500 || echo "Stage $stage_id failed")
1235
+ gh_checks_stage_update "$stage_id" "completed" "failure" "$fail_summary" 2>/dev/null || true
902
1236
  fi
903
1237
  }
904
1238
 
@@ -920,10 +1254,13 @@ initialize_state() {
920
1254
  STAGE_STATUSES=""
921
1255
  STAGE_TIMINGS=""
922
1256
  LOG_ENTRIES=""
1257
+ # Clear per-run tracking files
1258
+ rm -f "$ARTIFACTS_DIR/model-routing.log" "$ARTIFACTS_DIR/.plan-failure-sig.txt"
923
1259
  write_state
924
1260
  }
925
1261
 
926
1262
  write_state() {
1263
+ [[ -z "${STATE_FILE:-}" || -z "${ARTIFACTS_DIR:-}" ]] && return 0
927
1264
  local stages_yaml=""
928
1265
  while IFS=: read -r sid sstatus; do
929
1266
  [[ -z "$sid" ]] && continue
@@ -936,6 +1273,16 @@ write_state() {
936
1273
  total_dur=$(format_duration $(( $(now_epoch) - PIPELINE_START_EPOCH )))
937
1274
  fi
938
1275
 
1276
+ # Stage description and progress for dashboard enrichment
1277
+ local cur_stage_desc=""
1278
+ if [[ -n "${CURRENT_STAGE:-}" ]]; then
1279
+ cur_stage_desc=$(get_stage_description "$CURRENT_STAGE")
1280
+ fi
1281
+ local stage_progress=""
1282
+ if [[ -n "${PIPELINE_CONFIG:-}" && -f "${PIPELINE_CONFIG:-/dev/null}" ]]; then
1283
+ stage_progress=$(build_stage_progress)
1284
+ fi
1285
+
939
1286
  cat > "$STATE_FILE" <<EOF
940
1287
  ---
941
1288
  pipeline: $PIPELINE_NAME
@@ -945,6 +1292,8 @@ issue: "${GITHUB_ISSUE:-}"
945
1292
  branch: "${GIT_BRANCH:-}"
946
1293
  template: "${TASK_TYPE:+$(template_for_type "$TASK_TYPE")}"
947
1294
  current_stage: $CURRENT_STAGE
1295
+ current_stage_description: "${cur_stage_desc}"
1296
+ stage_progress: "${stage_progress}"
948
1297
  started_at: ${STARTED_AT:-$(now_iso)}
949
1298
  updated_at: $(now_iso)
950
1299
  elapsed: ${total_dur:-0s}
@@ -980,6 +1329,8 @@ resume_state() {
980
1329
  issue:*) GITHUB_ISSUE="$(echo "${line#issue:}" | sed 's/^ *"//;s/" *$//')" ;;
981
1330
  branch:*) GIT_BRANCH="$(echo "${line#branch:}" | sed 's/^ *"//;s/" *$//')" ;;
982
1331
  current_stage:*) CURRENT_STAGE="$(echo "${line#current_stage:}" | xargs)" ;;
1332
+ current_stage_description:*) ;; # computed field — skip on resume
1333
+ stage_progress:*) ;; # computed field — skip on resume
983
1334
  started_at:*) STARTED_AT="$(echo "${line#started_at:}" | xargs)" ;;
984
1335
  pr_number:*) PR_NUMBER="$(echo "${line#pr_number:}" | xargs)" ;;
985
1336
  progress_comment_id:*) PROGRESS_COMMENT_ID="$(echo "${line#progress_comment_id:}" | xargs)" ;;
@@ -1037,6 +1388,32 @@ ${sid}:${sst}"
1037
1388
 
1038
1389
  detect_task_type() {
1039
1390
  local goal="$1"
1391
+
1392
+ # Intelligence: Claude classification with confidence score
1393
+ if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
1394
+ local ai_result
1395
+ ai_result=$(claude --print --output-format text -p "Classify this task into exactly ONE category. Reply in format: CATEGORY|CONFIDENCE (0-100)
1396
+
1397
+ Categories: bug, refactor, testing, security, docs, devops, migration, architecture, feature
1398
+
1399
+ Task: ${goal}" --model haiku < /dev/null 2>/dev/null || true)
1400
+ if [[ -n "$ai_result" ]]; then
1401
+ local ai_type ai_conf
1402
+ ai_type=$(echo "$ai_result" | head -1 | cut -d'|' -f1 | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
1403
+ ai_conf=$(echo "$ai_result" | head -1 | cut -d'|' -f2 | grep -oE '[0-9]+' | head -1 || echo "0")
1404
+ # Use AI classification if confidence >= 70
1405
+ case "$ai_type" in
1406
+ bug|refactor|testing|security|docs|devops|migration|architecture|feature)
1407
+ if [[ "${ai_conf:-0}" -ge 70 ]] 2>/dev/null; then
1408
+ echo "$ai_type"
1409
+ return
1410
+ fi
1411
+ ;;
1412
+ esac
1413
+ fi
1414
+ fi
1415
+
1416
+ # Fallback: keyword matching
1040
1417
  local lower
1041
1418
  lower=$(echo "$goal" | tr '[:upper:]' '[:lower:]')
1042
1419
  case "$lower" in
@@ -1091,6 +1468,7 @@ show_stage_preview() {
1091
1468
  # ─── Stage Functions ────────────────────────────────────────────────────────
1092
1469
 
1093
1470
  stage_intake() {
1471
+ CURRENT_STAGE_ID="intake"
1094
1472
  local project_lang
1095
1473
  project_lang=$(detect_project_lang)
1096
1474
  info "Project: ${BOLD}$project_lang${RESET}"
@@ -1188,6 +1566,7 @@ Test cmd: ${TEST_CMD:-none detected}"
1188
1566
  }
1189
1567
 
1190
1568
  stage_plan() {
1569
+ CURRENT_STAGE_ID="plan"
1191
1570
  local plan_file="$ARTIFACTS_DIR/plan.md"
1192
1571
 
1193
1572
  if ! command -v claude &>/dev/null; then
@@ -1212,6 +1591,62 @@ ${ISSUE_BODY}
1212
1591
  "
1213
1592
  fi
1214
1593
 
1594
+ # Inject intelligence memory context for similar past plans
1595
+ if type intelligence_search_memory &>/dev/null 2>&1; then
1596
+ local plan_memory
1597
+ plan_memory=$(intelligence_search_memory "plan stage for ${TASK_TYPE:-feature}: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
1598
+ if [[ -n "$plan_memory" && "$plan_memory" != *'"results":[]'* && "$plan_memory" != *'"error"'* ]]; then
1599
+ local memory_summary
1600
+ memory_summary=$(echo "$plan_memory" | jq -r '.results[]? | "- \(.)"' 2>/dev/null | head -10 || true)
1601
+ if [[ -n "$memory_summary" ]]; then
1602
+ plan_prompt="${plan_prompt}
1603
+ ## Historical Context (from previous pipelines)
1604
+ Previous similar issues were planned as:
1605
+ ${memory_summary}
1606
+ "
1607
+ fi
1608
+ fi
1609
+ fi
1610
+
1611
+ # Inject architecture patterns from intelligence layer
1612
+ local repo_hash_plan
1613
+ repo_hash_plan=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
1614
+ local arch_file_plan="${HOME}/.shipwright/memory/${repo_hash_plan}/architecture.json"
1615
+ if [[ -f "$arch_file_plan" ]]; then
1616
+ local arch_patterns
1617
+ arch_patterns=$(jq -r '
1618
+ "Language: \(.language // "unknown")",
1619
+ "Framework: \(.framework // "unknown")",
1620
+ "Patterns: \((.patterns // []) | join(", "))",
1621
+ "Rules: \((.rules // []) | join("; "))"
1622
+ ' "$arch_file_plan" 2>/dev/null || true)
1623
+ if [[ -n "$arch_patterns" ]]; then
1624
+ plan_prompt="${plan_prompt}
1625
+ ## Architecture Patterns
1626
+ ${arch_patterns}
1627
+ "
1628
+ fi
1629
+ fi
1630
+
1631
+ # Task-type-specific guidance
1632
+ case "${TASK_TYPE:-feature}" in
1633
+ bug)
1634
+ plan_prompt="${plan_prompt}
1635
+ ## Task Type: Bug Fix
1636
+ Focus on: reproducing the bug, identifying root cause, minimal targeted fix, regression tests.
1637
+ " ;;
1638
+ refactor)
1639
+ plan_prompt="${plan_prompt}
1640
+ ## Task Type: Refactor
1641
+ Focus on: preserving all existing behavior, incremental changes, comprehensive test coverage.
1642
+ " ;;
1643
+ security)
1644
+ plan_prompt="${plan_prompt}
1645
+ ## Task Type: Security
1646
+ Focus on: threat modeling, OWASP top 10, input validation, authentication/authorization.
1647
+ " ;;
1648
+ esac
1649
+
1215
1650
  # Add project context
1216
1651
  local project_lang
1217
1652
  project_lang=$(detect_project_lang)
@@ -1247,10 +1682,14 @@ Checklist of completion criteria.
1247
1682
  plan_model=$(jq -r --arg id "plan" '(.stages[] | select(.id == $id) | .config.model) // .defaults.model // "opus"' "$PIPELINE_CONFIG" 2>/dev/null) || true
1248
1683
  [[ -n "$MODEL" ]] && plan_model="$MODEL"
1249
1684
  [[ -z "$plan_model" || "$plan_model" == "null" ]] && plan_model="opus"
1685
+ # Intelligence model routing (when no explicit CLI --model override)
1686
+ if [[ -z "$MODEL" && -n "${CLAUDE_MODEL:-}" ]]; then
1687
+ plan_model="$CLAUDE_MODEL"
1688
+ fi
1250
1689
 
1251
1690
  local _token_log="${ARTIFACTS_DIR}/.claude-tokens-plan.log"
1252
- claude --print --model "$plan_model" --max-turns 10 \
1253
- "$plan_prompt" > "$plan_file" 2>"$_token_log" || true
1691
+ claude --print --model "$plan_model" --max-turns 25 \
1692
+ "$plan_prompt" < /dev/null > "$plan_file" 2>"$_token_log" || true
1254
1693
  parse_claude_tokens "$_token_log"
1255
1694
 
1256
1695
  if [[ ! -s "$plan_file" ]]; then
@@ -1342,6 +1781,163 @@ CC_TASKS_EOF
1342
1781
  # Extract definition of done for quality gates
1343
1782
  sed -n '/[Dd]efinition [Oo]f [Dd]one/,/^#/p' "$plan_file" | head -20 > "$ARTIFACTS_DIR/dod.md" 2>/dev/null || true
1344
1783
 
1784
+ # ── Plan Validation Gate ──
1785
+ # Ask Claude to validate the plan before proceeding
1786
+ if command -v claude &>/dev/null && [[ -s "$plan_file" ]]; then
1787
+ local validation_attempts=0
1788
+ local max_validation_attempts=2
1789
+ local plan_valid=false
1790
+
1791
+ while [[ "$validation_attempts" -lt "$max_validation_attempts" ]]; do
1792
+ validation_attempts=$((validation_attempts + 1))
1793
+ info "Validating plan (attempt ${validation_attempts}/${max_validation_attempts})..."
1794
+
1795
+ # Build enriched validation prompt with learned context
1796
+ local validation_extra=""
1797
+
1798
+ # Inject rejected plan history from memory
1799
+ if type intelligence_search_memory &>/dev/null 2>&1; then
1800
+ local rejected_plans
1801
+ rejected_plans=$(intelligence_search_memory "rejected plan validation failures for: ${GOAL:-}" "${HOME}/.shipwright/memory" 3 2>/dev/null) || true
1802
+ if [[ -n "$rejected_plans" ]]; then
1803
+ validation_extra="${validation_extra}
1804
+ ## Previously Rejected Plans
1805
+ These issues were found in past plan validations for similar tasks:
1806
+ ${rejected_plans}
1807
+ "
1808
+ fi
1809
+ fi
1810
+
1811
+ # Inject repo conventions contextually
1812
+ local claudemd="$PROJECT_ROOT/.claude/CLAUDE.md"
1813
+ if [[ -f "$claudemd" ]]; then
1814
+ local conventions_summary
1815
+ conventions_summary=$(head -100 "$claudemd" 2>/dev/null | grep -E '^##|^-|^\*' | head -15 || true)
1816
+ if [[ -n "$conventions_summary" ]]; then
1817
+ validation_extra="${validation_extra}
1818
+ ## Repo Conventions
1819
+ ${conventions_summary}
1820
+ "
1821
+ fi
1822
+ fi
1823
+
1824
+ # Inject complexity estimate
1825
+ local complexity_hint=""
1826
+ if [[ -n "${INTELLIGENCE_COMPLEXITY:-}" && "${INTELLIGENCE_COMPLEXITY:-0}" -gt 0 ]]; then
1827
+ complexity_hint="This is estimated as complexity ${INTELLIGENCE_COMPLEXITY}/10. Plans for this complexity typically need ${INTELLIGENCE_COMPLEXITY} or more tasks."
1828
+ fi
1829
+
1830
+ local validation_prompt="You are a plan validator. Review this implementation plan and determine if it is valid.
1831
+
1832
+ ## Goal
1833
+ ${GOAL}
1834
+ ${complexity_hint:+
1835
+ ## Complexity Estimate
1836
+ ${complexity_hint}
1837
+ }
1838
+ ## Plan
1839
+ $(cat "$plan_file")
1840
+ ${validation_extra}
1841
+ Evaluate:
1842
+ 1. Are all requirements from the goal addressed?
1843
+ 2. Is the plan decomposed into clear, achievable tasks?
1844
+ 3. Are the implementation steps specific enough to execute?
1845
+
1846
+ Respond with EXACTLY one of these on the first line:
1847
+ VALID: true
1848
+ VALID: false
1849
+
1850
+ Then explain your reasoning briefly."
1851
+
1852
+ local validation_model="${plan_model:-opus}"
1853
+ local validation_result
1854
+ validation_result=$(claude --print --output-format text -p "$validation_prompt" --model "$validation_model" < /dev/null 2>"${ARTIFACTS_DIR}/.claude-tokens-plan-validate.log" || true)
1855
+ parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-plan-validate.log"
1856
+
1857
+ # Save validation result
1858
+ echo "$validation_result" > "$ARTIFACTS_DIR/plan-validation.md"
1859
+
1860
+ if echo "$validation_result" | head -5 | grep -qi "VALID: true"; then
1861
+ success "Plan validation passed"
1862
+ plan_valid=true
1863
+ break
1864
+ fi
1865
+
1866
+ warn "Plan validation failed (attempt ${validation_attempts}/${max_validation_attempts})"
1867
+
1868
+ # Analyze failure mode to decide how to recover
1869
+ local failure_mode="unknown"
1870
+ local validation_lower
1871
+ validation_lower=$(echo "$validation_result" | tr '[:upper:]' '[:lower:]')
1872
+ if echo "$validation_lower" | grep -qE 'requirements? unclear|goal.*vague|ambiguous|underspecified'; then
1873
+ failure_mode="requirements_unclear"
1874
+ elif echo "$validation_lower" | grep -qE 'insufficient detail|not specific|too high.level|missing.*steps|lacks.*detail'; then
1875
+ failure_mode="insufficient_detail"
1876
+ elif echo "$validation_lower" | grep -qE 'scope too (large|broad)|too many|overly complex|break.*down'; then
1877
+ failure_mode="scope_too_large"
1878
+ fi
1879
+
1880
+ emit_event "plan.validation_failure" \
1881
+ "issue=${ISSUE_NUMBER:-0}" \
1882
+ "attempt=$validation_attempts" \
1883
+ "failure_mode=$failure_mode"
1884
+
1885
+ # Track repeated failures — escalate if stuck in a loop
1886
+ if [[ -f "$ARTIFACTS_DIR/.plan-failure-sig.txt" ]]; then
1887
+ local prev_sig
1888
+ prev_sig=$(cat "$ARTIFACTS_DIR/.plan-failure-sig.txt" 2>/dev/null || true)
1889
+ if [[ "$failure_mode" == "$prev_sig" && "$failure_mode" != "unknown" ]]; then
1890
+ warn "Same validation failure mode repeated ($failure_mode) — escalating"
1891
+ emit_event "plan.validation_escalated" \
1892
+ "issue=${ISSUE_NUMBER:-0}" \
1893
+ "failure_mode=$failure_mode"
1894
+ break
1895
+ fi
1896
+ fi
1897
+ echo "$failure_mode" > "$ARTIFACTS_DIR/.plan-failure-sig.txt"
1898
+
1899
+ if [[ "$validation_attempts" -lt "$max_validation_attempts" ]]; then
1900
+ info "Regenerating plan with validation feedback (mode: ${failure_mode})..."
1901
+
1902
+ # Tailor regeneration prompt based on failure mode
1903
+ local failure_guidance=""
1904
+ case "$failure_mode" in
1905
+ requirements_unclear)
1906
+ failure_guidance="The validator found the requirements unclear. Add more specific acceptance criteria, input/output examples, and concrete success metrics." ;;
1907
+ insufficient_detail)
1908
+ failure_guidance="The validator found the plan lacks detail. Break each task into smaller, more specific implementation steps with exact file paths and function names." ;;
1909
+ scope_too_large)
1910
+ failure_guidance="The validator found the scope too large. Focus on the minimal viable implementation and defer non-essential features to follow-up tasks." ;;
1911
+ esac
1912
+
1913
+ local regen_prompt="${plan_prompt}
1914
+
1915
+ IMPORTANT: A previous plan was rejected by validation. Issues found:
1916
+ $(echo "$validation_result" | tail -20)
1917
+ ${failure_guidance:+
1918
+ GUIDANCE: ${failure_guidance}}
1919
+
1920
+ Fix these issues in the new plan."
1921
+
1922
+ claude --print --model "$plan_model" --max-turns 25 \
1923
+ "$regen_prompt" < /dev/null > "$plan_file" 2>"$_token_log" || true
1924
+ parse_claude_tokens "$_token_log"
1925
+
1926
+ line_count=$(wc -l < "$plan_file" | xargs)
1927
+ info "Regenerated plan: ${DIM}$plan_file${RESET} (${line_count} lines)"
1928
+ fi
1929
+ done
1930
+
1931
+ if [[ "$plan_valid" != "true" ]]; then
1932
+ warn "Plan validation did not pass after ${max_validation_attempts} attempts — proceeding anyway"
1933
+ fi
1934
+
1935
+ emit_event "plan.validated" \
1936
+ "issue=${ISSUE_NUMBER:-0}" \
1937
+ "valid=${plan_valid}" \
1938
+ "attempts=${validation_attempts}"
1939
+ fi
1940
+
1345
1941
  log_stage "plan" "Generated plan.md (${line_count} lines, $(echo "$checklist" | wc -l | xargs) tasks)"
1346
1942
  }
1347
1943
 
@@ -1364,8 +1960,49 @@ stage_design() {
1364
1960
 
1365
1961
  # Memory integration — inject context if memory system available
1366
1962
  local memory_context=""
1367
- if [[ -x "$SCRIPT_DIR/cct-memory.sh" ]]; then
1368
- memory_context=$(bash "$SCRIPT_DIR/cct-memory.sh" inject "design" 2>/dev/null) || true
1963
+ if type intelligence_search_memory &>/dev/null 2>&1; then
1964
+ local mem_dir="${HOME}/.shipwright/memory"
1965
+ memory_context=$(intelligence_search_memory "design stage architecture patterns for: ${GOAL:-}" "$mem_dir" 5 2>/dev/null) || true
1966
+ fi
1967
+ if [[ -z "$memory_context" ]] && [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
1968
+ memory_context=$(bash "$SCRIPT_DIR/sw-memory.sh" inject "design" 2>/dev/null) || true
1969
+ fi
1970
+
1971
+ # Inject architecture model patterns if available
1972
+ local arch_context=""
1973
+ local repo_hash
1974
+ repo_hash=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
1975
+ local arch_model_file="${HOME}/.shipwright/memory/${repo_hash}/architecture.json"
1976
+ if [[ -f "$arch_model_file" ]]; then
1977
+ local arch_patterns
1978
+ arch_patterns=$(jq -r '
1979
+ [.patterns // [] | .[] | "- \(.name // "unnamed"): \(.description // "no description")"] | join("\n")
1980
+ ' "$arch_model_file" 2>/dev/null) || true
1981
+ local arch_layers
1982
+ arch_layers=$(jq -r '
1983
+ [.layers // [] | .[] | "- \(.name // "unnamed"): \(.path // "")"] | join("\n")
1984
+ ' "$arch_model_file" 2>/dev/null) || true
1985
+ if [[ -n "$arch_patterns" || -n "$arch_layers" ]]; then
1986
+ arch_context="Previous designs in this repo follow these patterns:
1987
+ ${arch_patterns:+Patterns:
1988
+ ${arch_patterns}
1989
+ }${arch_layers:+Layers:
1990
+ ${arch_layers}}"
1991
+ fi
1992
+ fi
1993
+
1994
+ # Inject rejected design approaches and anti-patterns from memory
1995
+ local design_antipatterns=""
1996
+ if type intelligence_search_memory &>/dev/null 2>&1; then
1997
+ local rejected_designs
1998
+ rejected_designs=$(intelligence_search_memory "rejected design approaches anti-patterns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 3 2>/dev/null) || true
1999
+ if [[ -n "$rejected_designs" ]]; then
2000
+ design_antipatterns="
2001
+ ## Rejected Approaches (from past reviews)
2002
+ These design approaches were rejected in past reviews. Avoid repeating them:
2003
+ ${rejected_designs}
2004
+ "
2005
+ fi
1369
2006
  fi
1370
2007
 
1371
2008
  # Build design prompt with plan + project context
@@ -1387,7 +2024,10 @@ $(cat "$plan_file")
1387
2024
  ${memory_context:+
1388
2025
  ## Historical Context (from memory)
1389
2026
  ${memory_context}
1390
- }
2027
+ }${arch_context:+
2028
+ ## Architecture Model (from previous designs)
2029
+ ${arch_context}
2030
+ }${design_antipatterns}
1391
2031
  ## Required Output — Architecture Decision Record
1392
2032
 
1393
2033
  Produce this EXACT format:
@@ -1420,10 +2060,14 @@ Be concrete and specific. Reference actual file paths in the codebase. Consider
1420
2060
  design_model=$(jq -r --arg id "design" '(.stages[] | select(.id == $id) | .config.model) // .defaults.model // "opus"' "$PIPELINE_CONFIG" 2>/dev/null) || true
1421
2061
  [[ -n "$MODEL" ]] && design_model="$MODEL"
1422
2062
  [[ -z "$design_model" || "$design_model" == "null" ]] && design_model="opus"
2063
+ # Intelligence model routing (when no explicit CLI --model override)
2064
+ if [[ -z "$MODEL" && -n "${CLAUDE_MODEL:-}" ]]; then
2065
+ design_model="$CLAUDE_MODEL"
2066
+ fi
1423
2067
 
1424
2068
  local _token_log="${ARTIFACTS_DIR}/.claude-tokens-design.log"
1425
- claude --print --model "$design_model" --max-turns 10 \
1426
- "$design_prompt" > "$design_file" 2>"$_token_log" || true
2069
+ claude --print --model "$design_model" --max-turns 25 \
2070
+ "$design_prompt" < /dev/null > "$design_file" 2>"$_token_log" || true
1427
2071
  parse_claude_tokens "$_token_log"
1428
2072
 
1429
2073
  if [[ ! -s "$design_file" ]]; then
@@ -1475,8 +2119,12 @@ stage_build() {
1475
2119
 
1476
2120
  # Memory integration — inject context if memory system available
1477
2121
  local memory_context=""
1478
- if [[ -x "$SCRIPT_DIR/cct-memory.sh" ]]; then
1479
- memory_context=$(bash "$SCRIPT_DIR/cct-memory.sh" inject "build" 2>/dev/null) || true
2122
+ if type intelligence_search_memory &>/dev/null 2>&1; then
2123
+ local mem_dir="${HOME}/.shipwright/memory"
2124
+ memory_context=$(intelligence_search_memory "build stage for: ${GOAL:-}" "$mem_dir" 5 2>/dev/null) || true
2125
+ fi
2126
+ if [[ -z "$memory_context" ]] && [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
2127
+ memory_context=$(bash "$SCRIPT_DIR/sw-memory.sh" inject "build" 2>/dev/null) || true
1480
2128
  fi
1481
2129
 
1482
2130
  # Build enriched goal with full context
@@ -1512,6 +2160,44 @@ Task tracking (check off items as you complete them):
1512
2160
  $(cat "$TASKS_FILE")"
1513
2161
  fi
1514
2162
 
2163
+ # Inject file hotspots from GitHub intelligence
2164
+ if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_file_change_frequency &>/dev/null 2>&1; then
2165
+ local build_hotspots
2166
+ build_hotspots=$(gh_file_change_frequency 2>/dev/null | head -5 || true)
2167
+ if [[ -n "$build_hotspots" ]]; then
2168
+ enriched_goal="${enriched_goal}
2169
+
2170
+ File hotspots (most frequently changed — review these carefully):
2171
+ ${build_hotspots}"
2172
+ fi
2173
+ fi
2174
+
2175
+ # Inject security alerts context
2176
+ if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_security_alerts &>/dev/null 2>&1; then
2177
+ local build_alerts
2178
+ build_alerts=$(gh_security_alerts 2>/dev/null | head -3 || true)
2179
+ if [[ -n "$build_alerts" ]]; then
2180
+ enriched_goal="${enriched_goal}
2181
+
2182
+ Active security alerts (do not introduce new vulnerabilities):
2183
+ ${build_alerts}"
2184
+ fi
2185
+ fi
2186
+
2187
+ # Inject coverage baseline
2188
+ local repo_hash_build
2189
+ repo_hash_build=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
2190
+ local coverage_file_build="${HOME}/.shipwright/baselines/${repo_hash_build}/coverage.json"
2191
+ if [[ -f "$coverage_file_build" ]]; then
2192
+ local coverage_baseline
2193
+ coverage_baseline=$(jq -r '.coverage_percent // empty' "$coverage_file_build" 2>/dev/null || true)
2194
+ if [[ -n "$coverage_baseline" ]]; then
2195
+ enriched_goal="${enriched_goal}
2196
+
2197
+ Coverage baseline: ${coverage_baseline}% — do not decrease coverage."
2198
+ fi
2199
+ fi
2200
+
1515
2201
  loop_args+=("$enriched_goal")
1516
2202
 
1517
2203
  # Build loop args from pipeline config + CLI overrides
@@ -1530,6 +2216,8 @@ $(cat "$TASKS_FILE")"
1530
2216
  local max_iter
1531
2217
  max_iter=$(jq -r --arg id "build" '(.stages[] | select(.id == $id) | .config.max_iterations) // 20' "$PIPELINE_CONFIG" 2>/dev/null) || true
1532
2218
  [[ -z "$max_iter" || "$max_iter" == "null" ]] && max_iter=20
2219
+ # CLI --max-iterations override (from CI strategy engine)
2220
+ [[ -n "${MAX_ITERATIONS_OVERRIDE:-}" ]] && max_iter="$MAX_ITERATIONS_OVERRIDE"
1533
2221
 
1534
2222
  local agents="${AGENTS}"
1535
2223
  if [[ -z "$agents" ]]; then
@@ -1537,6 +2225,16 @@ $(cat "$TASKS_FILE")"
1537
2225
  [[ -z "$agents" || "$agents" == "null" ]] && agents=1
1538
2226
  fi
1539
2227
 
2228
+ # Intelligence: suggest parallelism if design indicates independent work
2229
+ if [[ "${agents:-1}" -le 1 ]] && [[ -s "$ARTIFACTS_DIR/design.md" ]]; then
2230
+ local design_lower
2231
+ design_lower=$(tr '[:upper:]' '[:lower:]' < "$ARTIFACTS_DIR/design.md" 2>/dev/null || true)
2232
+ if echo "$design_lower" | grep -qE 'independent (files|modules|components|services)|separate (modules|packages|directories)|parallel|no shared state'; then
2233
+ info "Design mentions independent modules — consider --agents 2 for parallelism"
2234
+ emit_event "build.parallelism_suggested" "issue=${ISSUE_NUMBER:-0}" "current_agents=$agents"
2235
+ fi
2236
+ fi
2237
+
1540
2238
  local audit
1541
2239
  audit=$(jq -r --arg id "build" '(.stages[] | select(.id == $id) | .config.audit) // false' "$PIPELINE_CONFIG" 2>/dev/null) || true
1542
2240
  local quality
@@ -1547,15 +2245,30 @@ $(cat "$TASKS_FILE")"
1547
2245
  build_model=$(jq -r '.defaults.model // "opus"' "$PIPELINE_CONFIG" 2>/dev/null) || true
1548
2246
  [[ -z "$build_model" || "$build_model" == "null" ]] && build_model="opus"
1549
2247
  fi
2248
+ # Intelligence model routing (when no explicit CLI --model override)
2249
+ if [[ -z "$MODEL" && -n "${CLAUDE_MODEL:-}" ]]; then
2250
+ build_model="$CLAUDE_MODEL"
2251
+ fi
1550
2252
 
1551
2253
  [[ -n "$test_cmd" && "$test_cmd" != "null" ]] && loop_args+=(--test-cmd "$test_cmd")
1552
2254
  loop_args+=(--max-iterations "$max_iter")
1553
2255
  loop_args+=(--model "$build_model")
1554
2256
  [[ "$agents" -gt 1 ]] 2>/dev/null && loop_args+=(--agents "$agents")
1555
- [[ "$audit" == "true" ]] && loop_args+=(--audit --audit-agent)
1556
- [[ "$quality" == "true" ]] && loop_args+=(--quality-gates)
2257
+
2258
+ # Quality gates: always enabled in CI, otherwise from template config
2259
+ if [[ "${CI_MODE:-false}" == "true" ]]; then
2260
+ loop_args+=(--audit --audit-agent --quality-gates)
2261
+ else
2262
+ [[ "$audit" == "true" ]] && loop_args+=(--audit --audit-agent)
2263
+ [[ "$quality" == "true" ]] && loop_args+=(--quality-gates)
2264
+ fi
2265
+
2266
+ # Definition of Done: use plan-extracted DoD if available
1557
2267
  [[ -s "$dod_file" ]] && loop_args+=(--definition-of-done "$dod_file")
1558
2268
 
2269
+ # Skip permissions in CI (no interactive terminal)
2270
+ [[ "${CI_MODE:-false}" == "true" ]] && loop_args+=(--skip-permissions)
2271
+
1559
2272
  info "Starting build loop: ${DIM}shipwright loop${RESET} (max ${max_iter} iterations, ${agents} agent(s))"
1560
2273
 
1561
2274
  # Post build start to GitHub
@@ -1564,7 +2277,8 @@ $(cat "$TASKS_FILE")"
1564
2277
  fi
1565
2278
 
1566
2279
  local _token_log="${ARTIFACTS_DIR}/.claude-tokens-build.log"
1567
- cct loop "${loop_args[@]}" 2>"$_token_log" || {
2280
+ export PIPELINE_JOB_ID="${PIPELINE_NAME:-pipeline-$$}"
2281
+ sw loop "${loop_args[@]}" < /dev/null 2>"$_token_log" || {
1568
2282
  parse_claude_tokens "$_token_log"
1569
2283
  error "Build loop failed"
1570
2284
  return 1
@@ -1576,6 +2290,29 @@ $(cat "$TASKS_FILE")"
1576
2290
  commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
1577
2291
  info "Build produced ${BOLD}$commit_count${RESET} commit(s)"
1578
2292
 
2293
+ # Commit quality evaluation when intelligence is enabled
2294
+ if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null && [[ "${commit_count:-0}" -gt 0 ]]; then
2295
+ local commit_msgs
2296
+ commit_msgs=$(git log --format="%s" "${BASE_BRANCH}..HEAD" 2>/dev/null | head -20)
2297
+ local quality_score
2298
+ quality_score=$(claude --print --output-format text -p "Rate the quality of these git commit messages on a scale of 0-100. Consider: focus (one thing per commit), clarity (describes the why), atomicity (small logical units). Reply with ONLY a number 0-100.
2299
+
2300
+ Commit messages:
2301
+ ${commit_msgs}" --model haiku < /dev/null 2>/dev/null || true)
2302
+ quality_score=$(echo "$quality_score" | grep -oE '^[0-9]+' | head -1 || true)
2303
+ if [[ -n "$quality_score" ]]; then
2304
+ emit_event "build.commit_quality" \
2305
+ "issue=${ISSUE_NUMBER:-0}" \
2306
+ "score=$quality_score" \
2307
+ "commit_count=$commit_count"
2308
+ if [[ "$quality_score" -lt 40 ]] 2>/dev/null; then
2309
+ warn "Commit message quality low (score: ${quality_score}/100)"
2310
+ else
2311
+ info "Commit quality score: ${quality_score}/100"
2312
+ fi
2313
+ fi
2314
+ fi
2315
+
1579
2316
  log_stage "build" "Build loop completed ($commit_count commits)"
1580
2317
  }
1581
2318
 
@@ -1603,34 +2340,53 @@ stage_test() {
1603
2340
 
1604
2341
  info "Running tests: ${DIM}$test_cmd${RESET}"
1605
2342
  local test_exit=0
1606
- eval "$test_cmd" > "$test_log" 2>&1 || test_exit=$?
2343
+ bash -c "$test_cmd" > "$test_log" 2>&1 || test_exit=$?
1607
2344
 
1608
2345
  if [[ "$test_exit" -eq 0 ]]; then
1609
2346
  success "Tests passed"
1610
2347
  else
1611
2348
  error "Tests failed (exit code: $test_exit)"
1612
- tail -20 "$test_log"
2349
+ # Extract most relevant error section (assertion failures, stack traces)
2350
+ local relevant_output=""
2351
+ relevant_output=$(grep -A5 -E 'FAIL|AssertionError|Expected.*but.*got|Error:|panic:|assert' "$test_log" 2>/dev/null | tail -40 || true)
2352
+ if [[ -z "$relevant_output" ]]; then
2353
+ relevant_output=$(tail -40 "$test_log")
2354
+ fi
2355
+ echo "$relevant_output"
1613
2356
 
1614
- # Post failure to GitHub
2357
+ # Post failure to GitHub with more context
1615
2358
  if [[ -n "$ISSUE_NUMBER" ]]; then
1616
- gh_comment_issue "$ISSUE_NUMBER" "❌ **Tests failed**
2359
+ local log_lines
2360
+ log_lines=$(wc -l < "$test_log" 2>/dev/null || echo "0")
2361
+ local log_excerpt
2362
+ if [[ "$log_lines" -lt 60 ]]; then
2363
+ log_excerpt="$(cat "$test_log" 2>/dev/null || true)"
2364
+ else
2365
+ log_excerpt="$(head -20 "$test_log" 2>/dev/null || true)
2366
+ ... (${log_lines} lines total, showing head + tail) ...
2367
+ $(tail -30 "$test_log" 2>/dev/null || true)"
2368
+ fi
2369
+ gh_comment_issue "$ISSUE_NUMBER" "❌ **Tests failed** (exit code: $test_exit, ${log_lines} lines)
1617
2370
  \`\`\`
1618
- $(tail -20 "$test_log")
2371
+ ${log_excerpt}
1619
2372
  \`\`\`"
1620
2373
  fi
1621
2374
  return 1
1622
2375
  fi
1623
2376
 
1624
- # Coverage check
2377
+ # Coverage check — only enforce when coverage data is actually detected
1625
2378
  local coverage=""
1626
2379
  if [[ "$coverage_min" -gt 0 ]] 2>/dev/null; then
1627
- coverage=$(grep -oE 'Statements\s*:\s*[0-9.]+' "$test_log" 2>/dev/null | grep -oE '[0-9.]+$' || \
1628
- grep -oE 'All files\s*\|\s*[0-9.]+' "$test_log" 2>/dev/null | grep -oE '[0-9.]+$' || echo "0")
1629
- if awk -v cov="$coverage" -v min="$coverage_min" 'BEGIN{exit !(cov < min)}' 2>/dev/null; then
2380
+ coverage=$(parse_coverage_from_output "$test_log")
2381
+ if [[ -z "$coverage" ]]; then
2382
+ # No coverage data found skip enforcement (project may not have coverage tooling)
2383
+ info "No coverage data detected — skipping coverage check (min: ${coverage_min}%)"
2384
+ elif awk -v cov="$coverage" -v min="$coverage_min" 'BEGIN{exit !(cov < min)}' 2>/dev/null; then
1630
2385
  warn "Coverage ${coverage}% below minimum ${coverage_min}%"
1631
2386
  return 1
2387
+ else
2388
+ info "Coverage: ${coverage}% (min: ${coverage_min}%)"
1632
2389
  fi
1633
- info "Coverage: ${coverage}% (min: ${coverage_min}%)"
1634
2390
  fi
1635
2391
 
1636
2392
  # Post test results to GitHub
@@ -1675,10 +2431,34 @@ stage_review() {
1675
2431
  diff_stats=$(git diff --stat "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null | tail -1 || echo "")
1676
2432
  info "Running AI code review... ${DIM}($diff_stats)${RESET}"
1677
2433
 
2434
+ # Semantic risk scoring when intelligence is enabled
2435
+ if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
2436
+ local diff_files
2437
+ diff_files=$(git diff --name-only "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null || true)
2438
+ local risk_score="low"
2439
+ # Fast heuristic: flag high-risk file patterns
2440
+ if echo "$diff_files" | grep -qiE 'migration|schema|auth|crypto|security|password|token|secret|\.env'; then
2441
+ risk_score="high"
2442
+ elif echo "$diff_files" | grep -qiE 'api|route|controller|middleware|hook'; then
2443
+ risk_score="medium"
2444
+ fi
2445
+ emit_event "review.risk_assessed" \
2446
+ "issue=${ISSUE_NUMBER:-0}" \
2447
+ "risk=$risk_score" \
2448
+ "files_changed=$(echo "$diff_files" | wc -l | xargs)"
2449
+ if [[ "$risk_score" == "high" ]]; then
2450
+ warn "High-risk changes detected (DB schema, auth, crypto, or secrets)"
2451
+ fi
2452
+ fi
2453
+
1678
2454
  local review_model="${MODEL:-opus}"
2455
+ # Intelligence model routing (when no explicit CLI --model override)
2456
+ if [[ -z "$MODEL" && -n "${CLAUDE_MODEL:-}" ]]; then
2457
+ review_model="$CLAUDE_MODEL"
2458
+ fi
1679
2459
 
1680
- claude --print --model "$review_model" --max-turns 15 \
1681
- "You are a senior code reviewer. Review this git diff thoroughly.
2460
+ # Build review prompt with project context
2461
+ local review_prompt="You are a senior code reviewer. Review this git diff thoroughly.
1682
2462
 
1683
2463
  For each issue found, use this format:
1684
2464
  - **[SEVERITY]** file:line — description
@@ -1691,24 +2471,102 @@ Focus on:
1691
2471
  3. Error handling gaps
1692
2472
  4. Performance issues
1693
2473
  5. Missing validation
2474
+ 6. Project convention violations (see conventions below)
1694
2475
 
1695
2476
  Be specific. Reference exact file paths and line numbers. Only flag genuine issues.
2477
+ If no issues are found, write: \"Review clean — no issues found.\"
2478
+ "
2479
+
2480
+ # Inject previous review findings and anti-patterns from memory
2481
+ if type intelligence_search_memory &>/dev/null 2>&1; then
2482
+ local review_memory
2483
+ review_memory=$(intelligence_search_memory "code review findings anti-patterns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
2484
+ if [[ -n "$review_memory" ]]; then
2485
+ review_prompt+="
2486
+ ## Known Issues from Previous Reviews
2487
+ These anti-patterns and issues have been found in past reviews of this codebase. Flag them if they recur:
2488
+ ${review_memory}
2489
+ "
2490
+ fi
2491
+ fi
2492
+
2493
+ # Inject project conventions if CLAUDE.md exists
2494
+ local claudemd="$PROJECT_ROOT/.claude/CLAUDE.md"
2495
+ if [[ -f "$claudemd" ]]; then
2496
+ local conventions
2497
+ conventions=$(grep -A2 'Common Pitfalls\|Shell Standards\|Bash 3.2' "$claudemd" 2>/dev/null | head -20 || true)
2498
+ if [[ -n "$conventions" ]]; then
2499
+ review_prompt+="
2500
+ ## Project Conventions
2501
+ ${conventions}
2502
+ "
2503
+ fi
2504
+ fi
2505
+
2506
+ # Inject CODEOWNERS focus areas for review
2507
+ if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_codeowners &>/dev/null 2>&1; then
2508
+ local review_owners
2509
+ review_owners=$(gh_codeowners 2>/dev/null | head -10 || true)
2510
+ if [[ -n "$review_owners" ]]; then
2511
+ review_prompt+="
2512
+ ## Code Owners (focus areas)
2513
+ ${review_owners}
2514
+ "
2515
+ fi
2516
+ fi
2517
+
2518
+ # Inject Definition of Done if present
2519
+ local dod_file="$PROJECT_ROOT/.claude/DEFINITION-OF-DONE.md"
2520
+ if [[ -f "$dod_file" ]]; then
2521
+ review_prompt+="
2522
+ ## Definition of Done (verify these)
2523
+ $(cat "$dod_file")
2524
+ "
2525
+ fi
2526
+
2527
+ review_prompt+="
2528
+ ## Diff to Review
2529
+ $(cat "$diff_file")"
2530
+
2531
+ # Build claude args — add --dangerously-skip-permissions in CI
2532
+ local review_args=(--print --model "$review_model" --max-turns 25)
2533
+ if [[ "${CI_MODE:-false}" == "true" ]]; then
2534
+ review_args+=(--dangerously-skip-permissions)
2535
+ fi
1696
2536
 
1697
- $(cat "$diff_file")" > "$review_file" 2>"${ARTIFACTS_DIR}/.claude-tokens-review.log" || true
2537
+ claude "${review_args[@]}" "$review_prompt" < /dev/null > "$review_file" 2>"${ARTIFACTS_DIR}/.claude-tokens-review.log" || true
1698
2538
  parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-review.log"
1699
2539
 
1700
2540
  if [[ ! -s "$review_file" ]]; then
1701
- warn "Review produced no output"
2541
+ warn "Review produced no output — check ${ARTIFACTS_DIR}/.claude-tokens-review.log for errors"
1702
2542
  return 0
1703
2543
  fi
1704
2544
 
1705
- local critical_count bug_count warning_count
1706
- critical_count=$(grep -ciE '\*\*\[?Critical\]?\*\*' "$review_file" 2>/dev/null || true)
1707
- critical_count="${critical_count:-0}"
1708
- bug_count=$(grep -ciE '\*\*\[?(Bug|Security)\]?\*\*' "$review_file" 2>/dev/null || true)
1709
- bug_count="${bug_count:-0}"
1710
- warning_count=$(grep -ciE '\*\*\[?(Warning|Suggestion)\]?\*\*' "$review_file" 2>/dev/null || true)
1711
- warning_count="${warning_count:-0}"
2545
+ # Extract severity counts — try JSON structure first, then grep fallback
2546
+ local critical_count=0 bug_count=0 warning_count=0
2547
+
2548
+ # Check if review output is structured JSON (e.g. from structured review tools)
2549
+ local json_parsed=false
2550
+ if head -1 "$review_file" 2>/dev/null | grep -q '^{' 2>/dev/null; then
2551
+ local j_critical j_bug j_warning
2552
+ j_critical=$(jq -r '.issues | map(select(.severity == "Critical")) | length' "$review_file" 2>/dev/null || echo "")
2553
+ if [[ -n "$j_critical" && "$j_critical" != "null" ]]; then
2554
+ critical_count="$j_critical"
2555
+ bug_count=$(jq -r '.issues | map(select(.severity == "Bug" or .severity == "Security")) | length' "$review_file" 2>/dev/null || echo "0")
2556
+ warning_count=$(jq -r '.issues | map(select(.severity == "Warning" or .severity == "Suggestion")) | length' "$review_file" 2>/dev/null || echo "0")
2557
+ json_parsed=true
2558
+ fi
2559
+ fi
2560
+
2561
+ # Grep fallback for markdown-formatted review output
2562
+ if [[ "$json_parsed" != "true" ]]; then
2563
+ critical_count=$(grep -ciE '\*\*\[?Critical\]?\*\*' "$review_file" 2>/dev/null || true)
2564
+ critical_count="${critical_count:-0}"
2565
+ bug_count=$(grep -ciE '\*\*\[?(Bug|Security)\]?\*\*' "$review_file" 2>/dev/null || true)
2566
+ bug_count="${bug_count:-0}"
2567
+ warning_count=$(grep -ciE '\*\*\[?(Warning|Suggestion)\]?\*\*' "$review_file" 2>/dev/null || true)
2568
+ warning_count="${warning_count:-0}"
2569
+ fi
1712
2570
  local total_issues=$((critical_count + bug_count + warning_count))
1713
2571
 
1714
2572
  if [[ "$critical_count" -gt 0 ]]; then
@@ -1721,6 +2579,68 @@ $(cat "$diff_file")" > "$review_file" 2>"${ARTIFACTS_DIR}/.claude-tokens-review.
1721
2579
  success "Review clean"
1722
2580
  fi
1723
2581
 
2582
+ # ── Review Blocking Gate ──
2583
+ # Block pipeline on critical/security issues unless compound_quality handles them
2584
+ local security_count
2585
+ security_count=$(grep -ciE '\*\*\[?Security\]?\*\*' "$review_file" 2>/dev/null || true)
2586
+ security_count="${security_count:-0}"
2587
+
2588
+ local blocking_issues=$((critical_count + security_count))
2589
+
2590
+ if [[ "$blocking_issues" -gt 0 ]]; then
2591
+ # Check if compound_quality stage is enabled — if so, let it handle issues
2592
+ local compound_enabled="false"
2593
+ if [[ -n "${PIPELINE_CONFIG:-}" && -f "${PIPELINE_CONFIG:-/dev/null}" ]]; then
2594
+ compound_enabled=$(jq -r '.stages[] | select(.id == "compound_quality") | .enabled' "$PIPELINE_CONFIG" 2>/dev/null) || true
2595
+ [[ -z "$compound_enabled" || "$compound_enabled" == "null" ]] && compound_enabled="false"
2596
+ fi
2597
+
2598
+ # Check if this is a fast template (don't block fast pipelines)
2599
+ local is_fast="false"
2600
+ if [[ "${PIPELINE_NAME:-}" == "fast" || "${PIPELINE_NAME:-}" == "hotfix" ]]; then
2601
+ is_fast="true"
2602
+ fi
2603
+
2604
+ if [[ "$compound_enabled" == "true" ]]; then
2605
+ info "Review found ${blocking_issues} critical/security issue(s) — compound_quality stage will handle"
2606
+ elif [[ "$is_fast" == "true" ]]; then
2607
+ warn "Review found ${blocking_issues} critical/security issue(s) — fast template, not blocking"
2608
+ elif [[ "${SKIP_GATES:-false}" == "true" ]]; then
2609
+ warn "Review found ${blocking_issues} critical/security issue(s) — skip-gates mode, not blocking"
2610
+ else
2611
+ error "Review found ${BOLD}${blocking_issues} critical/security issue(s)${RESET} — blocking pipeline"
2612
+ emit_event "review.blocked" \
2613
+ "issue=${ISSUE_NUMBER:-0}" \
2614
+ "critical=${critical_count}" \
2615
+ "security=${security_count}"
2616
+
2617
+ # Save blocking issues for self-healing context
2618
+ grep -iE '\*\*\[?(Critical|Security)\]?\*\*' "$review_file" > "$ARTIFACTS_DIR/review-blockers.md" 2>/dev/null || true
2619
+
2620
+ # Post review to GitHub before failing
2621
+ if [[ -n "$ISSUE_NUMBER" ]]; then
2622
+ local review_summary
2623
+ review_summary=$(head -40 "$review_file")
2624
+ gh_comment_issue "$ISSUE_NUMBER" "## 🔍 Code Review — ❌ Blocked
2625
+
2626
+ **Stats:** $diff_stats
2627
+ **Blocking issues:** ${blocking_issues} (${critical_count} critical, ${security_count} security)
2628
+
2629
+ <details>
2630
+ <summary>Review details</summary>
2631
+
2632
+ ${review_summary}
2633
+
2634
+ </details>
2635
+
2636
+ _Pipeline will attempt self-healing rebuild._"
2637
+ fi
2638
+
2639
+ log_stage "review" "BLOCKED: $blocking_issues critical/security issues found"
2640
+ return 1
2641
+ fi
2642
+ fi
2643
+
1724
2644
  # Post review to GitHub issue
1725
2645
  if [[ -n "$ISSUE_NUMBER" ]]; then
1726
2646
  local review_summary
@@ -1747,6 +2667,47 @@ stage_pr() {
1747
2667
  local test_log="$ARTIFACTS_DIR/test-results.log"
1748
2668
  local review_file="$ARTIFACTS_DIR/review.md"
1749
2669
 
2670
+ # ── PR Hygiene Checks (informational) ──
2671
+ local hygiene_commit_count
2672
+ hygiene_commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
2673
+ hygiene_commit_count="${hygiene_commit_count:-0}"
2674
+
2675
+ if [[ "$hygiene_commit_count" -gt 20 ]]; then
2676
+ warn "PR has ${hygiene_commit_count} commits — consider squashing before merge"
2677
+ fi
2678
+
2679
+ # Check for WIP/fixup/squash commits (expanded patterns)
2680
+ local wip_commits
2681
+ wip_commits=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | grep -ciE '^[0-9a-f]+ (WIP|fixup!|squash!|TODO|HACK|TEMP|BROKEN|wip[:-]|temp[:-]|broken[:-]|do not merge)' || true)
2682
+ wip_commits="${wip_commits:-0}"
2683
+ if [[ "$wip_commits" -gt 0 ]]; then
2684
+ warn "Branch has ${wip_commits} WIP/fixup/squash/temp commit(s) — consider cleaning up"
2685
+ fi
2686
+
2687
+ # ── PR Quality Gate: reject PRs with no real code changes ──
2688
+ local real_files
2689
+ real_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null | grep -v '^\.claude/' | grep -v '^\.github/' || true)
2690
+ if [[ -z "$real_files" ]]; then
2691
+ error "No real code changes detected — only pipeline artifacts (.claude/ logs)."
2692
+ error "The build agent did not produce meaningful changes. Skipping PR creation."
2693
+ emit_event "pr.rejected" "issue=${ISSUE_NUMBER:-0}" "reason=no_real_changes"
2694
+ # Mark issue so auto-retry knows not to retry empty builds
2695
+ if [[ -n "${ISSUE_NUMBER:-}" && "${ISSUE_NUMBER:-0}" != "0" ]]; then
2696
+ gh issue comment "$ISSUE_NUMBER" --body "<!-- SHIPWRIGHT-NO-CHANGES: true -->" 2>/dev/null || true
2697
+ fi
2698
+ return 1
2699
+ fi
2700
+ local real_file_count
2701
+ real_file_count=$(echo "$real_files" | wc -l | xargs)
2702
+ info "PR quality gate: ${real_file_count} real file(s) changed"
2703
+
2704
+ # Commit any uncommitted changes left by the build agent
2705
+ if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
2706
+ info "Committing remaining uncommitted changes..."
2707
+ git add -A 2>/dev/null || true
2708
+ git commit -m "chore: pipeline cleanup — commit remaining build changes" --no-verify 2>/dev/null || true
2709
+ fi
2710
+
1750
2711
  # Auto-rebase onto latest base branch before PR
1751
2712
  auto_rebase || {
1752
2713
  warn "Rebase/merge failed — pushing as-is"
@@ -1762,27 +2723,105 @@ stage_pr() {
1762
2723
  }
1763
2724
  }
1764
2725
 
1765
- # Build PR title
1766
- local pr_title
1767
- pr_title=$(head -1 "$plan_file" 2>/dev/null | sed 's/^#* *//' | cut -c1-70)
1768
- [[ -z "$pr_title" ]] && pr_title="$GOAL"
1769
-
1770
- # Build comprehensive PR body
1771
- local plan_summary=""
1772
- if [[ -s "$plan_file" ]]; then
1773
- plan_summary=$(head -20 "$plan_file" 2>/dev/null | tail -15)
1774
- fi
1775
-
1776
- local test_summary=""
1777
- if [[ -s "$test_log" ]]; then
1778
- test_summary=$(tail -10 "$test_log")
2726
+ # ── Developer Simulation (pre-PR review) ──
2727
+ local simulation_summary=""
2728
+ if type simulation_review &>/dev/null 2>&1; then
2729
+ local sim_enabled
2730
+ sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
2731
+ # Also check daemon-config
2732
+ local daemon_cfg=".claude/daemon-config.json"
2733
+ if [[ "$sim_enabled" != "true" && -f "$daemon_cfg" ]]; then
2734
+ sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
2735
+ fi
2736
+ if [[ "$sim_enabled" == "true" ]]; then
2737
+ info "Running developer simulation review..."
2738
+ local diff_for_sim
2739
+ diff_for_sim=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
2740
+ if [[ -n "$diff_for_sim" ]]; then
2741
+ local sim_result
2742
+ sim_result=$(simulation_review "$diff_for_sim" "${GOAL:-}" 2>/dev/null || echo "")
2743
+ if [[ -n "$sim_result" && "$sim_result" != *'"error"'* ]]; then
2744
+ echo "$sim_result" > "$ARTIFACTS_DIR/simulation-review.json"
2745
+ local sim_count
2746
+ sim_count=$(echo "$sim_result" | jq 'length' 2>/dev/null || echo "0")
2747
+ simulation_summary="**Developer simulation:** ${sim_count} reviewer concerns pre-addressed"
2748
+ success "Simulation complete: ${sim_count} concerns found and addressed"
2749
+ emit_event "simulation.complete" "issue=${ISSUE_NUMBER:-0}" "concerns=${sim_count}"
2750
+ else
2751
+ info "Simulation returned no actionable concerns"
2752
+ fi
2753
+ fi
2754
+ fi
2755
+ fi
2756
+
2757
+ # ── Architecture Validation (pre-PR check) ──
2758
+ local arch_summary=""
2759
+ if type architecture_validate_changes &>/dev/null 2>&1; then
2760
+ local arch_enabled
2761
+ arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
2762
+ local daemon_cfg=".claude/daemon-config.json"
2763
+ if [[ "$arch_enabled" != "true" && -f "$daemon_cfg" ]]; then
2764
+ arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
2765
+ fi
2766
+ if [[ "$arch_enabled" == "true" ]]; then
2767
+ info "Validating architecture..."
2768
+ local diff_for_arch
2769
+ diff_for_arch=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
2770
+ if [[ -n "$diff_for_arch" ]]; then
2771
+ local arch_result
2772
+ arch_result=$(architecture_validate_changes "$diff_for_arch" "" 2>/dev/null || echo "")
2773
+ if [[ -n "$arch_result" && "$arch_result" != *'"error"'* ]]; then
2774
+ echo "$arch_result" > "$ARTIFACTS_DIR/architecture-validation.json"
2775
+ local violation_count
2776
+ violation_count=$(echo "$arch_result" | jq '[.violations[]? | select(.severity == "critical" or .severity == "high")] | length' 2>/dev/null || echo "0")
2777
+ arch_summary="**Architecture validation:** ${violation_count} violations"
2778
+ if [[ "$violation_count" -gt 0 ]]; then
2779
+ warn "Architecture: ${violation_count} high/critical violations found"
2780
+ else
2781
+ success "Architecture validation passed"
2782
+ fi
2783
+ emit_event "architecture.validated" "issue=${ISSUE_NUMBER:-0}" "violations=${violation_count}"
2784
+ else
2785
+ info "Architecture validation returned no results"
2786
+ fi
2787
+ fi
2788
+ fi
2789
+ fi
2790
+
2791
+ # Build PR title — prefer GOAL over plan file first line
2792
+ # (plan file first line often contains Claude analysis text, not a clean title)
2793
+ local pr_title=""
2794
+ if [[ -n "${GOAL:-}" ]]; then
2795
+ pr_title=$(echo "$GOAL" | cut -c1-70)
2796
+ fi
2797
+ if [[ -z "$pr_title" ]] && [[ -s "$plan_file" ]]; then
2798
+ pr_title=$(head -1 "$plan_file" 2>/dev/null | sed 's/^#* *//' | cut -c1-70)
2799
+ fi
2800
+ [[ -z "$pr_title" ]] && pr_title="Pipeline changes for issue ${ISSUE_NUMBER:-unknown}"
2801
+
2802
+ # Build comprehensive PR body
2803
+ local plan_summary=""
2804
+ if [[ -s "$plan_file" ]]; then
2805
+ plan_summary=$(head -20 "$plan_file" 2>/dev/null | tail -15)
2806
+ fi
2807
+
2808
+ local test_summary=""
2809
+ if [[ -s "$test_log" ]]; then
2810
+ test_summary=$(tail -10 "$test_log")
1779
2811
  fi
1780
2812
 
1781
2813
  local review_summary=""
1782
2814
  if [[ -s "$review_file" ]]; then
1783
- local total_issues
1784
- total_issues=$(grep -ciE '\*\*\[?(Critical|Bug|Security|Warning|Suggestion)\]?\*\*' "$review_file" 2>/dev/null || true)
1785
- total_issues="${total_issues:-0}"
2815
+ local total_issues=0
2816
+ # Try JSON structured output first
2817
+ if head -1 "$review_file" 2>/dev/null | grep -q '^{' 2>/dev/null; then
2818
+ total_issues=$(jq -r '.issues | length' "$review_file" 2>/dev/null || echo "0")
2819
+ fi
2820
+ # Grep fallback for markdown
2821
+ if [[ "${total_issues:-0}" -eq 0 ]]; then
2822
+ total_issues=$(grep -ciE '\*\*\[?(Critical|Bug|Security|Warning|Suggestion)\]?\*\*' "$review_file" 2>/dev/null || true)
2823
+ total_issues="${total_issues:-0}"
2824
+ fi
1786
2825
  review_summary="**Code review:** $total_issues issues found"
1787
2826
  fi
1788
2827
 
@@ -1815,6 +2854,8 @@ ${test_summary:-No test output}
1815
2854
  \`\`\`
1816
2855
 
1817
2856
  ${review_summary}
2857
+ ${simulation_summary}
2858
+ ${arch_summary}
1818
2859
 
1819
2860
  ${closes_line}
1820
2861
 
@@ -1863,12 +2904,35 @@ EOF
1863
2904
  info "Milestone: ${DIM}$ISSUE_MILESTONE${RESET}"
1864
2905
  fi
1865
2906
 
1866
- info "Creating PR..."
1867
- local pr_url
1868
- pr_url=$(gh pr create "${pr_args[@]}" 2>&1) || {
1869
- error "PR creation failed: $pr_url"
1870
- return 1
1871
- }
2907
+ # Check for existing open PR on this branch to avoid duplicates (issue #12)
2908
+ local pr_url=""
2909
+ local existing_pr
2910
+ existing_pr=$(gh pr list --head "$GIT_BRANCH" --state open --json number,url --jq '.[0]' 2>/dev/null || echo "")
2911
+ if [[ -n "$existing_pr" && "$existing_pr" != "null" ]]; then
2912
+ local existing_pr_number existing_pr_url
2913
+ existing_pr_number=$(echo "$existing_pr" | jq -r '.number' 2>/dev/null || echo "")
2914
+ existing_pr_url=$(echo "$existing_pr" | jq -r '.url' 2>/dev/null || echo "")
2915
+ info "Updating existing PR #$existing_pr_number instead of creating duplicate"
2916
+ gh pr edit "$existing_pr_number" --title "$pr_title" --body "$pr_body" 2>/dev/null || true
2917
+ pr_url="$existing_pr_url"
2918
+ else
2919
+ info "Creating PR..."
2920
+ local pr_stderr pr_exit=0
2921
+ pr_url=$(gh pr create "${pr_args[@]}" 2>/tmp/shipwright-pr-stderr.txt) || pr_exit=$?
2922
+ pr_stderr=$(cat /tmp/shipwright-pr-stderr.txt 2>/dev/null || true)
2923
+ rm -f /tmp/shipwright-pr-stderr.txt
2924
+
2925
+ # gh pr create may return non-zero for reviewer issues but still create the PR
2926
+ if [[ "$pr_exit" -ne 0 ]]; then
2927
+ if [[ "$pr_url" == *"github.com"* ]]; then
2928
+ # PR was created but something non-fatal failed (e.g., reviewer not found)
2929
+ warn "PR created with warnings: ${pr_stderr:-unknown}"
2930
+ else
2931
+ error "PR creation failed: ${pr_stderr:-$pr_url}"
2932
+ return 1
2933
+ fi
2934
+ fi
2935
+ fi
1872
2936
 
1873
2937
  success "PR created: ${BOLD}$pr_url${RESET}"
1874
2938
  echo "$pr_url" > "$ARTIFACTS_DIR/pr-url.txt"
@@ -1876,6 +2940,54 @@ EOF
1876
2940
  # Extract PR number
1877
2941
  PR_NUMBER=$(echo "$pr_url" | grep -oE '[0-9]+$' || true)
1878
2942
 
2943
+ # ── Intelligent Reviewer Selection (GraphQL-enhanced) ──
2944
+ if [[ "${NO_GITHUB:-false}" != "true" && -n "$PR_NUMBER" && -z "$reviewers" ]]; then
2945
+ local reviewer_assigned=false
2946
+
2947
+ # Try CODEOWNERS-based routing via GraphQL API
2948
+ if type gh_codeowners &>/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
2949
+ local codeowners_json
2950
+ codeowners_json=$(gh_codeowners "$REPO_OWNER" "$REPO_NAME" 2>/dev/null || echo "[]")
2951
+ if [[ "$codeowners_json" != "[]" && -n "$codeowners_json" ]]; then
2952
+ local changed_files
2953
+ changed_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
2954
+ if [[ -n "$changed_files" ]]; then
2955
+ local co_reviewers
2956
+ co_reviewers=$(echo "$codeowners_json" | jq -r '.[].owners[]' 2>/dev/null | sort -u | head -3 || true)
2957
+ if [[ -n "$co_reviewers" ]]; then
2958
+ local rev
2959
+ while IFS= read -r rev; do
2960
+ rev="${rev#@}"
2961
+ [[ -n "$rev" ]] && gh pr edit "$PR_NUMBER" --add-reviewer "$rev" 2>/dev/null || true
2962
+ done <<< "$co_reviewers"
2963
+ info "Requested review from CODEOWNERS: $(echo "$co_reviewers" | tr '\n' ',' | sed 's/,$//')"
2964
+ reviewer_assigned=true
2965
+ fi
2966
+ fi
2967
+ fi
2968
+ fi
2969
+
2970
+ # Fallback: contributor-based routing via GraphQL API
2971
+ if [[ "$reviewer_assigned" != "true" ]] && type gh_contributors &>/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
2972
+ local contributors_json
2973
+ contributors_json=$(gh_contributors "$REPO_OWNER" "$REPO_NAME" 2>/dev/null || echo "[]")
2974
+ local top_contributor
2975
+ top_contributor=$(echo "$contributors_json" | jq -r '.[0].login // ""' 2>/dev/null || echo "")
2976
+ local current_user
2977
+ current_user=$(gh api user --jq '.login' 2>/dev/null || echo "")
2978
+ if [[ -n "$top_contributor" && "$top_contributor" != "$current_user" ]]; then
2979
+ gh pr edit "$PR_NUMBER" --add-reviewer "$top_contributor" 2>/dev/null || true
2980
+ info "Requested review from top contributor: $top_contributor"
2981
+ reviewer_assigned=true
2982
+ fi
2983
+ fi
2984
+
2985
+ # Final fallback: auto-approve if no reviewers assigned
2986
+ if [[ "$reviewer_assigned" != "true" ]]; then
2987
+ gh pr review "$PR_NUMBER" --approve 2>/dev/null || warn "Could not auto-approve PR"
2988
+ fi
2989
+ fi
2990
+
1879
2991
  # Update issue with PR link
1880
2992
  if [[ -n "$ISSUE_NUMBER" ]]; then
1881
2993
  gh_remove_label "$ISSUE_NUMBER" "pipeline/in-progress"
@@ -1883,6 +2995,9 @@ EOF
1883
2995
  gh_comment_issue "$ISSUE_NUMBER" "🎉 **PR created:** ${pr_url}
1884
2996
 
1885
2997
  Pipeline duration so far: ${total_dur:-unknown}"
2998
+
2999
+ # Notify tracker of review/PR creation
3000
+ "$SCRIPT_DIR/sw-tracker.sh" notify "review" "$ISSUE_NUMBER" "$pr_url" 2>/dev/null || true
1886
3001
  fi
1887
3002
 
1888
3003
  # Wait for CI if configured
@@ -1904,11 +3019,69 @@ stage_merge() {
1904
3019
  return 0
1905
3020
  fi
1906
3021
 
3022
+ # ── Branch Protection Check ──
3023
+ if type gh_branch_protection &>/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
3024
+ local protection_json
3025
+ protection_json=$(gh_branch_protection "$REPO_OWNER" "$REPO_NAME" "${BASE_BRANCH:-main}" 2>/dev/null || echo '{"protected": false}')
3026
+ local is_protected
3027
+ is_protected=$(echo "$protection_json" | jq -r '.protected // false' 2>/dev/null || echo "false")
3028
+ if [[ "$is_protected" == "true" ]]; then
3029
+ local required_reviews
3030
+ required_reviews=$(echo "$protection_json" | jq -r '.required_pull_request_reviews.required_approving_review_count // 0' 2>/dev/null || echo "0")
3031
+ local required_checks
3032
+ required_checks=$(echo "$protection_json" | jq -r '[.required_status_checks.contexts // [] | .[]] | length' 2>/dev/null || echo "0")
3033
+
3034
+ info "Branch protection: ${required_reviews} required review(s), ${required_checks} required check(s)"
3035
+
3036
+ if [[ "$required_reviews" -gt 0 ]]; then
3037
+ # Check if PR has enough approvals
3038
+ local prot_pr_number
3039
+ prot_pr_number=$(gh pr list --head "$GIT_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
3040
+ if [[ -n "$prot_pr_number" ]]; then
3041
+ local approvals
3042
+ approvals=$(gh pr view "$prot_pr_number" --json reviews --jq '[.reviews[] | select(.state == "APPROVED")] | length' 2>/dev/null || echo "0")
3043
+ if [[ "$approvals" -lt "$required_reviews" ]]; then
3044
+ warn "PR has $approvals approval(s), needs $required_reviews — skipping auto-merge"
3045
+ info "PR is ready for manual merge after required reviews"
3046
+ emit_event "merge.blocked" "issue=${ISSUE_NUMBER:-0}" "reason=insufficient_reviews" "have=$approvals" "need=$required_reviews"
3047
+ return 0
3048
+ fi
3049
+ fi
3050
+ fi
3051
+ fi
3052
+ fi
3053
+
1907
3054
  local merge_method wait_ci_timeout auto_delete_branch auto_merge auto_approve merge_strategy
1908
3055
  merge_method=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.merge_method) // "squash"' "$PIPELINE_CONFIG" 2>/dev/null) || true
1909
3056
  [[ -z "$merge_method" || "$merge_method" == "null" ]] && merge_method="squash"
1910
- wait_ci_timeout=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.wait_ci_timeout_s) // 600' "$PIPELINE_CONFIG" 2>/dev/null) || true
1911
- [[ -z "$wait_ci_timeout" || "$wait_ci_timeout" == "null" ]] && wait_ci_timeout=600
3057
+ wait_ci_timeout=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.wait_ci_timeout_s) // 0' "$PIPELINE_CONFIG" 2>/dev/null) || true
3058
+ [[ -z "$wait_ci_timeout" || "$wait_ci_timeout" == "null" ]] && wait_ci_timeout=0
3059
+
3060
+ # Adaptive CI timeout: 90th percentile of historical times × 1.5 safety margin
3061
+ if [[ "$wait_ci_timeout" -eq 0 ]] 2>/dev/null; then
3062
+ local repo_hash_ci
3063
+ repo_hash_ci=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
3064
+ local ci_times_file="${HOME}/.shipwright/baselines/${repo_hash_ci}/ci-times.json"
3065
+ if [[ -f "$ci_times_file" ]]; then
3066
+ local p90_time
3067
+ p90_time=$(jq '
3068
+ .times | sort |
3069
+ (length * 0.9 | floor) as $idx |
3070
+ .[$idx] // 600
3071
+ ' "$ci_times_file" 2>/dev/null || echo "0")
3072
+ if [[ -n "$p90_time" ]] && awk -v t="$p90_time" 'BEGIN{exit !(t > 0)}' 2>/dev/null; then
3073
+ # 1.5x safety margin, clamped to [120, 1800]
3074
+ wait_ci_timeout=$(awk -v p90="$p90_time" 'BEGIN{
3075
+ t = p90 * 1.5;
3076
+ if (t < 120) t = 120;
3077
+ if (t > 1800) t = 1800;
3078
+ printf "%d", t
3079
+ }')
3080
+ fi
3081
+ fi
3082
+ # Default fallback if no history
3083
+ [[ "$wait_ci_timeout" -eq 0 ]] && wait_ci_timeout=600
3084
+ fi
1912
3085
  auto_delete_branch=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.auto_delete_branch) // "true"' "$PIPELINE_CONFIG" 2>/dev/null) || true
1913
3086
  [[ -z "$auto_delete_branch" || "$auto_delete_branch" == "null" ]] && auto_delete_branch="true"
1914
3087
  auto_merge=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.auto_merge) // false' "$PIPELINE_CONFIG" 2>/dev/null) || true
@@ -1958,6 +3131,26 @@ stage_merge() {
1958
3131
  elapsed=$((elapsed + check_interval))
1959
3132
  done
1960
3133
 
3134
+ # Record CI wait time for adaptive timeout calculation
3135
+ if [[ "$elapsed" -gt 0 ]]; then
3136
+ local repo_hash_ci_rec
3137
+ repo_hash_ci_rec=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
3138
+ local ci_times_dir="${HOME}/.shipwright/baselines/${repo_hash_ci_rec}"
3139
+ local ci_times_rec_file="${ci_times_dir}/ci-times.json"
3140
+ mkdir -p "$ci_times_dir"
3141
+ local ci_history="[]"
3142
+ if [[ -f "$ci_times_rec_file" ]]; then
3143
+ ci_history=$(jq '.times // []' "$ci_times_rec_file" 2>/dev/null || echo "[]")
3144
+ fi
3145
+ local updated_ci
3146
+ updated_ci=$(echo "$ci_history" | jq --arg t "$elapsed" '. + [($t | tonumber)] | .[-20:]' 2>/dev/null || echo "[$elapsed]")
3147
+ local tmp_ci
3148
+ tmp_ci=$(mktemp "${ci_times_dir}/ci-times.json.XXXXXX")
3149
+ jq -n --argjson times "$updated_ci" --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
3150
+ '{times: $times, updated: $updated}' > "$tmp_ci" 2>/dev/null
3151
+ mv "$tmp_ci" "$ci_times_rec_file" 2>/dev/null || true
3152
+ fi
3153
+
1961
3154
  if [[ "$elapsed" -ge "$wait_ci_timeout" ]]; then
1962
3155
  warn "CI check timeout (${wait_ci_timeout}s) — proceeding with merge anyway"
1963
3156
  fi
@@ -2026,6 +3219,16 @@ stage_deploy() {
2026
3219
  return 0
2027
3220
  fi
2028
3221
 
3222
+ # Create GitHub deployment tracking
3223
+ local gh_deploy_env="production"
3224
+ [[ -n "$staging_cmd" && -z "$prod_cmd" ]] && gh_deploy_env="staging"
3225
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_start &>/dev/null 2>&1; then
3226
+ if [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
3227
+ gh_deploy_pipeline_start "$REPO_OWNER" "$REPO_NAME" "${GIT_BRANCH:-HEAD}" "$gh_deploy_env" 2>/dev/null || true
3228
+ info "GitHub Deployment: tracking as $gh_deploy_env"
3229
+ fi
3230
+ fi
3231
+
2029
3232
  # Post deploy start to GitHub
2030
3233
  if [[ -n "$ISSUE_NUMBER" ]]; then
2031
3234
  gh_comment_issue "$ISSUE_NUMBER" "🚀 **Deploy started**"
@@ -2033,9 +3236,13 @@ stage_deploy() {
2033
3236
 
2034
3237
  if [[ -n "$staging_cmd" ]]; then
2035
3238
  info "Deploying to staging..."
2036
- eval "$staging_cmd" > "$ARTIFACTS_DIR/deploy-staging.log" 2>&1 || {
3239
+ bash -c "$staging_cmd" > "$ARTIFACTS_DIR/deploy-staging.log" 2>&1 || {
2037
3240
  error "Staging deploy failed"
2038
3241
  [[ -n "$ISSUE_NUMBER" ]] && gh_comment_issue "$ISSUE_NUMBER" "❌ Staging deploy failed"
3242
+ # Mark GitHub deployment as failed
3243
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete &>/dev/null 2>&1; then
3244
+ gh_deploy_pipeline_complete "$REPO_OWNER" "$REPO_NAME" "$gh_deploy_env" false "Staging deploy failed" 2>/dev/null || true
3245
+ fi
2039
3246
  return 1
2040
3247
  }
2041
3248
  success "Staging deploy complete"
@@ -2043,13 +3250,17 @@ stage_deploy() {
2043
3250
 
2044
3251
  if [[ -n "$prod_cmd" ]]; then
2045
3252
  info "Deploying to production..."
2046
- eval "$prod_cmd" > "$ARTIFACTS_DIR/deploy-prod.log" 2>&1 || {
3253
+ bash -c "$prod_cmd" > "$ARTIFACTS_DIR/deploy-prod.log" 2>&1 || {
2047
3254
  error "Production deploy failed"
2048
3255
  if [[ -n "$rollback_cmd" ]]; then
2049
3256
  warn "Rolling back..."
2050
- eval "$rollback_cmd" 2>&1 || error "Rollback also failed!"
3257
+ bash -c "$rollback_cmd" 2>&1 || error "Rollback also failed!"
2051
3258
  fi
2052
3259
  [[ -n "$ISSUE_NUMBER" ]] && gh_comment_issue "$ISSUE_NUMBER" "❌ Production deploy failed — rollback ${rollback_cmd:+attempted}"
3260
+ # Mark GitHub deployment as failed
3261
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete &>/dev/null 2>&1; then
3262
+ gh_deploy_pipeline_complete "$REPO_OWNER" "$REPO_NAME" "$gh_deploy_env" false "Production deploy failed" 2>/dev/null || true
3263
+ fi
2053
3264
  return 1
2054
3265
  }
2055
3266
  success "Production deploy complete"
@@ -2060,6 +3271,13 @@ stage_deploy() {
2060
3271
  gh_add_labels "$ISSUE_NUMBER" "deployed"
2061
3272
  fi
2062
3273
 
3274
+ # Mark GitHub deployment as successful
3275
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete &>/dev/null 2>&1; then
3276
+ if [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
3277
+ gh_deploy_pipeline_complete "$REPO_OWNER" "$REPO_NAME" "$gh_deploy_env" true "" 2>/dev/null || true
3278
+ fi
3279
+ fi
3280
+
2063
3281
  log_stage "deploy" "Deploy complete"
2064
3282
  }
2065
3283
 
@@ -2079,7 +3297,7 @@ stage_validate() {
2079
3297
  # Smoke tests
2080
3298
  if [[ -n "$smoke_cmd" ]]; then
2081
3299
  info "Running smoke tests..."
2082
- eval "$smoke_cmd" > "$ARTIFACTS_DIR/smoke.log" 2>&1 || {
3300
+ bash -c "$smoke_cmd" > "$ARTIFACTS_DIR/smoke.log" 2>&1 || {
2083
3301
  error "Smoke tests failed"
2084
3302
  if [[ -n "$ISSUE_NUMBER" ]]; then
2085
3303
  gh issue create --title "Deploy validation failed: $GOAL" \
@@ -2171,6 +3389,24 @@ stage_monitor() {
2171
3389
  [[ "$health_url" == "null" ]] && health_url=""
2172
3390
  error_threshold=$(jq -r --arg id "monitor" '(.stages[] | select(.id == $id) | .config.error_threshold) // 5' "$PIPELINE_CONFIG" 2>/dev/null) || true
2173
3391
  [[ -z "$error_threshold" || "$error_threshold" == "null" ]] && error_threshold=5
3392
+
3393
+ # Adaptive monitor: use historical baselines if available
3394
+ local repo_hash
3395
+ repo_hash=$(echo "${PROJECT_ROOT:-$(pwd)}" | cksum | awk '{print $1}')
3396
+ local baseline_file="${HOME}/.shipwright/baselines/${repo_hash}/deploy-monitor.json"
3397
+ if [[ -f "$baseline_file" ]]; then
3398
+ local hist_duration hist_threshold
3399
+ hist_duration=$(jq -r '.p90_stabilization_minutes // empty' "$baseline_file" 2>/dev/null || true)
3400
+ hist_threshold=$(jq -r '.p90_error_threshold // empty' "$baseline_file" 2>/dev/null || true)
3401
+ if [[ -n "$hist_duration" && "$hist_duration" != "null" ]]; then
3402
+ duration_minutes="$hist_duration"
3403
+ info "Monitor duration: ${duration_minutes}m ${DIM}(from baseline)${RESET}"
3404
+ fi
3405
+ if [[ -n "$hist_threshold" && "$hist_threshold" != "null" ]]; then
3406
+ error_threshold="$hist_threshold"
3407
+ info "Error threshold: ${error_threshold} ${DIM}(from baseline)${RESET}"
3408
+ fi
3409
+ fi
2174
3410
  log_pattern=$(jq -r --arg id "monitor" '(.stages[] | select(.id == $id) | .config.log_pattern) // "ERROR|FATAL|PANIC"' "$PIPELINE_CONFIG" 2>/dev/null) || true
2175
3411
  [[ -z "$log_pattern" || "$log_pattern" == "null" ]] && log_pattern="ERROR|FATAL|PANIC"
2176
3412
  log_cmd=$(jq -r --arg id "monitor" '(.stages[] | select(.id == $id) | .config.log_cmd) // ""' "$PIPELINE_CONFIG" 2>/dev/null) || true
@@ -2237,7 +3473,7 @@ stage_monitor() {
2237
3473
  # Log command check
2238
3474
  if [[ -n "$log_cmd" ]]; then
2239
3475
  local log_output
2240
- log_output=$(eval "$log_cmd" 2>/dev/null || true)
3476
+ log_output=$(bash -c "$log_cmd" 2>/dev/null || true)
2241
3477
  local error_count=0
2242
3478
  if [[ -n "$log_output" ]]; then
2243
3479
  error_count=$(echo "$log_output" | grep -cE "$log_pattern" 2>/dev/null || true)
@@ -2278,7 +3514,7 @@ stage_monitor() {
2278
3514
  echo "" >> "$report_file"
2279
3515
  echo "## Rollback" >> "$report_file"
2280
3516
 
2281
- if eval "$rollback_cmd" >> "$report_file" 2>&1; then
3517
+ if bash -c "$rollback_cmd" >> "$report_file" 2>&1; then
2282
3518
  success "Rollback executed"
2283
3519
  echo "Rollback: ✅ success" >> "$report_file"
2284
3520
 
@@ -2289,7 +3525,7 @@ stage_monitor() {
2289
3525
 
2290
3526
  if [[ -n "$smoke_cmd" ]]; then
2291
3527
  info "Verifying rollback with smoke tests..."
2292
- if eval "$smoke_cmd" > "$ARTIFACTS_DIR/rollback-smoke.log" 2>&1; then
3528
+ if bash -c "$smoke_cmd" > "$ARTIFACTS_DIR/rollback-smoke.log" 2>&1; then
2293
3529
  success "Rollback verified — smoke tests pass"
2294
3530
  echo "Rollback verification: ✅ smoke tests pass" >> "$report_file"
2295
3531
  emit_event "monitor.rollback_verified" \
@@ -2369,6 +3605,30 @@ _Created automatically by \`shipwright pipeline\` monitor stage_" 2>/dev/null ||
2369
3605
  fi
2370
3606
 
2371
3607
  log_stage "monitor" "Clean — ${total_errors} errors in ${duration_minutes}m"
3608
+
3609
+ # Record baseline for adaptive monitoring on future runs
3610
+ local baseline_dir="${HOME}/.shipwright/baselines/${repo_hash}"
3611
+ mkdir -p "$baseline_dir" 2>/dev/null || true
3612
+ local baseline_tmp
3613
+ baseline_tmp="$(mktemp)"
3614
+ if [[ -f "${baseline_dir}/deploy-monitor.json" ]]; then
3615
+ # Append to history and recalculate p90
3616
+ jq --arg dur "$duration_minutes" --arg errs "$total_errors" \
3617
+ '.history += [{"duration_minutes": ($dur | tonumber), "errors": ($errs | tonumber)}] |
3618
+ .p90_stabilization_minutes = ([.history[].duration_minutes] | sort | .[length * 9 / 10 | floor]) |
3619
+ .p90_error_threshold = (([.history[].errors] | sort | .[length * 9 / 10 | floor]) + 2) |
3620
+ .updated_at = now' \
3621
+ "${baseline_dir}/deploy-monitor.json" > "$baseline_tmp" 2>/dev/null && \
3622
+ mv "$baseline_tmp" "${baseline_dir}/deploy-monitor.json" || rm -f "$baseline_tmp"
3623
+ else
3624
+ jq -n --arg dur "$duration_minutes" --arg errs "$total_errors" \
3625
+ '{history: [{"duration_minutes": ($dur | tonumber), "errors": ($errs | tonumber)}],
3626
+ p90_stabilization_minutes: ($dur | tonumber),
3627
+ p90_error_threshold: (($errs | tonumber) + 2),
3628
+ updated_at: now}' \
3629
+ > "$baseline_tmp" 2>/dev/null && \
3630
+ mv "$baseline_tmp" "${baseline_dir}/deploy-monitor.json" || rm -f "$baseline_tmp"
3631
+ fi
2372
3632
  }
2373
3633
 
2374
3634
  # ─── Multi-Dimensional Quality Checks ─────────────────────────────────────
@@ -2427,13 +3687,33 @@ quality_check_bundle_size() {
2427
3687
  local bundle_size=0
2428
3688
  local bundle_dir=""
2429
3689
 
2430
- # Find build output directory
2431
- for dir in dist build out .next; do
2432
- if [[ -d "$dir" ]]; then
2433
- bundle_dir="$dir"
2434
- break
3690
+ # Find build output directory — check config files first, then common dirs
3691
+ # Parse tsconfig.json outDir
3692
+ if [[ -z "$bundle_dir" && -f "tsconfig.json" ]]; then
3693
+ local ts_out
3694
+ ts_out=$(jq -r '.compilerOptions.outDir // empty' tsconfig.json 2>/dev/null || true)
3695
+ [[ -n "$ts_out" && -d "$ts_out" ]] && bundle_dir="$ts_out"
3696
+ fi
3697
+ # Parse package.json build script for output hints
3698
+ if [[ -z "$bundle_dir" && -f "package.json" ]]; then
3699
+ local build_script
3700
+ build_script=$(jq -r '.scripts.build // ""' package.json 2>/dev/null || true)
3701
+ if [[ -n "$build_script" ]]; then
3702
+ # Check for common output flags: --outDir, -o, --out-dir
3703
+ local parsed_out
3704
+ parsed_out=$(echo "$build_script" | grep -oE '(--outDir|--out-dir|-o)\s+[^ ]+' 2>/dev/null | awk '{print $NF}' | head -1 || true)
3705
+ [[ -n "$parsed_out" && -d "$parsed_out" ]] && bundle_dir="$parsed_out"
2435
3706
  fi
2436
- done
3707
+ fi
3708
+ # Fallback: check common directories
3709
+ if [[ -z "$bundle_dir" ]]; then
3710
+ for dir in dist build out .next target; do
3711
+ if [[ -d "$dir" ]]; then
3712
+ bundle_dir="$dir"
3713
+ break
3714
+ fi
3715
+ done
3716
+ fi
2437
3717
 
2438
3718
  if [[ -z "$bundle_dir" ]]; then
2439
3719
  info "No build output directory found — skipping bundle check"
@@ -2453,23 +3733,106 @@ quality_check_bundle_size() {
2453
3733
  "size_kb=$bundle_size" \
2454
3734
  "directory=$bundle_dir"
2455
3735
 
2456
- # Check against memory baseline if available
2457
- local baseline_size=""
2458
- if [[ -x "$SCRIPT_DIR/cct-memory.sh" ]]; then
2459
- baseline_size=$(bash "$SCRIPT_DIR/cct-memory.sh" get "bundle_size_kb" 2>/dev/null) || true
3736
+ # Adaptive bundle size check: statistical deviation from historical mean
3737
+ local repo_hash_bundle
3738
+ repo_hash_bundle=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
3739
+ local bundle_baselines_dir="${HOME}/.shipwright/baselines/${repo_hash_bundle}"
3740
+ local bundle_history_file="${bundle_baselines_dir}/bundle-history.json"
3741
+
3742
+ local bundle_history="[]"
3743
+ if [[ -f "$bundle_history_file" ]]; then
3744
+ bundle_history=$(jq '.sizes // []' "$bundle_history_file" 2>/dev/null || echo "[]")
3745
+ fi
3746
+
3747
+ local bundle_hist_count
3748
+ bundle_hist_count=$(echo "$bundle_history" | jq 'length' 2>/dev/null || echo "0")
3749
+
3750
+ if [[ "$bundle_hist_count" -ge 3 ]]; then
3751
+ # Statistical check: alert on growth > 2σ from historical mean
3752
+ local mean_size stddev_size
3753
+ mean_size=$(echo "$bundle_history" | jq 'add / length' 2>/dev/null || echo "0")
3754
+ stddev_size=$(echo "$bundle_history" | jq '
3755
+ (add / length) as $mean |
3756
+ (map(. - $mean | . * .) | add / length | sqrt)
3757
+ ' 2>/dev/null || echo "0")
3758
+
3759
+ # Adaptive tolerance: small repos (<1MB mean) get wider tolerance (3σ), large repos get 2σ
3760
+ local sigma_mult
3761
+ sigma_mult=$(awk -v mean="$mean_size" 'BEGIN{ print (mean < 1024 ? 3 : 2) }')
3762
+ local adaptive_max
3763
+ adaptive_max=$(awk -v mean="$mean_size" -v sd="$stddev_size" -v mult="$sigma_mult" \
3764
+ 'BEGIN{ t = mean + mult*sd; min_t = mean * 1.1; printf "%.0f", (t > min_t ? t : min_t) }')
3765
+
3766
+ echo "History: ${bundle_hist_count} runs | Mean: ${mean_size}KB | StdDev: ${stddev_size}KB | Max: ${adaptive_max}KB (${sigma_mult}σ)" >> "$metrics_log"
3767
+
3768
+ if [[ "$bundle_size" -gt "$adaptive_max" ]] 2>/dev/null; then
3769
+ local growth_pct
3770
+ growth_pct=$(awk -v cur="$bundle_size" -v mean="$mean_size" 'BEGIN{printf "%d", ((cur - mean) / mean) * 100}')
3771
+ warn "Bundle size ${growth_pct}% above average (${mean_size}KB → ${bundle_size}KB, ${sigma_mult}σ threshold: ${adaptive_max}KB)"
3772
+ return 1
3773
+ fi
3774
+ else
3775
+ # Fallback: legacy memory baseline with hardcoded 20% (not enough history)
3776
+ local baseline_size=""
3777
+ if [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
3778
+ baseline_size=$(bash "$SCRIPT_DIR/sw-memory.sh" get "bundle_size_kb" 2>/dev/null) || true
3779
+ fi
3780
+ if [[ -n "$baseline_size" && "$baseline_size" -gt 0 ]] 2>/dev/null; then
3781
+ local growth_pct
3782
+ growth_pct=$(awk -v cur="$bundle_size" -v base="$baseline_size" 'BEGIN{printf "%d", ((cur - base) / base) * 100}')
3783
+ echo "Baseline: ${baseline_size}KB | Growth: ${growth_pct}%" >> "$metrics_log"
3784
+ if [[ "$growth_pct" -gt 20 ]]; then
3785
+ warn "Bundle size grew ${growth_pct}% (${baseline_size}KB → ${bundle_size}KB)"
3786
+ return 1
3787
+ fi
3788
+ fi
2460
3789
  fi
2461
3790
 
2462
- if [[ -n "$baseline_size" && "$baseline_size" -gt 0 ]] 2>/dev/null; then
2463
- local growth_pct
2464
- growth_pct=$(awk -v cur="$bundle_size" -v base="$baseline_size" 'BEGIN{printf "%d", ((cur - base) / base) * 100}')
2465
- echo "Baseline: ${baseline_size}KB | Growth: ${growth_pct}%" >> "$metrics_log"
2466
- if [[ "$growth_pct" -gt 20 ]]; then
2467
- warn "Bundle size grew ${growth_pct}% (${baseline_size}KB → ${bundle_size}KB)"
2468
- return 1
3791
+ # Append current size to rolling history (keep last 10)
3792
+ mkdir -p "$bundle_baselines_dir"
3793
+ local updated_bundle_hist
3794
+ updated_bundle_hist=$(echo "$bundle_history" | jq --arg sz "$bundle_size" '
3795
+ . + [($sz | tonumber)] | .[-10:]
3796
+ ' 2>/dev/null || echo "[$bundle_size]")
3797
+ local tmp_bundle_hist
3798
+ tmp_bundle_hist=$(mktemp "${bundle_baselines_dir}/bundle-history.json.XXXXXX")
3799
+ jq -n --argjson sizes "$updated_bundle_hist" --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
3800
+ '{sizes: $sizes, updated: $updated}' > "$tmp_bundle_hist" 2>/dev/null
3801
+ mv "$tmp_bundle_hist" "$bundle_history_file" 2>/dev/null || true
3802
+
3803
+ # Intelligence: identify top dependency bloaters
3804
+ if type intelligence_search_memory &>/dev/null 2>&1 && [[ -f "package.json" ]] && command -v jq &>/dev/null; then
3805
+ local dep_sizes=""
3806
+ local deps
3807
+ deps=$(jq -r '.dependencies // {} | keys[]' package.json 2>/dev/null || true)
3808
+ if [[ -n "$deps" ]]; then
3809
+ while IFS= read -r dep; do
3810
+ [[ -z "$dep" ]] && continue
3811
+ local dep_dir="node_modules/${dep}"
3812
+ if [[ -d "$dep_dir" ]]; then
3813
+ local dep_size
3814
+ dep_size=$(du -sk "$dep_dir" 2>/dev/null | cut -f1 || echo "0")
3815
+ dep_sizes="${dep_sizes}${dep_size} ${dep}
3816
+ "
3817
+ fi
3818
+ done <<< "$deps"
3819
+ if [[ -n "$dep_sizes" ]]; then
3820
+ local top_bloaters
3821
+ top_bloaters=$(echo "$dep_sizes" | sort -rn | head -3)
3822
+ if [[ -n "$top_bloaters" ]]; then
3823
+ echo "" >> "$metrics_log"
3824
+ echo "Top 3 dependency sizes:" >> "$metrics_log"
3825
+ echo "$top_bloaters" | while IFS=' ' read -r sz nm; do
3826
+ [[ -z "$nm" ]] && continue
3827
+ echo " ${nm}: ${sz}KB" >> "$metrics_log"
3828
+ done
3829
+ info "Top bloaters: $(echo "$top_bloaters" | head -1 | awk '{print $2 ": " $1 "KB"}')"
3830
+ fi
3831
+ fi
2469
3832
  fi
2470
3833
  fi
2471
3834
 
2472
- info "Bundle size: ${bundle_size_human}"
3835
+ info "Bundle size: ${bundle_size_human}${bundle_hist_count:+ (${bundle_hist_count} historical samples)}"
2473
3836
  return 0
2474
3837
  }
2475
3838
 
@@ -2484,11 +3847,39 @@ quality_check_perf_regression() {
2484
3847
  return 0
2485
3848
  fi
2486
3849
 
2487
- # Extract test suite duration (common patterns)
3850
+ # Extract test suite duration multi-framework patterns
2488
3851
  local duration_ms=""
3852
+ # Jest/Vitest: "Time: 12.34 s" or "Duration 12.34s"
2489
3853
  duration_ms=$(grep -oE 'Time:\s*[0-9.]+\s*s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
3854
+ [[ -z "$duration_ms" ]] && duration_ms=$(grep -oE 'Duration\s+[0-9.]+\s*s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
3855
+ # pytest: "passed in 12.34s" or "====== 5 passed in 12.34 seconds ======"
3856
+ [[ -z "$duration_ms" ]] && duration_ms=$(grep -oE 'passed in [0-9.]+s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
3857
+ # Go test: "ok pkg 12.345s"
3858
+ [[ -z "$duration_ms" ]] && duration_ms=$(grep -oE '^ok\s+\S+\s+[0-9.]+s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+s' | grep -oE '[0-9.]+' | tail -1 || true)
3859
+ # Cargo test: "test result: ok. ... finished in 12.34s"
3860
+ [[ -z "$duration_ms" ]] && duration_ms=$(grep -oE 'finished in [0-9.]+s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
3861
+ # Generic: "12.34 seconds" or "12.34s"
2490
3862
  [[ -z "$duration_ms" ]] && duration_ms=$(grep -oE '[0-9.]+ ?s(econds?)?' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
2491
3863
 
3864
+ # Claude fallback: parse test output when no pattern matches
3865
+ if [[ -z "$duration_ms" ]]; then
3866
+ local intel_enabled="false"
3867
+ local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
3868
+ if [[ -f "$daemon_cfg" ]]; then
3869
+ intel_enabled=$(jq -r '.intelligence.enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
3870
+ fi
3871
+ if [[ "$intel_enabled" == "true" ]] && command -v claude &>/dev/null; then
3872
+ local tail_output
3873
+ tail_output=$(tail -30 "$test_log" 2>/dev/null || true)
3874
+ if [[ -n "$tail_output" ]]; then
3875
+ duration_ms=$(claude --print -p "Extract ONLY the total test suite duration in seconds from this output. Reply with ONLY a number (e.g. 12.34). If no duration found, reply NONE.
3876
+
3877
+ $tail_output" < /dev/null 2>/dev/null | grep -oE '^[0-9.]+$' | head -1 || true)
3878
+ [[ "$duration_ms" == "NONE" ]] && duration_ms=""
3879
+ fi
3880
+ fi
3881
+ fi
3882
+
2492
3883
  if [[ -z "$duration_ms" ]]; then
2493
3884
  info "Could not extract test duration — skipping perf check"
2494
3885
  echo "Duration not parseable" > "$metrics_log"
@@ -2501,23 +3892,73 @@ quality_check_perf_regression() {
2501
3892
  "issue=${ISSUE_NUMBER:-0}" \
2502
3893
  "duration_s=$duration_ms"
2503
3894
 
2504
- # Check against memory baseline if available
2505
- local baseline_dur=""
2506
- if [[ -x "$SCRIPT_DIR/cct-memory.sh" ]]; then
2507
- baseline_dur=$(bash "$SCRIPT_DIR/cct-memory.sh" get "test_duration_s" 2>/dev/null) || true
2508
- fi
2509
-
2510
- if [[ -n "$baseline_dur" ]] && awk -v cur="$duration_ms" -v base="$baseline_dur" 'BEGIN{exit !(base > 0)}' 2>/dev/null; then
2511
- local slowdown_pct
2512
- slowdown_pct=$(awk -v cur="$duration_ms" -v base="$baseline_dur" 'BEGIN{printf "%d", ((cur - base) / base) * 100}')
2513
- echo "Baseline: ${baseline_dur}s | Slowdown: ${slowdown_pct}%" >> "$metrics_log"
2514
- if [[ "$slowdown_pct" -gt 30 ]]; then
2515
- warn "Tests ${slowdown_pct}% slower (${baseline_dur}s → ${duration_ms}s)"
3895
+ # Adaptive performance check: from rolling 10-run average
3896
+ local repo_hash_perf
3897
+ repo_hash_perf=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
3898
+ local perf_baselines_dir="${HOME}/.shipwright/baselines/${repo_hash_perf}"
3899
+ local perf_history_file="${perf_baselines_dir}/perf-history.json"
3900
+
3901
+ # Read historical durations (rolling window of last 10 runs)
3902
+ local history_json="[]"
3903
+ if [[ -f "$perf_history_file" ]]; then
3904
+ history_json=$(jq '.durations // []' "$perf_history_file" 2>/dev/null || echo "[]")
3905
+ fi
3906
+
3907
+ local history_count
3908
+ history_count=$(echo "$history_json" | jq 'length' 2>/dev/null || echo "0")
3909
+
3910
+ if [[ "$history_count" -ge 3 ]]; then
3911
+ # Calculate mean and standard deviation from history
3912
+ local mean_dur stddev_dur
3913
+ mean_dur=$(echo "$history_json" | jq 'add / length' 2>/dev/null || echo "0")
3914
+ stddev_dur=$(echo "$history_json" | jq '
3915
+ (add / length) as $mean |
3916
+ (map(. - $mean | . * .) | add / length | sqrt)
3917
+ ' 2>/dev/null || echo "0")
3918
+
3919
+ # Threshold: mean + 2σ (but at least 10% above mean)
3920
+ local adaptive_threshold
3921
+ adaptive_threshold=$(awk -v mean="$mean_dur" -v sd="$stddev_dur" \
3922
+ 'BEGIN{ t = mean + 2*sd; min_t = mean * 1.1; printf "%.2f", (t > min_t ? t : min_t) }')
3923
+
3924
+ echo "History: ${history_count} runs | Mean: ${mean_dur}s | StdDev: ${stddev_dur}s | Threshold: ${adaptive_threshold}s" >> "$metrics_log"
3925
+
3926
+ if awk -v cur="$duration_ms" -v thresh="$adaptive_threshold" 'BEGIN{exit !(cur > thresh)}' 2>/dev/null; then
3927
+ local slowdown_pct
3928
+ slowdown_pct=$(awk -v cur="$duration_ms" -v mean="$mean_dur" 'BEGIN{printf "%d", ((cur - mean) / mean) * 100}')
3929
+ warn "Tests ${slowdown_pct}% slower than rolling average (${mean_dur}s → ${duration_ms}s, threshold: ${adaptive_threshold}s)"
2516
3930
  return 1
2517
3931
  fi
3932
+ else
3933
+ # Fallback: legacy memory baseline with hardcoded 30% (not enough history)
3934
+ local baseline_dur=""
3935
+ if [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
3936
+ baseline_dur=$(bash "$SCRIPT_DIR/sw-memory.sh" get "test_duration_s" 2>/dev/null) || true
3937
+ fi
3938
+ if [[ -n "$baseline_dur" ]] && awk -v cur="$duration_ms" -v base="$baseline_dur" 'BEGIN{exit !(base > 0)}' 2>/dev/null; then
3939
+ local slowdown_pct
3940
+ slowdown_pct=$(awk -v cur="$duration_ms" -v base="$baseline_dur" 'BEGIN{printf "%d", ((cur - base) / base) * 100}')
3941
+ echo "Baseline: ${baseline_dur}s | Slowdown: ${slowdown_pct}%" >> "$metrics_log"
3942
+ if [[ "$slowdown_pct" -gt 30 ]]; then
3943
+ warn "Tests ${slowdown_pct}% slower (${baseline_dur}s → ${duration_ms}s)"
3944
+ return 1
3945
+ fi
3946
+ fi
2518
3947
  fi
2519
3948
 
2520
- info "Test duration: ${duration_ms}s"
3949
+ # Append current duration to rolling history (keep last 10)
3950
+ mkdir -p "$perf_baselines_dir"
3951
+ local updated_history
3952
+ updated_history=$(echo "$history_json" | jq --arg dur "$duration_ms" '
3953
+ . + [($dur | tonumber)] | .[-10:]
3954
+ ' 2>/dev/null || echo "[$duration_ms]")
3955
+ local tmp_perf_hist
3956
+ tmp_perf_hist=$(mktemp "${perf_baselines_dir}/perf-history.json.XXXXXX")
3957
+ jq -n --argjson durations "$updated_history" --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
3958
+ '{durations: $durations, updated: $updated}' > "$tmp_perf_hist" 2>/dev/null
3959
+ mv "$tmp_perf_hist" "$perf_history_file" 2>/dev/null || true
3960
+
3961
+ info "Test duration: ${duration_ms}s${history_count:+ (${history_count} historical samples)}"
2521
3962
  return 0
2522
3963
  }
2523
3964
 
@@ -2525,7 +3966,7 @@ quality_check_api_compat() {
2525
3966
  info "API compatibility check..."
2526
3967
  local compat_log="$ARTIFACTS_DIR/api-compat.log"
2527
3968
 
2528
- # Look for OpenAPI/Swagger specs
3969
+ # Look for OpenAPI/Swagger specs — search beyond hardcoded paths
2529
3970
  local spec_file=""
2530
3971
  for candidate in openapi.json openapi.yaml swagger.json swagger.yaml api/openapi.json docs/openapi.yaml; do
2531
3972
  if [[ -f "$candidate" ]]; then
@@ -2533,6 +3974,10 @@ quality_check_api_compat() {
2533
3974
  break
2534
3975
  fi
2535
3976
  done
3977
+ # Broader search if nothing found at common paths
3978
+ if [[ -z "$spec_file" ]]; then
3979
+ spec_file=$(find . -maxdepth 4 \( -name "openapi*.json" -o -name "openapi*.yaml" -o -name "openapi*.yml" -o -name "swagger*.json" -o -name "swagger*.yaml" -o -name "swagger*.yml" \) -type f 2>/dev/null | head -1 || true)
3980
+ fi
2536
3981
 
2537
3982
  if [[ -z "$spec_file" ]]; then
2538
3983
  info "No OpenAPI/Swagger spec found — skipping API compat check"
@@ -2571,21 +4016,64 @@ quality_check_api_compat() {
2571
4016
  removed_endpoints=$(comm -23 <(echo "$old_paths") <(echo "$new_paths") 2>/dev/null || true)
2572
4017
  fi
2573
4018
 
4019
+ # Enhanced schema diff: parameter changes, response schema, auth changes
4020
+ local param_changes="" schema_changes=""
4021
+ if command -v jq &>/dev/null && [[ "$spec_file" == *.json ]]; then
4022
+ # Detect parameter changes on existing endpoints
4023
+ local common_paths
4024
+ common_paths=$(comm -12 <(echo "$old_spec" | jq -r '.paths | keys[]' 2>/dev/null | sort) <(jq -r '.paths | keys[]' "$spec_file" 2>/dev/null | sort) 2>/dev/null || true)
4025
+ if [[ -n "$common_paths" ]]; then
4026
+ while IFS= read -r path; do
4027
+ [[ -z "$path" ]] && continue
4028
+ local old_params new_params
4029
+ old_params=$(echo "$old_spec" | jq -r --arg p "$path" '.paths[$p] | to_entries[] | .value.parameters // [] | .[].name' 2>/dev/null | sort || true)
4030
+ new_params=$(jq -r --arg p "$path" '.paths[$p] | to_entries[] | .value.parameters // [] | .[].name' "$spec_file" 2>/dev/null | sort || true)
4031
+ local removed_params
4032
+ removed_params=$(comm -23 <(echo "$old_params") <(echo "$new_params") 2>/dev/null || true)
4033
+ [[ -n "$removed_params" ]] && param_changes="${param_changes}${path}: removed params: ${removed_params}
4034
+ "
4035
+ done <<< "$common_paths"
4036
+ fi
4037
+ fi
4038
+
4039
+ # Intelligence: semantic API diff for complex changes
4040
+ local semantic_diff=""
4041
+ if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
4042
+ local spec_git_diff
4043
+ spec_git_diff=$(git diff "${BASE_BRANCH}...HEAD" -- "$spec_file" 2>/dev/null | head -200 || true)
4044
+ if [[ -n "$spec_git_diff" ]]; then
4045
+ semantic_diff=$(claude --print --output-format text -p "Analyze this API spec diff for breaking changes. List: removed endpoints, changed parameters, altered response schemas, auth changes. Be concise.
4046
+
4047
+ ${spec_git_diff}" --model haiku < /dev/null 2>/dev/null || true)
4048
+ fi
4049
+ fi
4050
+
2574
4051
  {
2575
4052
  echo "Spec: $spec_file"
2576
4053
  echo "Changed: yes"
2577
4054
  if [[ -n "$removed_endpoints" ]]; then
2578
4055
  echo "BREAKING — Removed endpoints:"
2579
4056
  echo "$removed_endpoints"
2580
- else
4057
+ fi
4058
+ if [[ -n "$param_changes" ]]; then
4059
+ echo "BREAKING — Parameter changes:"
4060
+ echo "$param_changes"
4061
+ fi
4062
+ if [[ -n "$semantic_diff" ]]; then
4063
+ echo ""
4064
+ echo "Semantic analysis:"
4065
+ echo "$semantic_diff"
4066
+ fi
4067
+ if [[ -z "$removed_endpoints" && -z "$param_changes" ]]; then
2581
4068
  echo "No breaking changes detected"
2582
4069
  fi
2583
4070
  } > "$compat_log"
2584
4071
 
2585
- if [[ -n "$removed_endpoints" ]]; then
2586
- local removed_count
2587
- removed_count=$(echo "$removed_endpoints" | wc -l | xargs)
2588
- warn "API breaking changes: ${removed_count} endpoint(s) removed"
4072
+ if [[ -n "$removed_endpoints" || -n "$param_changes" ]]; then
4073
+ local issue_count=0
4074
+ [[ -n "$removed_endpoints" ]] && issue_count=$((issue_count + $(echo "$removed_endpoints" | wc -l | xargs)))
4075
+ [[ -n "$param_changes" ]] && issue_count=$((issue_count + $(echo "$param_changes" | grep -c '.' || true)))
4076
+ warn "API breaking changes: ${issue_count} issue(s) found"
2589
4077
  return 1
2590
4078
  fi
2591
4079
 
@@ -2602,11 +4090,28 @@ quality_check_coverage() {
2602
4090
  return 0
2603
4091
  fi
2604
4092
 
2605
- # Extract coverage percentage
4093
+ # Extract coverage percentage using shared parser
2606
4094
  local coverage=""
2607
- coverage=$(grep -oE 'Statements\s*:\s*[0-9.]+' "$test_log" 2>/dev/null | grep -oE '[0-9.]+$' || \
2608
- grep -oE 'All files\s*\|\s*[0-9.]+' "$test_log" 2>/dev/null | grep -oE '[0-9.]+$' || \
2609
- grep -oE 'TOTAL\s+[0-9]+\s+[0-9]+\s+([0-9]+)%' "$test_log" 2>/dev/null | grep -oE '[0-9]+%' | tr -d '%' || echo "")
4095
+ coverage=$(parse_coverage_from_output "$test_log")
4096
+
4097
+ # Claude fallback: parse test output when no pattern matches
4098
+ if [[ -z "$coverage" ]]; then
4099
+ local intel_enabled_cov="false"
4100
+ local daemon_cfg_cov="${PROJECT_ROOT}/.claude/daemon-config.json"
4101
+ if [[ -f "$daemon_cfg_cov" ]]; then
4102
+ intel_enabled_cov=$(jq -r '.intelligence.enabled // false' "$daemon_cfg_cov" 2>/dev/null || echo "false")
4103
+ fi
4104
+ if [[ "$intel_enabled_cov" == "true" ]] && command -v claude &>/dev/null; then
4105
+ local tail_cov_output
4106
+ tail_cov_output=$(tail -40 "$test_log" 2>/dev/null || true)
4107
+ if [[ -n "$tail_cov_output" ]]; then
4108
+ coverage=$(claude --print -p "Extract ONLY the overall code coverage percentage from this test output. Reply with ONLY a number (e.g. 85.5). If no coverage found, reply NONE.
4109
+
4110
+ $tail_cov_output" < /dev/null 2>/dev/null | grep -oE '^[0-9.]+$' | head -1 || true)
4111
+ [[ "$coverage" == "NONE" ]] && coverage=""
4112
+ fi
4113
+ fi
4114
+ fi
2610
4115
 
2611
4116
  if [[ -z "$coverage" ]]; then
2612
4117
  info "Could not extract coverage — skipping"
@@ -2622,16 +4127,30 @@ quality_check_coverage() {
2622
4127
  coverage_min=$(jq -r --arg id "test" '(.stages[] | select(.id == $id) | .config.coverage_min) // 0' "$PIPELINE_CONFIG" 2>/dev/null) || true
2623
4128
  [[ -z "$coverage_min" || "$coverage_min" == "null" ]] && coverage_min=0
2624
4129
 
2625
- # Check against memory baseline (detect coverage drops)
4130
+ # Adaptive baseline: read from baselines file, enforce no-regression (>= baseline - 2%)
4131
+ local repo_hash_cov
4132
+ repo_hash_cov=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
4133
+ local baselines_dir="${HOME}/.shipwright/baselines/${repo_hash_cov}"
4134
+ local coverage_baseline_file="${baselines_dir}/coverage.json"
4135
+
2626
4136
  local baseline_coverage=""
2627
- if [[ -x "$SCRIPT_DIR/cct-memory.sh" ]]; then
2628
- baseline_coverage=$(bash "$SCRIPT_DIR/cct-memory.sh" get "coverage_pct" 2>/dev/null) || true
4137
+ if [[ -f "$coverage_baseline_file" ]]; then
4138
+ baseline_coverage=$(jq -r '.baseline // empty' "$coverage_baseline_file" 2>/dev/null) || true
4139
+ fi
4140
+ # Fallback: try legacy memory baseline
4141
+ if [[ -z "$baseline_coverage" ]] && [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
4142
+ baseline_coverage=$(bash "$SCRIPT_DIR/sw-memory.sh" get "coverage_pct" 2>/dev/null) || true
2629
4143
  fi
2630
4144
 
2631
4145
  local dropped=false
2632
- if [[ -n "$baseline_coverage" ]] && awk -v cur="$coverage" -v base="$baseline_coverage" 'BEGIN{exit !(cur < base)}' 2>/dev/null; then
2633
- warn "Coverage dropped: ${baseline_coverage}% ${coverage}%"
2634
- dropped=true
4146
+ if [[ -n "$baseline_coverage" && "$baseline_coverage" != "0" ]] && awk -v cur="$coverage" -v base="$baseline_coverage" 'BEGIN{exit !(base > 0)}' 2>/dev/null; then
4147
+ # Adaptive: allow 2% regression tolerance from baseline
4148
+ local min_allowed
4149
+ min_allowed=$(awk -v base="$baseline_coverage" 'BEGIN{printf "%d", base - 2}')
4150
+ if awk -v cur="$coverage" -v min="$min_allowed" 'BEGIN{exit !(cur < min)}' 2>/dev/null; then
4151
+ warn "Coverage regression: ${baseline_coverage}% → ${coverage}% (adaptive min: ${min_allowed}%)"
4152
+ dropped=true
4153
+ fi
2635
4154
  fi
2636
4155
 
2637
4156
  if [[ "$coverage_min" -gt 0 ]] 2>/dev/null && awk -v cov="$coverage" -v min="$coverage_min" 'BEGIN{exit !(cov < min)}' 2>/dev/null; then
@@ -2643,7 +4162,17 @@ quality_check_coverage() {
2643
4162
  return 1
2644
4163
  fi
2645
4164
 
2646
- info "Coverage: ${coverage}%"
4165
+ # Update baseline on success (first run or improvement)
4166
+ if [[ -z "$baseline_coverage" ]] || awk -v cur="$coverage" -v base="$baseline_coverage" 'BEGIN{exit !(cur >= base)}' 2>/dev/null; then
4167
+ mkdir -p "$baselines_dir"
4168
+ local tmp_cov_baseline
4169
+ tmp_cov_baseline=$(mktemp "${baselines_dir}/coverage.json.XXXXXX")
4170
+ jq -n --arg baseline "$coverage" --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
4171
+ '{baseline: ($baseline | tonumber), updated: $updated}' > "$tmp_cov_baseline" 2>/dev/null
4172
+ mv "$tmp_cov_baseline" "$coverage_baseline_file" 2>/dev/null || true
4173
+ fi
4174
+
4175
+ info "Coverage: ${coverage}%${baseline_coverage:+ (baseline: ${baseline_coverage}%)}"
2647
4176
  return 0
2648
4177
  }
2649
4178
 
@@ -2660,6 +4189,57 @@ run_adversarial_review() {
2660
4189
  return 0
2661
4190
  fi
2662
4191
 
4192
+ # Delegate to sw-adversarial.sh module when available (uses intelligence cache)
4193
+ if type adversarial_review &>/dev/null 2>&1; then
4194
+ info "Using intelligence-backed adversarial review..."
4195
+ local json_result
4196
+ json_result=$(adversarial_review "$diff_content" "${GOAL:-}" 2>/dev/null || echo "[]")
4197
+
4198
+ # Save raw JSON result
4199
+ echo "$json_result" > "$ARTIFACTS_DIR/adversarial-review.json"
4200
+
4201
+ # Convert JSON findings to markdown for compatibility with compound_rebuild_with_feedback
4202
+ local critical_count high_count
4203
+ critical_count=$(echo "$json_result" | jq '[.[] | select(.severity == "critical")] | length' 2>/dev/null || echo "0")
4204
+ high_count=$(echo "$json_result" | jq '[.[] | select(.severity == "high")] | length' 2>/dev/null || echo "0")
4205
+ local total_findings
4206
+ total_findings=$(echo "$json_result" | jq 'length' 2>/dev/null || echo "0")
4207
+
4208
+ # Generate markdown report from JSON
4209
+ {
4210
+ echo "# Adversarial Review (Intelligence-backed)"
4211
+ echo ""
4212
+ echo "Total findings: ${total_findings} (${critical_count} critical, ${high_count} high)"
4213
+ echo ""
4214
+ echo "$json_result" | jq -r '.[] | "- **[\(.severity // "unknown")]** \(.location // "unknown") — \(.description // .concern // "no description")"' 2>/dev/null || true
4215
+ } > "$ARTIFACTS_DIR/adversarial-review.md"
4216
+
4217
+ emit_event "adversarial.delegated" \
4218
+ "issue=${ISSUE_NUMBER:-0}" \
4219
+ "findings=$total_findings" \
4220
+ "critical=$critical_count" \
4221
+ "high=$high_count"
4222
+
4223
+ if [[ "$critical_count" -gt 0 ]]; then
4224
+ warn "Adversarial review: ${critical_count} critical, ${high_count} high"
4225
+ return 1
4226
+ elif [[ "$high_count" -gt 0 ]]; then
4227
+ warn "Adversarial review: ${high_count} high-severity issues"
4228
+ return 1
4229
+ fi
4230
+
4231
+ success "Adversarial review: clean"
4232
+ return 0
4233
+ fi
4234
+
4235
+ # Fallback: inline Claude call when module not loaded
4236
+
4237
+ # Inject previous adversarial findings from memory
4238
+ local adv_memory=""
4239
+ if type intelligence_search_memory &>/dev/null 2>&1; then
4240
+ adv_memory=$(intelligence_search_memory "adversarial review security findings for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
4241
+ fi
4242
+
2663
4243
  local prompt="You are a hostile code reviewer. Your job is to find EVERY possible issue in this diff.
2664
4244
  Look for:
2665
4245
  - Bugs (logic errors, off-by-one, null/undefined access, race conditions)
@@ -2672,12 +4252,16 @@ Look for:
2672
4252
 
2673
4253
  Be thorough and adversarial. List every issue with severity [Critical/Bug/Warning].
2674
4254
  Format: **[Severity]** file:line — description
2675
-
4255
+ ${adv_memory:+
4256
+ ## Known Security Issues from Previous Reviews
4257
+ These security issues have been found in past reviews. Check if any recur:
4258
+ ${adv_memory}
4259
+ }
2676
4260
  Diff:
2677
4261
  $diff_content"
2678
4262
 
2679
4263
  local review_output
2680
- review_output=$(claude --print "$prompt" 2>"${ARTIFACTS_DIR}/.claude-tokens-adversarial.log" || true)
4264
+ review_output=$(claude --print "$prompt" < /dev/null 2>"${ARTIFACTS_DIR}/.claude-tokens-adversarial.log" || true)
2681
4265
  parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-adversarial.log"
2682
4266
 
2683
4267
  echo "$review_output" > "$ARTIFACTS_DIR/adversarial-review.md"
@@ -2721,6 +4305,12 @@ $(head -200 "$file" 2>/dev/null || true)
2721
4305
  fi
2722
4306
  done <<< "$changed_files"
2723
4307
 
4308
+ # Inject previous negative prompting findings from memory
4309
+ local neg_memory=""
4310
+ if type intelligence_search_memory &>/dev/null 2>&1; then
4311
+ neg_memory=$(intelligence_search_memory "negative prompting findings common concerns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
4312
+ fi
4313
+
2724
4314
  local prompt="You are a pessimistic engineer who assumes everything will break.
2725
4315
  Review these changes and answer:
2726
4316
  1. What could go wrong in production?
@@ -2730,7 +4320,11 @@ Review these changes and answer:
2730
4320
  5. What happens under load/stress?
2731
4321
  6. What happens with malicious input?
2732
4322
  7. Are there any implicit dependencies that could break?
2733
-
4323
+ ${neg_memory:+
4324
+ ## Known Concerns from Previous Reviews
4325
+ These issues have been found in past reviews of this codebase. Check if any apply to the current changes:
4326
+ ${neg_memory}
4327
+ }
2734
4328
  Be specific. Reference actual code. Categorize each concern as [Critical/Concern/Minor].
2735
4329
 
2736
4330
  Files changed: $changed_files
@@ -2738,7 +4332,7 @@ Files changed: $changed_files
2738
4332
  $file_contents"
2739
4333
 
2740
4334
  local review_output
2741
- review_output=$(claude --print "$prompt" 2>"${ARTIFACTS_DIR}/.claude-tokens-negative.log" || true)
4335
+ review_output=$(claude --print "$prompt" < /dev/null 2>"${ARTIFACTS_DIR}/.claude-tokens-negative.log" || true)
2742
4336
  parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-negative.log"
2743
4337
 
2744
4338
  echo "$review_output" > "$ARTIFACTS_DIR/negative-review.md"
@@ -2768,7 +4362,7 @@ run_e2e_validation() {
2768
4362
  fi
2769
4363
 
2770
4364
  info "Running E2E validation: $test_cmd"
2771
- if eval "$test_cmd" > "$ARTIFACTS_DIR/e2e-validation.log" 2>&1; then
4365
+ if bash -c "$test_cmd" > "$ARTIFACTS_DIR/e2e-validation.log" 2>&1; then
2772
4366
  success "E2E validation passed"
2773
4367
  return 0
2774
4368
  else
@@ -2782,7 +4376,7 @@ run_dod_audit() {
2782
4376
 
2783
4377
  if [[ ! -f "$dod_file" ]]; then
2784
4378
  # Check for alternative locations
2785
- for alt in "$PROJECT_ROOT/DEFINITION-OF-DONE.md" "$HOME/.claude-teams/templates/definition-of-done.example.md"; do
4379
+ for alt in "$PROJECT_ROOT/DEFINITION-OF-DONE.md" "$HOME/.shipwright/templates/definition-of-done.example.md"; do
2786
4380
  if [[ -f "$alt" ]]; then
2787
4381
  dod_file="$alt"
2788
4382
  break
@@ -2936,6 +4530,9 @@ stage_compound_quality() {
2936
4530
  strict_quality=$(jq -r --arg id "compound_quality" '(.stages[] | select(.id == $id) | .config.strict_quality) // false' "$PIPELINE_CONFIG" 2>/dev/null) || true
2937
4531
  [[ -z "$strict_quality" || "$strict_quality" == "null" ]] && strict_quality="false"
2938
4532
 
4533
+ # Convergence tracking
4534
+ local prev_issue_count=-1
4535
+
2939
4536
  local cycle=0
2940
4537
  while [[ "$cycle" -lt "$max_cycles" ]]; do
2941
4538
  cycle=$((cycle + 1))
@@ -2966,7 +4563,87 @@ stage_compound_quality() {
2966
4563
  fi
2967
4564
  fi
2968
4565
 
2969
- # 3. E2E Validation
4566
+ # 3. Developer Simulation (intelligence module)
4567
+ if type simulation_review &>/dev/null 2>&1; then
4568
+ local sim_enabled
4569
+ sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
4570
+ local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
4571
+ if [[ "$sim_enabled" != "true" && -f "$daemon_cfg" ]]; then
4572
+ sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
4573
+ fi
4574
+ if [[ "$sim_enabled" == "true" ]]; then
4575
+ echo ""
4576
+ info "Running developer simulation review..."
4577
+ local sim_diff
4578
+ sim_diff=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
4579
+ if [[ -n "$sim_diff" ]]; then
4580
+ local sim_result
4581
+ sim_result=$(simulation_review "$sim_diff" "${GOAL:-}" 2>/dev/null || echo "[]")
4582
+ if [[ -n "$sim_result" && "$sim_result" != "[]" && "$sim_result" != *'"error"'* ]]; then
4583
+ echo "$sim_result" > "$ARTIFACTS_DIR/compound-simulation-review.json"
4584
+ local sim_critical
4585
+ sim_critical=$(echo "$sim_result" | jq '[.[] | select(.severity == "critical" or .severity == "high")] | length' 2>/dev/null || echo "0")
4586
+ local sim_total
4587
+ sim_total=$(echo "$sim_result" | jq 'length' 2>/dev/null || echo "0")
4588
+ if [[ "$sim_critical" -gt 0 ]]; then
4589
+ warn "Developer simulation: ${sim_critical} critical/high concerns (${sim_total} total)"
4590
+ all_passed=false
4591
+ else
4592
+ success "Developer simulation: ${sim_total} concerns (none critical/high)"
4593
+ fi
4594
+ emit_event "compound.simulation" \
4595
+ "issue=${ISSUE_NUMBER:-0}" \
4596
+ "cycle=$cycle" \
4597
+ "total=$sim_total" \
4598
+ "critical=$sim_critical"
4599
+ else
4600
+ success "Developer simulation: no concerns"
4601
+ fi
4602
+ fi
4603
+ fi
4604
+ fi
4605
+
4606
+ # 4. Architecture Enforcer (intelligence module)
4607
+ if type architecture_validate_changes &>/dev/null 2>&1; then
4608
+ local arch_enabled
4609
+ arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
4610
+ local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
4611
+ if [[ "$arch_enabled" != "true" && -f "$daemon_cfg" ]]; then
4612
+ arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
4613
+ fi
4614
+ if [[ "$arch_enabled" == "true" ]]; then
4615
+ echo ""
4616
+ info "Running architecture validation..."
4617
+ local arch_diff
4618
+ arch_diff=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
4619
+ if [[ -n "$arch_diff" ]]; then
4620
+ local arch_result
4621
+ arch_result=$(architecture_validate_changes "$arch_diff" "" 2>/dev/null || echo "[]")
4622
+ if [[ -n "$arch_result" && "$arch_result" != "[]" && "$arch_result" != *'"error"'* ]]; then
4623
+ echo "$arch_result" > "$ARTIFACTS_DIR/compound-architecture-validation.json"
4624
+ local arch_violations
4625
+ arch_violations=$(echo "$arch_result" | jq '[.[] | select(.severity == "critical" or .severity == "high")] | length' 2>/dev/null || echo "0")
4626
+ local arch_total
4627
+ arch_total=$(echo "$arch_result" | jq 'length' 2>/dev/null || echo "0")
4628
+ if [[ "$arch_violations" -gt 0 ]]; then
4629
+ warn "Architecture validation: ${arch_violations} critical/high violations (${arch_total} total)"
4630
+ all_passed=false
4631
+ else
4632
+ success "Architecture validation: ${arch_total} violations (none critical/high)"
4633
+ fi
4634
+ emit_event "compound.architecture" \
4635
+ "issue=${ISSUE_NUMBER:-0}" \
4636
+ "cycle=$cycle" \
4637
+ "total=$arch_total" \
4638
+ "violations=$arch_violations"
4639
+ else
4640
+ success "Architecture validation: no violations"
4641
+ fi
4642
+ fi
4643
+ fi
4644
+ fi
4645
+
4646
+ # 5. E2E Validation
2970
4647
  if [[ "$e2e_enabled" == "true" ]]; then
2971
4648
  echo ""
2972
4649
  info "Running E2E validation..."
@@ -2975,7 +4652,7 @@ stage_compound_quality() {
2975
4652
  fi
2976
4653
  fi
2977
4654
 
2978
- # 4. DoD Audit
4655
+ # 6. DoD Audit
2979
4656
  if [[ "$dod_enabled" == "true" ]]; then
2980
4657
  echo ""
2981
4658
  info "Running Definition of Done audit..."
@@ -2984,7 +4661,7 @@ stage_compound_quality() {
2984
4661
  fi
2985
4662
  fi
2986
4663
 
2987
- # 5. Multi-dimensional quality checks
4664
+ # 7. Multi-dimensional quality checks
2988
4665
  echo ""
2989
4666
  info "Running multi-dimensional quality checks..."
2990
4667
  local quality_failures=0
@@ -3016,15 +4693,37 @@ stage_compound_quality() {
3016
4693
  success "Multi-dimensional quality: all checks passed"
3017
4694
  fi
3018
4695
 
4696
+ # ── Convergence Detection ──
4697
+ # Count critical/high issues from all review artifacts
4698
+ local current_issue_count=0
4699
+ if [[ -f "$ARTIFACTS_DIR/adversarial-review.md" ]]; then
4700
+ local adv_issues
4701
+ adv_issues=$(grep -ciE '\*\*\[?(Critical|Bug|critical|high)\]?\*\*' "$ARTIFACTS_DIR/adversarial-review.md" 2>/dev/null || true)
4702
+ current_issue_count=$((current_issue_count + ${adv_issues:-0}))
4703
+ fi
4704
+ if [[ -f "$ARTIFACTS_DIR/adversarial-review.json" ]]; then
4705
+ local adv_json_issues
4706
+ adv_json_issues=$(jq '[.[] | select(.severity == "critical" or .severity == "high")] | length' "$ARTIFACTS_DIR/adversarial-review.json" 2>/dev/null || echo "0")
4707
+ current_issue_count=$((current_issue_count + ${adv_json_issues:-0}))
4708
+ fi
4709
+ if [[ -f "$ARTIFACTS_DIR/negative-review.md" ]]; then
4710
+ local neg_issues
4711
+ neg_issues=$(grep -ciE '\[Critical\]' "$ARTIFACTS_DIR/negative-review.md" 2>/dev/null || true)
4712
+ current_issue_count=$((current_issue_count + ${neg_issues:-0}))
4713
+ fi
4714
+ current_issue_count=$((current_issue_count + quality_failures))
4715
+
3019
4716
  emit_event "compound.cycle" \
3020
4717
  "issue=${ISSUE_NUMBER:-0}" \
3021
4718
  "cycle=$cycle" \
3022
4719
  "max_cycles=$max_cycles" \
3023
4720
  "passed=$all_passed" \
4721
+ "critical_issues=$current_issue_count" \
3024
4722
  "self_heal_count=$SELF_HEAL_COUNT"
3025
4723
 
3026
- if $all_passed; then
3027
- success "Compound quality passed on cycle ${cycle}"
4724
+ # Early exit: zero critical/high issues
4725
+ if [[ "$current_issue_count" -eq 0 ]] && $all_passed; then
4726
+ success "Compound quality passed on cycle ${cycle} — zero critical/high issues"
3028
4727
 
3029
4728
  if [[ -n "$ISSUE_NUMBER" ]]; then
3030
4729
  gh_comment_issue "$ISSUE_NUMBER" "✅ **Compound quality passed** — cycle ${cycle}/${max_cycles}
@@ -3032,6 +4731,8 @@ stage_compound_quality() {
3032
4731
  All quality checks clean:
3033
4732
  - Adversarial review: ✅
3034
4733
  - Negative prompting: ✅
4734
+ - Developer simulation: ✅
4735
+ - Architecture validation: ✅
3035
4736
  - E2E validation: ✅
3036
4737
  - DoD audit: ✅
3037
4738
  - Security audit: ✅
@@ -3045,6 +4746,36 @@ All quality checks clean:
3045
4746
  return 0
3046
4747
  fi
3047
4748
 
4749
+ if $all_passed; then
4750
+ success "Compound quality passed on cycle ${cycle}"
4751
+
4752
+ if [[ -n "$ISSUE_NUMBER" ]]; then
4753
+ gh_comment_issue "$ISSUE_NUMBER" "✅ **Compound quality passed** — cycle ${cycle}/${max_cycles}" 2>/dev/null || true
4754
+ fi
4755
+
4756
+ log_stage "compound_quality" "Passed on cycle ${cycle}/${max_cycles}"
4757
+ return 0
4758
+ fi
4759
+
4760
+ # Check for plateau: issue count unchanged between cycles
4761
+ if [[ "$prev_issue_count" -ge 0 && "$current_issue_count" -eq "$prev_issue_count" && "$cycle" -gt 1 ]]; then
4762
+ warn "Convergence: quality plateau — ${current_issue_count} issues unchanged between cycles"
4763
+ emit_event "compound.plateau" \
4764
+ "issue=${ISSUE_NUMBER:-0}" \
4765
+ "cycle=$cycle" \
4766
+ "issue_count=$current_issue_count"
4767
+
4768
+ if [[ -n "$ISSUE_NUMBER" ]]; then
4769
+ gh_comment_issue "$ISSUE_NUMBER" "⚠️ **Compound quality plateau** — ${current_issue_count} issues unchanged after cycle ${cycle}. Stopping early." 2>/dev/null || true
4770
+ fi
4771
+
4772
+ log_stage "compound_quality" "Plateau at cycle ${cycle}/${max_cycles} (${current_issue_count} issues)"
4773
+ return 1
4774
+ fi
4775
+ prev_issue_count="$current_issue_count"
4776
+
4777
+ info "Convergence: ${current_issue_count} critical/high issues remaining"
4778
+
3048
4779
  # Not all passed — rebuild if we have cycles left
3049
4780
  if [[ "$cycle" -lt "$max_cycles" ]]; then
3050
4781
  warn "Quality checks failed — rebuilding with feedback (cycle $((cycle + 1))/${max_cycles})"
@@ -3074,6 +4805,100 @@ Quality issues remain. Check artifacts for details." 2>/dev/null || true
3074
4805
  return 1
3075
4806
  }
3076
4807
 
4808
+ # ─── Error Classification ──────────────────────────────────────────────────
4809
+ # Classifies errors to determine whether retrying makes sense.
4810
+ # Returns: "infrastructure", "logic", "configuration", or "unknown"
4811
+
4812
+ classify_error() {
4813
+ local stage_id="$1"
4814
+ local log_file="${ARTIFACTS_DIR}/${stage_id}-results.log"
4815
+ [[ ! -f "$log_file" ]] && log_file="${ARTIFACTS_DIR}/test-results.log"
4816
+ [[ ! -f "$log_file" ]] && { echo "unknown"; return; }
4817
+
4818
+ local log_tail
4819
+ log_tail=$(tail -50 "$log_file" 2>/dev/null || echo "")
4820
+
4821
+ # Generate error signature for history lookup
4822
+ local error_sig
4823
+ error_sig=$(echo "$log_tail" | grep -iE 'error|fail|exception|fatal' 2>/dev/null | head -3 | cksum | awk '{print $1}' || echo "0")
4824
+
4825
+ # Check classification history first (learned from previous runs)
4826
+ local class_history="${HOME}/.shipwright/optimization/error-classifications.json"
4827
+ if [[ -f "$class_history" ]]; then
4828
+ local cached_class
4829
+ cached_class=$(jq -r --arg sig "$error_sig" '.[$sig].classification // empty' "$class_history" 2>/dev/null || true)
4830
+ if [[ -n "$cached_class" && "$cached_class" != "null" ]]; then
4831
+ echo "$cached_class"
4832
+ return
4833
+ fi
4834
+ fi
4835
+
4836
+ local classification="unknown"
4837
+
4838
+ # Infrastructure errors: timeout, OOM, network — retry makes sense
4839
+ if echo "$log_tail" | grep -qiE 'timeout|timed out|ETIMEDOUT|ECONNREFUSED|ECONNRESET|network|socket hang up|OOM|out of memory|killed|signal 9|Cannot allocate memory'; then
4840
+ classification="infrastructure"
4841
+ # Configuration errors: missing env, wrong path — don't retry, escalate
4842
+ elif echo "$log_tail" | grep -qiE 'ENOENT|not found|No such file|command not found|MODULE_NOT_FOUND|Cannot find module|missing.*env|undefined variable|permission denied|EACCES'; then
4843
+ classification="configuration"
4844
+ # Logic errors: assertion failures, type errors — retry won't help without code change
4845
+ elif echo "$log_tail" | grep -qiE 'AssertionError|assert.*fail|Expected.*but.*got|TypeError|ReferenceError|SyntaxError|CompileError|type mismatch|cannot assign|incompatible type'; then
4846
+ classification="logic"
4847
+ # Build errors: compilation failures
4848
+ elif echo "$log_tail" | grep -qiE 'error\[E[0-9]+\]|error: aborting|FAILED.*compile|build failed|tsc.*error|eslint.*error'; then
4849
+ classification="logic"
4850
+ # Intelligence fallback: Claude classification for unknown errors
4851
+ elif [[ "$classification" == "unknown" ]] && type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
4852
+ local ai_class
4853
+ ai_class=$(claude --print --output-format text -p "Classify this error as exactly one of: infrastructure, configuration, logic, unknown.
4854
+
4855
+ Error output:
4856
+ $(echo "$log_tail" | tail -20)
4857
+
4858
+ Reply with ONLY the classification word, nothing else." --model haiku < /dev/null 2>/dev/null || true)
4859
+ ai_class=$(echo "$ai_class" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
4860
+ case "$ai_class" in
4861
+ infrastructure|configuration|logic) classification="$ai_class" ;;
4862
+ esac
4863
+ fi
4864
+
4865
+ # Map retry categories to shared taxonomy (from lib/compat.sh SW_ERROR_CATEGORIES)
4866
+ # Retry uses: infrastructure, configuration, logic, unknown
4867
+ # Shared uses: test_failure, build_error, lint_error, timeout, dependency, flaky, config, security, permission, unknown
4868
+ local canonical_category="unknown"
4869
+ case "$classification" in
4870
+ infrastructure) canonical_category="timeout" ;;
4871
+ configuration) canonical_category="config" ;;
4872
+ logic)
4873
+ case "$stage_id" in
4874
+ test) canonical_category="test_failure" ;;
4875
+ *) canonical_category="build_error" ;;
4876
+ esac
4877
+ ;;
4878
+ esac
4879
+
4880
+ # Record classification for future runs (using both retry and canonical categories)
4881
+ if [[ -n "$error_sig" && "$error_sig" != "0" ]]; then
4882
+ local class_dir="${HOME}/.shipwright/optimization"
4883
+ mkdir -p "$class_dir" 2>/dev/null || true
4884
+ local tmp_class
4885
+ tmp_class="$(mktemp)"
4886
+ if [[ -f "$class_history" ]]; then
4887
+ jq --arg sig "$error_sig" --arg cls "$classification" --arg canon "$canonical_category" --arg stage "$stage_id" \
4888
+ '.[$sig] = {"classification": $cls, "canonical": $canon, "stage": $stage, "recorded_at": now}' \
4889
+ "$class_history" > "$tmp_class" 2>/dev/null && \
4890
+ mv "$tmp_class" "$class_history" || rm -f "$tmp_class"
4891
+ else
4892
+ jq -n --arg sig "$error_sig" --arg cls "$classification" --arg canon "$canonical_category" --arg stage "$stage_id" \
4893
+ '{($sig): {"classification": $cls, "canonical": $canon, "stage": $stage, "recorded_at": now}}' \
4894
+ > "$tmp_class" 2>/dev/null && \
4895
+ mv "$tmp_class" "$class_history" || rm -f "$tmp_class"
4896
+ fi
4897
+ fi
4898
+
4899
+ echo "$classification"
4900
+ }
4901
+
3077
4902
  # ─── Stage Runner ───────────────────────────────────────────────────────────
3078
4903
 
3079
4904
  run_stage_with_retry() {
@@ -3083,6 +4908,7 @@ run_stage_with_retry() {
3083
4908
  [[ -z "$max_retries" || "$max_retries" == "null" ]] && max_retries=0
3084
4909
 
3085
4910
  local attempt=0
4911
+ local prev_error_class=""
3086
4912
  while true; do
3087
4913
  if "stage_${stage_id}"; then
3088
4914
  return 0
@@ -3093,8 +4919,53 @@ run_stage_with_retry() {
3093
4919
  return 1
3094
4920
  fi
3095
4921
 
3096
- warn "Stage $stage_id failed (attempt $attempt/$((max_retries + 1))) retrying..."
3097
- sleep 2
4922
+ # Classify the error to decide whether retry makes sense
4923
+ local error_class
4924
+ error_class=$(classify_error "$stage_id")
4925
+
4926
+ emit_event "retry.classified" \
4927
+ "issue=${ISSUE_NUMBER:-0}" \
4928
+ "stage=$stage_id" \
4929
+ "attempt=$attempt" \
4930
+ "error_class=$error_class"
4931
+
4932
+ case "$error_class" in
4933
+ infrastructure)
4934
+ info "Error classified as infrastructure (timeout/network/OOM) — retry makes sense"
4935
+ ;;
4936
+ configuration)
4937
+ error "Error classified as configuration (missing env/path) — skipping retry, escalating"
4938
+ emit_event "retry.escalated" \
4939
+ "issue=${ISSUE_NUMBER:-0}" \
4940
+ "stage=$stage_id" \
4941
+ "reason=configuration_error"
4942
+ return 1
4943
+ ;;
4944
+ logic)
4945
+ if [[ "$error_class" == "$prev_error_class" ]]; then
4946
+ error "Error classified as logic (assertion/type error) with same class — retry won't help without code change"
4947
+ emit_event "retry.skipped" \
4948
+ "issue=${ISSUE_NUMBER:-0}" \
4949
+ "stage=$stage_id" \
4950
+ "reason=repeated_logic_error"
4951
+ return 1
4952
+ fi
4953
+ warn "Error classified as logic — retrying once in case build fixes it"
4954
+ ;;
4955
+ *)
4956
+ info "Error classification: unknown — retrying"
4957
+ ;;
4958
+ esac
4959
+ prev_error_class="$error_class"
4960
+
4961
+ warn "Stage $stage_id failed (attempt $attempt/$((max_retries + 1)), class: $error_class) — retrying..."
4962
+ # Exponential backoff with jitter to avoid thundering herd
4963
+ local backoff=$((2 ** attempt))
4964
+ [[ "$backoff" -gt 16 ]] && backoff=16
4965
+ local jitter=$(( RANDOM % (backoff + 1) ))
4966
+ local total_sleep=$((backoff + jitter))
4967
+ info "Backing off ${total_sleep}s before retry..."
4968
+ sleep "$total_sleep"
3098
4969
  done
3099
4970
  }
3100
4971
 
@@ -3107,6 +4978,25 @@ self_healing_build_test() {
3107
4978
  local max_cycles="$BUILD_TEST_RETRIES"
3108
4979
  local last_test_error=""
3109
4980
 
4981
+ # Convergence tracking
4982
+ local prev_error_sig="" consecutive_same_error=0
4983
+ local prev_fail_count=0 zero_convergence_streak=0
4984
+
4985
+ # Intelligence: adaptive iteration limit
4986
+ if type composer_estimate_iterations &>/dev/null 2>&1; then
4987
+ local estimated
4988
+ estimated=$(composer_estimate_iterations \
4989
+ "${INTELLIGENCE_ANALYSIS:-{}}" \
4990
+ "${HOME}/.shipwright/optimization/iteration-model.json" 2>/dev/null || echo "")
4991
+ if [[ -n "$estimated" && "$estimated" =~ ^[0-9]+$ && "$estimated" -gt 0 ]]; then
4992
+ max_cycles="$estimated"
4993
+ emit_event "intelligence.adaptive_iterations" \
4994
+ "issue=${ISSUE_NUMBER:-0}" \
4995
+ "estimated=$estimated" \
4996
+ "original=$BUILD_TEST_RETRIES"
4997
+ fi
4998
+ fi
4999
+
3110
5000
  while [[ "$cycle" -le "$max_cycles" ]]; do
3111
5001
  cycle=$((cycle + 1))
3112
5002
 
@@ -3182,6 +5072,9 @@ Focus on fixing the failing tests while keeping all passing tests working."
3182
5072
  local timing
3183
5073
  timing=$(get_stage_timing "test")
3184
5074
  success "Stage ${BOLD}test${RESET} complete ${DIM}(${timing})${RESET}"
5075
+ emit_event "convergence.tests_passed" \
5076
+ "issue=${ISSUE_NUMBER:-0}" \
5077
+ "cycle=$cycle"
3185
5078
  return 0 # Tests passed!
3186
5079
  fi
3187
5080
 
@@ -3190,6 +5083,59 @@ Focus on fixing the failing tests while keeping all passing tests working."
3190
5083
  last_test_error=$(tail -30 "$test_log" 2>/dev/null || echo "Test command failed with no output")
3191
5084
  mark_stage_failed "test"
3192
5085
 
5086
+ # ── Convergence Detection ──
5087
+ # Hash the error output to detect repeated failures
5088
+ local error_sig
5089
+ error_sig=$(echo "$last_test_error" | shasum -a 256 2>/dev/null | cut -c1-16 || echo "unknown")
5090
+
5091
+ # Count failing tests (extract from common patterns)
5092
+ local current_fail_count=0
5093
+ current_fail_count=$(grep -ciE 'fail|error|FAIL' "$test_log" 2>/dev/null || true)
5094
+ current_fail_count="${current_fail_count:-0}"
5095
+
5096
+ if [[ "$error_sig" == "$prev_error_sig" ]]; then
5097
+ consecutive_same_error=$((consecutive_same_error + 1))
5098
+ else
5099
+ consecutive_same_error=1
5100
+ fi
5101
+ prev_error_sig="$error_sig"
5102
+
5103
+ # Check: same error 3 times consecutively → stuck
5104
+ if [[ "$consecutive_same_error" -ge 3 ]]; then
5105
+ error "Convergence: stuck on same error for 3 consecutive cycles — exiting early"
5106
+ emit_event "convergence.stuck" \
5107
+ "issue=${ISSUE_NUMBER:-0}" \
5108
+ "cycle=$cycle" \
5109
+ "error_sig=$error_sig" \
5110
+ "consecutive=$consecutive_same_error"
5111
+ notify "Build Convergence" "Stuck on unfixable error after ${cycle} cycles" "error"
5112
+ return 1
5113
+ fi
5114
+
5115
+ # Track convergence rate: did we reduce failures?
5116
+ if [[ "$cycle" -gt 1 && "$prev_fail_count" -gt 0 ]]; then
5117
+ if [[ "$current_fail_count" -ge "$prev_fail_count" ]]; then
5118
+ zero_convergence_streak=$((zero_convergence_streak + 1))
5119
+ else
5120
+ zero_convergence_streak=0
5121
+ fi
5122
+
5123
+ # Check: zero convergence for 2 consecutive iterations → plateau
5124
+ if [[ "$zero_convergence_streak" -ge 2 ]]; then
5125
+ error "Convergence: no progress for 2 consecutive cycles (${current_fail_count} failures remain) — exiting early"
5126
+ emit_event "convergence.plateau" \
5127
+ "issue=${ISSUE_NUMBER:-0}" \
5128
+ "cycle=$cycle" \
5129
+ "fail_count=$current_fail_count" \
5130
+ "streak=$zero_convergence_streak"
5131
+ notify "Build Convergence" "No progress after ${cycle} cycles — plateau reached" "error"
5132
+ return 1
5133
+ fi
5134
+ fi
5135
+ prev_fail_count="$current_fail_count"
5136
+
5137
+ info "Convergence: error_sig=${error_sig:0:8} repeat=${consecutive_same_error} failures=${current_fail_count} no_progress=${zero_convergence_streak}"
5138
+
3193
5139
  if [[ "$cycle" -le "$max_cycles" ]]; then
3194
5140
  warn "Tests failed — will attempt self-healing (cycle $((cycle + 1))/$((max_cycles + 1)))"
3195
5141
  notify "Self-Healing" "Tests failed on cycle ${cycle}, retrying..." "warn"
@@ -3256,7 +5202,7 @@ run_pipeline() {
3256
5202
  use_self_healing=true
3257
5203
  fi
3258
5204
 
3259
- while IFS= read -r stage; do
5205
+ while IFS= read -r -u 3 stage; do
3260
5206
  local id enabled gate
3261
5207
  id=$(echo "$stage" | jq -r '.id')
3262
5208
  enabled=$(echo "$stage" | jq -r '.enabled')
@@ -3264,6 +5210,34 @@ run_pipeline() {
3264
5210
 
3265
5211
  CURRENT_STAGE_ID="$id"
3266
5212
 
5213
+ # Human intervention: check for skip-stage directive
5214
+ if [[ -f "$ARTIFACTS_DIR/skip-stage.txt" ]]; then
5215
+ local skip_list
5216
+ skip_list="$(cat "$ARTIFACTS_DIR/skip-stage.txt" 2>/dev/null || true)"
5217
+ if echo "$skip_list" | grep -qx "$id" 2>/dev/null; then
5218
+ info "Stage ${BOLD}${id}${RESET} skipped by human directive"
5219
+ emit_event "stage.skipped" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "reason=human_skip"
5220
+ # Remove this stage from the skip file
5221
+ local tmp_skip
5222
+ tmp_skip="$(mktemp)"
5223
+ grep -vx "$id" "$ARTIFACTS_DIR/skip-stage.txt" > "$tmp_skip" 2>/dev/null || true
5224
+ mv "$tmp_skip" "$ARTIFACTS_DIR/skip-stage.txt"
5225
+ continue
5226
+ fi
5227
+ fi
5228
+
5229
+ # Human intervention: check for human message
5230
+ if [[ -f "$ARTIFACTS_DIR/human-message.txt" ]]; then
5231
+ local human_msg
5232
+ human_msg="$(cat "$ARTIFACTS_DIR/human-message.txt" 2>/dev/null || true)"
5233
+ if [[ -n "$human_msg" ]]; then
5234
+ echo ""
5235
+ echo -e " ${PURPLE}${BOLD}💬 Human message:${RESET} $human_msg"
5236
+ emit_event "pipeline.human_message" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "message=$human_msg"
5237
+ rm -f "$ARTIFACTS_DIR/human-message.txt"
5238
+ fi
5239
+ fi
5240
+
3267
5241
  if [[ "$enabled" != "true" ]]; then
3268
5242
  echo -e " ${DIM}○ ${id} — skipped (disabled)${RESET}"
3269
5243
  continue
@@ -3277,6 +5251,15 @@ run_pipeline() {
3277
5251
  continue
3278
5252
  fi
3279
5253
 
5254
+ # CI resume: skip stages marked as completed from previous run
5255
+ if [[ -n "${COMPLETED_STAGES:-}" ]] && echo "$COMPLETED_STAGES" | tr ',' '\n' | grep -qx "$id"; then
5256
+ echo -e " ${GREEN}✓ ${id}${RESET} ${DIM}— skipped (CI resume)${RESET}"
5257
+ set_stage_status "$id" "complete"
5258
+ completed=$((completed + 1))
5259
+ emit_event "stage.skipped" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "reason=ci_resume"
5260
+ continue
5261
+ fi
5262
+
3280
5263
  # Self-healing build→test loop: when we hit build, run both together
3281
5264
  if [[ "$id" == "build" && "$use_self_healing" == "true" ]]; then
3282
5265
  # Gate check for build
@@ -3325,9 +5308,9 @@ run_pipeline() {
3325
5308
  fi
3326
5309
 
3327
5310
  # Budget enforcement check (skip with --ignore-budget)
3328
- if [[ "$IGNORE_BUDGET" != "true" ]] && [[ -x "$SCRIPT_DIR/cct-cost.sh" ]]; then
5311
+ if [[ "$IGNORE_BUDGET" != "true" ]] && [[ -x "$SCRIPT_DIR/sw-cost.sh" ]]; then
3329
5312
  local budget_rc=0
3330
- bash "$SCRIPT_DIR/cct-cost.sh" check-budget 2>/dev/null || budget_rc=$?
5313
+ bash "$SCRIPT_DIR/sw-cost.sh" check-budget 2>/dev/null || budget_rc=$?
3331
5314
  if [[ "$budget_rc" -eq 2 ]]; then
3332
5315
  warn "Daily budget exceeded — pausing pipeline before stage ${BOLD}$id${RESET}"
3333
5316
  warn "Resume with --ignore-budget to override, or wait until tomorrow"
@@ -3337,6 +5320,71 @@ run_pipeline() {
3337
5320
  fi
3338
5321
  fi
3339
5322
 
5323
+ # Intelligence: per-stage model routing with A/B testing
5324
+ if type intelligence_recommend_model &>/dev/null 2>&1; then
5325
+ local stage_complexity="${INTELLIGENCE_COMPLEXITY:-5}"
5326
+ local budget_remaining=""
5327
+ if [[ -x "$SCRIPT_DIR/sw-cost.sh" ]]; then
5328
+ budget_remaining=$(bash "$SCRIPT_DIR/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
5329
+ fi
5330
+ local recommended_model
5331
+ recommended_model=$(intelligence_recommend_model "$id" "$stage_complexity" "$budget_remaining" 2>/dev/null || echo "")
5332
+ if [[ -n "$recommended_model" && "$recommended_model" != "null" ]]; then
5333
+ # A/B testing: decide whether to use the recommended model
5334
+ local ab_ratio=20 # default 20% use recommended model
5335
+ local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
5336
+ if [[ -f "$daemon_cfg" ]]; then
5337
+ local cfg_ratio
5338
+ cfg_ratio=$(jq -r '.intelligence.ab_test_ratio // 0.2' "$daemon_cfg" 2>/dev/null || echo "0.2")
5339
+ # Convert ratio (0.0-1.0) to percentage (0-100)
5340
+ ab_ratio=$(awk -v r="$cfg_ratio" 'BEGIN{printf "%d", r * 100}' 2>/dev/null || echo "20")
5341
+ fi
5342
+
5343
+ # Check if we have enough data points to graduate from A/B testing
5344
+ local routing_file="${HOME}/.shipwright/optimization/model-routing.json"
5345
+ local use_recommended=false
5346
+ local ab_group="control"
5347
+
5348
+ if [[ -f "$routing_file" ]]; then
5349
+ local stage_samples
5350
+ stage_samples=$(jq -r --arg s "$id" '.[$s].sonnet_samples // 0' "$routing_file" 2>/dev/null || echo "0")
5351
+ local total_samples
5352
+ total_samples=$(jq -r --arg s "$id" '((.[$s].sonnet_samples // 0) + (.[$s].opus_samples // 0))' "$routing_file" 2>/dev/null || echo "0")
5353
+
5354
+ if [[ "$total_samples" -ge 50 ]]; then
5355
+ # Enough data — use optimizer's recommendation as default
5356
+ use_recommended=true
5357
+ ab_group="graduated"
5358
+ fi
5359
+ fi
5360
+
5361
+ if [[ "$use_recommended" != "true" ]]; then
5362
+ # A/B test: RANDOM % 100 < ab_ratio → use recommended
5363
+ local roll=$((RANDOM % 100))
5364
+ if [[ "$roll" -lt "$ab_ratio" ]]; then
5365
+ use_recommended=true
5366
+ ab_group="experiment"
5367
+ else
5368
+ ab_group="control"
5369
+ fi
5370
+ fi
5371
+
5372
+ if [[ "$use_recommended" == "true" ]]; then
5373
+ export CLAUDE_MODEL="$recommended_model"
5374
+ else
5375
+ export CLAUDE_MODEL="opus"
5376
+ fi
5377
+
5378
+ emit_event "intelligence.model_ab" \
5379
+ "issue=${ISSUE_NUMBER:-0}" \
5380
+ "stage=$id" \
5381
+ "recommended=$recommended_model" \
5382
+ "applied=$CLAUDE_MODEL" \
5383
+ "ab_group=$ab_group" \
5384
+ "ab_ratio=$ab_ratio"
5385
+ fi
5386
+ fi
5387
+
3340
5388
  echo ""
3341
5389
  echo -e "${CYAN}${BOLD}▸ Stage: ${id}${RESET} ${DIM}[$((completed + 1))/${enabled_count}]${RESET}"
3342
5390
  update_status "running" "$id"
@@ -3345,6 +5393,12 @@ run_pipeline() {
3345
5393
  stage_start_epoch=$(now_epoch)
3346
5394
  emit_event "stage.started" "issue=${ISSUE_NUMBER:-0}" "stage=$id"
3347
5395
 
5396
+ # Mark GitHub Check Run as in-progress
5397
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update &>/dev/null 2>&1; then
5398
+ gh_checks_stage_update "$id" "in_progress" "" "Stage $id started" 2>/dev/null || true
5399
+ fi
5400
+
5401
+ local stage_model_used="${CLAUDE_MODEL:-${MODEL:-opus}}"
3348
5402
  if run_stage_with_retry "$id"; then
3349
5403
  mark_stage_complete "$id"
3350
5404
  completed=$((completed + 1))
@@ -3353,6 +5407,8 @@ run_pipeline() {
3353
5407
  stage_dur_s=$(( $(now_epoch) - stage_start_epoch ))
3354
5408
  success "Stage ${BOLD}$id${RESET} complete ${DIM}(${timing})${RESET}"
3355
5409
  emit_event "stage.completed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s"
5410
+ # Log model used for prediction feedback
5411
+ echo "${id}|${stage_model_used}|true" >> "${ARTIFACTS_DIR}/model-routing.log"
3356
5412
  else
3357
5413
  mark_stage_failed "$id"
3358
5414
  local stage_dur_s
@@ -3360,9 +5416,11 @@ run_pipeline() {
3360
5416
  error "Pipeline failed at stage: ${BOLD}$id${RESET}"
3361
5417
  update_status "failed" "$id"
3362
5418
  emit_event "stage.failed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s"
5419
+ # Log model used for prediction feedback
5420
+ echo "${id}|${stage_model_used}|false" >> "${ARTIFACTS_DIR}/model-routing.log"
3363
5421
  return 1
3364
5422
  fi
3365
- done <<< "$stages"
5423
+ done 3<<< "$stages"
3366
5424
 
3367
5425
  # Pipeline complete!
3368
5426
  update_status "complete" ""
@@ -3388,8 +5446,8 @@ run_pipeline() {
3388
5446
  echo ""
3389
5447
 
3390
5448
  # Capture learnings to memory (success or failure)
3391
- if [[ -x "$SCRIPT_DIR/cct-memory.sh" ]]; then
3392
- bash "$SCRIPT_DIR/cct-memory.sh" capture "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
5449
+ if [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
5450
+ bash "$SCRIPT_DIR/sw-memory.sh" capture "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
3393
5451
  fi
3394
5452
 
3395
5453
  # Final GitHub progress update
@@ -3482,7 +5540,7 @@ pipeline_start() {
3482
5540
  # Register worktree cleanup on exit (chain with existing cleanup)
3483
5541
  if [[ "$CLEANUP_WORKTREE" == "true" ]]; then
3484
5542
  trap 'pipeline_cleanup_worktree; cleanup_on_exit' SIGINT SIGTERM
3485
- trap 'pipeline_cleanup_worktree' EXIT
5543
+ trap 'pipeline_cleanup_worktree; cleanup_on_exit' EXIT
3486
5544
  fi
3487
5545
 
3488
5546
  setup_dirs
@@ -3508,6 +5566,30 @@ pipeline_start() {
3508
5566
  load_pipeline_config
3509
5567
  initialize_state
3510
5568
 
5569
+ # CI resume: restore branch + goal context when intake is skipped
5570
+ if [[ -n "${COMPLETED_STAGES:-}" ]] && echo "$COMPLETED_STAGES" | tr ',' '\n' | grep -qx "intake"; then
5571
+ # Intake was completed in a previous run — restore context
5572
+ # The workflow merges the partial work branch, so code changes are on HEAD
5573
+
5574
+ # Restore GOAL from issue if not already set
5575
+ if [[ -z "$GOAL" && -n "$ISSUE_NUMBER" ]]; then
5576
+ GOAL=$(gh issue view "$ISSUE_NUMBER" --json title -q .title 2>/dev/null || echo "Issue #${ISSUE_NUMBER}")
5577
+ info "CI resume: goal from issue — ${GOAL}"
5578
+ fi
5579
+
5580
+ # Restore branch context
5581
+ if [[ -z "$GIT_BRANCH" ]]; then
5582
+ local ci_branch="ci/issue-${ISSUE_NUMBER}"
5583
+ info "CI resume: creating branch ${ci_branch} from current HEAD"
5584
+ git checkout -b "$ci_branch" 2>/dev/null || git checkout "$ci_branch" 2>/dev/null || true
5585
+ GIT_BRANCH="$ci_branch"
5586
+ elif [[ "$(git branch --show-current 2>/dev/null)" != "$GIT_BRANCH" ]]; then
5587
+ info "CI resume: checking out branch ${GIT_BRANCH}"
5588
+ git checkout -b "$GIT_BRANCH" 2>/dev/null || git checkout "$GIT_BRANCH" 2>/dev/null || true
5589
+ fi
5590
+ write_state 2>/dev/null || true
5591
+ fi
5592
+
3511
5593
  echo ""
3512
5594
  echo -e "${PURPLE}${BOLD}╔═══════════════════════════════════════════════════════════════════╗${RESET}"
3513
5595
  echo -e "${PURPLE}${BOLD}║ shipwright pipeline — Autonomous Feature Delivery ║${RESET}"
@@ -3556,6 +5638,21 @@ pipeline_start() {
3556
5638
  return 0
3557
5639
  fi
3558
5640
 
5641
+ # Start background heartbeat writer
5642
+ start_heartbeat
5643
+
5644
+ # Initialize GitHub Check Runs for all pipeline stages
5645
+ if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_pipeline_start &>/dev/null 2>&1; then
5646
+ local head_sha
5647
+ head_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
5648
+ if [[ -n "$head_sha" && -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
5649
+ local stages_json
5650
+ stages_json=$(jq -c '[.stages[] | select(.enabled == true) | .id]' "$PIPELINE_CONFIG" 2>/dev/null || echo '[]')
5651
+ gh_checks_pipeline_start "$REPO_OWNER" "$REPO_NAME" "$head_sha" "$stages_json" >/dev/null 2>/dev/null || true
5652
+ info "GitHub Checks: created check runs for pipeline stages"
5653
+ fi
5654
+ fi
5655
+
3559
5656
  # Send start notification
3560
5657
  notify "Pipeline Started" "Goal: ${GOAL}\nPipeline: ${PIPELINE_NAME}" "info"
3561
5658
 
@@ -3597,12 +5694,56 @@ pipeline_start() {
3597
5694
  "self_heal_count=$SELF_HEAL_COUNT"
3598
5695
 
3599
5696
  # Capture failure learnings to memory
3600
- if [[ -x "$SCRIPT_DIR/cct-memory.sh" ]]; then
3601
- bash "$SCRIPT_DIR/cct-memory.sh" capture "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
3602
- bash "$SCRIPT_DIR/cct-memory.sh" analyze-failure "$ARTIFACTS_DIR/.claude-tokens-${CURRENT_STAGE_ID:-build}.log" "${CURRENT_STAGE_ID:-unknown}" 2>/dev/null || true
5697
+ if [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
5698
+ bash "$SCRIPT_DIR/sw-memory.sh" capture "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
5699
+ bash "$SCRIPT_DIR/sw-memory.sh" analyze-failure "$ARTIFACTS_DIR/.claude-tokens-${CURRENT_STAGE_ID:-build}.log" "${CURRENT_STAGE_ID:-unknown}" 2>/dev/null || true
3603
5700
  fi
3604
5701
  fi
3605
5702
 
5703
+ # ── Prediction Validation Events ──
5704
+ # Compare predicted vs actual outcomes for feedback loop calibration
5705
+ local pipeline_success="false"
5706
+ [[ "$exit_code" -eq 0 ]] && pipeline_success="true"
5707
+
5708
+ # Complexity prediction vs actual iterations
5709
+ emit_event "prediction.validated" \
5710
+ "issue=${ISSUE_NUMBER:-0}" \
5711
+ "predicted_complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
5712
+ "actual_iterations=$SELF_HEAL_COUNT" \
5713
+ "success=$pipeline_success"
5714
+
5715
+ # Template outcome tracking
5716
+ emit_event "template.outcome" \
5717
+ "issue=${ISSUE_NUMBER:-0}" \
5718
+ "template=${PIPELINE_NAME}" \
5719
+ "success=$pipeline_success" \
5720
+ "duration_s=${total_dur_s:-0}"
5721
+
5722
+ # Risk prediction vs actual failure
5723
+ local predicted_risk="${INTELLIGENCE_RISK_SCORE:-0}"
5724
+ emit_event "risk.outcome" \
5725
+ "issue=${ISSUE_NUMBER:-0}" \
5726
+ "predicted_risk=$predicted_risk" \
5727
+ "actual_failure=$([[ "$exit_code" -ne 0 ]] && echo "true" || echo "false")"
5728
+
5729
+ # Per-stage model outcome events (read from stage timings)
5730
+ local routing_log="${ARTIFACTS_DIR}/model-routing.log"
5731
+ if [[ -f "$routing_log" ]]; then
5732
+ while IFS='|' read -r s_stage s_model s_success; do
5733
+ [[ -z "$s_stage" ]] && continue
5734
+ emit_event "model.outcome" \
5735
+ "issue=${ISSUE_NUMBER:-0}" \
5736
+ "stage=$s_stage" \
5737
+ "model=$s_model" \
5738
+ "success=$s_success"
5739
+ done < "$routing_log"
5740
+ fi
5741
+
5742
+ # Record pipeline outcome for model routing feedback loop
5743
+ if type optimize_analyze_outcome &>/dev/null 2>&1; then
5744
+ optimize_analyze_outcome "$STATE_FILE" 2>/dev/null || true
5745
+ fi
5746
+
3606
5747
  # Emit cost event
3607
5748
  local model_key="${MODEL:-sonnet}"
3608
5749
  local input_cost output_cost total_cost
@@ -3754,7 +5895,7 @@ pipeline_abort() {
3754
5895
  pipeline_list() {
3755
5896
  local locations=(
3756
5897
  "$REPO_DIR/templates/pipelines"
3757
- "$HOME/.claude-teams/pipelines"
5898
+ "$HOME/.shipwright/pipelines"
3758
5899
  )
3759
5900
 
3760
5901
  echo ""
@@ -3832,7 +5973,7 @@ case "$SUBCOMMAND" in
3832
5973
  show) pipeline_show ;;
3833
5974
  test)
3834
5975
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3835
- exec "$SCRIPT_DIR/cct-pipeline-test.sh" "$@"
5976
+ exec "$SCRIPT_DIR/sw-pipeline-test.sh" "$@"
3836
5977
  ;;
3837
5978
  help|--help|-h) show_help ;;
3838
5979
  *)