shipwright-cli 1.9.0 → 1.10.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 (53) hide show
  1. package/.claude/hooks/post-tool-use.sh +12 -5
  2. package/package.json +2 -2
  3. package/scripts/sw +9 -1
  4. package/scripts/sw-adversarial.sh +1 -1
  5. package/scripts/sw-architecture-enforcer.sh +1 -1
  6. package/scripts/sw-checkpoint.sh +79 -1
  7. package/scripts/sw-cleanup.sh +192 -7
  8. package/scripts/sw-connect.sh +1 -1
  9. package/scripts/sw-cost.sh +1 -1
  10. package/scripts/sw-daemon.sh +409 -37
  11. package/scripts/sw-dashboard.sh +1 -1
  12. package/scripts/sw-developer-simulation.sh +1 -1
  13. package/scripts/sw-docs.sh +1 -1
  14. package/scripts/sw-doctor.sh +1 -1
  15. package/scripts/sw-fix.sh +1 -1
  16. package/scripts/sw-fleet.sh +1 -1
  17. package/scripts/sw-github-checks.sh +1 -1
  18. package/scripts/sw-github-deploy.sh +1 -1
  19. package/scripts/sw-github-graphql.sh +1 -1
  20. package/scripts/sw-heartbeat.sh +1 -1
  21. package/scripts/sw-init.sh +1 -1
  22. package/scripts/sw-intelligence.sh +1 -1
  23. package/scripts/sw-jira.sh +1 -1
  24. package/scripts/sw-launchd.sh +4 -4
  25. package/scripts/sw-linear.sh +1 -1
  26. package/scripts/sw-logs.sh +1 -1
  27. package/scripts/sw-loop.sh +444 -49
  28. package/scripts/sw-memory.sh +198 -3
  29. package/scripts/sw-pipeline-composer.sh +8 -8
  30. package/scripts/sw-pipeline-vitals.sh +1096 -0
  31. package/scripts/sw-pipeline.sh +1692 -84
  32. package/scripts/sw-predictive.sh +1 -1
  33. package/scripts/sw-prep.sh +1 -1
  34. package/scripts/sw-ps.sh +4 -3
  35. package/scripts/sw-reaper.sh +5 -3
  36. package/scripts/sw-remote.sh +1 -1
  37. package/scripts/sw-self-optimize.sh +109 -8
  38. package/scripts/sw-session.sh +31 -9
  39. package/scripts/sw-setup.sh +1 -1
  40. package/scripts/sw-status.sh +192 -1
  41. package/scripts/sw-templates.sh +1 -1
  42. package/scripts/sw-tmux.sh +1 -1
  43. package/scripts/sw-tracker.sh +1 -1
  44. package/scripts/sw-upgrade.sh +1 -1
  45. package/scripts/sw-worktree.sh +1 -1
  46. package/templates/pipelines/autonomous.json +8 -1
  47. package/templates/pipelines/cost-aware.json +21 -0
  48. package/templates/pipelines/deployed.json +40 -6
  49. package/templates/pipelines/enterprise.json +16 -2
  50. package/templates/pipelines/fast.json +19 -0
  51. package/templates/pipelines/full.json +16 -2
  52. package/templates/pipelines/hotfix.json +19 -0
  53. package/templates/pipelines/standard.json +19 -0
@@ -34,17 +34,25 @@ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
34
34
 
35
35
  # ─── Defaults ─────────────────────────────────────────────────────────────────
36
36
  GOAL=""
37
+ ORIGINAL_GOAL="" # Preserved across restarts — GOAL gets appended to
37
38
  MAX_ITERATIONS="${SW_MAX_ITERATIONS:-20}"
38
39
  TEST_CMD=""
40
+ FAST_TEST_CMD=""
41
+ FAST_TEST_INTERVAL=5
42
+ TEST_LOG_FILE=""
39
43
  MODEL="${SW_MODEL:-opus}"
40
44
  AGENTS=1
45
+ AGENT_ROLES=""
41
46
  USE_WORKTREE=false
42
47
  SKIP_PERMISSIONS=false
43
48
  MAX_TURNS=""
44
49
  RESUME=false
45
50
  VERBOSE=false
46
51
  MAX_ITERATIONS_EXPLICIT=false
47
- VERSION="1.9.0"
52
+ MAX_RESTARTS=0
53
+ SESSION_RESTART=false
54
+ RESTART_COUNT=0
55
+ VERSION="1.10.0"
48
56
 
49
57
  # ─── Flexible Iteration Defaults ────────────────────────────────────────────
50
58
  AUTO_EXTEND=true # Auto-extend iterations when work is incomplete
