shipwright-cli 1.9.0 → 2.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 (117) hide show
  1. package/.claude/hooks/post-tool-use.sh +12 -5
  2. package/README.md +114 -36
  3. package/completions/_shipwright +212 -32
  4. package/completions/shipwright.bash +97 -25
  5. package/docs/strategy/01-market-research.md +619 -0
  6. package/docs/strategy/02-mission-and-brand.md +587 -0
  7. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  8. package/docs/strategy/QUICK-START.txt +289 -0
  9. package/docs/strategy/README.md +172 -0
  10. package/package.json +4 -2
  11. package/scripts/sw +217 -2
  12. package/scripts/sw-activity.sh +500 -0
  13. package/scripts/sw-adaptive.sh +925 -0
  14. package/scripts/sw-adversarial.sh +1 -1
  15. package/scripts/sw-architecture-enforcer.sh +1 -1
  16. package/scripts/sw-auth.sh +613 -0
  17. package/scripts/sw-autonomous.sh +664 -0
  18. package/scripts/sw-changelog.sh +704 -0
  19. package/scripts/sw-checkpoint.sh +79 -1
  20. package/scripts/sw-ci.sh +602 -0
  21. package/scripts/sw-cleanup.sh +192 -7
  22. package/scripts/sw-code-review.sh +637 -0
  23. package/scripts/sw-connect.sh +1 -1
  24. package/scripts/sw-context.sh +605 -0
  25. package/scripts/sw-cost.sh +1 -1
  26. package/scripts/sw-daemon.sh +812 -138
  27. package/scripts/sw-dashboard.sh +1 -1
  28. package/scripts/sw-db.sh +540 -0
  29. package/scripts/sw-decompose.sh +539 -0
  30. package/scripts/sw-deps.sh +551 -0
  31. package/scripts/sw-developer-simulation.sh +1 -1
  32. package/scripts/sw-discovery.sh +412 -0
  33. package/scripts/sw-docs-agent.sh +539 -0
  34. package/scripts/sw-docs.sh +1 -1
  35. package/scripts/sw-doctor.sh +59 -1
  36. package/scripts/sw-dora.sh +615 -0
  37. package/scripts/sw-durable.sh +710 -0
  38. package/scripts/sw-e2e-orchestrator.sh +535 -0
  39. package/scripts/sw-eventbus.sh +393 -0
  40. package/scripts/sw-feedback.sh +471 -0
  41. package/scripts/sw-fix.sh +1 -1
  42. package/scripts/sw-fleet-discover.sh +567 -0
  43. package/scripts/sw-fleet-viz.sh +404 -0
  44. package/scripts/sw-fleet.sh +8 -1
  45. package/scripts/sw-github-app.sh +596 -0
  46. package/scripts/sw-github-checks.sh +1 -1
  47. package/scripts/sw-github-deploy.sh +1 -1
  48. package/scripts/sw-github-graphql.sh +1 -1
  49. package/scripts/sw-guild.sh +569 -0
  50. package/scripts/sw-heartbeat.sh +1 -1
  51. package/scripts/sw-hygiene.sh +559 -0
  52. package/scripts/sw-incident.sh +617 -0
  53. package/scripts/sw-init.sh +88 -1
  54. package/scripts/sw-instrument.sh +699 -0
  55. package/scripts/sw-intelligence.sh +1 -1
  56. package/scripts/sw-jira.sh +1 -1
  57. package/scripts/sw-launchd.sh +366 -31
  58. package/scripts/sw-linear.sh +1 -1
  59. package/scripts/sw-logs.sh +1 -1
  60. package/scripts/sw-loop.sh +507 -51
  61. package/scripts/sw-memory.sh +198 -3
  62. package/scripts/sw-mission-control.sh +487 -0
  63. package/scripts/sw-model-router.sh +545 -0
  64. package/scripts/sw-otel.sh +596 -0
  65. package/scripts/sw-oversight.sh +689 -0
  66. package/scripts/sw-pipeline-composer.sh +8 -8
  67. package/scripts/sw-pipeline-vitals.sh +1096 -0
  68. package/scripts/sw-pipeline.sh +2451 -180
  69. package/scripts/sw-pm.sh +693 -0
  70. package/scripts/sw-pr-lifecycle.sh +522 -0
  71. package/scripts/sw-predictive.sh +1 -1
  72. package/scripts/sw-prep.sh +1 -1
  73. package/scripts/sw-ps.sh +4 -3
  74. package/scripts/sw-public-dashboard.sh +798 -0
  75. package/scripts/sw-quality.sh +595 -0
  76. package/scripts/sw-reaper.sh +5 -3
  77. package/scripts/sw-recruit.sh +573 -0
  78. package/scripts/sw-regression.sh +642 -0
  79. package/scripts/sw-release-manager.sh +736 -0
  80. package/scripts/sw-release.sh +706 -0
  81. package/scripts/sw-remote.sh +1 -1
  82. package/scripts/sw-replay.sh +520 -0
  83. package/scripts/sw-retro.sh +691 -0
  84. package/scripts/sw-scale.sh +444 -0
  85. package/scripts/sw-security-audit.sh +505 -0
  86. package/scripts/sw-self-optimize.sh +109 -8
  87. package/scripts/sw-session.sh +31 -9
  88. package/scripts/sw-setup.sh +1 -1
  89. package/scripts/sw-standup.sh +712 -0
  90. package/scripts/sw-status.sh +192 -1
  91. package/scripts/sw-strategic.sh +658 -0
  92. package/scripts/sw-stream.sh +450 -0
  93. package/scripts/sw-swarm.sh +583 -0
  94. package/scripts/sw-team-stages.sh +511 -0
  95. package/scripts/sw-templates.sh +1 -1
  96. package/scripts/sw-testgen.sh +515 -0
  97. package/scripts/sw-tmux-pipeline.sh +554 -0
  98. package/scripts/sw-tmux.sh +1 -1
  99. package/scripts/sw-trace.sh +485 -0
  100. package/scripts/sw-tracker-github.sh +188 -0
  101. package/scripts/sw-tracker-jira.sh +172 -0
  102. package/scripts/sw-tracker-linear.sh +251 -0
  103. package/scripts/sw-tracker.sh +117 -2
  104. package/scripts/sw-triage.sh +603 -0
  105. package/scripts/sw-upgrade.sh +1 -1
  106. package/scripts/sw-ux.sh +677 -0
  107. package/scripts/sw-webhook.sh +627 -0
  108. package/scripts/sw-widgets.sh +530 -0
  109. package/scripts/sw-worktree.sh +1 -1
  110. package/templates/pipelines/autonomous.json +8 -1
  111. package/templates/pipelines/cost-aware.json +21 -0
  112. package/templates/pipelines/deployed.json +40 -6
  113. package/templates/pipelines/enterprise.json +16 -2
  114. package/templates/pipelines/fast.json +19 -0
  115. package/templates/pipelines/full.json +16 -2
  116. package/templates/pipelines/hotfix.json +19 -0
  117. package/templates/pipelines/standard.json +19 -0
