shipwright-cli 2.3.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +95 -28
  2. package/completions/_shipwright +1 -1
  3. package/completions/shipwright.bash +3 -8
  4. package/completions/shipwright.fish +1 -1
  5. package/config/defaults.json +111 -0
  6. package/config/event-schema.json +81 -0
  7. package/config/policy.json +155 -2
  8. package/config/policy.schema.json +162 -1
  9. package/dashboard/coverage/coverage-summary.json +14 -0
  10. package/dashboard/public/index.html +1 -1
  11. package/dashboard/server.ts +306 -17
  12. package/dashboard/src/components/charts/bar.test.ts +79 -0
  13. package/dashboard/src/components/charts/donut.test.ts +68 -0
  14. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  15. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  16. package/dashboard/src/core/api.test.ts +309 -0
  17. package/dashboard/src/core/helpers.test.ts +301 -0
  18. package/dashboard/src/core/router.test.ts +307 -0
  19. package/dashboard/src/core/router.ts +7 -0
  20. package/dashboard/src/core/sse.test.ts +144 -0
  21. package/dashboard/src/views/metrics.test.ts +186 -0
  22. package/dashboard/src/views/overview.test.ts +173 -0
  23. package/dashboard/src/views/pipelines.test.ts +183 -0
  24. package/dashboard/src/views/team.test.ts +253 -0
  25. package/dashboard/vitest.config.ts +14 -5
  26. package/docs/TIPS.md +1 -1
  27. package/docs/patterns/README.md +1 -1
  28. package/package.json +15 -5
  29. package/scripts/adapters/docker-deploy.sh +1 -1
  30. package/scripts/adapters/tmux-adapter.sh +11 -1
  31. package/scripts/adapters/wezterm-adapter.sh +1 -1
  32. package/scripts/check-version-consistency.sh +1 -1
  33. package/scripts/lib/architecture.sh +126 -0
  34. package/scripts/lib/bootstrap.sh +75 -0
  35. package/scripts/lib/compat.sh +89 -6
  36. package/scripts/lib/config.sh +91 -0
  37. package/scripts/lib/daemon-adaptive.sh +3 -3
  38. package/scripts/lib/daemon-dispatch.sh +39 -16
  39. package/scripts/lib/daemon-health.sh +1 -1
  40. package/scripts/lib/daemon-patrol.sh +24 -12
  41. package/scripts/lib/daemon-poll.sh +37 -25
  42. package/scripts/lib/daemon-state.sh +115 -23
  43. package/scripts/lib/daemon-triage.sh +30 -8
  44. package/scripts/lib/fleet-failover.sh +63 -0
  45. package/scripts/lib/helpers.sh +30 -6
  46. package/scripts/lib/pipeline-detection.sh +2 -2
  47. package/scripts/lib/pipeline-github.sh +9 -9
  48. package/scripts/lib/pipeline-intelligence.sh +85 -35
  49. package/scripts/lib/pipeline-quality-checks.sh +16 -16
  50. package/scripts/lib/pipeline-quality.sh +1 -1
  51. package/scripts/lib/pipeline-stages.sh +242 -28
  52. package/scripts/lib/pipeline-state.sh +40 -4
  53. package/scripts/lib/test-helpers.sh +247 -0
  54. package/scripts/postinstall.mjs +3 -11
  55. package/scripts/sw +10 -4
  56. package/scripts/sw-activity.sh +1 -11
  57. package/scripts/sw-adaptive.sh +109 -85
  58. package/scripts/sw-adversarial.sh +4 -14
  59. package/scripts/sw-architecture-enforcer.sh +1 -11
  60. package/scripts/sw-auth.sh +8 -17
  61. package/scripts/sw-autonomous.sh +111 -49
  62. package/scripts/sw-changelog.sh +1 -11
  63. package/scripts/sw-checkpoint.sh +144 -20
  64. package/scripts/sw-ci.sh +2 -12
  65. package/scripts/sw-cleanup.sh +13 -17
  66. package/scripts/sw-code-review.sh +16 -36
  67. package/scripts/sw-connect.sh +5 -12
  68. package/scripts/sw-context.sh +9 -26
  69. package/scripts/sw-cost.sh +6 -16
  70. package/scripts/sw-daemon.sh +75 -70
  71. package/scripts/sw-dashboard.sh +57 -17
  72. package/scripts/sw-db.sh +506 -15
  73. package/scripts/sw-decompose.sh +1 -11
  74. package/scripts/sw-deps.sh +15 -25
  75. package/scripts/sw-developer-simulation.sh +1 -11
  76. package/scripts/sw-discovery.sh +112 -30
  77. package/scripts/sw-doc-fleet.sh +7 -17
  78. package/scripts/sw-docs-agent.sh +6 -16
  79. package/scripts/sw-docs.sh +4 -12
  80. package/scripts/sw-doctor.sh +134 -43
  81. package/scripts/sw-dora.sh +11 -19
  82. package/scripts/sw-durable.sh +35 -52
  83. package/scripts/sw-e2e-orchestrator.sh +11 -27
  84. package/scripts/sw-eventbus.sh +115 -115
  85. package/scripts/sw-evidence.sh +748 -0
  86. package/scripts/sw-feedback.sh +3 -13
  87. package/scripts/sw-fix.sh +2 -20
  88. package/scripts/sw-fleet-discover.sh +1 -11
  89. package/scripts/sw-fleet-viz.sh +10 -18
  90. package/scripts/sw-fleet.sh +13 -17
  91. package/scripts/sw-github-app.sh +6 -16
  92. package/scripts/sw-github-checks.sh +1 -11
  93. package/scripts/sw-github-deploy.sh +1 -11
  94. package/scripts/sw-github-graphql.sh +2 -12
  95. package/scripts/sw-guild.sh +1 -11
  96. package/scripts/sw-heartbeat.sh +49 -12
  97. package/scripts/sw-hygiene.sh +45 -43
  98. package/scripts/sw-incident.sh +284 -67
  99. package/scripts/sw-init.sh +35 -37
  100. package/scripts/sw-instrument.sh +1 -11
  101. package/scripts/sw-intelligence.sh +362 -51
  102. package/scripts/sw-jira.sh +5 -14
  103. package/scripts/sw-launchd.sh +2 -12
  104. package/scripts/sw-linear.sh +8 -17
  105. package/scripts/sw-logs.sh +4 -12
  106. package/scripts/sw-loop.sh +641 -90
  107. package/scripts/sw-memory.sh +243 -17
  108. package/scripts/sw-mission-control.sh +2 -12
  109. package/scripts/sw-model-router.sh +73 -34
  110. package/scripts/sw-otel.sh +11 -21
  111. package/scripts/sw-oversight.sh +1 -11
  112. package/scripts/sw-patrol-meta.sh +5 -11
  113. package/scripts/sw-pipeline-composer.sh +7 -17
  114. package/scripts/sw-pipeline-vitals.sh +1 -11
  115. package/scripts/sw-pipeline.sh +478 -122
  116. package/scripts/sw-pm.sh +2 -12
  117. package/scripts/sw-pr-lifecycle.sh +203 -29
  118. package/scripts/sw-predictive.sh +16 -22
  119. package/scripts/sw-prep.sh +6 -16
  120. package/scripts/sw-ps.sh +1 -11
  121. package/scripts/sw-public-dashboard.sh +2 -12
  122. package/scripts/sw-quality.sh +77 -10
  123. package/scripts/sw-reaper.sh +1 -11
  124. package/scripts/sw-recruit.sh +15 -25
  125. package/scripts/sw-regression.sh +11 -21
  126. package/scripts/sw-release-manager.sh +19 -28
  127. package/scripts/sw-release.sh +8 -16
  128. package/scripts/sw-remote.sh +1 -11
  129. package/scripts/sw-replay.sh +48 -44
  130. package/scripts/sw-retro.sh +70 -92
  131. package/scripts/sw-review-rerun.sh +220 -0
  132. package/scripts/sw-scale.sh +109 -32
  133. package/scripts/sw-security-audit.sh +12 -22
  134. package/scripts/sw-self-optimize.sh +239 -23
  135. package/scripts/sw-session.sh +3 -13
  136. package/scripts/sw-setup.sh +8 -18
  137. package/scripts/sw-standup.sh +5 -15
  138. package/scripts/sw-status.sh +32 -23
  139. package/scripts/sw-strategic.sh +129 -13
  140. package/scripts/sw-stream.sh +1 -11
  141. package/scripts/sw-swarm.sh +76 -36
  142. package/scripts/sw-team-stages.sh +10 -20
  143. package/scripts/sw-templates.sh +4 -14
  144. package/scripts/sw-testgen.sh +3 -13
  145. package/scripts/sw-tmux-pipeline.sh +1 -19
  146. package/scripts/sw-tmux-role-color.sh +0 -10
  147. package/scripts/sw-tmux-status.sh +3 -11
  148. package/scripts/sw-tmux.sh +2 -20
  149. package/scripts/sw-trace.sh +1 -19
  150. package/scripts/sw-tracker-github.sh +0 -10
  151. package/scripts/sw-tracker-jira.sh +1 -11
  152. package/scripts/sw-tracker-linear.sh +1 -11
  153. package/scripts/sw-tracker.sh +7 -24
  154. package/scripts/sw-triage.sh +24 -34
  155. package/scripts/sw-upgrade.sh +5 -23
  156. package/scripts/sw-ux.sh +1 -19
  157. package/scripts/sw-webhook.sh +18 -32
  158. package/scripts/sw-widgets.sh +3 -21
  159. package/scripts/sw-worktree.sh +11 -27
  160. package/scripts/update-homebrew-sha.sh +67 -0
  161. package/templates/pipelines/tdd.json +72 -0
  162. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -23,6 +23,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
23
  # Canonical helpers (colors, output, events)
24
24
  # shellcheck source=lib/helpers.sh