@@ -75,12 +83,16 @@ show_help() {
75
83
  echo -e "${BOLD}OPTIONS${RESET}"
76
84
  echo -e " ${CYAN}--max-iterations${RESET} N Max loop iterations (default: 20)"
77
85
  echo -e " ${CYAN}--test-cmd${RESET} \"cmd\" Test command to run between iterations"
86
+ echo -e " ${CYAN}--fast-test-cmd${RESET} \"cmd\" Fast/subset test command (alternates with full)"
87
+ echo -e " ${CYAN}--fast-test-interval${RESET} N Run full tests every N iterations (default: 5)"
78
88
  echo -e " ${CYAN}--model${RESET} MODEL Claude model to use (default: opus)"
79
89
  echo -e " ${CYAN}--agents${RESET} N Number of parallel agents (default: 1)"
90
+ echo -e " ${CYAN}--roles${RESET} \"r1,r2,...\" Role per agent: builder,reviewer,tester,optimizer,docs,security"
80
91
  echo -e " ${CYAN}--worktree${RESET} Use git worktrees for isolation (auto if agents > 1)"
81
92
  echo -e " ${CYAN}--skip-permissions${RESET} Pass --dangerously-skip-permissions to Claude"
82
93
  echo -e " ${CYAN}--max-turns${RESET} N Max API turns per Claude session"
83
94
  echo -e " ${CYAN}--resume${RESET} Resume from existing .claude/loop-state.md"
95
+ echo -e " ${CYAN}--max-restarts${RESET} N Max session restarts on exhaustion (default: 0)"
84
96
  echo -e " ${CYAN}--verbose${RESET} Show full Claude output (default: summary)"
85
97
  echo -e " ${CYAN}--help${RESET} Show this help"
86
98
  echo ""
@@ -175,6 +187,30 @@ while [[ $# -gt 0 ]]; do
175
187
  shift 2
176
188
  ;;
177
189
  --max-extensions=*) MAX_EXTENSIONS="${1#--max-extensions=}"; shift ;;
190
+ --fast-test-cmd)
191
+ FAST_TEST_CMD="${2:-}"
192
+ [[ -z "$FAST_TEST_CMD" ]] && { error "Missing value for --fast-test-cmd"; exit 1; }
193
+ shift 2
194
+ ;;
195
+ --fast-test-cmd=*) FAST_TEST_CMD="${1#--fast-test-cmd=}"; shift ;;
196
+ --fast-test-interval)
197
+ FAST_TEST_INTERVAL="${2:-}"
198
+ [[ -z "$FAST_TEST_INTERVAL" ]] && { error "Missing value for --fast-test-interval"; exit 1; }
199
+ shift 2
200
+ ;;
201
+ --fast-test-interval=*) FAST_TEST_INTERVAL="${1#--fast-test-interval=}"; shift ;;
202
+ --max-restarts)
203
+ MAX_RESTARTS="${2:-}"
204
+ [[ -z "$MAX_RESTARTS" ]] && { error "Missing value for --max-restarts"; exit 1; }
205
+ shift 2
206
+ ;;
207
+ --max-restarts=*) MAX_RESTARTS="${1#--max-restarts=}"; shift ;;
208
+ --roles)
209
+ AGENT_ROLES="${2:-}"
210
+ [[ -z "$AGENT_ROLES" ]] && { error "Missing value for --roles"; exit 1; }
211
+ shift 2
212
+ ;;
213
+ --roles=*) AGENT_ROLES="${1#--roles=}"; shift ;;
178
214
  --help|-h)
179
215
  show_help
180
216
  exit 0
@@ -203,6 +239,27 @@ if [[ "$AGENTS" -gt 1 ]]; then
203
239
  USE_WORKTREE=true
204
240
  fi
205
241
 
242
+ # Warn if --roles without --agents
243
+ if [[ -n "$AGENT_ROLES" ]] && [[ "$AGENTS" -le 1 ]]; then
244
+ warn "--roles requires --agents > 1 (roles are ignored in single-agent mode)"
245
+ fi
246
+
247
+ # Warn if --max-restarts with --agents > 1 (not yet supported)
248
+ if [[ "${MAX_RESTARTS:-0}" -gt 0 ]] && [[ "$AGENTS" -gt 1 ]]; then
249
+ warn "--max-restarts is ignored in multi-agent mode (restart support is single-agent only)"
250
+ MAX_RESTARTS=0
251
+ fi
252
+
253
+ # Validate numeric flags
254
+ if ! [[ "$FAST_TEST_INTERVAL" =~ ^[1-9][0-9]*$ ]]; then
255
+ error "--fast-test-interval must be a positive integer (got: $FAST_TEST_INTERVAL)"
256
+ exit 1
257
+ fi
258
+ if ! [[ "$MAX_RESTARTS" =~ ^[0-9]+$ ]]; then
259
+ error "--max-restarts must be a non-negative integer (got: $MAX_RESTARTS)"
260
+ exit 1
261
+ fi
262
+
206
263
  # ─── Validate Inputs ─────────────────────────────────────────────────────────
207
264
 
208
265
  if ! $RESUME && [[ -z "$GOAL" ]]; then
@@ -224,6 +281,9 @@ if ! git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then
224
281
  exit 1
225
282
  fi
226
283
 
284
+ # Preserve original goal before any appending (memory fixes, human feedback)
285
+ ORIGINAL_GOAL="$GOAL"
286
+
227
287
  # ─── Timeout Detection ────────────────────────────────────────────────────────
228
288
  TIMEOUT_CMD=""
229
289
  if command -v timeout &>/dev/null; then