@@ -10,6 +10,11 @@
10
10
  set -euo pipefail
11
11
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
12
12
 
13
+ # Allow spawning Claude CLI from within a Claude Code session (daemon, fleet, etc.)
14
+ unset CLAUDECODE 2>/dev/null || true
15
+ # Ignore SIGHUP so tmux attach/detach doesn't kill long-running agent sessions
16
+ trap '' HUP
17
+
13
18
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
19
 
15
20
  # ─── Colors (matches shipwright theme) ──────────────────────────────────────────────
@@ -34,17 +39,25 @@ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
34
39
 
35
40
  # ─── Defaults ─────────────────────────────────────────────────────────────────
36
41
  GOAL=""
42
+ ORIGINAL_GOAL="" # Preserved across restarts — GOAL gets appended to
37
43
  MAX_ITERATIONS="${SW_MAX_ITERATIONS:-20}"
38
44
  TEST_CMD=""
45
+ FAST_TEST_CMD=""
46
+ FAST_TEST_INTERVAL=5
47
+ TEST_LOG_FILE=""
39
48
  MODEL="${SW_MODEL:-opus}"
40
49
  AGENTS=1
50
+ AGENT_ROLES=""
41
51
  USE_WORKTREE=false
42
52
  SKIP_PERMISSIONS=false
43
53
  MAX_TURNS=""
44
54
  RESUME=false
45
55
  VERBOSE=false
46
56
  MAX_ITERATIONS_EXPLICIT=false
47
- VERSION="1.9.0"
57
+ MAX_RESTARTS=0
58
+ SESSION_RESTART=false
59
+ RESTART_COUNT=0
60
+ VERSION="2.0.0"
48
61
 
49
62
  # ─── Flexible Iteration Defaults ────────────────────────────────────────────
50
63
  AUTO_EXTEND=true # Auto-extend iterations when work is incomplete
@@ -75,12 +88,16 @@ show_help() {
75
88
  echo -e "${BOLD}OPTIONS${RESET}"
76
89
  echo -e " ${CYAN}--max-iterations${RESET} N Max loop iterations (default: 20)"
77
90
  echo -e " ${CYAN}--test-cmd${RESET} \"cmd\" Test command to run between iterations"
91
+ echo -e " ${CYAN}--fast-test-cmd${RESET} \"cmd\" Fast/subset test command (alternates with full)"
92
+ echo -e " ${CYAN}--fast-test-interval${RESET} N Run full tests every N iterations (default: 5)"
78
93
  echo -e " ${CYAN}--model${RESET} MODEL Claude model to use (default: opus)"
79
94
  echo -e " ${CYAN}--agents${RESET} N Number of parallel agents (default: 1)"
95
+ echo -e " ${CYAN}--roles${RESET} \"r1,r2,...\" Role per agent: builder,reviewer,tester,optimizer,docs,security"
80
96
  echo -e " ${CYAN}--worktree${RESET} Use git worktrees for isolation (auto if agents > 1)"
81
97
  echo -e " ${CYAN}--skip-permissions${RESET} Pass --dangerously-skip-permissions to Claude"
82
98
  echo -e " ${CYAN}--max-turns${RESET} N Max API turns per Claude session"
83
99
  echo -e " ${CYAN}--resume${RESET} Resume from existing .claude/loop-state.md"
100
+ echo -e " ${CYAN}--max-restarts${RESET} N Max session restarts on exhaustion (default: 0)"
84
101
  echo -e " ${CYAN}--verbose${RESET} Show full Claude output (default: summary)"
85
102
  echo -e " ${CYAN}--help${RESET} Show this help"
86
103
  echo ""
@@ -175,6 +192,30 @@ while [[ $# -gt 0 ]]; do
175
192
  shift 2
176
193
  ;;
177
194
  --max-extensions=*) MAX_EXTENSIONS="${1#--max-extensions=}"; shift ;;
195
+ --fast-test-cmd)
196
+ FAST_TEST_CMD="${2:-}"
197
+ [[ -z "$FAST_TEST_CMD" ]] && { error "Missing value for --fast-test-cmd"; exit 1; }
198
+ shift 2
199
+ ;;
200
+ --fast-test-cmd=*) FAST_TEST_CMD="${1#--fast-test-cmd=}"; shift ;;
201
+ --fast-test-interval)
202
+ FAST_TEST_INTERVAL="${2:-}"
203
+ [[ -z "$FAST_TEST_INTERVAL" ]] && { error "Missing value for --fast-test-interval"; exit 1; }
204
+ shift 2
205
+ ;;
206
+ --fast-test-interval=*) FAST_TEST_INTERVAL="${1#--fast-test-interval=}"; shift ;;
207
+ --max-restarts)
208
+ MAX_RESTARTS="${2:-}"
209
+ [[ -z "$MAX_RESTARTS" ]] && { error "Missing value for --max-restarts"; exit 1; }
210
+ shift 2
211
+ ;;
212
+ --max-restarts=*) MAX_RESTARTS="${1#--max-restarts=}"; shift ;;
213
+ --roles)
214
+ AGENT_ROLES="${2:-}"
215
+ [[ -z "$AGENT_ROLES" ]] && { error "Missing value for --roles"; exit 1; }
216
+ shift 2
217
+ ;;
218
+ --roles=*) AGENT_ROLES="${1#--roles=}"; shift ;;
178
219
  --help|-h)
179
220
  show_help
180
221
  exit 0
@@ -203,6 +244,27 @@ if [[ "$AGENTS" -gt 1 ]]; then
203
244
  USE_WORKTREE=true
204
245
  fi
205
246
 
247
+ # Warn if --roles without --agents
248
+ if [[ -n "$AGENT_ROLES" ]] && [[ "$AGENTS" -le 1 ]]; then
249
+ warn "--roles requires --agents > 1 (roles are ignored in single-agent mode)"
250
+ fi
251
+
252
+ # Warn if --max-restarts with --agents > 1 (not yet supported)
253
+ if [[ "${MAX_RESTARTS:-0}" -gt 0 ]] && [[ "$AGENTS" -gt 1 ]]; then
254
+ warn "--max-restarts is ignored in multi-agent mode (restart support is single-agent only)"
255
+ MAX_RESTARTS=0
256
+ fi
257
+
258
+ # Validate numeric flags
259
+ if ! [[ "$FAST_TEST_INTERVAL" =~ ^[1-9][0-9]*$ ]]; then
260
+ error "--fast-test-interval must be a positive integer (got: $FAST_TEST_INTERVAL)"
261
+ exit 1
262
+ fi
263
+ if ! [[ "$MAX_RESTARTS" =~ ^[0-9]+$ ]]; then
264
+ error "--max-restarts must be a non-negative integer (got: $MAX_RESTARTS)"
265
+ exit 1
266
+ fi
267
+
206
268
  # ─── Validate Inputs ─────────────────────────────────────────────────────────