25
25
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
26
+ [[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
26
27
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
27
28
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
28
29
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
@@ -40,15 +41,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
40
41
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
41
42
  }
42
43
  fi
43
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
44
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
45
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
46
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
47
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
48
- RED="${RED:-\033[38;2;248;113;113m}"
49
- DIM="${DIM:-\033[2m}"
50
- BOLD="${BOLD:-\033[1m}"
51
- RESET="${RESET:-\033[0m}"
52
44
 
53
45
  # ─── Defaults ─────────────────────────────────────────────────────────────────
54
46
  GOAL=""
@@ -67,11 +59,11 @@ MAX_TURNS=""
67
59
  RESUME=false
68
60
  VERBOSE=false
69
61
  MAX_ITERATIONS_EXPLICIT=false
70
- MAX_RESTARTS=0
62
+ MAX_RESTARTS=$(_config_get_int "loop.max_restarts" 0 2>/dev/null || echo 0)
71
63
  SESSION_RESTART=false
72
64
  RESTART_COUNT=0
73
65
  REPO_OVERRIDE=""
74
- VERSION="2.3.1"
66
+ VERSION="3.0.0"
75
67
 
76
68
  # ─── Token Tracking ─────────────────────────────────────────────────────────
77
69
  LOOP_INPUT_TOKENS=0
@@ -335,13 +327,13 @@ if [[ -n "$REPO_OVERRIDE" ]]; then
335
327
  info "Using repository: $(pwd)"
336
328
  fi
337
329
 
338
- if ! command -v claude &>/dev/null; then
330
+ if ! command -v claude >/dev/null 2>&1; then
339
331
  error "Claude Code CLI not found. Install it first:"
340
332
  echo -e " ${DIM}npm install -g @anthropic-ai/claude-code${RESET}"
341
333
  exit 1
342
334
  fi
343
335
 
344
- if ! git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then
336
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
345
337
  error "Not inside a git repository. The loop requires git for progress tracking."
346
338
  exit 1
347
339
  fi
@@ -351,15 +343,15 @@ ORIGINAL_GOAL="$GOAL"
351
343
 
352
344
  # ─── Timeout Detection ────────────────────────────────────────────────────────
353
345
  TIMEOUT_CMD=""
354
- if command -v timeout &>/dev/null; then
346
+ if command -v timeout >/dev/null 2>&1; then
355
347
  TIMEOUT_CMD="timeout"
356
- elif command -v gtimeout &>/dev/null; then
348
+ elif command -v gtimeout >/dev/null 2>&1; then
357
349
  TIMEOUT_CMD="gtimeout"
358
350
  fi
359
- CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-1800}" # 30 min default
351
+ CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-$(_config_get_int "loop.claude_timeout" 1800 2>/dev/null || echo 1800)}" # 30 min default
360
352
 
361
353
  if [[ "$AGENTS" -gt 1 ]]; then
362
- if ! command -v tmux &>/dev/null; then
354
+ if ! command -v tmux >/dev/null 2>&1; then
363
355
  error "tmux is required for multi-agent mode."
364
356
  echo -e " ${DIM}brew install tmux${RESET} (macOS)"
365
357
  exit 1