@@ -266,6 +326,17 @@ select_adaptive_model() {
266
326
  echo "$default_model"
267
327
  return 0
268
328
  fi
329
+ # Read learned model routing
330
+ local _routing_file="${HOME}/.shipwright/optimization/model-routing.json"
331
+ if [[ -f "$_routing_file" ]] && command -v jq &>/dev/null; then
332
+ local _routed_model
333
+ _routed_model=$(jq -r --arg r "$role" '.routes[$r].model // ""' "$_routing_file" 2>/dev/null) || true
334
+ if [[ -n "${_routed_model:-}" && "${_routed_model:-}" != "null" ]]; then
335
+ echo "${_routed_model}"
336
+ return 0
337
+ fi
338
+ fi
339
+
269
340
  # Try intelligence-based recommendation
270
341
  if type intelligence_recommend_model &>/dev/null 2>&1; then
271
342
  local rec
@@ -317,6 +388,18 @@ apply_adaptive_budget() {
317
388
  [[ -n "$tuned_cb" && "$tuned_cb" != "null" ]] && CIRCUIT_BREAKER_THRESHOLD="$tuned_cb"
318
389
  fi
319
390
 
391
+ # Read learned iteration model
392
+ local _iter_model="${HOME}/.shipwright/optimization/iteration-model.json"
393
+ if [[ -f "$_iter_model" ]] && ! $MAX_ITERATIONS_EXPLICIT && command -v jq &>/dev/null; then
394
+ local _complexity="${ISSUE_COMPLEXITY:-${COMPLEXITY:-medium}}"
395
+ local _predicted_max
396
+ _predicted_max=$(jq -r --arg c "$_complexity" '.predictions[$c].max_iterations // ""' "$_iter_model" 2>/dev/null) || true
397
+ if [[ -n "${_predicted_max:-}" && "${_predicted_max:-}" != "null" && "${_predicted_max:-0}" -gt 0 ]]; then
398
+ MAX_ITERATIONS="${_predicted_max}"
399
+ info "Iteration model: ${_complexity} complexity → max ${_predicted_max} iterations"
400
+ fi
401
+ fi
402
+
320
403
  # Try intelligence-based iteration estimate
321
404
  if type intelligence_estimate_iterations &>/dev/null 2>&1 && ! $MAX_ITERATIONS_EXPLICIT; then
322
405
  local est
@@ -481,31 +564,67 @@ resume_state() {
481
564
  }
482
565
 
483
566
  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
567
+ local tmp_state="${STATE_FILE}.tmp.$$"
568
+ # Use printf instead of heredoc to avoid delimiter injection from GOAL
569
+ {
570
+ printf -- '---\n'
571
+ printf 'goal: "%s"\n' "$GOAL"
572
+ printf 'iteration: %s\n' "$ITERATION"
573
+ printf 'max_iterations: %s\n' "$MAX_ITERATIONS"
574
+ printf 'status: %s\n' "$STATUS"
575
+ printf 'test_cmd: "%s"\n' "$TEST_CMD"
576
+ printf 'model: %s\n' "$MODEL"
577
+ printf 'agents: %s\n' "$AGENTS"
578
+ printf 'started_at: %s\n' "$(now_iso)"
579
+ printf 'last_iteration_at: %s\n' "$(now_iso)"
580
+ printf 'consecutive_failures: %s\n' "$CONSECUTIVE_FAILURES"
581
+ printf 'total_commits: %s\n' "$TOTAL_COMMITS"
582
+ printf 'audit_enabled: %s\n' "$AUDIT_ENABLED"
583
+ printf 'audit_agent_enabled: %s\n' "$AUDIT_AGENT_ENABLED"
584
+ printf 'quality_gates_enabled: %s\n' "$QUALITY_GATES_ENABLED"
585
+ printf 'dod_file: "%s"\n' "$DOD_FILE"
586
+ printf 'auto_extend: %s\n' "$AUTO_EXTEND"
587
+ printf 'extension_count: %s\n' "$EXTENSION_COUNT"
588
+ printf 'max_extensions: %s\n' "$MAX_EXTENSIONS"
589
+ printf -- '---\n\n'
590
+ printf '## Log\n'
591
+ printf '%s\n' "$LOG_ENTRIES"
592
+ } > "$tmp_state"
593
+ if ! mv "$tmp_state" "$STATE_FILE" 2>/dev/null; then
594
+ warn "Failed to write state file: $STATE_FILE"
595
+ fi
596
+ }
597
+
598
+ write_progress() {
599
+ local progress_file="$LOG_DIR/progress.md"
600
+ local recent_commits
601
+ recent_commits=$(git -C "$PROJECT_ROOT" log --oneline -5 2>/dev/null || echo "(no commits)")
602
+ local changed_files
603
+ changed_files=$(git -C "$PROJECT_ROOT" diff --name-only HEAD~3 2>/dev/null | head -20 || echo "(none)")
604
+ local last_error=""
605
+ local prev_test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
606
+ if [[ -f "$prev_test_log" ]] && [[ "${TEST_PASSED:-}" == "false" ]]; then
607
+ last_error=$(tail -10 "$prev_test_log" 2>/dev/null || true)
608
+ fi
609
+
610
+ # Use printf to avoid heredoc delimiter injection from GOAL content
611
+ local tmp_progress="${progress_file}.tmp.$$"
612
+ {
613
+ printf '# Session Progress (Auto-Generated)\n\n'
614
+ printf '## Goal\n%s\n\n' "${GOAL}"
615
+ printf '## Status\n'
616
+ printf -- '- Iteration: %s/%s\n' "${ITERATION}" "${MAX_ITERATIONS}"
617
+ printf -- '- Session restart: %s/%s\n' "${RESTART_COUNT:-0}" "${MAX_RESTARTS:-0}"
618
+ printf -- '- Tests passing: %s\n' "${TEST_PASSED:-unknown}"
619
+ printf -- '- Status: %s\n\n' "${STATUS:-running}"
620
+ printf '## Recent Commits\n%s\n\n' "${recent_commits}"
621
+ printf '## Changed Files\n%s\n\n' "${changed_files}"
622
+ if [[ -n "$last_error" ]]; then
623
+ printf '## Last Error\n%s\n\n' "$last_error"
624
+ fi
625
+ printf '## Timestamp\n%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
626
+ } > "$tmp_progress" 2>/dev/null
627
+ mv "$tmp_progress" "$progress_file" 2>/dev/null || rm -f "$tmp_progress" 2>/dev/null
509
628
  }
510
629
 
511
630
  append_log_entry() {
@@ -568,6 +687,30 @@ check_completion() {
568
687
  }
569
688
 
570
689
  check_circuit_breaker() {
690
+ # Vitals-driven circuit breaker (preferred over static threshold)
691
+ if type pipeline_compute_vitals &>/dev/null 2>&1 && type pipeline_health_verdict &>/dev/null 2>&1; then
692
+ local _vitals_json _verdict
693
+ local _loop_state="${STATE_FILE:-}"
694
+ local _loop_artifacts="${ARTIFACTS_DIR:-}"
695
+ local _loop_issue="${ISSUE_NUMBER:-}"
696
+ _vitals_json=$(pipeline_compute_vitals "$_loop_state" "$_loop_artifacts" "$_loop_issue" 2>/dev/null) || true
697
+ if [[ -n "$_vitals_json" && "$_vitals_json" != "{}" ]]; then
698
+ _verdict=$(echo "$_vitals_json" | jq -r '.verdict // "continue"' 2>/dev/null || echo "continue")
699
+ if [[ "$_verdict" == "abort" ]]; then
700
+ local _health_score
701
+ _health_score=$(echo "$_vitals_json" | jq -r '.health_score // 0' 2>/dev/null || echo "0")
702
+ error "Vitals circuit breaker: health score ${_health_score}/100 — aborting (${CONSECUTIVE_FAILURES} stagnant iterations)"
703
+ STATUS="circuit_breaker"
704
+ return 1
705
+ fi
706
+ # Vitals say continue/warn/intervene — don't trip circuit breaker yet
707
+ if [[ "$_verdict" == "continue" || "$_verdict" == "warn" ]]; then
708
+ return 0
709
+ fi
710
+ fi
711
+ fi
712
+
713
+ # Fallback: static threshold circuit breaker
571
714
  if [[ "$CONSECUTIVE_FAILURES" -ge "$CIRCUIT_BREAKER_THRESHOLD" ]]; then
572
715
  error "Circuit breaker tripped: ${CIRCUIT_BREAKER_THRESHOLD} consecutive iterations with no meaningful progress."
573
716
  STATUS="circuit_breaker"
@@ -646,16 +789,88 @@ run_test_gate() {
646
789
  return
647
790
  fi
648
791
 
792
+ # Determine which test command to use this iteration
793
+ local active_test_cmd="$TEST_CMD"
794
+ local test_mode="full"
795
+ if [[ -n "$FAST_TEST_CMD" ]]; then
796
+ # Use full test every FAST_TEST_INTERVAL iterations, on first iteration, and on final iteration
797
+ if [[ "$ITERATION" -eq 1 ]] || [[ $(( ITERATION % FAST_TEST_INTERVAL )) -eq 0 ]] || [[ "$ITERATION" -ge "$MAX_ITERATIONS" ]]; then
798
+ active_test_cmd="$TEST_CMD"
799
+ test_mode="full"
800
+ else
801
+ active_test_cmd="$FAST_TEST_CMD"
802
+ test_mode="fast"
803
+ fi
804
+ fi
805
+
649
806
  local test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
650
- if bash -c "$TEST_CMD" > "$test_log" 2>&1; then
807
+ TEST_LOG_FILE="$test_log"
808
+ echo -e " ${DIM}Running ${test_mode} tests...${RESET}"
809
+ # Wrap test command with timeout (5 min default) to prevent hanging
810
+ local test_timeout="${SW_TEST_TIMEOUT:-300}"
811
+ local test_wrapper="$active_test_cmd"
812
+ if command -v timeout &>/dev/null; then
813
+ test_wrapper="timeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
814
+ elif command -v gtimeout &>/dev/null; then
815
+ test_wrapper="gtimeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
816
+ fi
817
+ if bash -c "$test_wrapper" > "$test_log" 2>&1; then
651
818
  TEST_PASSED=true
652
- TEST_OUTPUT="All tests passed."
819
+ TEST_OUTPUT="All tests passed (${test_mode} mode)."
653
820
  else
654
821
  TEST_PASSED=false
655
822
  TEST_OUTPUT="$(tail -50 "$test_log")"
656
823
  fi
657
824
  }
658
825
 
826
+ write_error_summary() {
827
+ local error_json="$LOG_DIR/error-summary.json"
828
+
829
+ # Only write on test failure
830
+ if [[ "${TEST_PASSED:-}" != "false" ]]; then
831
+ # Clear previous error summary on success
832
+ rm -f "$error_json" 2>/dev/null || true
833
+ return
834
+ fi
835
+
836
+ local test_log="${TEST_LOG_FILE:-$LOG_DIR/tests-iter-${ITERATION}.log}"
837
+ [[ ! -f "$test_log" ]] && return
838
+
839
+ # Extract error lines (last 30 lines, grep for error patterns)
840
+ local error_lines_raw
841
+ error_lines_raw=$(tail -30 "$test_log" 2>/dev/null | grep -iE '(error|fail|assert|exception|panic|FAIL|TypeError|ReferenceError|SyntaxError)' | head -10 || true)
842
+
843
+ local error_count=0
844
+ if [[ -n "$error_lines_raw" ]]; then
845
+ error_count=$(echo "$error_lines_raw" | wc -l | tr -d ' ')
846
+ fi
847
+
848
+ local tmp_json="${error_json}.tmp.$$"
849
+
850
+ # Build JSON with jq (preferred) or plain-text fallback
851
+ if command -v jq &>/dev/null; then
852
+ jq -n \
853
+ --argjson iteration "${ITERATION:-0}" \
854
+ --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
855
+ --argjson error_count "${error_count:-0}" \
856
+ --arg error_lines "$error_lines_raw" \
857
+ --arg test_cmd "${TEST_CMD:-}" \
858
+ '{
859
+ iteration: $iteration,
860
+ timestamp: $timestamp,
861
+ error_count: $error_count,
862
+ error_lines: ($error_lines | split("\n") | map(select(length > 0))),
863
+ test_cmd: $test_cmd
864
+ }' > "$tmp_json" 2>/dev/null && mv "$tmp_json" "$error_json" || rm -f "$tmp_json" 2>/dev/null
865
+ else
866
+ # Fallback: write plain-text error summary (still machine-parseable)
867
+ cat > "$tmp_json" <<ERRJSON
868
+ {"iteration":${ITERATION:-0},"error_count":${error_count:-0},"error_lines":[],"test_cmd":"test"}
869
+ ERRJSON
870
+ mv "$tmp_json" "$error_json" 2>/dev/null || rm -f "$tmp_json" 2>/dev/null
871
+ fi
872
+ }
873
+
659
874
  # ─── Audit Agent ─────────────────────────────────────────────────────────────
660
875
 
661
876
  run_audit_agent() {
@@ -884,6 +1099,21 @@ compose_prompt() {
884
1099
  $TEST_OUTPUT"
885
1100
  fi
886
1101
 
1102
+ # Structured error context (machine-readable)
1103
+ local error_summary_section=""
1104
+ local error_json="$LOG_DIR/error-summary.json"
1105
+ if [[ -f "$error_json" ]]; then
1106
+ local err_count err_lines
1107
+ err_count=$(jq -r '.error_count // 0' "$error_json" 2>/dev/null || echo "0")
1108
+ err_lines=$(jq -r '.error_lines[]? // empty' "$error_json" 2>/dev/null | head -10 || true)
1109
+ if [[ "$err_count" -gt 0 ]] && [[ -n "$err_lines" ]]; then
1110
+ error_summary_section="## Structured Error Summary (${err_count} errors detected)
1111
+ ${err_lines}
1112
+
1113
+ Fix these specific errors. Each line above is one distinct error from the test output."
1114
+ fi
1115
+ fi
1116
+
887
1117
  # Build audit sections (captured before heredoc to avoid nested heredoc issues)
888
1118
  local audit_section
889
1119
  audit_section="$(compose_audit_section)"
@@ -1006,6 +1236,16 @@ ${last_error}"
1006
1236
  local stuckness_section=""
1007
1237
  stuckness_section="$(detect_stuckness)"
1008
1238
 
1239
+ # Session restart context — inject previous session progress
1240
+ local restart_section=""
1241
+ if [[ "$SESSION_RESTART" == "true" ]] && [[ -f "$LOG_DIR/progress.md" ]]; then
1242
+ restart_section="## Previous Session Progress
1243
+ $(cat "$LOG_DIR/progress.md")
1244
+
1245
+ You are starting a FRESH session after the previous one exhausted its iterations.
1246
+ Read the progress above and continue from where it left off. Do NOT repeat work already done."
1247
+ fi
1248
+
1009
1249
  cat <<PROMPT
1010
1250
  You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
1011
1251
 
@@ -1021,6 +1261,8 @@ ${git_log}
1021
1261
  ## Test Results (Previous Iteration)
1022
1262
  ${test_section}
1023
1263
 
1264
+ ${error_summary_section:+$error_summary_section
1265
+ }
1024
1266
  ${memory_section:+## Memory Context
1025
1267
  $memory_section
1026
1268
  }
@@ -1028,6 +1270,8 @@ ${dora_section:+$dora_section
1028
1270
  }
1029
1271
  ${intelligence_section:+$intelligence_section
1030
1272
  }
1273
+ ${restart_section:+$restart_section
1274
+ }
1031
1275
  ## Instructions
1032
1276
  1. Read the codebase and understand the current state
1033
1277
  2. Identify the highest-priority remaining work toward the goal
@@ -1189,6 +1433,37 @@ compose_worker_prompt() {
1189
1433
  local base_prompt
1190
1434
  base_prompt="$(compose_prompt)"
1191
1435
 
1436
+ # Role-specific instructions
1437
+ local role_section=""
1438
+ if [[ -n "$AGENT_ROLES" ]] && [[ "${agent_num:-0}" -ge 1 ]]; then
1439
+ # Split comma-separated roles and get role for this agent
1440
+ local role=""
1441
+ local IFS_BAK="$IFS"
1442
+ IFS=',' read -ra _roles <<< "$AGENT_ROLES"
1443
+ IFS="$IFS_BAK"
1444
+ if [[ "$agent_num" -le "${#_roles[@]}" ]]; then
1445
+ role="${_roles[$((agent_num - 1))]}"
1446
+ # Trim whitespace and skip empty roles (handles trailing comma)
1447
+ role="$(echo "$role" | tr -d ' ')"
1448
+ fi
1449
+
1450
+ if [[ -n "$role" ]]; then
1451
+ local role_desc=""
1452
+ case "$role" in
1453
+ builder) role_desc="Focus on implementation — writing code, fixing bugs, building features. You are the primary builder." ;;
1454
+ reviewer) role_desc="Focus on code review — look for bugs, security issues, edge cases in recent commits. Make fixes via commits." ;;
1455
+ tester) role_desc="Focus on test coverage — write new tests, fix failing tests, improve assertions and edge case coverage." ;;
1456
+ optimizer) role_desc="Focus on performance — profile hot paths, reduce complexity, optimize algorithms and data structures." ;;
1457
+ docs) role_desc="Focus on documentation — update README, add docstrings, write usage guides for new features." ;;
1458
+ security) role_desc="Focus on security — audit for vulnerabilities, fix injection risks, validate inputs, check auth boundaries." ;;
1459
+ *) role_desc="Focus on: ${role}. Apply your expertise in this area to advance the goal." ;;
1460
+ esac
1461
+ role_section="## Your Role: ${role}
1462
+ ${role_desc}
1463
+ Prioritize work in your area of expertise. Coordinate with other agents via git log."
1464
+ fi
1465
+ fi
1466
+
1192
1467
  cat <<PROMPT