207
269
 
208
270
  if ! $RESUME && [[ -z "$GOAL" ]]; then
@@ -224,6 +286,9 @@ if ! git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then
224
286
  exit 1
225
287
  fi
226
288
 
289
+ # Preserve original goal before any appending (memory fixes, human feedback)
290
+ ORIGINAL_GOAL="$GOAL"
291
+
227
292
  # ─── Timeout Detection ────────────────────────────────────────────────────────
228
293
  TIMEOUT_CMD=""
229
294
  if command -v timeout &>/dev/null; then
@@ -266,6 +331,17 @@ select_adaptive_model() {
266
331
  echo "$default_model"
267
332
  return 0
268
333
  fi
334
+ # Read learned model routing
335
+ local _routing_file="${HOME}/.shipwright/optimization/model-routing.json"
336
+ if [[ -f "$_routing_file" ]] && command -v jq &>/dev/null; then
337
+ local _routed_model
338
+ _routed_model=$(jq -r --arg r "$role" '.routes[$r].model // ""' "$_routing_file" 2>/dev/null) || true
339
+ if [[ -n "${_routed_model:-}" && "${_routed_model:-}" != "null" ]]; then
340
+ echo "${_routed_model}"
341
+ return 0
342
+ fi
343
+ fi
344
+
269
345
  # Try intelligence-based recommendation
270
346
  if type intelligence_recommend_model &>/dev/null 2>&1; then
271
347
  local rec
@@ -317,6 +393,18 @@ apply_adaptive_budget() {
317
393
  [[ -n "$tuned_cb" && "$tuned_cb" != "null" ]] && CIRCUIT_BREAKER_THRESHOLD="$tuned_cb"
318
394
  fi
319
395
 
396
+ # Read learned iteration model
397
+ local _iter_model="${HOME}/.shipwright/optimization/iteration-model.json"
398
+ if [[ -f "$_iter_model" ]] && ! $MAX_ITERATIONS_EXPLICIT && command -v jq &>/dev/null; then
399
+ local _complexity="${ISSUE_COMPLEXITY:-${COMPLEXITY:-medium}}"
400
+ local _predicted_max
401
+ _predicted_max=$(jq -r --arg c "$_complexity" '.predictions[$c].max_iterations // ""' "$_iter_model" 2>/dev/null) || true
402
+ if [[ -n "${_predicted_max:-}" && "${_predicted_max:-}" != "null" && "${_predicted_max:-0}" -gt 0 ]]; then
403
+ MAX_ITERATIONS="${_predicted_max}"
404
+ info "Iteration model: ${_complexity} complexity → max ${_predicted_max} iterations"
405
+ fi
406
+ fi
407
+
320
408
  # Try intelligence-based iteration estimate
321
409
  if type intelligence_estimate_iterations &>/dev/null 2>&1 && ! $MAX_ITERATIONS_EXPLICIT; then
322
410
  local est
@@ -481,31 +569,67 @@ resume_state() {
481
569
  }
482
570
 
483
571
  write_state() {
484
- cat > "$STATE_FILE" <<EOF
485
- ---
486
- goal: "$GOAL"
487
- iteration: $ITERATION
488
- max_iterations: $MAX_ITERATIONS
489
- status: $STATUS
490
- test_cmd: "$TEST_CMD"
491
- model: $MODEL
492
- agents: $AGENTS
493
- started_at: $(now_iso)
494
- last_iteration_at: $(now_iso)
495
- consecutive_failures: $CONSECUTIVE_FAILURES
496
- total_commits: $TOTAL_COMMITS
497
- audit_enabled: $AUDIT_ENABLED
498
- audit_agent_enabled: $AUDIT_AGENT_ENABLED
499
- quality_gates_enabled: $QUALITY_GATES_ENABLED
500
- dod_file: "$DOD_FILE"
501
- auto_extend: $AUTO_EXTEND
502
- extension_count: $EXTENSION_COUNT
503
- max_extensions: $MAX_EXTENSIONS
504
- ---
505
-
506
- ## Log
507
- $LOG_ENTRIES
508
- EOF
572
+ local tmp_state="${STATE_FILE}.tmp.$$"
573
+ # Use printf instead of heredoc to avoid delimiter injection from GOAL
574
+ {
575
+ printf -- '---\n'
576
+ printf 'goal: "%s"\n' "$GOAL"
577
+ printf 'iteration: %s\n' "$ITERATION"
578
+ printf 'max_iterations: %s\n' "$MAX_ITERATIONS"
579
+ printf 'status: %s\n' "$STATUS"
580
+ printf 'test_cmd: "%s"\n' "$TEST_CMD"
581
+ printf 'model: %s\n' "$MODEL"
582
+ printf 'agents: %s\n' "$AGENTS"
583
+ printf 'started_at: %s\n' "$(now_iso)"
584
+ printf 'last_iteration_at: %s\n' "$(now_iso)"
585
+ printf 'consecutive_failures: %s\n' "$CONSECUTIVE_FAILURES"
586
+ printf 'total_commits: %s\n' "$TOTAL_COMMITS"
587
+ printf 'audit_enabled: %s\n' "$AUDIT_ENABLED"
588
+ printf 'audit_agent_enabled: %s\n' "$AUDIT_AGENT_ENABLED"
589
+ printf 'quality_gates_enabled: %s\n' "$QUALITY_GATES_ENABLED"
590
+ printf 'dod_file: "%s"\n' "$DOD_FILE"
591
+ printf 'auto_extend: %s\n' "$AUTO_EXTEND"
592
+ printf 'extension_count: %s\n' "$EXTENSION_COUNT"
593
+ printf 'max_extensions: %s\n' "$MAX_EXTENSIONS"
594
+ printf -- '---\n\n'
595
+ printf '## Log\n'
596
+ printf '%s\n' "$LOG_ENTRIES"
597
+ } > "$tmp_state"
598
+ if ! mv "$tmp_state" "$STATE_FILE" 2>/dev/null; then
599
+ warn "Failed to write state file: $STATE_FILE"
600
+ fi
601
+ }
602
+
603
+ write_progress() {
604
+ local progress_file="$LOG_DIR/progress.md"
605
+ local recent_commits
606
+ recent_commits=$(git -C "$PROJECT_ROOT" log --oneline -5 2>/dev/null || echo "(no commits)")
607
+ local changed_files
608
+ changed_files=$(git -C "$PROJECT_ROOT" diff --name-only HEAD~3 2>/dev/null | head -20 || echo "(none)")
609
+ local last_error=""
610
+ local prev_test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
611
+ if [[ -f "$prev_test_log" ]] && [[ "${TEST_PASSED:-}" == "false" ]]; then
612
+ last_error=$(tail -10 "$prev_test_log" 2>/dev/null || true)
613
+ fi
614
+
615
+ # Use printf to avoid heredoc delimiter injection from GOAL content
616
+ local tmp_progress="${progress_file}.tmp.$$"
617
+ {
618
+ printf '# Session Progress (Auto-Generated)\n\n'
619
+ printf '## Goal\n%s\n\n' "${GOAL}"
620
+ printf '## Status\n'
621
+ printf -- '- Iteration: %s/%s\n' "${ITERATION}" "${MAX_ITERATIONS}"
622
+ printf -- '- Session restart: %s/%s\n' "${RESTART_COUNT:-0}" "${MAX_RESTARTS:-0}"
623
+ printf -- '- Tests passing: %s\n' "${TEST_PASSED:-unknown}"
624
+ printf -- '- Status: %s\n\n' "${STATUS:-running}"
625
+ printf '## Recent Commits\n%s\n\n' "${recent_commits}"
626
+ printf '## Changed Files\n%s\n\n' "${changed_files}"
627
+ if [[ -n "$last_error" ]]; then
628
+ printf '## Last Error\n%s\n\n' "$last_error"
629
+ fi
630
+ printf '## Timestamp\n%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
631
+ } > "$tmp_progress" 2>/dev/null
632
+ mv "$tmp_progress" "$progress_file" 2>/dev/null || rm -f "$tmp_progress" 2>/dev/null
509
633
  }
510
634
 
511
635
  append_log_entry() {
@@ -549,11 +673,50 @@ git_auto_commit() {
549
673
  return 0
550
674
  }
551
675
 
676
+ # ─── Fatal Error Detection ────────────────────────────────────────────────────
677
+
678
+ check_fatal_error() {
679
+ local log_file="$1"
680
+ local cli_exit_code="${2:-0}"
681
+ [[ -f "$log_file" ]] || return 1
682
+
683
+ # Known fatal error patterns from Claude CLI / Anthropic API
684
+ local fatal_patterns="Invalid API key|invalid_api_key|authentication_error|API key expired"
685
+ fatal_patterns="${fatal_patterns}|rate_limit_error|overloaded_error|billing"
686
+ fatal_patterns="${fatal_patterns}|Could not resolve host|connection refused|ECONNREFUSED"
687
+ fatal_patterns="${fatal_patterns}|ANTHROPIC_API_KEY.*not set|No API key"
688
+
689
+ if grep -qiE "$fatal_patterns" "$log_file" 2>/dev/null; then
690
+ local match
691
+ match=$(grep -iE "$fatal_patterns" "$log_file" 2>/dev/null | head -1 | cut -c1-120)
692
+ error "Fatal CLI error: $match"
693
+ return 0 # fatal error detected
694
+ fi
695
+
696
+ # Non-zero exit + tiny output = likely CLI crash
697
+ if [[ "$cli_exit_code" -ne 0 ]]; then
698
+ local line_count
699
+ line_count=$(grep -cv '^$' "$log_file" 2>/dev/null || echo 0)
700
+ if [[ "$line_count" -lt 3 ]]; then
701
+ local content
702
+ content=$(head -3 "$log_file" 2>/dev/null | cut -c1-120)
703
+ error "CLI exited $cli_exit_code with minimal output: $content"
704
+ return 0
705
+ fi
706
+ fi
707
+
708
+ return 1 # no fatal error
709
+ }
710
+
552
711
  # ─── Progress & Circuit Breaker ───────────────────────────────────────────────
553
712
 
554
713
  check_progress() {
555
714
  local changes
556
- changes="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 2>/dev/null | tail -1 || echo "")"
715
+ # Exclude loop bookkeeping files only count real code changes as progress
716
+ changes="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 \
717
+ -- . ':!.claude/loop-state.md' ':!.claude/pipeline-state.md' \
718
+ ':!**/progress.md' ':!**/error-summary.json' \
719
+ 2>/dev/null | tail -1 || echo "")"
557
720
  local insertions
558
721
  insertions="$(echo "$changes" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)"
559
722
  if [[ "${insertions:-0}" -lt "$MIN_PROGRESS_LINES" ]]; then
@@ -568,6 +731,30 @@ check_completion() {
568
731
  }
569
732
 
570
733
  check_circuit_breaker() {
734
+ # Vitals-driven circuit breaker (preferred over static threshold)
735
+ if type pipeline_compute_vitals &>/dev/null 2>&1 && type pipeline_health_verdict &>/dev/null 2>&1; then
736
+ local _vitals_json _verdict
737
+ local _loop_state="${STATE_FILE:-}"
738
+ local _loop_artifacts="${ARTIFACTS_DIR:-}"
739
+ local _loop_issue="${ISSUE_NUMBER:-}"
740
+ _vitals_json=$(pipeline_compute_vitals "$_loop_state" "$_loop_artifacts" "$_loop_issue" 2>/dev/null) || true
741
+ if [[ -n "$_vitals_json" && "$_vitals_json" != "{}" ]]; then
742
+ _verdict=$(echo "$_vitals_json" | jq -r '.verdict // "continue"' 2>/dev/null || echo "continue")
743
+ if [[ "$_verdict" == "abort" ]]; then
744
+ local _health_score
745
+ _health_score=$(echo "$_vitals_json" | jq -r '.health_score // 0' 2>/dev/null || echo "0")
746
+ error "Vitals circuit breaker: health score ${_health_score}/100 — aborting (${CONSECUTIVE_FAILURES} stagnant iterations)"
747
+ STATUS="circuit_breaker"
748
+ return 1
749
+ fi
750
+ # Vitals say continue/warn/intervene — don't trip circuit breaker yet
751
+ if [[ "$_verdict" == "continue" || "$_verdict" == "warn" ]]; then
752
+ return 0
753
+ fi
754
+ fi
755
+ fi
756
+
757
+ # Fallback: static threshold circuit breaker
571
758
  if [[ "$CONSECUTIVE_FAILURES" -ge "$CIRCUIT_BREAKER_THRESHOLD" ]]; then
572
759
  error "Circuit breaker tripped: ${CIRCUIT_BREAKER_THRESHOLD} consecutive iterations with no meaningful progress."
573
760
  STATUS="circuit_breaker"
@@ -646,16 +833,88 @@ run_test_gate() {
646
833
  return
647
834
  fi
648
835
 
836
+ # Determine which test command to use this iteration
837
+ local active_test_cmd="$TEST_CMD"
838
+ local test_mode="full"
839
+ if [[ -n "$FAST_TEST_CMD" ]]; then
840
+ # Use full test every FAST_TEST_INTERVAL iterations, on first iteration, and on final iteration
841
+ if [[ "$ITERATION" -eq 1 ]] || [[ $(( ITERATION % FAST_TEST_INTERVAL )) -eq 0 ]] || [[ "$ITERATION" -ge "$MAX_ITERATIONS" ]]; then
842
+ active_test_cmd="$TEST_CMD"
843
+ test_mode="full"
844
+ else
845
+ active_test_cmd="$FAST_TEST_CMD"
846
+ test_mode="fast"
847
+ fi
848
+ fi
849
+
649
850
  local test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
650
- if bash -c "$TEST_CMD" > "$test_log" 2>&1; then
851
+ TEST_LOG_FILE="$test_log"
852
+ echo -e " ${DIM}Running ${test_mode} tests...${RESET}"
853
+ # Wrap test command with timeout (5 min default) to prevent hanging
854
+ local test_timeout="${SW_TEST_TIMEOUT:-300}"
855
+ local test_wrapper="$active_test_cmd"
856
+ if command -v timeout &>/dev/null; then
857
+ test_wrapper="timeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
858
+ elif command -v gtimeout &>/dev/null; then
859
+ test_wrapper="gtimeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
860
+ fi
861
+ if bash -c "$test_wrapper" > "$test_log" 2>&1; then
651
862
  TEST_PASSED=true
652
- TEST_OUTPUT="All tests passed."
863
+ TEST_OUTPUT="All tests passed (${test_mode} mode)."
653
864
  else
654
865
  TEST_PASSED=false
655
866
  TEST_OUTPUT="$(tail -50 "$test_log")"
656
867
  fi
657
868
  }
658
869
 
870
+ write_error_summary() {
871
+ local error_json="$LOG_DIR/error-summary.json"
872
+
873
+ # Only write on test failure
874
+ if [[ "${TEST_PASSED:-}" != "false" ]]; then
875
+ # Clear previous error summary on success
876
+ rm -f "$error_json" 2>/dev/null || true
877
+ return
878
+ fi
879
+
880
+ local test_log="${TEST_LOG_FILE:-$LOG_DIR/tests-iter-${ITERATION}.log}"
881
+ [[ ! -f "$test_log" ]] && return
882
+
883
+ # Extract error lines (last 30 lines, grep for error patterns)
884
+ local error_lines_raw
885
+ error_lines_raw=$(tail -30 "$test_log" 2>/dev/null | grep -iE '(error|fail|assert|exception|panic|FAIL|TypeError|ReferenceError|SyntaxError)' | head -10 || true)
886
+
887
+ local error_count=0
888
+ if [[ -n "$error_lines_raw" ]]; then
889
+ error_count=$(echo "$error_lines_raw" | wc -l | tr -d ' ')
890
+ fi
891
+
892
+ local tmp_json="${error_json}.tmp.$$"
893
+
894
+ # Build JSON with jq (preferred) or plain-text fallback
895
+ if command -v jq &>/dev/null; then
896
+ jq -n \
897
+ --argjson iteration "${ITERATION:-0}" \
898
+ --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
899
+ --argjson error_count "${error_count:-0}" \
900
+ --arg error_lines "$error_lines_raw" \
901
+ --arg test_cmd "${TEST_CMD:-}" \
902
+ '{
903
+ iteration: $iteration,
904
+ timestamp: $timestamp,
905
+ error_count: $error_count,
906
+ error_lines: ($error_lines | split("\n") | map(select(length > 0))),
907
+ test_cmd: $test_cmd
908
+ }' > "$tmp_json" 2>/dev/null && mv "$tmp_json" "$error_json" || rm -f "$tmp_json" 2>/dev/null
909
+ else
910
+ # Fallback: write plain-text error summary (still machine-parseable)
911
+ cat > "$tmp_json" <<ERRJSON
912
+ {"iteration":${ITERATION:-0},"error_count":${error_count:-0},"error_lines":[],"test_cmd":"test"}
913
+ ERRJSON
914
+ mv "$tmp_json" "$error_json" 2>/dev/null || rm -f "$tmp_json" 2>/dev/null
915
+ fi
916
+ }
917
+
659
918
  # ─── Audit Agent ─────────────────────────────────────────────────────────────
660
919
 
661
920
  run_audit_agent() {
@@ -884,6 +1143,21 @@ compose_prompt() {
884
1143
  $TEST_OUTPUT"
885
1144
  fi
886
1145
 
1146
+ # Structured error context (machine-readable)
1147
+ local error_summary_section=""
1148
+ local error_json="$LOG_DIR/error-summary.json"
1149
+ if [[ -f "$error_json" ]]; then
1150
+ local err_count err_lines
1151
+ err_count=$(jq -r '.error_count // 0' "$error_json" 2>/dev/null || echo "0")
1152
+ err_lines=$(jq -r '.error_lines[]? // empty' "$error_json" 2>/dev/null | head -10 || true)
1153
+ if [[ "$err_count" -gt 0 ]] && [[ -n "$err_lines" ]]; then
1154
+ error_summary_section="## Structured Error Summary (${err_count} errors detected)
1155
+ ${err_lines}
1156
+
1157
+ Fix these specific errors. Each line above is one distinct error from the test output."
1158
+ fi
1159
+ fi
1160
+
887
1161
  # Build audit sections (captured before heredoc to avoid nested heredoc issues)
888
1162
  local audit_section
889
1163
  audit_section="$(compose_audit_section)"
@@ -1006,6 +1280,16 @@ ${last_error}"
1006
1280
  local stuckness_section=""
1007
1281
  stuckness_section="$(detect_stuckness)"
1008
1282
 
1283
+ # Session restart context — inject previous session progress
1284
+ local restart_section=""
1285
+ if [[ "$SESSION_RESTART" == "true" ]] && [[ -f "$LOG_DIR/progress.md" ]]; then
1286
+ restart_section="## Previous Session Progress
1287
+ $(cat "$LOG_DIR/progress.md")
1288
+
1289
+ You are starting a FRESH session after the previous one exhausted its iterations.
1290
+ Read the progress above and continue from where it left off. Do NOT repeat work already done."
1291
+ fi
1292
+
1009
1293
  cat <<PROMPT
1010
1294
  You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
1011
1295
 
@@ -1021,6 +1305,8 @@ ${git_log}
1021
1305
  ## Test Results (Previous Iteration)
1022
1306
  ${test_section}
1023
1307
 
1308
+ ${error_summary_section:+$error_summary_section
1309
+ }
1024
1310
  ${memory_section:+## Memory Context
1025
1311
  $memory_section
1026
1312
  }
@@ -1028,6 +1314,8 @@ ${dora_section:+$dora_section
1028
1314
  }
1029
1315
  ${intelligence_section:+$intelligence_section
1030
1316
  }
1317
+ ${restart_section:+$restart_section
1318
+ }
1031
1319
  ## Instructions
1032
1320
  1. Read the codebase and understand the current state
1033
1321
  2. Identify the highest-priority remaining work toward the goal
@@ -1189,6 +1477,37 @@ compose_worker_prompt() {
1189
1477
  local base_prompt
1190
1478
  base_prompt="$(compose_prompt)"
1191
1479
 
1480
+ # Role-specific instructions
1481
+ local role_section=""
1482
+ if [[ -n "$AGENT_ROLES" ]] && [[ "${agent_num:-0}" -ge 1 ]]; then
1483
+ # Split comma-separated roles and get role for this agent
1484
+ local role=""
1485
+ local IFS_BAK="$IFS"
1486
+ IFS=',' read -ra _roles <<< "$AGENT_ROLES"
1487
+ IFS="$IFS_BAK"
1488
+ if [[ "$agent_num" -le "${#_roles[@]}" ]]; then
1489
+ role="${_roles[$((agent_num - 1))]}"
1490
+ # Trim whitespace and skip empty roles (handles trailing comma)
1491
+ role="$(echo "$role" | tr -d ' ')"
1492
+ fi
1493
+
1494
+ if [[ -n "$role" ]]; then
1495
+ local role_desc=""
1496
+ case "$role" in
1497
+ builder) role_desc="Focus on implementation — writing code, fixing bugs, building features. You are the primary builder." ;;
1498
+ reviewer) role_desc="Focus on code review — look for bugs, security issues, edge cases in recent commits. Make fixes via commits." ;;
1499
+ tester) role_desc="Focus on test coverage — write new tests, fix failing tests, improve assertions and edge case coverage." ;;
1500
+ optimizer) role_desc="Focus on performance — profile hot paths, reduce complexity, optimize algorithms and data structures." ;;
1501
+ docs) role_desc="Focus on documentation — update README, add docstrings, write usage guides for new features." ;;
1502
+ security) role_desc="Focus on security — audit for vulnerabilities, fix injection risks, validate inputs, check auth boundaries." ;;
1503
+ *) role_desc="Focus on: ${role}. Apply your expertise in this area to advance the goal." ;;
1504
+ esac
1505
+ role_section="## Your Role: ${role}
1506
+ ${role_desc}
1507
+ Prioritize work in your area of expertise. Coordinate with other agents via git log."
1508
+ fi
1509
+ fi
1510
+
1192
1511
  cat <<PROMPT