@@ -393,7 +385,7 @@ select_adaptive_model() {
393
385
  fi
394
386
  # Read learned model routing
395
387
  local _routing_file="${HOME}/.shipwright/optimization/model-routing.json"
396
- if [[ -f "$_routing_file" ]] && command -v jq &>/dev/null; then
388
+ if [[ -f "$_routing_file" ]] && command -v jq >/dev/null 2>&1; then
397
389
  local _routed_model
398
390
  _routed_model=$(jq -r --arg r "$role" '.routes[$r].model // ""' "$_routing_file" 2>/dev/null) || true
399
391
  if [[ -n "${_routed_model:-}" && "${_routed_model:-}" != "null" ]]; then
@@ -403,7 +395,7 @@ select_adaptive_model() {
403
395
  fi
404
396
 
405
397
  # Try intelligence-based recommendation
406
- if type intelligence_recommend_model &>/dev/null 2>&1; then
398
+ if type intelligence_recommend_model >/dev/null 2>&1; then
407
399
  local rec
408
400
  rec=$(intelligence_recommend_model "$role" "${COMPLEXITY:-5}" "${BUDGET:-0}" 2>/dev/null || echo "")
409
401
  if [[ -n "$rec" ]]; then
@@ -422,7 +414,7 @@ select_adaptive_model() {
422
414
  select_audit_model() {
423
415
  local default_model="haiku"
424
416
  local opt_file="$HOME/.shipwright/optimization/audit-tuning.json"
425
- if [[ -f "$opt_file" ]] && command -v jq &>/dev/null; then
417
+ if [[ -f "$opt_file" ]] && command -v jq >/dev/null 2>&1; then
426
418
  local success_rate
427
419
  success_rate=$(jq -r '.haiku_success_rate // 100' "$opt_file" 2>/dev/null || echo "100")
428
420
  if [[ "${success_rate%%.*}" -lt 90 ]]; then
@@ -442,7 +434,7 @@ accumulate_loop_tokens() {
442
434
  [[ ! -f "$log_file" ]] && return 0
443
435
 
444
436
  # If jq is available and the file looks like JSON, parse structured output
445
- if command -v jq &>/dev/null && head -c1 "$log_file" 2>/dev/null | grep -q '\['; then
437
+ if command -v jq >/dev/null 2>&1 && head -c1 "$log_file" 2>/dev/null | grep -q '\['; then
446
438
  local input_tok output_tok cache_read cache_create cost_usd
447
439
  # The result object is the last element in the JSON array
448
440
  input_tok=$(jq -r '.[-1].usage.input_tokens // 0' "$log_file" 2>/dev/null || echo "0")
@@ -458,6 +450,20 @@ accumulate_loop_tokens() {
458
450
  local cost_millicents
459
451
  cost_millicents=$(echo "$cost_usd" | awk '{printf "%.0f", $1 * 100000}' 2>/dev/null || echo "0")
460
452
  LOOP_COST_MILLICENTS=$(( ${LOOP_COST_MILLICENTS:-0} + ${cost_millicents:-0} ))
453
+ else
454
+ # Estimate cost from tokens when Claude doesn't provide it (rates per million tokens)
455
+ local total_in total_out
456
+ total_in=$(( ${input_tok:-0} + ${cache_read:-0} + ${cache_create:-0} ))
457
+ total_out=${output_tok:-0}
458
+ local cost=0
459
+ case "${MODEL:-${CLAUDE_MODEL:-sonnet}}" in
460
+ *opus*) cost=$(awk -v i="$total_in" -v o="$total_out" 'BEGIN{printf "%.6f", (i * 15 + o * 75) / 1000000}') ;;
461
+ *sonnet*) cost=$(awk -v i="$total_in" -v o="$total_out" 'BEGIN{printf "%.6f", (i * 3 + o * 15) / 1000000}') ;;
462
+ *haiku*) cost=$(awk -v i="$total_in" -v o="$total_out" 'BEGIN{printf "%.6f", (i * 0.25 + o * 1.25) / 1000000}') ;;
463
+ *) cost=$(awk -v i="$total_in" -v o="$total_out" 'BEGIN{printf "%.6f", (i * 3 + o * 15) / 1000000}') ;;
464
+ esac
465
+ cost_millicents=$(echo "$cost" | awk '{printf "%.0f", $1 * 100000}' 2>/dev/null || echo "0")
466
+ LOOP_COST_MILLICENTS=$(( ${LOOP_COST_MILLICENTS:-0} + ${cost_millicents:-0} ))
461
467
  fi
462
468
  else
463
469
  # Fallback: regex-based parsing for non-JSON output
@@ -491,7 +497,7 @@ _extract_text_from_json() {
491
497
  first_char=$(head -c1 "$json_file" 2>/dev/null || true)
492
498
 
493
499
  # Case 2: Valid JSON array — extract .result from last element
494
- if [[ "$first_char" == "[" ]] && command -v jq &>/dev/null; then
500
+ if [[ "$first_char" == "[" ]] && command -v jq >/dev/null 2>&1; then
495
501
  local extracted
496
502
  extracted=$(jq -r '.[-1].result // empty' "$json_file" 2>/dev/null) || true
497
503
  if [[ -n "$extracted" ]]; then
@@ -542,7 +548,7 @@ TOKJSON
542
548
  # Reads tuning config for smarter iteration/circuit-breaker thresholds.
543
549
  apply_adaptive_budget() {
544
550
  local tuning_file="$HOME/.shipwright/optimization/loop-tuning.json"
545
- if [[ -f "$tuning_file" ]] && command -v jq &>/dev/null; then
551
+ if [[ -f "$tuning_file" ]] && command -v jq >/dev/null 2>&1; then
546
552
  local tuned_max tuned_ext tuned_ext_count tuned_cb
547
553
  tuned_max=$(jq -r '.max_iterations // ""' "$tuning_file" 2>/dev/null || echo "")
548
554
  tuned_ext=$(jq -r '.extension_size // ""' "$tuning_file" 2>/dev/null || echo "")
@@ -560,7 +566,7 @@ apply_adaptive_budget() {
560
566
 
561
567
  # Read learned iteration model
562
568
  local _iter_model="${HOME}/.shipwright/optimization/iteration-model.json"
563
- if [[ -f "$_iter_model" ]] && ! $MAX_ITERATIONS_EXPLICIT && command -v jq &>/dev/null; then
569
+ if [[ -f "$_iter_model" ]] && ! $MAX_ITERATIONS_EXPLICIT && command -v jq >/dev/null 2>&1; then
564
570
  local _complexity="${ISSUE_COMPLEXITY:-${COMPLEXITY:-medium}}"
565
571
  local _predicted_max
566
572
  _predicted_max=$(jq -r --arg c "$_complexity" '.predictions[$c].max_iterations // ""' "$_iter_model" 2>/dev/null) || true
@@ -571,7 +577,7 @@ apply_adaptive_budget() {
571
577
  fi
572
578
 
573
579
  # Try intelligence-based iteration estimate
574
- if type intelligence_estimate_iterations &>/dev/null 2>&1 && ! $MAX_ITERATIONS_EXPLICIT; then
580
+ if type intelligence_estimate_iterations >/dev/null 2>&1 && ! $MAX_ITERATIONS_EXPLICIT; then
575
581
  local est
576
582
  est=$(intelligence_estimate_iterations "${GOAL:-}" "${COMPLEXITY:-5}" 2>/dev/null || echo "")
577
583
  if [[ -n "$est" && "$est" =~ ^[0-9]+$ ]]; then
@@ -619,9 +625,6 @@ compute_velocity_avg() {
619
625
 
620
626
  # ─── Timing Helpers ───────────────────────────────────────────────────────────
621
627
 
622
- now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
623
- now_epoch() { date +%s; }
624
-
625
628
  format_duration() {
626
629
  local secs="$1"
627
630
  local mins=$(( secs / 60 ))
@@ -730,6 +733,21 @@ resume_state() {
730
733
  exit 0
731
734
  fi
732
735
 
736
+ # Restore Claude context for meaningful resume (source so exports persist to this shell)
737
+ if [[ -f "$SCRIPT_DIR/sw-checkpoint.sh" ]] && [[ -d "${PROJECT_ROOT:-}" ]]; then
738
+ source "$SCRIPT_DIR/sw-checkpoint.sh"
739
+ local _orig_pwd="$PWD"
740
+ cd "$PROJECT_ROOT" 2>/dev/null || true
741
+ if checkpoint_restore_context "build" 2>/dev/null; then
742
+ RESUMED_FROM_ITERATION="${RESTORED_ITERATION:-}"
743
+ RESUMED_MODIFIED="${RESTORED_MODIFIED:-}"
744
+ RESUMED_FINDINGS="${RESTORED_FINDINGS:-}"
745
+ RESUMED_TEST_OUTPUT="${RESTORED_TEST_OUTPUT:-}"
746
+ [[ -n "${RESTORED_ITERATION:-}" && "${RESTORED_ITERATION:-0}" -gt 0 ]] && info "Restored context from iteration ${RESTORED_ITERATION}"
747
+ fi
748
+ cd "$_orig_pwd" 2>/dev/null || true
749
+ fi
750
+
733
751
  success "Resumed: iteration $ITERATION/$MAX_ITERATIONS"
734
752
  }
735
753
 
@@ -807,6 +825,85 @@ ${entry}"
807
825
  fi
808
826
  }
809
827
 
828
+ # ─── Semantic Validation for Claude Output ─────────────────────────────────────
829
+ # Validates changed files before commit to catch syntax errors and API error leakage.
830
+ validate_claude_output() {
831
+ local workdir="${1:-.}"
832
+ local issues=0
833
+
834
+ # Check for syntax errors in changed files
835
+ local changed_files
836
+ changed_files=$(git -C "$workdir" diff --cached --name-only 2>/dev/null || git -C "$workdir" diff --name-only 2>/dev/null)
837
+
838
+ while IFS= read -r file; do
839
+ [[ -z "$file" ]] && continue
840
+ [[ ! -f "$workdir/$file" ]] && continue
841
+
842
+ case "$file" in
843
+ *.sh)
844
+ if ! bash -n "$workdir/$file" 2>/dev/null; then
845
+ warn "Syntax error in shell script: $file"
846
+ issues=$((issues + 1))
847
+ fi
848
+ ;;
849
+ *.py)
850
+ if command -v python3 >/dev/null 2>&1; then
851
+ if ! python3 -c "import ast, sys; ast.parse(open(sys.argv[1]).read())" "$workdir/$file" 2>/dev/null; then
852
+ warn "Syntax error in Python file: $file"
853
+ issues=$((issues + 1))
854
+ fi
855
+ fi
856
+ ;;
857
+ *.json)
858
+ if command -v jq >/dev/null 2>&1 && ! jq empty "$workdir/$file" 2>/dev/null; then
859
+ warn "Invalid JSON: $file"
860
+ issues=$((issues + 1))
861
+ fi
862
+ ;;
863
+ *.ts|*.js|*.tsx|*.jsx)
864
+ # Check for obvious corruption: API error text leaked into source
865
+ if grep -qE '(CLAUDE_CODE_OAUTH_TOKEN|api key|rate limit|503 Service|DOCTYPE html)' "$workdir/$file" 2>/dev/null; then
866
+ warn "Claude API error leaked into source file: $file"
867
+ issues=$((issues + 1))
868
+ fi
869
+ ;;
870
+ esac
871
+ done <<< "$changed_files"
872
+
873
+ # Check for obviously corrupt output (API errors dumped as code)
874
+ local total_changed
875
+ total_changed=$(echo "$changed_files" | grep -c '.' 2>/dev/null || echo "0")
876
+ if [[ "$total_changed" -eq 0 ]]; then
877
+ warn "Claude iteration produced no file changes"
878
+ issues=$((issues + 1))
879
+ fi
880
+
881
+ return "$issues"
882
+ }
883
+
884
+ # ─── Budget Gate (hard stop when exhausted) ───────────────────────────────────
885
+ check_budget_gate() {
886
+ [[ ! -x "$SCRIPT_DIR/sw-cost.sh" ]] && return 0
887
+ local remaining
888
+ remaining=$(bash "$SCRIPT_DIR/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
889
+ [[ -z "$remaining" ]] && return 0
890
+ [[ "$remaining" == "unlimited" ]] && return 0
891
+
892
+ # Parse remaining as float, check if <= 0
893
+ if awk -v r="$remaining" 'BEGIN { exit !(r <= 0) }' 2>/dev/null; then
894
+ error "Budget exhausted (remaining: \$${remaining}) — stopping pipeline"
895
+ emit_event "pipeline.budget_exhausted" "remaining=$remaining"
896
+ return 1
897
+ fi
898
+
899
+ # Warn at 10% threshold (remaining < 1.0 when typical job ~$5+)
900
+ if awk -v r="$remaining" 'BEGIN { exit !(r < 1.0) }' 2>/dev/null; then
901
+ warn "Budget low: \$${remaining} remaining"
902
+ fi
903
+
904
+ return 0
905
+ }
906
+
810
907
  # ─── Git Helpers ──────────────────────────────────────────────────────────────
811
908
 
812
909
  git_commit_count() {
@@ -834,6 +931,14 @@ git_auto_commit() {
834
931
  fi
835
932
 
836
933
  git -C "$work_dir" add -A 2>/dev/null || true
934
+
935
+ # Semantic validation before commit — skip commit if validation fails
936
+ if ! validate_claude_output "$work_dir"; then
937
+ warn "Validation failed — skipping commit for this iteration"
938
+ git -C "$work_dir" reset --hard HEAD 2>/dev/null || true
939
+ return 1
940
+ fi
941
+
837
942
  git -C "$work_dir" commit -m "loop: iteration $ITERATION — autonomous progress" --no-verify 2>/dev/null || return 1
838
943
  return 0
839
944
  }
@@ -897,7 +1002,7 @@ check_completion() {
897
1002
 
898
1003
  check_circuit_breaker() {
899
1004
  # Vitals-driven circuit breaker (preferred over static threshold)
900
- if type pipeline_compute_vitals &>/dev/null 2>&1 && type pipeline_health_verdict &>/dev/null 2>&1; then
1005
+ if type pipeline_compute_vitals >/dev/null 2>&1 && type pipeline_health_verdict >/dev/null 2>&1; then
901
1006
  local _vitals_json _verdict
902
1007
  local _loop_state="${STATE_FILE:-}"
903
1008
  local _loop_artifacts="${ARTIFACTS_DIR:-}"
@@ -989,6 +1094,113 @@ check_max_iterations() {
989
1094
  return 1
990
1095
  }
991
1096
 
1097
+ # ─── Failure Diagnosis ─────────────────────────────────────────────────────────
1098
+ # Pattern-based root-cause classification for smarter retries (no Claude needed).
1099
+ # Returns markdown context to inject into the next iteration's goal.
1100
+
1101
+ diagnose_failure() {
1102
+ local error_output="$1"
1103
+ local changed_files="$2"
1104
+ local iteration="$3"
1105
+
1106
+ local diagnosis=""
1107
+ local strategy="retry_with_context" # default
1108
+
1109
+ # Pattern-based classification (fast, no Claude needed)
1110
+ if echo "$error_output" | grep -qiE 'import.*not found|cannot find module|no module named'; then
1111
+ diagnosis="missing_import"
1112
+ strategy="fix_imports"
1113
+ elif echo "$error_output" | grep -qiE 'syntax error|unexpected token|parse error'; then
1114
+ diagnosis="syntax_error"
1115
+ strategy="fix_syntax"
1116
+ elif echo "$error_output" | grep -qiE 'type.*not assignable|type error|TypeError'; then
1117
+ diagnosis="type_error"
1118
+ strategy="fix_types"
1119
+ elif echo "$error_output" | grep -qiE 'undefined.*variable|not defined|ReferenceError'; then
1120
+ diagnosis="undefined_reference"
1121
+ strategy="fix_references"
1122
+ elif echo "$error_output" | grep -qiE 'timeout|timed out|ETIMEDOUT'; then
1123
+ diagnosis="timeout"
1124
+ strategy="optimize_performance"
1125
+ elif echo "$error_output" | grep -qiE 'assertion.*fail|expect.*to|AssertionError'; then
1126
+ diagnosis="test_assertion"
1127
+ strategy="fix_logic"
1128
+ elif echo "$error_output" | grep -qiE 'permission denied|EACCES|forbidden'; then
1129
+ diagnosis="permission_error"
1130
+ strategy="fix_permissions"
1131
+ elif echo "$error_output" | grep -qiE 'out of memory|heap|OOM|ENOMEM'; then
1132
+ diagnosis="resource_error"
1133
+ strategy="reduce_resource_usage"
1134
+ else
1135
+ diagnosis="unknown"
1136
+ strategy="retry_with_context"
1137
+ fi
1138
+
1139
+ # Check if we've seen this diagnosis before in this session
1140
+ local diagnosis_file="${LOG_DIR:-/tmp}/diagnoses.txt"
1141
+ local repeat_count=0
1142
+ if [[ -f "$diagnosis_file" ]]; then
1143
+ repeat_count=$(grep -c "^${diagnosis}$" "$diagnosis_file" 2>/dev/null || echo "0")
1144
+ fi
1145
+ echo "$diagnosis" >> "$diagnosis_file"
1146
+
1147
+ # Escalate strategy if same diagnosis repeats
1148
+ if [[ "$repeat_count" -ge 2 ]]; then
1149
+ strategy="alternative_approach"
1150
+ fi
1151
+
1152
+ # Try memory-based fix lookup
1153
+ local known_fix=""
1154
+ if type memory_query_fix_for_error &>/dev/null; then
1155
+ local fix_json
1156
+ fix_json=$(memory_query_fix_for_error "$error_output" 2>/dev/null || true)
1157
+ if [[ -n "$fix_json" && "$fix_json" != "null" ]]; then
1158
+ known_fix=$(echo "$fix_json" | jq -r '.fix // ""' 2>/dev/null | head -5)
1159
+ fi
1160
+ fi
1161
+
1162
+ # Build diagnosis context for Claude
1163
+ local diagnosis_context="## Failure Diagnosis (Iteration $iteration)
1164
+ Classification: $diagnosis
1165
+ Strategy: $strategy
1166
+ Repeat count: $repeat_count"
1167
+
1168
+ if [[ -n "$known_fix" ]]; then
1169
+ diagnosis_context+="
1170
+ Known fix from memory: $known_fix"
1171
+ fi
1172
+
1173
+ # Strategy-specific guidance
1174
+ case "$strategy" in
1175
+ fix_imports)
1176
+ diagnosis_context+="
1177
+ INSTRUCTION: The error is about missing imports/modules. Check that all imports are correct, packages are installed, and paths are right. Do NOT change the logic - just fix the imports."
1178
+ ;;
1179
+ fix_syntax)
1180
+ diagnosis_context+="
1181
+ INSTRUCTION: This is a syntax error. Carefully check the exact line mentioned in the error. Look for missing brackets, semicolons, commas, or mismatched quotes."
1182
+ ;;
1183
+ fix_types)
1184
+ diagnosis_context+="
1185
+ INSTRUCTION: Type mismatch error. Check the types at the error location. Ensure function signatures match their usage."
1186
+ ;;
1187
+ fix_logic)
1188
+ diagnosis_context+="
1189
+ INSTRUCTION: Test assertion failure. The code logic is wrong, not the syntax. Re-read the test expectations and fix the implementation to match."
1190
+ ;;
1191
+ alternative_approach)
1192
+ diagnosis_context+="
1193
+ INSTRUCTION: This error has occurred $repeat_count times. The previous approach is not working. Try a FUNDAMENTALLY DIFFERENT approach:
1194
+ - If you were modifying existing code, try rewriting the function from scratch
1195
+ - If you were using one library, try a different one
1196
+ - If you were adding to a file, try creating a new file instead
1197
+ - Step back and reconsider the requirements"
1198
+ ;;
1199
+ esac
1200
+
1201
+ echo "$diagnosis_context"
1202
+ }
1203
+
992
1204
  # ─── Test Gate ────────────────────────────────────────────────────────────────
993
1205
 
994
1206
  run_test_gate() {
@@ -1018,9 +1230,9 @@ run_test_gate() {
1018
1230
  # Wrap test command with timeout (5 min default) to prevent hanging
1019
1231
  local test_timeout="${SW_TEST_TIMEOUT:-300}"
1020
1232
  local test_wrapper="$active_test_cmd"
1021
- if command -v timeout &>/dev/null; then
1233
+ if command -v timeout >/dev/null 2>&1; then
1022
1234
  test_wrapper="timeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
1023
- elif command -v gtimeout &>/dev/null; then
1235
+ elif command -v gtimeout >/dev/null 2>&1; then
1024
1236
  test_wrapper="gtimeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
1025
1237
  fi
1026
1238
  if bash -c "$test_wrapper" > "$test_log" 2>&1; then
@@ -1072,7 +1284,7 @@ write_error_summary() {
1072
1284
  local tmp_json="${error_json}.tmp.$$"
1073
1285
 
1074
1286
  # Build JSON with jq (preferred) or plain-text fallback
1075
- if command -v jq &>/dev/null; then
1287
+ if command -v jq >/dev/null 2>&1; then
1076
1288
  jq -n \
1077
1289
  --argjson iteration "${ITERATION:-0}" \
1078
1290
  --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
@@ -1298,6 +1510,79 @@ guard_completion() {
1298
1510
  return 0
1299
1511
  }
1300
1512
 
1513
+ # ─── Context Window Management ───────────────────────────────────────────────
1514
+ # Prevents prompt from exceeding Claude's context limit (~200K tokens).
1515
+ # Trims least-critical sections first when over budget.
1516
+
1517
+ CONTEXT_BUDGET_CHARS="${CONTEXT_BUDGET_CHARS:-180000}" # ~45K tokens at 4 chars/token
1518
+
1519
+ manage_context_window() {
1520
+ local prompt="$1"
1521
+ local budget="${CONTEXT_BUDGET_CHARS}"
1522
+ local current_len=${#prompt}
1523
+
1524
+ if [[ "$current_len" -le "$budget" ]]; then
1525
+ echo "$prompt"
1526
+ return
1527
+ fi
1528
+
1529
+ # Over budget — progressively trim sections (least important first)
1530
+ local trimmed="$prompt"
1531
+
1532
+ # 1. Trim DORA/Performance baselines (least critical for code generation)
1533
+ if [[ "${#trimmed}" -gt "$budget" ]]; then
1534
+ trimmed=$(echo "$trimmed" | awk '/^## Performance Baselines/{skip=1; next} skip && /^## [^#]/{skip=0} !skip{print}')
1535
+ fi
1536
+
1537
+ # 2. Trim file hotspots to top 5
1538
+ if [[ "${#trimmed}" -gt "$budget" ]]; then
1539
+ trimmed=$(echo "$trimmed" | awk '/## File Hotspots/{p=1; c=0} p && /^- /{c++; if(c>5) next} {print}')
1540
+ fi
1541
+
1542
+ # 3. Trim git log to last 10 entries
1543
+ if [[ "${#trimmed}" -gt "$budget" ]]; then
1544
+ trimmed=$(echo "$trimmed" | awk '/## Recent Git Activity/{p=1; c=0} p && /^[a-f0-9]/{c++; if(c>10) next} {print}')
1545
+ fi
1546
+
1547
+ # 4. Truncate memory context to first 20K chars
1548
+ if [[ "${#trimmed}" -gt "$budget" ]]; then
1549
+ trimmed=$(echo "$trimmed" | awk -v max=20000 '
1550
+ /## Memory Context/{mem=1; skip_rest=0; chars=0; print; next}
1551
+ mem && /^## [^#]/{mem=0; print; next}
1552
+ mem{chars+=length($0)+1; if(chars>max){print "... (memory truncated for context budget)"; skip_rest=1; mem=0; next}}
1553
+ skip_rest && /^## [^#]/{skip_rest=0; print; next}
1554
+ skip_rest{next}
1555
+ {print}
1556
+ ')
1557
+ fi
1558
+
1559
+ # 5. Truncate test output to last 50 lines
1560
+ if [[ "${#trimmed}" -gt "$budget" ]]; then
1561
+ trimmed=$(echo "$trimmed" | awk '
1562
+ /## Test Results/{found=1; buf=""; print; next}
1563
+ found && /^## [^#]/{found=0; n=split(buf,arr,"\n"); start=(n>50)?(n-49):1; for(i=start;i<=n;i++) if(arr[i]!="") print arr[i]; print; next}
1564
+ found{buf=buf $0 "\n"; next}
1565
+ {print}
1566
+ ')
1567
+ fi
1568
+
1569
+ # 6. Last resort: hard truncate with notice
1570
+ if [[ "${#trimmed}" -gt "$budget" ]]; then
1571
+ trimmed="${trimmed:0:$budget}
1572
+
1573
+ ... [CONTEXT TRUNCATED: prompt exceeded ${budget} char budget. Focus on the goal and most recent errors.]"
1574
+ fi
1575
+
1576
+ # Log the trimming
1577
+ local final_len=${#trimmed}
1578
+ if [[ "$final_len" -lt "$current_len" ]]; then
1579
+ warn "Context trimmed from ${current_len} to ${final_len} chars (budget: ${budget})"
1580
+ emit_event "loop.context_trimmed" "original=$current_len" "trimmed=$final_len" "budget=$budget" 2>/dev/null || true
1581
+ fi
1582
+
1583
+ echo "$trimmed"
1584
+ }
1585
+
1301
1586
  # ─── Prompt Composition ──────────────────────────────────────────────────────
1302
1587
 
1303
1588
  compose_prompt() {
@@ -1348,7 +1633,7 @@ Fix these specific errors. Each line above is one distinct error from the test o
1348
1633
 
1349
1634
  # Memory context injection (failure patterns + past learnings)
1350
1635
  local memory_section=""
1351
- if type memory_inject_context &>/dev/null 2>&1; then
1636
+ if type memory_inject_context >/dev/null 2>&1; then
1352
1637
  memory_section="$(memory_inject_context "build" 2>/dev/null || true)"
1353
1638
  elif [[ -f "$SCRIPT_DIR/sw-memory.sh" ]]; then
1354
1639
  memory_section="$("$SCRIPT_DIR/sw-memory.sh" inject build 2>/dev/null || true)"
@@ -1356,7 +1641,7 @@ Fix these specific errors. Each line above is one distinct error from the test o
1356
1641
 
1357
1642
  # DORA baselines for context
1358
1643
  local dora_section=""
1359
- if type memory_get_dora_baseline &>/dev/null 2>&1; then
1644
+ if type memory_get_dora_baseline >/dev/null 2>&1; then
1360
1645
  local dora_json
1361
1646
  dora_json="$(memory_get_dora_baseline 7 2>/dev/null || echo "{}")"
1362
1647
  local dora_total
@@ -1385,7 +1670,7 @@ $(cat "$memory_refresh_file")"
1385
1670
  local intelligence_section=""
1386
1671
  if [[ "${NO_GITHUB:-}" != "true" ]]; then
1387
1672
  # File hotspots — top 5 most-changed files
1388
- if type gh_file_change_frequency &>/dev/null 2>&1; then
1673
+ if type gh_file_change_frequency >/dev/null 2>&1; then
1389
1674
  local hotspots
1390
1675
  hotspots=$(gh_file_change_frequency 2>/dev/null | head -5 || true)
1391
1676
  if [[ -n "$hotspots" ]]; then
@@ -1396,7 +1681,7 @@ ${hotspots}"
1396
1681
  fi
1397
1682
 
1398
1683
  # CODEOWNERS context
1399
- if type gh_codeowners &>/dev/null 2>&1; then
1684
+ if type gh_codeowners >/dev/null 2>&1; then
1400
1685
  local owners
1401
1686
  owners=$(gh_codeowners 2>/dev/null | head -10 || true)
1402
1687
  if [[ -n "$owners" ]]; then
@@ -1407,7 +1692,7 @@ ${owners}"
1407
1692
  fi
1408
1693
 
1409
1694
  # Active security alerts
1410
- if type gh_security_alerts &>/dev/null 2>&1; then
1695
+ if type gh_security_alerts >/dev/null 2>&1; then
1411
1696
  local alerts
1412
1697
  alerts=$(gh_security_alerts 2>/dev/null | head -5 || true)
1413
1698
  if [[ -n "$alerts" ]]; then
@@ -1459,6 +1744,34 @@ ${last_error}"
1459
1744
  # Stuckness detection — compare last 3 iteration outputs
1460
1745
  local stuckness_section=""
1461
1746
  stuckness_section="$(detect_stuckness)"
1747
+ local _stuck_ret=$?
1748
+ local stuckness_detected=false
1749
+ [[ "$_stuck_ret" -eq 0 ]] && stuckness_detected=true
1750
+
1751
+ # Strategy exploration when stuck — append alternative strategy to GOAL
1752
+ if [[ "$stuckness_detected" == "true" ]]; then
1753
+ local last_error diagnosis
1754
+ last_error=$(tail -1 "${ARTIFACTS_DIR:-${PROJECT_ROOT:-.}/.claude/pipeline-artifacts}/error-log.jsonl" 2>/dev/null | jq -r '"Type: \(.type), Exit: \(.exit_code), Error: \(.error | split("\n") | first)"' 2>/dev/null || true)
1755
+ [[ -z "$last_error" || "$last_error" == "null" ]] && last_error="unknown"
1756
+ diagnosis="${STUCKNESS_DIAGNOSIS:-}"
1757
+ local alt_strategy
1758
+ alt_strategy=$(explore_alternative_strategy "$last_error" "${ITERATION:-0}" "$diagnosis")
1759
+ GOAL="${GOAL}
1760
+
1761
+ ${alt_strategy}"
1762
+
1763
+ # Handle model escalation
1764
+ if [[ "${ESCALATE_MODEL:-}" == "true" ]]; then
1765
+ if [[ -f "$SCRIPT_DIR/sw-model-router.sh" ]]; then
1766
+ source "$SCRIPT_DIR/sw-model-router.sh" 2>/dev/null || true
1767
+ fi
1768
+ if type escalate_model &>/dev/null; then
1769
+ MODEL=$(escalate_model "${MODEL:-sonnet}")
1770
+ info "Escalated to model: $MODEL"
1771
+ fi
1772
+ unset ESCALATE_MODEL
1773
+ fi
1774
+ fi
1462
1775
 
1463
1776
  # Session restart context — inject previous session progress
1464
1777
  local restart_section=""
@@ -1470,9 +1783,36 @@ You are starting a FRESH session after the previous one exhausted its iterations
1470
1783
  Read the progress above and continue from where it left off. Do NOT repeat work already done."
1471
1784
  fi
1472
1785
 
1786
+ # Resume-from-checkpoint context — reconstruct Claude context for meaningful resume
1787
+ local resume_section=""
1788
+ if [[ -n "${RESUMED_FROM_ITERATION:-}" && "${RESUMED_FROM_ITERATION:-0}" -gt 0 ]]; then
1789
+ local _test_tail=" (none recorded)"
1790
+ [[ -n "${RESUMED_TEST_OUTPUT:-}" ]] && _test_tail="$(echo "$RESUMED_TEST_OUTPUT" | tail -20)"
1791
+ resume_section="## RESUMING FROM ITERATION ${RESUMED_FROM_ITERATION}
1792
+
1793
+ Continue from where you left off. Do NOT repeat work already done.
1794
+
1795
+ Previous work modified these files:
1796
+ ${RESUMED_MODIFIED:- (none recorded)}
1797
+
1798
+ Previous findings/errors from earlier iterations:
1799
+ ${RESUMED_FINDINGS:- (none recorded)}
1800
+
1801
+ Last test output (fix any failures, tail):
1802
+ ${_test_tail}
1803
+
1804
+ ---
1805
+ "
1806
+ # Clear after first use so we don't keep injecting on every iteration
1807
+ RESUMED_FROM_ITERATION=""
1808
+ RESUMED_MODIFIED=""
1809
+ RESUMED_FINDINGS=""
1810
+ RESUMED_TEST_OUTPUT=""
1811
+ fi
1812
+
1473
1813
  cat <<PROMPT
1474
1814
  You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
1475
-
1815
+ ${resume_section}
1476
1816
  ## Your Goal
1477
1817
  ${GOAL}
1478
1818
 
@@ -1522,55 +1862,163 @@ PROMPT
1522
1862
  }
1523
1863
 
1524
1864
  # ─── Stuckness Detection ─────────────────────────────────────────────────────
1525
- # Compares last 3 iteration log outputs for high overlap (>90% similar lines).
1865
+ # Multi-signal detection: text overlap, git diff hash, error repetition, exit code pattern, iteration budget.
1866
+ # Returns 0 when stuck, 1 when not. Outputs stuckness section and sets STUCKNESS_HINT when stuck.
1867
+ # When stuck: increments STUCKNESS_COUNT, emits event; if STUCKNESS_COUNT >= 3, caller triggers session restart.
1868
+ STUCKNESS_COUNT=0
1869
+ STUCKNESS_TRACKING_FILE=""
1870
+
1871
+ record_iteration_stuckness_data() {
1872
+ local exit_code="${1:-0}"
1873
+ [[ -z "$LOG_DIR" ]] && return 0
1874
+ local tracking_file="${STUCKNESS_TRACKING_FILE:-$LOG_DIR/stuckness-tracking.txt}"
1875
+ local diff_hash error_hash
1876
+ diff_hash=$(git -C "${PROJECT_ROOT:-.}" diff HEAD 2>/dev/null | (md5 -q 2>/dev/null || md5sum 2>/dev/null | cut -d' ' -f1) || echo "none")
1877
+ local error_log="${ARTIFACTS_DIR:-${STATE_DIR:-${PROJECT_ROOT:-.}/.claude}/pipeline-artifacts}/error-log.jsonl"
1878
+ if [[ -f "$error_log" ]]; then
1879
+ error_hash=$(tail -5 "$error_log" 2>/dev/null | sort -u | (md5 -q 2>/dev/null || md5sum 2>/dev/null | cut -d' ' -f1) || echo "none")
1880
+ else
1881
+ error_hash="none"
1882
+ fi
1883
+ echo "${diff_hash}|${error_hash}|${exit_code}" >> "$tracking_file"
1884
+ }
1885
+
1526
1886
  detect_stuckness() {
1527
- if [[ "$ITERATION" -lt 3 ]]; then
1528
- return 0
1887
+ STUCKNESS_HINT=""
1888
+ local iteration="${ITERATION:-0}"
1889
+ local stuckness_signals=0
1890
+ local stuckness_reasons=()
1891
+ local tracking_file="${STUCKNESS_TRACKING_FILE:-$LOG_DIR/stuckness-tracking.txt}"
1892
+ local tracking_lines
1893
+ tracking_lines=$(wc -l < "$tracking_file" 2>/dev/null || echo "0")
1894
+
1895
+ # Signal 1: Text overlap (existing logic) — compare last 2 iteration logs
1896
+ if [[ "$iteration" -ge 3 ]]; then
1897
+ local log1="$LOG_DIR/iteration-$(( iteration - 1 )).log"
1898
+ local log2="$LOG_DIR/iteration-$(( iteration - 2 )).log"
1899
+ local log3="$LOG_DIR/iteration-$(( iteration - 3 )).log"
1900
+
1901
+ if [[ -f "$log1" && -f "$log2" ]]; then
1902
+ local lines1 lines2 common total overlap_pct
1903
+ lines1=$(tail -50 "$log1" 2>/dev/null | grep -v '^$' | sort || true)
1904
+ lines2=$(tail -50 "$log2" 2>/dev/null | grep -v '^$' | sort || true)
1905
+
1906
+ if [[ -n "$lines1" && -n "$lines2" ]]; then
1907
+ total=$(echo "$lines1" | wc -l | tr -d ' ')
1908
+ common=$(comm -12 <(echo "$lines1") <(echo "$lines2") 2>/dev/null | wc -l | tr -d ' ' || echo "0")
1909
+ if [[ "$total" -gt 0 ]]; then
1910
+ overlap_pct=$(( common * 100 / total ))
1911
+ else
1912
+ overlap_pct=0
1913
+ fi
1914
+ if [[ "${overlap_pct:-0}" -ge 90 ]]; then
1915
+ stuckness_signals=$((stuckness_signals + 1))
1916
+ stuckness_reasons+=("high text overlap (${overlap_pct}%) between iterations")
1917
+ fi
1918
+ fi
1919
+ fi
1529
1920
  fi
1530
1921
 
1531
- local log1="$LOG_DIR/iteration-$(( ITERATION - 1 )).log"
1532
- local log2="$LOG_DIR/iteration-$(( ITERATION - 2 )).log"
1533
- local log3="$LOG_DIR/iteration-$(( ITERATION - 3 )).log"
1922
+ # Signal 2: Git diff hash — last 3 iterations produced zero or identical diffs
1923
+ if [[ -f "$tracking_file" ]] && [[ "$tracking_lines" -ge 3 ]]; then
1924
+ local last_three
1925
+ last_three=$(tail -3 "$tracking_file" 2>/dev/null | cut -d'|' -f1 || true)
1926
+ local unique_hashes
1927
+ unique_hashes=$(echo "$last_three" | sort -u | grep -v '^$' | wc -l | tr -d ' ')
1928
+ if [[ "$unique_hashes" -le 1 ]] && [[ -n "$last_three" ]]; then
1929
+ stuckness_signals=$((stuckness_signals + 1))
1930
+ stuckness_reasons+=("identical or zero git diffs in last 3 iterations")
1931
+ fi
1932
+ fi
1534
1933
 
1535
- # Need at least 2 previous logs
1536
- if [[ ! -f "$log1" || ! -f "$log2" ]]; then
1537
- return 0
1934
+ # Signal 3: Error repetition same error hash in last 3 iterations
1935
+ if [[ -f "$tracking_file" ]] && [[ "$tracking_lines" -ge 3 ]]; then
1936
+ local last_three_errors
1937
+ last_three_errors=$(tail -3 "$tracking_file" 2>/dev/null | cut -d'|' -f2 || true)
1938
+ local unique_error_hashes
1939
+ unique_error_hashes=$(echo "$last_three_errors" | sort -u | grep -v '^none$' | grep -v '^$' | wc -l | tr -d ' ')
1940
+ if [[ "$unique_error_hashes" -eq 1 ]] && [[ -n "$(echo "$last_three_errors" | grep -v '^none$')" ]]; then
1941
+ stuckness_signals=$((stuckness_signals + 1))
1942
+ stuckness_reasons+=("same error in last 3 iterations")
1943
+ fi
1538
1944
  fi
1539
1945
 
1540
- # Compare last 50 lines of each (ignoring timestamps and blank lines)
1541
- local lines1 lines2 common total overlap_pct
1542
- lines1=$(tail -50 "$log1" 2>/dev/null | grep -v '^$' | sort || true)
1543
- lines2=$(tail -50 "$log2" 2>/dev/null | grep -v '^$' | sort || true)
1946
+ # Signal 4: Same error repeating 3+ times (legacy check on error-log content)
1947
+ local error_log
1948
+ error_log="${ARTIFACTS_DIR:-$PROJECT_ROOT/.claude/pipeline-artifacts}/error-log.jsonl"
1949
+ if [[ -f "$error_log" ]]; then
1950
+ local last_errors
1951
+ last_errors=$(tail -5 "$error_log" 2>/dev/null | jq -r '.error // .message // .error_hash // empty' 2>/dev/null | sort | uniq -c | sort -rn | head -1 || true)
1952
+ local repeat_count
1953
+ repeat_count=$(echo "$last_errors" | awk '{print $1}' 2>/dev/null || echo "0")
1954
+ if [[ "${repeat_count:-0}" -ge 3 ]]; then
1955
+ stuckness_signals=$((stuckness_signals + 1))
1956
+ stuckness_reasons+=("same error repeated ${repeat_count} times")
1957
+ fi
1958
+ fi
1544
1959
 
1545
- if [[ -z "$lines1" || -z "$lines2" ]]; then
1546
- return 0
1960
+ # Signal 5: Exit code pattern — last 3 iterations had same non-zero exit code
1961
+ if [[ -f "$tracking_file" ]] && [[ "$tracking_lines" -ge 3 ]]; then
1962
+ local last_three_exits
1963
+ last_three_exits=$(tail -3 "$tracking_file" 2>/dev/null | cut -d'|' -f3 || true)
1964
+ local first_exit
1965
+ first_exit=$(echo "$last_three_exits" | head -1)
1966
+ if [[ "$first_exit" =~ ^[0-9]+$ ]] && [[ "$first_exit" -ne 0 ]]; then
1967
+ local all_same=true
1968
+ while IFS= read -r ex; do
1969
+ [[ "$ex" != "$first_exit" ]] && all_same=false
1970
+ done <<< "$last_three_exits"
1971
+ if [[ "$all_same" == true ]]; then
1972
+ stuckness_signals=$((stuckness_signals + 1))
1973
+ stuckness_reasons+=("same non-zero exit code (${first_exit}) in last 3 iterations")
1974
+ fi
1975
+ fi
1547
1976
  fi
1548
1977
 
1549
- total=$(echo "$lines1" | wc -l | tr -d ' ')
1550
- common=$(comm -12 <(echo "$lines1") <(echo "$lines2") 2>/dev/null | wc -l | tr -d ' ' || echo "0")
1978
+ # Signal 6: Git diff size no or minimal code changes (existing)
1979
+ local diff_lines
1980
+ diff_lines=$(git -C "${PROJECT_ROOT:-.}" diff HEAD 2>/dev/null | wc -l | tr -d ' ' || echo "0")
1981
+ if [[ "${diff_lines:-0}" -lt 5 ]] && [[ "$iteration" -gt 2 ]]; then
1982
+ stuckness_signals=$((stuckness_signals + 1))
1983
+ stuckness_reasons+=("no code changes in last iteration")
1984
+ fi
1551
1985
 
1552
- if [[ "$total" -gt 0 ]]; then
1553
- overlap_pct=$(( common * 100 / total ))
1554
- else
1555
- overlap_pct=0
1986
+ # Signal 7: Iteration budget used >70% without passing tests
1987
+ local max_iter="${MAX_ITERATIONS:-20}"
1988
+ local progress_pct=0
1989
+ if [[ "$max_iter" -gt 0 ]]; then
1990
+ progress_pct=$(( iteration * 100 / max_iter ))
1556
1991
  fi
1992
+ if [[ "$progress_pct" -gt 70 ]] && [[ "${TEST_PASSED:-false}" != "true" ]]; then
1993
+ stuckness_signals=$((stuckness_signals + 1))
1994
+ stuckness_reasons+=("used ${progress_pct}% of iteration budget without passing tests")
1995
+ fi
1996
+
1997
+ # Decision: 2+ signals = stuck
1998
+ if [[ "$stuckness_signals" -ge 2 ]]; then
1999
+ STUCKNESS_COUNT=$(( STUCKNESS_COUNT + 1 ))
2000
+ STUCKNESS_DIAGNOSIS="${stuckness_reasons[*]}"
2001
+ if type emit_event >/dev/null 2>&1; then
2002
+ emit_event "loop.stuckness_detected" "signals=$stuckness_signals" "count=$STUCKNESS_COUNT" "iteration=$iteration" "reasons=${stuckness_reasons[*]}"
2003
+ fi
2004
+ STUCKNESS_HINT="IMPORTANT: The loop appears stuck. Previous approaches have not worked. You MUST try a fundamentally different strategy. Reasons: ${stuckness_reasons[*]}"
2005
+ warn "Stuckness detected (${stuckness_signals} signals, count ${STUCKNESS_COUNT}): ${stuckness_reasons[*]}"
1557
2006
 
1558
- if [[ "$overlap_pct" -ge 90 ]]; then
1559
2007
  local diff_summary=""
1560
- if [[ -f "$log3" ]]; then
2008
+ local log1="$LOG_DIR/iteration-$(( iteration - 1 )).log"
2009
+ local log3="$LOG_DIR/iteration-$(( iteration - 3 )).log"
2010
+ if [[ -f "$log3" && -f "$log1" ]]; then
1561
2011
  diff_summary=$(diff <(tail -30 "$log3" 2>/dev/null) <(tail -30 "$log1" 2>/dev/null) 2>/dev/null | head -10 || true)
1562
2012
  fi
1563
2013
 
1564
- # Gather memory-based alternative approaches
1565
2014
  local alternatives=""
1566
- if type memory_inject_context &>/dev/null 2>&1; then
2015
+ if type memory_inject_context >/dev/null 2>&1; then
1567
2016
  alternatives=$(memory_inject_context "build" 2>/dev/null | grep -i "fix:" | head -3 || true)
1568
2017
  fi
1569
2018
 
1570
2019
  cat <<STUCK_SECTION
1571
2020
  ## Stuckness Detected
1572
- Your last ${CONSECUTIVE_FAILURES:-2}+ iterations produced very similar output (${overlap_pct}% overlap).
1573
- You appear to be stuck on the same approach.
2021
+ ${STUCKNESS_HINT}
1574
2022
 
1575
2023
  ${diff_summary:+Changes between recent iterations:
1576
2024
  $diff_summary
@@ -1584,7 +2032,10 @@ Try a fundamentally different approach:
1584
2032
  - Check if there's a dependency or configuration issue blocking progress
1585
2033
  - Read error messages more carefully — the root cause may differ from your assumption
1586
2034
  STUCK_SECTION
2035
+ return 0
1587
2036
  fi
2037
+
2038
+ return 1
1588
2039
  }
1589
2040
 
1590
2041
  compose_audit_section() {
@@ -1675,7 +2126,7 @@ compose_worker_prompt() {
1675
2126
  local role_desc=""
1676
2127
  # Try to pull description from recruit's roles DB first
1677
2128
  local recruit_roles_db="${HOME}/.shipwright/recruitment/roles.json"
1678
- if [[ -f "$recruit_roles_db" ]] && command -v jq &>/dev/null; then
2129
+ if [[ -f "$recruit_roles_db" ]] && command -v jq >/dev/null 2>&1; then
1679
2130
  local recruit_desc
1680
2131
  recruit_desc=$(jq -r --arg r "$role" '.[$r].description // ""' "$recruit_roles_db" 2>/dev/null) || true
1681
2132
  if [[ -n "$recruit_desc" && "$recruit_desc" != "null" ]]; then
@@ -1735,6 +2186,12 @@ run_claude_iteration() {
1735
2186
  local json_file="$LOG_DIR/iteration-${ITERATION}.json"
1736
2187
  local prompt
1737
2188
  prompt="$(compose_prompt)"
2189
+ local final_prompt
2190
+ final_prompt=$(manage_context_window "$prompt")
2191
+
2192
+ local prompt_chars=${#final_prompt}
2193
+ local approx_tokens=$((prompt_chars / 4))
2194
+ info "Prompt: ~${approx_tokens} tokens (${prompt_chars} chars)"
1738
2195
 
1739
2196
  local flags
1740
2197
  flags="$(build_claude_flags)"
@@ -1750,9 +2207,9 @@ run_claude_iteration() {
1750
2207
  # shellcheck disable=SC2086
1751
2208
  local err_file="${json_file%.json}.stderr"
1752
2209
  if [[ -n "$TIMEOUT_CMD" ]]; then
1753
- $TIMEOUT_CMD "$CLAUDE_TIMEOUT" claude -p "$prompt" $flags > "$json_file" 2>"$err_file" &
2210
+ $TIMEOUT_CMD "$CLAUDE_TIMEOUT" claude -p "$final_prompt" $flags > "$json_file" 2>"$err_file" &
1754
2211
  else
1755
- claude -p "$prompt" $flags > "$json_file" 2>"$err_file" &
2212
+ claude -p "$final_prompt" $flags > "$json_file" 2>"$err_file" &
1756
2213
  fi
1757
2214
  CHILD_PID=$!
1758
2215
  wait "$CHILD_PID" 2>/dev/null || exit_code=$?
@@ -1835,12 +2292,13 @@ show_summary() {
1835
2292
 
1836
2293
  local status_display
1837
2294
  case "$STATUS" in
1838
- complete) status_display="${GREEN}✓ Complete (LOOP_COMPLETE detected)${RESET}" ;;
1839
- circuit_breaker) status_display="${RED}✗ Circuit breaker tripped${RESET}" ;;
1840
- max_iterations) status_display="${YELLOW}⚠ Max iterations reached${RESET}" ;;
1841
- interrupted) status_display="${YELLOW} Interrupted by user${RESET}" ;;
1842
- error) status_display="${RED} Error${RESET}" ;;
1843
- *) status_display="${DIM}$STATUS${RESET}" ;;
2295
+ complete) status_display="${GREEN}✓ Complete (LOOP_COMPLETE detected)${RESET}" ;;
2296
+ circuit_breaker) status_display="${RED}✗ Circuit breaker tripped${RESET}" ;;
2297
+ max_iterations) status_display="${YELLOW}⚠ Max iterations reached${RESET}" ;;
2298
+ budget_exhausted) status_display="${RED} Budget exhausted${RESET}" ;;
2299
+ interrupted) status_display="${YELLOW} Interrupted by user${RESET}" ;;
2300
+ error) status_display="${RED}✗ Error${RESET}" ;;
2301
+ *) status_display="${DIM}$STATUS${RESET}" ;;
1844
2302
  esac
1845
2303
 
1846
2304
  local test_display
@@ -1909,6 +2367,15 @@ cleanup() {
1909
2367
  --iteration "$ITERATION" \
1910
2368
  --git-sha "$(git rev-parse HEAD 2>/dev/null || echo unknown)" 2>/dev/null || true
1911
2369
 
2370
+ # Save Claude context for meaningful resume (goal, findings, test output)
2371
+ export SW_LOOP_GOAL="$GOAL"
2372
+ export SW_LOOP_ITERATION="$ITERATION"
2373
+ export SW_LOOP_STATUS="$STATUS"
2374
+ export SW_LOOP_TEST_OUTPUT="${TEST_OUTPUT:-}"
2375
+ export SW_LOOP_FINDINGS="${LOG_ENTRIES:-}"
2376
+ export SW_LOOP_MODIFIED="$(git diff --name-only HEAD 2>/dev/null | head -50 | tr '\n' ',' | sed 's/,$//')"
2377
+ "$SCRIPT_DIR/sw-checkpoint.sh" save-context --stage build 2>/dev/null || true
2378
+
1912
2379
  # Clear heartbeat
1913
2380
  "$SCRIPT_DIR/sw-heartbeat.sh" clear "${PIPELINE_JOB_ID:-loop-$$}" 2>/dev/null || true
1914
2381
 
@@ -1934,7 +2401,7 @@ setup_worktrees() {
1934
2401
  fi
1935
2402
 
1936
2403
  # Create branch if it doesn't exist
1937
- if ! git -C "$PROJECT_ROOT" rev-parse --verify "$branch_name" &>/dev/null; then
2404
+ if ! git -C "$PROJECT_ROOT" rev-parse --verify "$branch_name" >/dev/null 2>&1; then
1938
2405
  git -C "$PROJECT_ROOT" branch "$branch_name" HEAD 2>/dev/null || true
1939
2406
  fi
1940
2407
 
@@ -1996,6 +2463,17 @@ CONSECUTIVE_FAILURES=0
1996
2463
  echo -e "${CYAN}${BOLD}▸${RESET} Agent ${AGENT_NUM}/${TOTAL_AGENTS} starting in ${WORK_DIR}"
1997
2464
 
1998
2465
  while [[ "$ITERATION" -lt "$MAX_ITERATIONS" ]]; do
2466
+ # Budget gate: stop if daily budget exhausted
2467
+ if [[ -x "$SCRIPT_DIR/sw-cost.sh" ]]; then
2468
+ budget_remaining=$("$SCRIPT_DIR/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
2469
+ if [[ -n "$budget_remaining" && "$budget_remaining" != "unlimited" ]]; then
2470
+ if awk -v r="$budget_remaining" 'BEGIN { exit !(r <= 0) }' 2>/dev/null; then
2471
+ echo -e " ${RED}✗${RESET} Budget exhausted (\$${budget_remaining}) — stopping agent ${AGENT_NUM}"
2472
+ break
2473
+ fi
2474
+ fi
2475
+ fi
2476
+
1999
2477
  ITERATION=$(( ITERATION + 1 ))
2000
2478
  echo -e "\n${CYAN}${BOLD}▸${RESET} Agent ${AGENT_NUM} — Iteration ${ITERATION}/${MAX_ITERATIONS}"
2001
2479
 
@@ -2064,8 +2542,12 @@ PROMPT
2064
2542
  # Auto-commit
2065
2543
  git add -A 2>/dev/null || true
2066
2544
  if git commit -m "agent-${AGENT_NUM}: iteration ${ITERATION}" --no-verify 2>/dev/null; then
2067
- git push origin "loop/agent-${AGENT_NUM}" 2>/dev/null || true
2068
- echo -e " ${GREEN}✓${RESET} Committed and pushed"
2545
+ if ! git push origin "loop/agent-${AGENT_NUM}" 2>/dev/null; then
2546
+ echo -e " ${YELLOW}⚠${RESET} git push failed for loop/agent-${AGENT_NUM} — remote may be out of sync"
2547
+ type emit_event >/dev/null 2>&1 && emit_event "loop.push_failed" "branch=loop/agent-${AGENT_NUM}"
2548
+ else
2549
+ echo -e " ${GREEN}✓${RESET} Committed and pushed"
2550
+ fi
2069
2551
  fi
2070
2552
 
2071
2553
  # Circuit breaker: check for progress
@@ -2083,7 +2565,7 @@ PROMPT
2083
2565
  break
2084
2566
  fi
2085
2567
 
2086
- sleep 2
2568
+ sleep __SLEEP_BETWEEN_ITERATIONS__
2087
2569
  done
2088
2570
 
2089
2571
  echo -e "\n${DIM}Agent ${AGENT_NUM} finished after ${ITERATION} iterations${RESET}"
@@ -2094,11 +2576,14 @@ WORKEREOF
2094
2576
  sed_i "s|__AGENT_NUM__|${agent_num}|g" "$worker_script"
2095
2577
  sed_i "s|__TOTAL_AGENTS__|${total_agents}|g" "$worker_script"
2096
2578
  sed_i "s|__MAX_ITERATIONS__|${MAX_ITERATIONS}|g" "$worker_script"
2579
+ sed_i "s|__SLEEP_BETWEEN_ITERATIONS__|$(_config_get_int "loop.sleep_between_iterations" 2 2>/dev/null || echo 2)|g" "$worker_script"
2097
2580
  # Paths and commands may contain sed-special chars — use awk
2098
2581
  awk -v val="$wt_path" '{gsub(/__WORK_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
2099
2582
  && mv "${worker_script}.tmp" "$worker_script"
2100
2583
  awk -v val="$LOG_DIR" '{gsub(/__LOG_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
2101
2584
  && mv "${worker_script}.tmp" "$worker_script"
2585
+ awk -v val="$SCRIPT_DIR" '{gsub(/__SCRIPT_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
2586
+ && mv "${worker_script}.tmp" "$worker_script"
2102
2587
  awk -v val="$TEST_CMD" '{gsub(/__TEST_CMD__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
2103
2588
  && mv "${worker_script}.tmp" "$worker_script"
2104
2589
  awk -v val="$claude_flags" '{gsub(/__CLAUDE_FLAGS__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
@@ -2137,11 +2622,12 @@ launch_multi_agent() {
2137
2622
  local worker_script
2138
2623
  worker_script="$(generate_worker_script "$i" "$AGENTS")"
2139
2624
 
2140
- tmux split-window -t "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT"
2625
+ local worker_pane_id
2626
+ worker_pane_id="$(tmux split-window -t "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT" -P -F '#{pane_id}')"
2141
2627
  sleep 0.1
2142
- tmux send-keys -t "$MULTI_WINDOW_NAME" "printf '\\033]2;agent-${i}\\033\\\\'" Enter
2628
+ tmux send-keys -t "$worker_pane_id" "printf '\\033]2;agent-${i}\\033\\\\'" Enter
2143
2629
  sleep 0.1
2144
- tmux send-keys -t "$MULTI_WINDOW_NAME" "bash '$worker_script'" Enter
2630
+ tmux send-keys -t "$worker_pane_id" "bash '$worker_script'" Enter
2145
2631
  done
2146
2632
 
2147
2633
  # Layout: monitor pane on top (35%), worker agents tile below
@@ -2181,7 +2667,7 @@ wait_for_multi_completion() {
2181
2667
  latest_log="$(ls -t "$LOG_DIR"/agent-"${i}"-iter-*.log 2>/dev/null | head -1)"
2182
2668
  if [[ -n "$latest_log" ]]; then
2183
2669
  local age
2184
- age=$(( $(now_epoch) - $(stat -f %m "$latest_log" 2>/dev/null || echo 0) ))
2670
+ age=$(( $(now_epoch) - $(file_mtime "$latest_log") ))
2185
2671
  if [[ $age -lt 300 ]]; then # Active within 5 minutes
2186
2672
  running=$(( running + 1 ))
2187
2673
  fi
@@ -2200,7 +2686,7 @@ wait_for_multi_completion() {
2200
2686
  fi
2201
2687
  fi
2202
2688
 
2203
- sleep 5
2689
+ sleep "$(_config_get_int "loop.multi_agent_sleep" 5 2>/dev/null || echo 5)"
2204
2690
  done
2205
2691
  }
2206
2692
 
@@ -2239,6 +2725,10 @@ run_single_agent_loop() {
2239
2725
 
2240
2726
  # Track applied memory fix patterns for outcome recording
2241
2727
  _applied_fix_pattern=""
2728
+ STUCKNESS_COUNT=0
2729
+ STUCKNESS_TRACKING_FILE="$LOG_DIR/stuckness-tracking.txt"
2730
+ : > "$STUCKNESS_TRACKING_FILE" 2>/dev/null || true
2731
+ : > "${LOG_DIR:-/tmp}/strategy-attempts.txt" 2>/dev/null || true
2242
2732
 
2243
2733
  show_banner
2244
2734
 
@@ -2246,17 +2736,48 @@ run_single_agent_loop() {
2246
2736
  # Pre-checks (before incrementing — ITERATION tracks completed count)
2247
2737
  check_circuit_breaker || break
2248
2738
  check_max_iterations || break
2739
+ check_budget_gate || {
2740
+ STATUS="budget_exhausted"
2741
+ write_state
2742
+ write_progress
2743
+ error "Budget exhausted — stopping pipeline"
2744
+ show_summary
2745
+ return 1
2746
+ }
2249
2747
  ITERATION=$(( ITERATION + 1 ))
2250
2748
 
2251
- # Try memory-based fix suggestion on retry after test failure
2749
+ # Root-cause diagnosis and memory-based fix on retry after test failure
2252
2750
  if [[ "${TEST_PASSED:-}" == "false" ]]; then
2751
+ # Source memory module for diagnosis and fix lookup
2752
+ [[ -f "$SCRIPT_DIR/sw-memory.sh" ]] && source "$SCRIPT_DIR/sw-memory.sh" 2>/dev/null || true
2753
+
2754
+ # Capture failure for memory (enables memory_analyze_failure and future fix lookup)
2755
+ if type memory_capture_failure &>/dev/null && [[ -n "${TEST_OUTPUT:-}" ]]; then
2756
+ memory_capture_failure "test" "$TEST_OUTPUT" 2>/dev/null || true
2757
+ fi
2758
+
2759
+ # Pattern-based diagnosis (no Claude needed) — inject into goal for smarter retry
2760
+ local _changed_files=""
2761
+ _changed_files=$(git diff --name-only HEAD 2>/dev/null | head -50 | tr '\n' ',' | sed 's/,$//')
2762
+ local _diagnosis
2763
+ _diagnosis=$(diagnose_failure "${TEST_OUTPUT:-}" "$_changed_files" "$ITERATION" 2>/dev/null || true)
2764
+
2765
+ if [[ -n "$_diagnosis" ]]; then
2766
+ GOAL="${GOAL}
2767
+
2768
+ ${_diagnosis}"
2769
+ info "Failure diagnosis injected (classification from error pattern)"
2770
+ fi
2771
+
2772
+ # Memory-based fix suggestion (from past successful fixes)
2253
2773
  local _last_error=""
2254
2774
  local _prev_log="$LOG_DIR/iteration-$(( ITERATION - 1 )).log"
2255
2775
  if [[ -f "$_prev_log" ]]; then
2256
2776
  _last_error=$(tail -20 "$_prev_log" 2>/dev/null | grep -iE '(error|fail|exception)' | head -1 || true)
2257
2777
  fi
2778
+ [[ -z "$_last_error" ]] && _last_error=$(echo "${TEST_OUTPUT:-}" | head -3 | tr '\n' ' ')
2258
2779
  local _fix_suggestion=""
2259
- if type memory_closed_loop_inject &>/dev/null 2>&1 && [[ -n "${_last_error:-}" ]]; then
2780
+ if type memory_closed_loop_inject >/dev/null 2>&1 && [[ -n "${_last_error:-}" ]]; then
2260
2781
  _fix_suggestion=$(memory_closed_loop_inject "$_last_error" 2>/dev/null) || true
2261
2782
  fi
2262
2783
  if [[ -n "${_fix_suggestion:-}" ]]; then
@@ -2266,6 +2787,14 @@ run_single_agent_loop() {
2266
2787
  ${GOAL}"
2267
2788
  info "Memory fix injected: ${_fix_suggestion:0:80}"
2268
2789
  fi
2790
+
2791
+ # Analyze failure via Claude (background, non-blocking) for richer root_cause/fix in memory
2792
+ if type memory_analyze_failure &>/dev/null && [[ "${INTELLIGENCE_ENABLED:-auto}" != "false" ]]; then
2793
+ local _test_log="${TEST_LOG_FILE:-$LOG_DIR/tests-iter-$(( ITERATION - 1 )).log}"
2794
+ if [[ -f "$_test_log" ]]; then
2795
+ memory_analyze_failure "$_test_log" "test" 2>/dev/null &
2796
+ fi
2797
+ fi
2269
2798
  fi
2270
2799
 
2271
2800
  # Run Claude
@@ -2274,6 +2803,9 @@ ${GOAL}"
2274
2803
 
2275
2804
  local log_file="$LOG_DIR/iteration-${ITERATION}.log"
2276
2805
 
2806
+ # Record iteration data for stuckness detection (diff hash, error hash, exit code)
2807
+ record_iteration_stuckness_data "$exit_code"
2808
+
2277
2809
  # Detect fatal CLI errors (API key, auth, network) — abort immediately
2278
2810
  if check_fatal_error "$log_file" "$exit_code"; then
2279
2811
  STATUS="error"
@@ -2285,7 +2817,7 @@ ${GOAL}"
2285
2817
  fi
2286
2818
 
2287
2819
  # Mid-loop memory refresh — re-query with current error context after iteration 3
2288
- if [[ "$ITERATION" -ge 3 ]] && type memory_inject_context &>/dev/null 2>&1; then
2820
+ if [[ "$ITERATION" -ge 3 ]] && type memory_inject_context >/dev/null 2>&1; then
2289
2821
  local refresh_ctx
2290
2822
  refresh_ctx=$(tail -20 "$log_file" 2>/dev/null || true)
2291
2823
  if [[ -n "$refresh_ctx" ]]; then
@@ -2331,7 +2863,7 @@ ${GOAL}"
2331
2863
 
2332
2864
  # Track fix outcome for memory effectiveness
2333
2865
  if [[ -n "${_applied_fix_pattern:-}" ]]; then
2334
- if type memory_record_fix_outcome &>/dev/null 2>&1; then
2866
+ if type memory_record_fix_outcome >/dev/null 2>&1; then
2335
2867
  if [[ "${TEST_PASSED:-}" == "true" ]]; then
2336
2868
  memory_record_fix_outcome "$_applied_fix_pattern" "true" "true" 2>/dev/null || true
2337
2869
  else
@@ -2341,6 +2873,15 @@ ${GOAL}"
2341
2873
  _applied_fix_pattern=""
2342
2874
  fi
2343
2875
 
2876
+ # Save Claude context for checkpoint resume (goal, findings, test output)
2877
+ export SW_LOOP_GOAL="$GOAL"
2878
+ export SW_LOOP_ITERATION="$ITERATION"
2879
+ export SW_LOOP_STATUS="${STATUS:-running}"
2880
+ export SW_LOOP_TEST_OUTPUT="${TEST_OUTPUT:-}"
2881
+ export SW_LOOP_FINDINGS="${LOG_ENTRIES:-}"
2882
+ export SW_LOOP_MODIFIED="$(git diff --name-only HEAD 2>/dev/null | head -50 | tr '\n' ',' | sed 's/,$//')"
2883
+ "$SCRIPT_DIR/sw-checkpoint.sh" save-context --stage build 2>/dev/null || true
2884
+
2344
2885
  # Audit agent (reviews implementer's work)
2345
2886
  run_audit_agent
2346
2887
 
@@ -2396,7 +2937,16 @@ HUMAN FEEDBACK (received after iteration $ITERATION): $human_msg"
2396
2937
  fi
2397
2938
  fi
2398
2939
 
2399
- sleep 2
2940
+ # Stuckness-triggered restart: if detected 3+ times, break to allow session restart
2941
+ if [[ "${STUCKNESS_COUNT:-0}" -ge 3 ]]; then
2942
+ STATUS="stuck_restart"
2943
+ write_state
2944
+ write_progress
2945
+ warn "Stuckness detected 3+ times — triggering session restart"
2946
+ break
2947
+ fi
2948
+
2949
+ sleep "$(_config_get_int "loop.sleep_between_iterations" 2 2>/dev/null || echo 2)"
2400
2950
  done
2401
2951
 
2402
2952
  # Write final state after loop exits
@@ -2437,7 +2987,7 @@ run_loop_with_restarts() {
2437
2987
  fi
2438
2988
 
2439
2989
  RESTART_COUNT=$(( RESTART_COUNT + 1 ))
2440
- if type emit_event &>/dev/null 2>&1; then
2990
+ if type emit_event >/dev/null 2>&1; then
2441
2991
  emit_event "loop.restart" "restart=$RESTART_COUNT" "max=$MAX_RESTARTS" "iteration=$ITERATION"
2442
2992
  fi
2443
2993
  info "Session restart ${RESTART_COUNT}/${MAX_RESTARTS} — resetting iteration counter"
@@ -2448,6 +2998,7 @@ run_loop_with_restarts() {
2448
2998
  ITERATION=0
2449
2999
  CONSECUTIVE_FAILURES=0
2450
3000
  EXTENSION_COUNT=0
3001
+ STUCKNESS_COUNT=0
2451
3002
  STATUS="running"
2452
3003
  LOG_ENTRIES=""
2453
3004
  TEST_PASSED=""
@@ -2469,7 +3020,7 @@ run_loop_with_restarts() {
2469
3020
 
2470
3021
  write_state
2471
3022
 
2472
- sleep 2
3023
+ sleep "$(_config_get_int "loop.sleep_between_iterations" 2 2>/dev/null || echo 2)"
2473
3024
  done
2474
3025
  }
2475
3026