1193
1468
  ${base_prompt}
1194
1469
 
@@ -1196,6 +1471,8 @@ ${base_prompt}
1196
1471
  You are Agent ${agent_num} of ${total_agents}. Other agents are working in parallel.
1197
1472
  Check git log to see what they've done — avoid duplicating their work.
1198
1473
  Focus on areas they haven't touched yet.
1474
+
1475
+ ${role_section}
1199
1476
  PROMPT
1200
1477
  }
1201
1478
 
@@ -1547,17 +1824,21 @@ done
1547
1824
  echo -e "\n${DIM}Agent ${AGENT_NUM} finished after ${ITERATION} iterations${RESET}"
1548
1825
  WORKEREOF
1549
1826
 
1550
- # Replace placeholders
1827
+ # Replace placeholders — use awk for all values to avoid sed injection
1828
+ # (sed breaks on & | \ in paths and test commands)
1551
1829
  sed_i "s|__AGENT_NUM__|${agent_num}|g" "$worker_script"
1552
1830
  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
1831
  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" \
1832
+ # Paths and commands may contain sed-special chars — use awk
1833
+ awk -v val="$wt_path" '{gsub(/__WORK_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1834
+ && mv "${worker_script}.tmp" "$worker_script"
1835
+ awk -v val="$LOG_DIR" '{gsub(/__LOG_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1836
+ && mv "${worker_script}.tmp" "$worker_script"
1837
+ awk -v val="$TEST_CMD" '{gsub(/__TEST_CMD__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1838
+ && mv "${worker_script}.tmp" "$worker_script"
1839
+ awk -v val="$claude_flags" '{gsub(/__CLAUDE_FLAGS__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1840
+ && mv "${worker_script}.tmp" "$worker_script"
1841
+ awk -v val="$GOAL" '{gsub(/__GOAL__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
1561
1842
  && mv "${worker_script}.tmp" "$worker_script"