1193
1512
  ${base_prompt}
1194
1513
 
@@ -1196,6 +1515,8 @@ ${base_prompt}
1196
1515
  You are Agent ${agent_num} of ${total_agents}. Other agents are working in parallel.
1197
1516
  Check git log to see what they've done — avoid duplicating their work.
1198
1517
  Focus on areas they haven't touched yet.
1518
+
1519
+ ${role_section}
1199
1520
  PROMPT
1200
1521
  }
1201
1522
 
@@ -1268,7 +1589,14 @@ extract_summary() {
1268
1589
  local summary
1269
1590
  summary="$(grep -v '^$' "$log_file" | tail -5 | head -3 2>/dev/null || echo "(no output)")"
1270
1591
  # Truncate long lines
1271
- echo "$summary" | cut -c1-120
1592
+ summary="$(echo "$summary" | cut -c1-120)"
1593
+
1594
+ # Sanitize: if summary is just a CLI/API error, replace with generic text
1595
+ if echo "$summary" | grep -qiE 'Invalid API key|authentication_error|rate_limit|API key expired|ANTHROPIC_API_KEY'; then
1596
+ summary="(CLI error — no useful output this iteration)"
1597
+ fi
1598
+
1599
+ echo "$summary"
1272
1600
  }
1273
1601
 
1274
1602
  # ─── Display Helpers ─────────────────────────────────────────────────────────
@@ -1547,17 +1875,21 @@ done
1547
1875
  echo -e "\n${DIM}Agent ${AGENT_NUM} finished after ${ITERATION} iterations${RESET}"
1548
1876
  WORKEREOF
1549
1877
 
1550
- # Replace placeholders
1878
+ # Replace placeholders — use awk for all values to avoid sed injection
1879
+ # (sed breaks on & | \ in paths and test commands)
1551
1880
  sed_i "s|__AGENT_NUM__|${agent_num}|g" "$worker_script"
1552
1881
  sed_i "s|__TOTAL_AGENTS__|${total_agents}|g" "$worker_script"
1553
- sed_i "s|__WORK_DIR__|${wt_path}|g" "$worker_script"
1554
- sed_i "s|__LOG_DIR__|${LOG_DIR}|g" "$worker_script"
1555
1882
  sed_i "s|__MAX_ITERATIONS__|${MAX_ITERATIONS}|g" "$worker_script"
1556
- sed_i "s|__TEST_CMD__|${TEST_CMD}|g" "$worker_script"
1557
- sed_i "s|__CLAUDE_FLAGS__|${claude_flags}|g" "$worker_script"
1558
- # Goal needs special handling for sed (may contain special chars)
1559
- # Use awk for safe string replacement without python
1560
- awk -v goal="$GOAL" '{gsub(/__GOAL__/, goal); print}' "$worker_script" > "${worker_script}.tmp" \
1883
+ # Paths and commands may contain sed-special chars — use awk
1884
+ awk -v val="$wt_path" '{gsub(/__WORK_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1885
+ && mv "${worker_script}.tmp" "$worker_script"
1886
+ awk -v val="$LOG_DIR" '{gsub(/__LOG_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1887
+ && mv "${worker_script}.tmp" "$worker_script"
1888
+ awk -v val="$TEST_CMD" '{gsub(/__TEST_CMD__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1889
+ && mv "${worker_script}.tmp" "$worker_script"
1890
+ awk -v val="$claude_flags" '{gsub(/__CLAUDE_FLAGS__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1891
+ && mv "${worker_script}.tmp" "$worker_script"
1892
+ awk -v val="$GOAL" '{gsub(/__GOAL__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1561
1893
  && mv "${worker_script}.tmp" "$worker_script"