1562
1843
  chmod +x "$worker_script"
1563
1844
  echo "$worker_script"
@@ -1577,10 +1858,14 @@ launch_multi_agent() {
1577
1858
  MULTI_WINDOW_NAME="sw-loop-$(date +%s)"
1578
1859
  tmux new-window -n "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT"
1579
1860
 
1861
+ # Capture the first pane's ID (stable regardless of pane-base-index)
1862
+ local monitor_pane_id
1863
+ monitor_pane_id="$(tmux list-panes -t "$MULTI_WINDOW_NAME" -F '#{pane_id}' 2>/dev/null | head -1)"
1864
+
1580
1865
  # First pane becomes monitor
1581
- tmux send-keys -t "$MULTI_WINDOW_NAME" "printf '\\033]2;loop-monitor\\033\\\\'" Enter
1866
+ tmux send-keys -t "$monitor_pane_id" "printf '\\033]2;loop-monitor\\033\\\\'" Enter
1582
1867
  sleep 0.2
1583
- tmux send-keys -t "$MULTI_WINDOW_NAME" "clear && echo 'Loop Monitor — watching agent logs...'" Enter
1868
+ tmux send-keys -t "$monitor_pane_id" "clear && echo 'Loop Monitor — watching agent logs...'" Enter
1584
1869
 
1585
1870
  # Create worker panes
1586
1871
  for i in $(seq 1 "$AGENTS"); do
@@ -1596,12 +1881,12 @@ launch_multi_agent() {
1596
1881
 
1597
1882
  # Layout: monitor pane on top (35%), worker agents tile below
1598
1883
  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
1884
+ tmux resize-pane -t "$monitor_pane_id" -y 35% 2>/dev/null || true
1600
1885
 
1601
1886
  # In the monitor pane, tail all agent logs
1602
- tmux select-pane -t "$MULTI_WINDOW_NAME.0"
1887
+ tmux select-pane -t "$monitor_pane_id"
1603
1888
  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
1889
+ 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
1890
 
1606
1891
  success "Launched $AGENTS worker agents in window: $MULTI_WINDOW_NAME"
1607
1892
  echo ""
@@ -1656,12 +1941,13 @@ wait_for_multi_completion() {
1656
1941
 
1657
1942
  cleanup_multi_agent() {
1658
1943
  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
1944
+ # Send Ctrl-C to all panes using stable pane IDs (not indices)
1945
+ # Pane IDs (%0, %1, ...) are unaffected by pane-base-index setting
1946
+ local pane_id
1947
+ while IFS= read -r pane_id; do
1948
+ [[ -z "$pane_id" ]] && continue
1949
+ tmux send-keys -t "$pane_id" C-c 2>/dev/null || true
1950
+ done < <(tmux list-panes -t "$MULTI_WINDOW_NAME" -F '#{pane_id}' 2>/dev/null || true)
1665
1951
  sleep 1
1666
1952
  tmux kill-window -t "$MULTI_WINDOW_NAME" 2>/dev/null || true
1667
1953
  fi
@@ -1673,7 +1959,10 @@ cleanup_multi_agent() {
1673
1959
  # ─── Main: Single-Agent Loop ─────────────────────────────────────────────────
1674
1960
 
1675
1961
  run_single_agent_loop() {
1676
- if $RESUME; then
1962
+ if [[ "$SESSION_RESTART" == "true" ]]; then
1963
+ # Restart: state already reset by run_loop_with_restarts, skip init
1964
+ info "Session restart ${RESTART_COUNT}/${MAX_RESTARTS} — fresh context, reading progress"
1965
+ elif $RESUME; then
1677
1966
  resume_state
1678
1967
  else
1679
1968
  initialize_state
@@ -1683,6 +1972,9 @@ run_single_agent_loop() {
1683
1972
  apply_adaptive_budget
1684
1973
  MODEL="$(select_adaptive_model "build" "$MODEL")"
1685
1974
 
1975
+ # Track applied memory fix patterns for outcome recording
1976
+ _applied_fix_pattern=""
1977
+
1686
1978
  show_banner
1687
1979
 
1688
1980
  while true; do
@@ -1691,6 +1983,26 @@ run_single_agent_loop() {
1691
1983
  check_max_iterations || break
1692
1984
  ITERATION=$(( ITERATION + 1 ))
1693
1985
 
1986
+ # Try memory-based fix suggestion on retry after test failure
1987
+ if [[ "${TEST_PASSED:-}" == "false" ]]; then
1988
+ local _last_error=""
1989
+ local _prev_log="$LOG_DIR/iteration-$(( ITERATION - 1 )).log"
1990
+ if [[ -f "$_prev_log" ]]; then
1991
+ _last_error=$(tail -20 "$_prev_log" 2>/dev/null | grep -iE '(error|fail|exception)' | head -1 || true)
1992
+ fi
1993
+ local _fix_suggestion=""
1994
+ if type memory_closed_loop_inject &>/dev/null 2>&1 && [[ -n "${_last_error:-}" ]]; then
1995
+ _fix_suggestion=$(memory_closed_loop_inject "$_last_error" 2>/dev/null) || true
1996
+ fi
1997
+ if [[ -n "${_fix_suggestion:-}" ]]; then
1998
+ _applied_fix_pattern="${_last_error}"
1999
+ GOAL="KNOWN FIX (from past success): ${_fix_suggestion}
2000
+
2001
+ ${GOAL}"
2002
+ info "Memory fix injected: ${_fix_suggestion:0:80}"
2003
+ fi
2004
+ fi
2005
+
1694
2006
  # Run Claude
1695
2007
  local exit_code=0
1696
2008
  run_claude_iteration || exit_code=$?
@@ -1733,6 +2045,7 @@ run_single_agent_loop() {
1733
2045
 
1734
2046
  # Test gate
1735
2047
  run_test_gate
2048
+ write_error_summary
1736
2049
  if [[ -n "$TEST_CMD" ]]; then
1737
2050
  if [[ "$TEST_PASSED" == "true" ]]; then
1738
2051
  echo -e " ${GREEN}✓${RESET} Tests: passed"
@@ -1741,6 +2054,18 @@ run_single_agent_loop() {
1741
2054
  fi
1742
2055
  fi
1743
2056
 
2057
+ # Track fix outcome for memory effectiveness
2058
+ if [[ -n "${_applied_fix_pattern:-}" ]]; then
2059
+ if type memory_record_fix_outcome &>/dev/null 2>&1; then
2060
+ if [[ "${TEST_PASSED:-}" == "true" ]]; then
2061
+ memory_record_fix_outcome "$_applied_fix_pattern" "true" "true" 2>/dev/null || true
2062
+ else
2063
+ memory_record_fix_outcome "$_applied_fix_pattern" "true" "false" 2>/dev/null || true
2064
+ fi
2065
+ fi
2066
+ _applied_fix_pattern=""
2067
+ fi
2068
+
1744
2069
  # Audit agent (reviews implementer's work)
1745
2070
  run_audit_agent
1746
2071
 
@@ -1751,6 +2076,7 @@ run_single_agent_loop() {
1751
2076
  if guard_completion; then
1752
2077
  STATUS="complete"
1753
2078
  write_state
2079
+ write_progress
1754
2080
  show_summary
1755
2081
  return 0
1756
2082
  fi
@@ -1771,6 +2097,7 @@ run_single_agent_loop() {
1771
2097
  $summary
1772
2098
  "
1773
2099
  write_state
2100
+ write_progress
1774
2101
 
1775
2102
  # Update heartbeat
1776
2103
  "$SCRIPT_DIR/sw-heartbeat.sh" write "${PIPELINE_JOB_ID:-loop-$$}" \
@@ -1799,9 +2126,77 @@ HUMAN FEEDBACK (received after iteration $ITERATION): $human_msg"
1799
2126
 
1800
2127
  # Write final state after loop exits
1801
2128
  write_state
2129
+ write_progress
1802
2130
  show_summary
1803
2131
  }
1804
2132
 
2133
+ # ─── Session Restart Wrapper ─────────────────────────────────────────────────
2134
+
2135
+ run_loop_with_restarts() {
2136
+ while true; do
2137
+ local loop_exit=0
2138
+ run_single_agent_loop || loop_exit=$?
2139
+
2140
+ # If completed successfully or no restarts configured, exit
2141
+ if [[ "$STATUS" == "complete" ]]; then
2142
+ return 0
2143
+ fi
2144
+ if [[ "$MAX_RESTARTS" -le 0 ]]; then
2145
+ return "$loop_exit"
2146
+ fi
2147
+ if [[ "$RESTART_COUNT" -ge "$MAX_RESTARTS" ]]; then
2148
+ warn "Max restarts ($MAX_RESTARTS) reached — stopping"
2149
+ return "$loop_exit"
2150
+ fi
2151
+ # Hard cap safety net
2152
+ if [[ "$RESTART_COUNT" -ge 5 ]]; then
2153
+ warn "Hard restart cap (5) reached — stopping"
2154
+ return "$loop_exit"
2155
+ fi
2156
+
2157
+ # Check if tests are still failing (worth restarting)
2158
+ if [[ "${TEST_PASSED:-}" == "true" ]]; then
2159
+ info "Tests passing but loop incomplete — restarting session"
2160
+ else
2161
+ info "Tests failing and loop exhausted — restarting with fresh context"
2162
+ fi
2163
+
2164
+ RESTART_COUNT=$(( RESTART_COUNT + 1 ))
2165
+ if type emit_event &>/dev/null 2>&1; then
2166
+ emit_event "loop.restart" "restart=$RESTART_COUNT" "max=$MAX_RESTARTS" "iteration=$ITERATION"
2167
+ fi
2168
+ info "Session restart ${RESTART_COUNT}/${MAX_RESTARTS} — resetting iteration counter"
2169
+
2170
+ # Reset ALL iteration-level state for the new session
2171
+ # SESSION_RESTART tells run_single_agent_loop to skip init/resume
2172
+ SESSION_RESTART=true
2173
+ ITERATION=0
2174
+ CONSECUTIVE_FAILURES=0
2175
+ EXTENSION_COUNT=0
2176
+ STATUS="running"
2177
+ LOG_ENTRIES=""
2178
+ TEST_PASSED=""
2179
+ TEST_OUTPUT=""
2180
+ TEST_LOG_FILE=""
2181
+ # Reset GOAL to original — prevent unbounded growth from memory/human injections
2182
+ GOAL="$ORIGINAL_GOAL"
2183
+
2184
+ # Archive old artifacts so they don't get overwritten or pollute new session
2185
+ local restart_archive="$LOG_DIR/restart-${RESTART_COUNT}"
2186
+ mkdir -p "$restart_archive"
2187
+ for old_log in "$LOG_DIR"/iteration-*.log "$LOG_DIR"/tests-iter-*.log; do
2188
+ [[ -f "$old_log" ]] && mv "$old_log" "$restart_archive/" 2>/dev/null || true
2189
+ done
2190
+ # Archive progress.md and error-summary.json from previous session
2191
+ [[ -f "$LOG_DIR/progress.md" ]] && cp "$LOG_DIR/progress.md" "$restart_archive/progress.md" 2>/dev/null || true
2192
+ [[ -f "$LOG_DIR/error-summary.json" ]] && mv "$LOG_DIR/error-summary.json" "$restart_archive/" 2>/dev/null || true
2193
+
2194
+ write_state
2195
+
2196
+ sleep 2
2197
+ done
2198
+ }
2199
+
1805
2200
  # ─── Main: Entry Point ───────────────────────────────────────────────────────
1806
2201
 
1807
2202
  main() {
@@ -1815,7 +2210,7 @@ main() {
1815
2210
  launch_multi_agent
1816
2211
  show_summary
1817
2212
  else
1818
- run_single_agent_loop
2213
+ run_loop_with_restarts
1819
2214
  fi
1820
2215
  }
1821
2216