1562
1894
  chmod +x "$worker_script"
1563
1895
  echo "$worker_script"
@@ -1577,10 +1909,14 @@ launch_multi_agent() {
1577
1909
  MULTI_WINDOW_NAME="sw-loop-$(date +%s)"
1578
1910
  tmux new-window -n "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT"
1579
1911
 
1912
+ # Capture the first pane's ID (stable regardless of pane-base-index)
1913
+ local monitor_pane_id
1914
+ monitor_pane_id="$(tmux list-panes -t "$MULTI_WINDOW_NAME" -F '#{pane_id}' 2>/dev/null | head -1)"
1915
+
1580
1916
  # First pane becomes monitor
1581
- tmux send-keys -t "$MULTI_WINDOW_NAME" "printf '\\033]2;loop-monitor\\033\\\\'" Enter
1917
+ tmux send-keys -t "$monitor_pane_id" "printf '\\033]2;loop-monitor\\033\\\\'" Enter
1582
1918
  sleep 0.2
1583
- tmux send-keys -t "$MULTI_WINDOW_NAME" "clear && echo 'Loop Monitor — watching agent logs...'" Enter
1919
+ tmux send-keys -t "$monitor_pane_id" "clear && echo 'Loop Monitor — watching agent logs...'" Enter
1584
1920
 
1585
1921
  # Create worker panes
1586
1922
  for i in $(seq 1 "$AGENTS"); do
@@ -1596,12 +1932,12 @@ launch_multi_agent() {
1596
1932
 
1597
1933
  # Layout: monitor pane on top (35%), worker agents tile below
1598
1934
  tmux select-layout -t "$MULTI_WINDOW_NAME" main-vertical 2>/dev/null || true
1599
- tmux resize-pane -t "$MULTI_WINDOW_NAME.0" -y 35% 2>/dev/null || true
1935
+ tmux resize-pane -t "$monitor_pane_id" -y 35% 2>/dev/null || true
1600
1936
 
1601
1937
  # In the monitor pane, tail all agent logs
1602
- tmux select-pane -t "$MULTI_WINDOW_NAME.0"
1938
+ tmux select-pane -t "$monitor_pane_id"
1603
1939
  sleep 0.5
1604
- tmux send-keys -t "$MULTI_WINDOW_NAME.0" "clear && tail -f $LOG_DIR/agent-*-iter-*.log 2>/dev/null || echo 'Waiting for agent logs...'" Enter
1940
+ tmux send-keys -t "$monitor_pane_id" "clear && tail -f $LOG_DIR/agent-*-iter-*.log 2>/dev/null || echo 'Waiting for agent logs...'" Enter
1605
1941
 
1606
1942
  success "Launched $AGENTS worker agents in window: $MULTI_WINDOW_NAME"
1607
1943
  echo ""
@@ -1656,12 +1992,13 @@ wait_for_multi_completion() {
1656
1992
 
1657
1993
  cleanup_multi_agent() {
1658
1994
  if [[ -n "$MULTI_WINDOW_NAME" ]]; then
1659
- # Send Ctrl-C to all panes in the worker window
1660
- local pane_count
1661
- pane_count="$(tmux list-panes -t "$MULTI_WINDOW_NAME" 2>/dev/null | wc -l | tr -d ' ')"
1662
- for i in $(seq 0 $(( pane_count - 1 ))); do
1663
- tmux send-keys -t "$MULTI_WINDOW_NAME.$i" C-c 2>/dev/null || true
1664
- done
1995
+ # Send Ctrl-C to all panes using stable pane IDs (not indices)
1996
+ # Pane IDs (%0, %1, ...) are unaffected by pane-base-index setting
1997
+ local pane_id
1998
+ while IFS= read -r pane_id; do
1999
+ [[ -z "$pane_id" ]] && continue
2000
+ tmux send-keys -t "$pane_id" C-c 2>/dev/null || true
2001
+ done < <(tmux list-panes -t "$MULTI_WINDOW_NAME" -F '#{pane_id}' 2>/dev/null || true)
1665
2002
  sleep 1
1666
2003
  tmux kill-window -t "$MULTI_WINDOW_NAME" 2>/dev/null || true
1667
2004
  fi
@@ -1673,7 +2010,10 @@ cleanup_multi_agent() {
1673
2010
  # ─── Main: Single-Agent Loop ─────────────────────────────────────────────────
1674
2011
 
1675
2012
  run_single_agent_loop() {
1676
- if $RESUME; then
2013
+ if [[ "$SESSION_RESTART" == "true" ]]; then
2014
+ # Restart: state already reset by run_loop_with_restarts, skip init
2015
+ info "Session restart ${RESTART_COUNT}/${MAX_RESTARTS} — fresh context, reading progress"
2016
+ elif $RESUME; then
1677
2017
  resume_state
1678
2018
  else
1679
2019
  initialize_state
@@ -1683,6 +2023,9 @@ run_single_agent_loop() {
1683
2023
  apply_adaptive_budget
1684
2024
  MODEL="$(select_adaptive_model "build" "$MODEL")"
1685
2025
 
2026
+ # Track applied memory fix patterns for outcome recording
2027
+ _applied_fix_pattern=""
2028
+
1686
2029
  show_banner
1687
2030
 
1688
2031
  while true; do
@@ -1691,12 +2034,42 @@ run_single_agent_loop() {
1691
2034
  check_max_iterations || break
1692
2035
  ITERATION=$(( ITERATION + 1 ))
1693
2036
 
2037
+ # Try memory-based fix suggestion on retry after test failure
2038
+ if [[ "${TEST_PASSED:-}" == "false" ]]; then
2039
+ local _last_error=""
2040
+ local _prev_log="$LOG_DIR/iteration-$(( ITERATION - 1 )).log"
2041
+ if [[ -f "$_prev_log" ]]; then
2042
+ _last_error=$(tail -20 "$_prev_log" 2>/dev/null | grep -iE '(error|fail|exception)' | head -1 || true)
2043
+ fi
2044
+ local _fix_suggestion=""
2045
+ if type memory_closed_loop_inject &>/dev/null 2>&1 && [[ -n "${_last_error:-}" ]]; then
2046
+ _fix_suggestion=$(memory_closed_loop_inject "$_last_error" 2>/dev/null) || true
2047
+ fi
2048
+ if [[ -n "${_fix_suggestion:-}" ]]; then
2049
+ _applied_fix_pattern="${_last_error}"
2050
+ GOAL="KNOWN FIX (from past success): ${_fix_suggestion}
2051
+
2052
+ ${GOAL}"
2053
+ info "Memory fix injected: ${_fix_suggestion:0:80}"
2054
+ fi
2055
+ fi
2056
+
1694
2057
  # Run Claude
1695
2058
  local exit_code=0
1696
2059
  run_claude_iteration || exit_code=$?
1697
2060
 
1698
2061
  local log_file="$LOG_DIR/iteration-${ITERATION}.log"
1699
2062
 
2063
+ # Detect fatal CLI errors (API key, auth, network) — abort immediately
2064
+ if check_fatal_error "$log_file" "$exit_code"; then
2065
+ STATUS="error"
2066
+ write_state
2067
+ write_progress
2068
+ error "Fatal CLI error detected — aborting loop (see iteration log)"
2069
+ show_summary
2070
+ return 1
2071
+ fi
2072
+
1700
2073
  # Mid-loop memory refresh — re-query with current error context after iteration 3
1701
2074
  if [[ "$ITERATION" -ge 3 ]] && type memory_inject_context &>/dev/null 2>&1; then
1702
2075
  local refresh_ctx
@@ -1733,6 +2106,7 @@ run_single_agent_loop() {
1733
2106
 
1734
2107
  # Test gate
1735
2108
  run_test_gate
2109
+ write_error_summary
1736
2110
  if [[ -n "$TEST_CMD" ]]; then
1737
2111
  if [[ "$TEST_PASSED" == "true" ]]; then
1738
2112
  echo -e " ${GREEN}✓${RESET} Tests: passed"
@@ -1741,6 +2115,18 @@ run_single_agent_loop() {
1741
2115
  fi
1742
2116
  fi
1743
2117
 
2118
+ # Track fix outcome for memory effectiveness
2119
+ if [[ -n "${_applied_fix_pattern:-}" ]]; then
2120
+ if type memory_record_fix_outcome &>/dev/null 2>&1; then
2121
+ if [[ "${TEST_PASSED:-}" == "true" ]]; then
2122
+ memory_record_fix_outcome "$_applied_fix_pattern" "true" "true" 2>/dev/null || true
2123
+ else
2124
+ memory_record_fix_outcome "$_applied_fix_pattern" "true" "false" 2>/dev/null || true
2125
+ fi
2126
+ fi
2127
+ _applied_fix_pattern=""
2128
+ fi
2129
+
1744
2130
  # Audit agent (reviews implementer's work)
1745
2131
  run_audit_agent
1746
2132
 
@@ -1751,6 +2137,7 @@ run_single_agent_loop() {
1751
2137
  if guard_completion; then
1752
2138
  STATUS="complete"
1753
2139
  write_state
2140
+ write_progress
1754
2141
  show_summary
1755
2142
  return 0
1756
2143
  fi
@@ -1771,6 +2158,7 @@ run_single_agent_loop() {
1771
2158
  $summary
1772
2159
  "
1773
2160
  write_state
2161
+ write_progress
1774
2162
 
1775
2163
  # Update heartbeat
1776
2164
  "$SCRIPT_DIR/sw-heartbeat.sh" write "${PIPELINE_JOB_ID:-loop-$$}" \
@@ -1799,9 +2187,77 @@ HUMAN FEEDBACK (received after iteration $ITERATION): $human_msg"
1799
2187
 
1800
2188
  # Write final state after loop exits
1801
2189
  write_state
2190
+ write_progress
1802
2191
  show_summary
1803
2192
  }
1804
2193
 
2194
+ # ─── Session Restart Wrapper ─────────────────────────────────────────────────
2195
+
2196
+ run_loop_with_restarts() {
2197
+ while true; do
2198
+ local loop_exit=0
2199
+ run_single_agent_loop || loop_exit=$?
2200
+
2201
+ # If completed successfully or no restarts configured, exit
2202
+ if [[ "$STATUS" == "complete" ]]; then
2203
+ return 0
2204
+ fi
2205
+ if [[ "$MAX_RESTARTS" -le 0 ]]; then
2206
+ return "$loop_exit"
2207
+ fi
2208
+ if [[ "$RESTART_COUNT" -ge "$MAX_RESTARTS" ]]; then
2209
+ warn "Max restarts ($MAX_RESTARTS) reached — stopping"
2210
+ return "$loop_exit"
2211
+ fi
2212
+ # Hard cap safety net
2213
+ if [[ "$RESTART_COUNT" -ge 5 ]]; then
2214
+ warn "Hard restart cap (5) reached — stopping"
2215
+ return "$loop_exit"
2216
+ fi
2217
+
2218
+ # Check if tests are still failing (worth restarting)
2219
+ if [[ "${TEST_PASSED:-}" == "true" ]]; then
2220
+ info "Tests passing but loop incomplete — restarting session"
2221
+ else
2222
+ info "Tests failing and loop exhausted — restarting with fresh context"
2223
+ fi
2224
+
2225
+ RESTART_COUNT=$(( RESTART_COUNT + 1 ))
2226
+ if type emit_event &>/dev/null 2>&1; then
2227
+ emit_event "loop.restart" "restart=$RESTART_COUNT" "max=$MAX_RESTARTS" "iteration=$ITERATION"
2228
+ fi
2229
+ info "Session restart ${RESTART_COUNT}/${MAX_RESTARTS} — resetting iteration counter"
2230
+
2231
+ # Reset ALL iteration-level state for the new session
2232
+ # SESSION_RESTART tells run_single_agent_loop to skip init/resume
2233
+ SESSION_RESTART=true
2234
+ ITERATION=0
2235
+ CONSECUTIVE_FAILURES=0
2236
+ EXTENSION_COUNT=0
2237
+ STATUS="running"
2238
+ LOG_ENTRIES=""
2239
+ TEST_PASSED=""
2240
+ TEST_OUTPUT=""
2241
+ TEST_LOG_FILE=""
2242
+ # Reset GOAL to original — prevent unbounded growth from memory/human injections
2243
+ GOAL="$ORIGINAL_GOAL"
2244
+
2245
+ # Archive old artifacts so they don't get overwritten or pollute new session
2246
+ local restart_archive="$LOG_DIR/restart-${RESTART_COUNT}"
2247
+ mkdir -p "$restart_archive"
2248
+ for old_log in "$LOG_DIR"/iteration-*.log "$LOG_DIR"/tests-iter-*.log; do
2249
+ [[ -f "$old_log" ]] && mv "$old_log" "$restart_archive/" 2>/dev/null || true
2250
+ done
2251
+ # Archive progress.md and error-summary.json from previous session
2252
+ [[ -f "$LOG_DIR/progress.md" ]] && cp "$LOG_DIR/progress.md" "$restart_archive/progress.md" 2>/dev/null || true
2253
+ [[ -f "$LOG_DIR/error-summary.json" ]] && mv "$LOG_DIR/error-summary.json" "$restart_archive/" 2>/dev/null || true
2254
+
2255
+ write_state
2256
+
2257
+ sleep 2
2258
+ done
2259
+ }
2260
+
1805
2261
  # ─── Main: Entry Point ───────────────────────────────────────────────────────
1806
2262
 
1807
2263
  main() {
@@ -1815,7 +2271,7 @@ main() {
1815
2271
  launch_multi_agent
1816
2272
  show_summary
1817
2273
  else
1818
- run_single_agent_loop
2274
+ run_loop_with_restarts
1819
2275
  fi
1820
2276
  }
1821
2277