shipwright-cli 1.7.1 → 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 (115) hide show
  1. package/.claude/agents/code-reviewer.md +90 -0
  2. package/.claude/agents/devops-engineer.md +142 -0
  3. package/.claude/agents/pipeline-agent.md +80 -0
  4. package/.claude/agents/shell-script-specialist.md +150 -0
  5. package/.claude/agents/test-specialist.md +196 -0
  6. package/.claude/hooks/post-tool-use.sh +45 -0
  7. package/.claude/hooks/pre-tool-use.sh +25 -0
  8. package/.claude/hooks/session-started.sh +37 -0
  9. package/README.md +212 -814
  10. package/claude-code/CLAUDE.md.shipwright +54 -0
  11. package/claude-code/hooks/notify-idle.sh +2 -2
  12. package/claude-code/hooks/session-start.sh +24 -0
  13. package/claude-code/hooks/task-completed.sh +6 -2
  14. package/claude-code/settings.json.template +12 -0
  15. package/dashboard/public/app.js +4422 -0
  16. package/dashboard/public/index.html +816 -0
  17. package/dashboard/public/styles.css +4755 -0
  18. package/dashboard/server.ts +4315 -0
  19. package/docs/KNOWN-ISSUES.md +18 -10
  20. package/docs/TIPS.md +38 -26
  21. package/docs/patterns/README.md +33 -23
  22. package/package.json +9 -5
  23. package/scripts/adapters/iterm2-adapter.sh +1 -1
  24. package/scripts/adapters/tmux-adapter.sh +52 -23
  25. package/scripts/adapters/wezterm-adapter.sh +26 -14
  26. package/scripts/lib/compat.sh +200 -0
  27. package/scripts/lib/helpers.sh +72 -0
  28. package/scripts/postinstall.mjs +72 -13
  29. package/scripts/{cct → sw} +118 -22
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +468 -0
  33. package/scripts/sw-cleanup.sh +359 -0
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/sw-daemon.sh +5574 -0
  37. package/scripts/sw-dashboard.sh +477 -0
  38. package/scripts/sw-developer-simulation.sh +252 -0
  39. package/scripts/sw-docs.sh +635 -0
  40. package/scripts/sw-doctor.sh +907 -0
  41. package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
  42. package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
  43. package/scripts/sw-github-checks.sh +521 -0
  44. package/scripts/sw-github-deploy.sh +533 -0
  45. package/scripts/sw-github-graphql.sh +972 -0
  46. package/scripts/sw-heartbeat.sh +293 -0
  47. package/scripts/{cct-init.sh → sw-init.sh} +144 -11
  48. package/scripts/sw-intelligence.sh +1196 -0
  49. package/scripts/sw-jira.sh +643 -0
  50. package/scripts/sw-launchd.sh +364 -0
  51. package/scripts/sw-linear.sh +648 -0
  52. package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
  53. package/scripts/sw-loop.sh +2217 -0
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +514 -36
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/sw-pipeline-vitals.sh +1096 -0
  58. package/scripts/sw-pipeline.sh +7593 -0
  59. package/scripts/sw-predictive.sh +820 -0
  60. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  61. package/scripts/{cct-ps.sh → sw-ps.sh} +9 -6
  62. package/scripts/{cct-reaper.sh → sw-reaper.sh} +10 -6
  63. package/scripts/sw-remote.sh +687 -0
  64. package/scripts/sw-self-optimize.sh +1048 -0
  65. package/scripts/sw-session.sh +541 -0
  66. package/scripts/sw-setup.sh +234 -0
  67. package/scripts/sw-status.sh +796 -0
  68. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  69. package/scripts/sw-tmux.sh +591 -0
  70. package/scripts/sw-tracker-jira.sh +277 -0
  71. package/scripts/sw-tracker-linear.sh +292 -0
  72. package/scripts/sw-tracker.sh +409 -0
  73. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  74. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  75. package/templates/pipelines/autonomous.json +35 -6
  76. package/templates/pipelines/cost-aware.json +21 -0
  77. package/templates/pipelines/deployed.json +40 -6
  78. package/templates/pipelines/enterprise.json +16 -2
  79. package/templates/pipelines/fast.json +19 -0
  80. package/templates/pipelines/full.json +28 -2
  81. package/templates/pipelines/hotfix.json +19 -0
  82. package/templates/pipelines/standard.json +31 -0
  83. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  84. package/tmux/templates/accessibility.json +34 -0
  85. package/tmux/templates/api-design.json +35 -0
  86. package/tmux/templates/architecture.json +1 -0
  87. package/tmux/templates/bug-fix.json +9 -0
  88. package/tmux/templates/code-review.json +1 -0
  89. package/tmux/templates/compliance.json +36 -0
  90. package/tmux/templates/data-pipeline.json +36 -0
  91. package/tmux/templates/debt-paydown.json +34 -0
  92. package/tmux/templates/devops.json +1 -0
  93. package/tmux/templates/documentation.json +1 -0
  94. package/tmux/templates/exploration.json +1 -0
  95. package/tmux/templates/feature-dev.json +1 -0
  96. package/tmux/templates/full-stack.json +8 -0
  97. package/tmux/templates/i18n.json +34 -0
  98. package/tmux/templates/incident-response.json +36 -0
  99. package/tmux/templates/migration.json +1 -0
  100. package/tmux/templates/observability.json +35 -0
  101. package/tmux/templates/onboarding.json +33 -0
  102. package/tmux/templates/performance.json +35 -0
  103. package/tmux/templates/refactor.json +1 -0
  104. package/tmux/templates/release.json +35 -0
  105. package/tmux/templates/security-audit.json +8 -0
  106. package/tmux/templates/spike.json +34 -0
  107. package/tmux/templates/testing.json +1 -0
  108. package/tmux/tmux.conf +98 -9
  109. package/scripts/cct-cleanup.sh +0 -172
  110. package/scripts/cct-daemon.sh +0 -3189
  111. package/scripts/cct-doctor.sh +0 -414
  112. package/scripts/cct-loop.sh +0 -1332
  113. package/scripts/cct-pipeline.sh +0 -3844
  114. package/scripts/cct-session.sh +0 -284
  115. package/scripts/cct-status.sh +0 -169
@@ -0,0 +1,2217 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright loop — Continuous agent loop harness for Claude Code ║
4
+ # ║ ║
5
+ # ║ Runs Claude Code in a headless loop until a goal is achieved. ║
6
+ # ║ Supports single-agent and multi-agent (parallel worktree) modes. ║
7
+ # ║ ║
8
+ # ║ Inspired by Anthropic's autonomous 16-agent C compiler build. ║
9
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
10
+ set -euo pipefail
11
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+
15
+ # ─── Colors (matches shipwright theme) ──────────────────────────────────────────────
16
+ CYAN='\033[38;2;0;212;255m'
17
+ PURPLE='\033[38;2;124;58;237m'
18
+ BLUE='\033[38;2;0;102;255m'
19
+ GREEN='\033[38;2;74;222;128m'
20
+ YELLOW='\033[38;2;250;204;21m'
21
+ RED='\033[38;2;248;113;113m'
22
+ DIM='\033[2m'
23
+ BOLD='\033[1m'
24
+ RESET='\033[0m'
25
+
26
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
27
+ # shellcheck source=lib/compat.sh
28
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
29
+
30
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
31
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
32
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
33
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
34
+
35
+ # ─── Defaults ─────────────────────────────────────────────────────────────────
36
+ GOAL=""
37
+ ORIGINAL_GOAL="" # Preserved across restarts — GOAL gets appended to
38
+ MAX_ITERATIONS="${SW_MAX_ITERATIONS:-20}"
39
+ TEST_CMD=""
40
+ FAST_TEST_CMD=""
41
+ FAST_TEST_INTERVAL=5
42
+ TEST_LOG_FILE=""
43
+ MODEL="${SW_MODEL:-opus}"
44
+ AGENTS=1
45
+ AGENT_ROLES=""
46
+ USE_WORKTREE=false
47
+ SKIP_PERMISSIONS=false
48
+ MAX_TURNS=""
49
+ RESUME=false
50
+ VERBOSE=false
51
+ MAX_ITERATIONS_EXPLICIT=false
52
+ MAX_RESTARTS=0
53
+ SESSION_RESTART=false
54
+ RESTART_COUNT=0
55
+ VERSION="1.10.0"
56
+
57
+ # ─── Flexible Iteration Defaults ────────────────────────────────────────────
58
+ AUTO_EXTEND=true # Auto-extend iterations when work is incomplete
59
+ EXTENSION_SIZE=5 # Additional iterations per extension
60
+ MAX_EXTENSIONS=3 # Max number of extensions (hard cap safety net)
61
+ EXTENSION_COUNT=0 # Current number of extensions applied
62
+
63
+ # ─── Circuit Breaker Defaults ──────────────────────────────────────────────
64
+ CIRCUIT_BREAKER_THRESHOLD=3 # Consecutive low-progress iterations before stopping
65
+ MIN_PROGRESS_LINES=5 # Minimum insertions to count as progress
66
+
67
+ # ─── Audit & Quality Gate Defaults ───────────────────────────────────────────
68
+ AUDIT_ENABLED=false
69
+ AUDIT_AGENT_ENABLED=false
70
+ DOD_FILE=""
71
+ QUALITY_GATES_ENABLED=false
72
+ AUDIT_RESULT=""
73
+ COMPLETION_REJECTED=false
74
+ QUALITY_GATE_PASSED=true
75
+
76
+ # ─── Parse Arguments ──────────────────────────────────────────────────────────
77
+ show_help() {
78
+ echo -e "${CYAN}${BOLD}shipwright${RESET} ${DIM}v${VERSION}${RESET} — ${BOLD}Continuous Loop${RESET}"
79
+ echo ""
80
+ echo -e "${BOLD}USAGE${RESET}"
81
+ echo -e " ${CYAN}shipwright loop${RESET} \"<goal>\" [options]"
82
+ echo ""
83
+ echo -e "${BOLD}OPTIONS${RESET}"
84
+ echo -e " ${CYAN}--max-iterations${RESET} N Max loop iterations (default: 20)"
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)"
88
+ echo -e " ${CYAN}--model${RESET} MODEL Claude model to use (default: opus)"
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"
91
+ echo -e " ${CYAN}--worktree${RESET} Use git worktrees for isolation (auto if agents > 1)"
92
+ echo -e " ${CYAN}--skip-permissions${RESET} Pass --dangerously-skip-permissions to Claude"
93
+ echo -e " ${CYAN}--max-turns${RESET} N Max API turns per Claude session"
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)"
96
+ echo -e " ${CYAN}--verbose${RESET} Show full Claude output (default: summary)"
97
+ echo -e " ${CYAN}--help${RESET} Show this help"
98
+ echo ""
99
+ echo -e "${BOLD}AUDIT & QUALITY${RESET}"
100
+ echo -e " ${CYAN}--audit${RESET} Inject self-audit checklist into agent prompt"
101
+ echo -e " ${CYAN}--audit-agent${RESET} Run separate auditor agent (haiku) after each iteration"
102
+ echo -e " ${CYAN}--quality-gates${RESET} Enable automated quality gates before accepting completion"
103
+ echo -e " ${CYAN}--definition-of-done${RESET} FILE DoD checklist file — evaluated by AI against git diff"
104
+ echo -e " ${CYAN}--no-auto-extend${RESET} Disable auto-extension when max iterations reached"
105
+ echo -e " ${CYAN}--extension-size${RESET} N Additional iterations per extension (default: 5)"
106
+ echo -e " ${CYAN}--max-extensions${RESET} N Max number of auto-extensions (default: 3)"
107
+ echo ""
108
+ echo -e "${BOLD}EXAMPLES${RESET}"
109
+ echo -e " ${DIM}shipwright loop \"Build user auth with JWT\"${RESET}"
110
+ echo -e " ${DIM}shipwright loop \"Add payment processing\" --test-cmd \"npm test\" --max-iterations 30${RESET}"
111
+ echo -e " ${DIM}shipwright loop \"Refactor the database layer\" --agents 3 --model sonnet${RESET}"
112
+ echo -e " ${DIM}shipwright loop \"Fix all lint errors\" --skip-permissions --verbose${RESET}"
113
+ echo -e " ${DIM}shipwright loop \"Add auth\" --audit --audit-agent --quality-gates${RESET}"
114
+ echo -e " ${DIM}shipwright loop \"Ship feature\" --quality-gates --definition-of-done dod.md${RESET}"
115
+ echo ""
116
+ echo -e "${BOLD}COMPLETION & CIRCUIT BREAKER${RESET}"
117
+ echo -e " The loop completes when:"
118
+ echo -e " ${DIM}• Claude outputs LOOP_COMPLETE and all quality gates pass${RESET}"
119
+ echo -e " ${DIM}• Max iterations reached (auto-extends if work is incomplete)${RESET}"
120
+ echo -e " The loop stops (circuit breaker) if:"
121
+ echo -e " ${DIM}• ${CIRCUIT_BREAKER_THRESHOLD} consecutive iterations with < ${MIN_PROGRESS_LINES} lines changed${RESET}"
122
+ echo -e " ${DIM}• Hard cap reached (max_iterations + max_extensions * extension_size)${RESET}"
123
+ echo -e " ${DIM}• Ctrl-C (graceful shutdown with summary)${RESET}"
124
+ echo ""
125
+ echo -e "${BOLD}STATE & LOGS${RESET}"
126
+ echo -e " ${DIM}State file: .claude/loop-state.md${RESET}"
127
+ echo -e " ${DIM}Logs dir: .claude/loop-logs/${RESET}"
128
+ echo -e " ${DIM}Resume: shipwright loop --resume${RESET}"
129
+ }
130
+
131
+ while [[ $# -gt 0 ]]; do
132
+ case "$1" in
133
+ --max-iterations)
134
+ MAX_ITERATIONS="${2:-}"
135
+ MAX_ITERATIONS_EXPLICIT=true
136
+ [[ -z "$MAX_ITERATIONS" ]] && { error "Missing value for --max-iterations"; exit 1; }
137
+ shift 2
138
+ ;;
139
+ --max-iterations=*) MAX_ITERATIONS="${1#--max-iterations=}"; MAX_ITERATIONS_EXPLICIT=true; shift ;;
140
+ --test-cmd)
141
+ TEST_CMD="${2:-}"
142
+ [[ -z "$TEST_CMD" ]] && { error "Missing value for --test-cmd"; exit 1; }
143
+ shift 2
144
+ ;;
145
+ --test-cmd=*) TEST_CMD="${1#--test-cmd=}"; shift ;;
146
+ --model)
147
+ MODEL="${2:-}"
148
+ [[ -z "$MODEL" ]] && { error "Missing value for --model"; exit 1; }
149
+ shift 2
150
+ ;;
151
+ --model=*) MODEL="${1#--model=}"; shift ;;
152
+ --agents)
153
+ AGENTS="${2:-}"
154
+ [[ -z "$AGENTS" ]] && { error "Missing value for --agents"; exit 1; }
155
+ shift 2
156
+ ;;
157
+ --agents=*) AGENTS="${1#--agents=}"; shift ;;
158
+ --worktree) USE_WORKTREE=true; shift ;;
159
+ --skip-permissions) SKIP_PERMISSIONS=true; shift ;;
160
+ --max-turns)
161
+ MAX_TURNS="${2:-}"
162
+ [[ -z "$MAX_TURNS" ]] && { error "Missing value for --max-turns"; exit 1; }
163
+ shift 2
164
+ ;;
165
+ --max-turns=*) MAX_TURNS="${1#--max-turns=}"; shift ;;
166
+ --resume) RESUME=true; shift ;;
167
+ --verbose) VERBOSE=true; shift ;;
168
+ --audit) AUDIT_ENABLED=true; shift ;;
169
+ --audit-agent) AUDIT_AGENT_ENABLED=true; shift ;;
170
+ --definition-of-done)
171
+ DOD_FILE="${2:-}"
172
+ [[ -z "$DOD_FILE" ]] && { error "Missing value for --definition-of-done"; exit 1; }
173
+ shift 2
174
+ ;;
175
+ --definition-of-done=*) DOD_FILE="${1#--definition-of-done=}"; shift ;;
176
+ --quality-gates) QUALITY_GATES_ENABLED=true; shift ;;
177
+ --no-auto-extend) AUTO_EXTEND=false; shift ;;
178
+ --extension-size)
179
+ EXTENSION_SIZE="${2:-}"
180
+ [[ -z "$EXTENSION_SIZE" ]] && { error "Missing value for --extension-size"; exit 1; }
181
+ shift 2
182
+ ;;
183
+ --extension-size=*) EXTENSION_SIZE="${1#--extension-size=}"; shift ;;
184
+ --max-extensions)
185
+ MAX_EXTENSIONS="${2:-}"
186
+ [[ -z "$MAX_EXTENSIONS" ]] && { error "Missing value for --max-extensions"; exit 1; }
187
+ shift 2
188
+ ;;
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 ;;
214
+ --help|-h)
215
+ show_help
216
+ exit 0
217
+ ;;
218
+ -*)
219
+ error "Unknown option: $1"
220
+ echo ""
221
+ show_help
222
+ exit 1
223
+ ;;
224
+ *)
225
+ # Positional: goal
226
+ if [[ -z "$GOAL" ]]; then
227
+ GOAL="$1"
228
+ else
229
+ error "Unexpected argument: $1"
230
+ exit 1
231
+ fi
232
+ shift
233
+ ;;
234
+ esac
235
+ done
236
+
237
+ # Auto-enable worktree for multi-agent
238
+ if [[ "$AGENTS" -gt 1 ]]; then
239
+ USE_WORKTREE=true
240
+ fi
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
+
263
+ # ─── Validate Inputs ─────────────────────────────────────────────────────────
264
+
265
+ if ! $RESUME && [[ -z "$GOAL" ]]; then
266
+ error "Missing goal. Usage: shipwright loop \"<goal>\" [options]"
267
+ echo ""
268
+ echo -e " ${DIM}shipwright loop \"Build user auth with JWT\"${RESET}"
269
+ echo -e " ${DIM}shipwright loop --resume${RESET}"
270
+ exit 1
271
+ fi
272
+
273
+ if ! command -v claude &>/dev/null; then
274
+ error "Claude Code CLI not found. Install it first:"
275
+ echo -e " ${DIM}npm install -g @anthropic-ai/claude-code${RESET}"
276
+ exit 1
277
+ fi
278
+
279
+ if ! git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then
280
+ error "Not inside a git repository. The loop requires git for progress tracking."
281
+ exit 1
282
+ fi
283
+
284
+ # Preserve original goal before any appending (memory fixes, human feedback)
285
+ ORIGINAL_GOAL="$GOAL"
286
+
287
+ # ─── Timeout Detection ────────────────────────────────────────────────────────
288
+ TIMEOUT_CMD=""
289
+ if command -v timeout &>/dev/null; then
290
+ TIMEOUT_CMD="timeout"
291
+ elif command -v gtimeout &>/dev/null; then
292
+ TIMEOUT_CMD="gtimeout"
293
+ fi
294
+ CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-1800}" # 30 min default
295
+
296
+ if [[ "$AGENTS" -gt 1 ]]; then
297
+ if ! command -v tmux &>/dev/null; then
298
+ error "tmux is required for multi-agent mode."
299
+ echo -e " ${DIM}brew install tmux${RESET} (macOS)"
300
+ exit 1
301
+ fi
302
+ if [[ -z "${TMUX:-}" ]]; then
303
+ error "Multi-agent mode requires running inside tmux."
304
+ echo -e " ${DIM}tmux new -s work${RESET}"
305
+ exit 1
306
+ fi
307
+ fi
308
+
309
+ # ─── Directory Setup ─────────────────────────────────────────────────────────
310
+
311
+ PROJECT_ROOT="$(git rev-parse --show-toplevel)"
312
+ STATE_DIR="$PROJECT_ROOT/.claude"
313
+ STATE_FILE="$STATE_DIR/loop-state.md"
314
+ LOG_DIR="$STATE_DIR/loop-logs"
315
+ WORKTREE_DIR="$PROJECT_ROOT/.worktrees"
316
+
317
+ mkdir -p "$STATE_DIR" "$LOG_DIR"
318
+
319
+ # ─── Adaptive Model Selection ────────────────────────────────────────────────
320
+ # Uses intelligence engine when available, falls back to defaults.
321
+ select_adaptive_model() {
322
+ local role="${1:-build}"
323
+ local default_model="${2:-opus}"
324
+ # If user explicitly set --model, respect it
325
+ if [[ "$default_model" != "${SW_MODEL:-opus}" ]]; then
326
+ echo "$default_model"
327
+ return 0
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
+
340
+ # Try intelligence-based recommendation
341
+ if type intelligence_recommend_model &>/dev/null 2>&1; then
342
+ local rec
343
+ rec=$(intelligence_recommend_model "$role" "${COMPLEXITY:-5}" "${BUDGET:-0}" 2>/dev/null || echo "")
344
+ if [[ -n "$rec" ]]; then
345
+ local recommended
346
+ recommended=$(echo "$rec" | jq -r '.model // ""' 2>/dev/null || echo "")
347
+ if [[ -n "$recommended" && "$recommended" != "null" ]]; then
348
+ echo "$recommended"
349
+ return 0
350
+ fi
351
+ fi
352
+ fi
353
+ echo "$default_model"
354
+ }
355
+
356
+ # Select audit/DoD model — uses haiku if success rate is high enough, else sonnet
357
+ select_audit_model() {
358
+ local default_model="haiku"
359
+ local opt_file="$HOME/.shipwright/optimization/audit-tuning.json"
360
+ if [[ -f "$opt_file" ]] && command -v jq &>/dev/null; then
361
+ local success_rate
362
+ success_rate=$(jq -r '.haiku_success_rate // 100' "$opt_file" 2>/dev/null || echo "100")
363
+ if [[ "${success_rate%%.*}" -lt 90 ]]; then
364
+ echo "sonnet"
365
+ return 0
366
+ fi
367
+ fi
368
+ echo "$default_model"
369
+ }
370
+
371
+ # ─── Adaptive Iteration Budget ──────────────────────────────────────────────
372
+ # Reads tuning config for smarter iteration/circuit-breaker thresholds.
373
+ apply_adaptive_budget() {
374
+ local tuning_file="$HOME/.shipwright/optimization/loop-tuning.json"
375
+ if [[ -f "$tuning_file" ]] && command -v jq &>/dev/null; then
376
+ local tuned_max tuned_ext tuned_ext_count tuned_cb
377
+ tuned_max=$(jq -r '.max_iterations // ""' "$tuning_file" 2>/dev/null || echo "")
378
+ tuned_ext=$(jq -r '.extension_size // ""' "$tuning_file" 2>/dev/null || echo "")
379
+ tuned_ext_count=$(jq -r '.max_extensions // ""' "$tuning_file" 2>/dev/null || echo "")
380
+ tuned_cb=$(jq -r '.circuit_breaker_threshold // ""' "$tuning_file" 2>/dev/null || echo "")
381
+
382
+ # Only apply tuned values if user didn't explicitly set them
383
+ if ! $MAX_ITERATIONS_EXPLICIT && [[ -n "$tuned_max" && "$tuned_max" != "null" ]]; then
384
+ MAX_ITERATIONS="$tuned_max"
385
+ fi
386
+ [[ -n "$tuned_ext" && "$tuned_ext" != "null" ]] && EXTENSION_SIZE="$tuned_ext"
387
+ [[ -n "$tuned_ext_count" && "$tuned_ext_count" != "null" ]] && MAX_EXTENSIONS="$tuned_ext_count"
388
+ [[ -n "$tuned_cb" && "$tuned_cb" != "null" ]] && CIRCUIT_BREAKER_THRESHOLD="$tuned_cb"
389
+ fi
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
+
403
+ # Try intelligence-based iteration estimate
404
+ if type intelligence_estimate_iterations &>/dev/null 2>&1 && ! $MAX_ITERATIONS_EXPLICIT; then
405
+ local est
406
+ est=$(intelligence_estimate_iterations "${GOAL:-}" "${COMPLEXITY:-5}" 2>/dev/null || echo "")
407
+ if [[ -n "$est" && "$est" =~ ^[0-9]+$ ]]; then
408
+ MAX_ITERATIONS="$est"
409
+ fi
410
+ fi
411
+ }
412
+
413
+ # ─── Progress Velocity Tracking ─────────────────────────────────────────────
414
+ ITERATION_LINES_CHANGED=""
415
+ VELOCITY_HISTORY=""
416
+
417
+ track_iteration_velocity() {
418
+ local changes
419
+ changes="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 2>/dev/null | tail -1 || echo "")"
420
+ local insertions
421
+ insertions="$(echo "$changes" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)"
422
+ ITERATION_LINES_CHANGED="${insertions:-0}"
423
+ if [[ -n "$VELOCITY_HISTORY" ]]; then
424
+ VELOCITY_HISTORY="${VELOCITY_HISTORY},${ITERATION_LINES_CHANGED}"
425
+ else
426
+ VELOCITY_HISTORY="${ITERATION_LINES_CHANGED}"
427
+ fi
428
+ }
429
+
430
+ # Compute average lines/iteration from recent history
431
+ compute_velocity_avg() {
432
+ if [[ -z "$VELOCITY_HISTORY" ]]; then
433
+ echo "0"
434
+ return 0
435
+ fi
436
+ local total=0 count=0
437
+ local IFS=','
438
+ local val
439
+ for val in $VELOCITY_HISTORY; do
440
+ total=$((total + val))
441
+ count=$((count + 1))
442
+ done
443
+ if [[ "$count" -gt 0 ]]; then
444
+ echo $((total / count))
445
+ else
446
+ echo "0"
447
+ fi
448
+ }
449
+
450
+ # ─── Timing Helpers ───────────────────────────────────────────────────────────
451
+
452
+ now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
453
+ now_epoch() { date +%s; }
454
+
455
+ format_duration() {
456
+ local secs="$1"
457
+ local mins=$(( secs / 60 ))
458
+ local remaining_secs=$(( secs % 60 ))
459
+ if [[ $mins -gt 0 ]]; then
460
+ printf "%dm %ds" "$mins" "$remaining_secs"
461
+ else
462
+ printf "%ds" "$remaining_secs"
463
+ fi
464
+ }
465
+
466
+ # ─── State Management ────────────────────────────────────────────────────────
467
+
468
+ ITERATION=0
469
+ CONSECUTIVE_FAILURES=0
470
+ TOTAL_COMMITS=0
471
+ START_EPOCH=""
472
+ STATUS="running"
473
+ TEST_PASSED=""
474
+ TEST_OUTPUT=""
475
+ LOG_ENTRIES=""
476
+
477
+ initialize_state() {
478
+ ITERATION=0
479
+ CONSECUTIVE_FAILURES=0
480
+ TOTAL_COMMITS=0
481
+ START_EPOCH="$(now_epoch)"
482
+ STATUS="running"
483
+ LOG_ENTRIES=""
484
+
485
+ write_state
486
+ }
487
+
488
+ resume_state() {
489
+ if [[ ! -f "$STATE_FILE" ]]; then
490
+ error "No state file found at $STATE_FILE"
491
+ echo -e " Start a new loop instead: ${DIM}shipwright loop \"<goal>\"${RESET}"
492
+ exit 1
493
+ fi
494
+
495
+ info "Resuming from $STATE_FILE"
496
+
497
+ # Save CLI values before parsing state (CLI takes precedence)
498
+ local cli_max_iterations="$MAX_ITERATIONS"
499
+
500
+ # Parse YAML front matter
501
+ local in_frontmatter=false
502
+ while IFS= read -r line; do
503
+ if [[ "$line" == "---" ]]; then
504
+ if $in_frontmatter; then
505
+ break
506
+ else
507
+ in_frontmatter=true
508
+ continue
509
+ fi
510
+ fi
511
+ if $in_frontmatter; then
512
+ case "$line" in
513
+ goal:*) [[ -z "$GOAL" ]] && GOAL="$(echo "${line#goal:}" | sed 's/^ *"//;s/" *$//')" ;;
514
+ iteration:*) ITERATION="$(echo "${line#iteration:}" | tr -d ' ')" ;;
515
+ max_iterations:*) MAX_ITERATIONS="$(echo "${line#max_iterations:}" | tr -d ' ')" ;;
516
+ status:*) STATUS="$(echo "${line#status:}" | tr -d ' ')" ;;
517
+ test_cmd:*) [[ -z "$TEST_CMD" ]] && TEST_CMD="$(echo "${line#test_cmd:}" | sed 's/^ *"//;s/" *$//')" ;;
518
+ model:*) MODEL="$(echo "${line#model:}" | tr -d ' ')" ;;
519
+ agents:*) AGENTS="$(echo "${line#agents:}" | tr -d ' ')" ;;
520
+ consecutive_failures:*) CONSECUTIVE_FAILURES="$(echo "${line#consecutive_failures:}" | tr -d ' ')" ;;
521
+ total_commits:*) TOTAL_COMMITS="$(echo "${line#total_commits:}" | tr -d ' ')" ;;
522
+ audit_enabled:*) AUDIT_ENABLED="$(echo "${line#audit_enabled:}" | tr -d ' ')" ;;
523
+ audit_agent_enabled:*) AUDIT_AGENT_ENABLED="$(echo "${line#audit_agent_enabled:}" | tr -d ' ')" ;;
524
+ quality_gates_enabled:*) QUALITY_GATES_ENABLED="$(echo "${line#quality_gates_enabled:}" | tr -d ' ')" ;;
525
+ dod_file:*) DOD_FILE="$(echo "${line#dod_file:}" | sed 's/^ *"//;s/" *$//')" ;;
526
+ auto_extend:*) AUTO_EXTEND="$(echo "${line#auto_extend:}" | tr -d ' ')" ;;
527
+ extension_count:*) EXTENSION_COUNT="$(echo "${line#extension_count:}" | tr -d ' ')" ;;
528
+ max_extensions:*) MAX_EXTENSIONS="$(echo "${line#max_extensions:}" | tr -d ' ')" ;;
529
+ esac
530
+ fi
531
+ done < "$STATE_FILE"
532
+
533
+ # CLI --max-iterations overrides state file
534
+ if $MAX_ITERATIONS_EXPLICIT; then
535
+ MAX_ITERATIONS="$cli_max_iterations"
536
+ fi
537
+
538
+ # Extract the log section (everything after ## Log)
539
+ LOG_ENTRIES="$(sed -n '/^## Log$/,$ { /^## Log$/d; p; }' "$STATE_FILE" 2>/dev/null || true)"
540
+
541
+ if [[ -z "$GOAL" ]]; then
542
+ error "Could not parse goal from state file."
543
+ exit 1
544
+ fi
545
+
546
+ if [[ "$STATUS" == "complete" ]]; then
547
+ warn "Previous loop completed. Start a new one or edit the state file."
548
+ exit 0
549
+ fi
550
+
551
+ # Reset circuit breaker on resume
552
+ CONSECUTIVE_FAILURES=0
553
+ START_EPOCH="$(now_epoch)"
554
+ STATUS="running"
555
+
556
+ # If we hit max iterations before, warn user to extend
557
+ if [[ "$ITERATION" -ge "$MAX_ITERATIONS" ]] && ! $MAX_ITERATIONS_EXPLICIT; then
558
+ warn "Previous run stopped at iteration $ITERATION/$MAX_ITERATIONS."
559
+ echo -e " Extend with: ${DIM}shipwright loop --resume --max-iterations $(( MAX_ITERATIONS + 10 ))${RESET}"
560
+ exit 0
561
+ fi
562
+
563
+ success "Resumed: iteration $ITERATION/$MAX_ITERATIONS"
564
+ }
565
+
566
+ write_state() {
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
628
+ }
629
+
630
+ append_log_entry() {
631
+ local entry="$1"
632
+ if [[ -n "$LOG_ENTRIES" ]]; then
633
+ LOG_ENTRIES="${LOG_ENTRIES}
634
+ ${entry}"
635
+ else
636
+ LOG_ENTRIES="$entry"
637
+ fi
638
+ }
639
+
640
+ # ─── Git Helpers ──────────────────────────────────────────────────────────────
641
+
642
+ git_commit_count() {
643
+ git -C "$PROJECT_ROOT" rev-list --count HEAD 2>/dev/null || echo 0
644
+ }
645
+
646
+ git_recent_log() {
647
+ git -C "$PROJECT_ROOT" log --oneline -20 2>/dev/null || echo "(no commits)"
648
+ }
649
+
650
+ git_diff_stat() {
651
+ git -C "$PROJECT_ROOT" diff --stat HEAD~1 2>/dev/null | tail -1 || echo ""
652
+ }
653
+
654
+ git_auto_commit() {
655
+ local work_dir="${1:-$PROJECT_ROOT}"
656
+ # Only commit if there are changes
657
+ if git -C "$work_dir" diff --quiet && git -C "$work_dir" diff --cached --quiet; then
658
+ # Check for untracked files
659
+ local untracked
660
+ untracked="$(git -C "$work_dir" ls-files --others --exclude-standard | head -1)"
661
+ if [[ -z "$untracked" ]]; then
662
+ return 1 # Nothing to commit
663
+ fi
664
+ fi
665
+
666
+ git -C "$work_dir" add -A 2>/dev/null || true
667
+ git -C "$work_dir" commit -m "loop: iteration $ITERATION — autonomous progress" --no-verify 2>/dev/null || return 1
668
+ return 0
669
+ }
670
+
671
+ # ─── Progress & Circuit Breaker ───────────────────────────────────────────────
672
+
673
+ check_progress() {
674
+ local changes
675
+ changes="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 2>/dev/null | tail -1 || echo "")"
676
+ local insertions
677
+ insertions="$(echo "$changes" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)"
678
+ if [[ "${insertions:-0}" -lt "$MIN_PROGRESS_LINES" ]]; then
679
+ return 1 # No meaningful progress
680
+ fi
681
+ return 0
682
+ }
683
+
684
+ check_completion() {
685
+ local log_file="$1"
686
+ grep -q "LOOP_COMPLETE" "$log_file" 2>/dev/null
687
+ }
688
+
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
714
+ if [[ "$CONSECUTIVE_FAILURES" -ge "$CIRCUIT_BREAKER_THRESHOLD" ]]; then
715
+ error "Circuit breaker tripped: ${CIRCUIT_BREAKER_THRESHOLD} consecutive iterations with no meaningful progress."
716
+ STATUS="circuit_breaker"
717
+ return 1
718
+ fi
719
+ return 0
720
+ }
721
+
722
+ check_max_iterations() {
723
+ if [[ "$ITERATION" -le "$MAX_ITERATIONS" ]]; then
724
+ return 0
725
+ fi
726
+
727
+ # Hit the cap — check if we should auto-extend
728
+ if ! $AUTO_EXTEND || [[ "$EXTENSION_COUNT" -ge "$MAX_EXTENSIONS" ]]; then
729
+ if [[ "$EXTENSION_COUNT" -ge "$MAX_EXTENSIONS" ]]; then
730
+ warn "Hard cap reached: ${EXTENSION_COUNT} extensions applied (max ${MAX_EXTENSIONS})."
731
+ fi
732
+ warn "Max iterations ($MAX_ITERATIONS) reached."
733
+ STATUS="max_iterations"
734
+ return 1
735
+ fi
736
+
737
+ # Checkpoint audit: is there meaningful progress worth extending for?
738
+ echo -e "\n ${CYAN}${BOLD}▸ Checkpoint${RESET} — max iterations ($MAX_ITERATIONS) reached, evaluating progress..."
739
+
740
+ local should_extend=false
741
+ local extension_reason=""
742
+
743
+ # Check 1: recent meaningful progress (not stuck)
744
+ if [[ "${CONSECUTIVE_FAILURES:-0}" -lt 2 ]]; then
745
+ # Check 2: agent hasn't signaled completion (if it did, guard_completion handles it)
746
+ local last_log="$LOG_DIR/iteration-$(( ITERATION - 1 )).log"
747
+ if [[ -f "$last_log" ]] && ! grep -q "LOOP_COMPLETE" "$last_log" 2>/dev/null; then
748
+ should_extend=true
749
+ extension_reason="work in progress with recent progress"
750
+ fi
751
+ fi
752
+
753
+ # Check 3: if quality gates or tests are failing, extend to let agent fix them
754
+ if [[ "$TEST_PASSED" == "false" ]] || ! $QUALITY_GATE_PASSED; then
755
+ should_extend=true
756
+ extension_reason="quality gates or tests not yet passing"
757
+ fi
758
+
759
+ if $should_extend; then
760
+ # Scale extension size by velocity — good progress earns more iterations
761
+ local velocity_avg
762
+ velocity_avg="$(compute_velocity_avg)"
763
+ local effective_extension="$EXTENSION_SIZE"
764
+ if [[ "$velocity_avg" -gt 20 ]]; then
765
+ # High velocity: grant more iterations
766
+ effective_extension=$(( EXTENSION_SIZE + 3 ))
767
+ elif [[ "$velocity_avg" -lt 5 ]]; then
768
+ # Low velocity: grant fewer iterations
769
+ effective_extension=$(( EXTENSION_SIZE > 2 ? EXTENSION_SIZE - 2 : 1 ))
770
+ fi
771
+ EXTENSION_COUNT=$(( EXTENSION_COUNT + 1 ))
772
+ MAX_ITERATIONS=$(( MAX_ITERATIONS + effective_extension ))
773
+ echo -e " ${GREEN}✓${RESET} Auto-extending: +${effective_extension} iterations (now ${MAX_ITERATIONS} max, extension ${EXTENSION_COUNT}/${MAX_EXTENSIONS})"
774
+ echo -e " ${DIM}Reason: ${extension_reason} | velocity: ~${velocity_avg} lines/iter${RESET}"
775
+ return 0
776
+ fi
777
+
778
+ warn "Max iterations reached — no recent progress detected."
779
+ STATUS="max_iterations"
780
+ return 1
781
+ }
782
+
783
+ # ─── Test Gate ────────────────────────────────────────────────────────────────
784
+
785
+ run_test_gate() {
786
+ if [[ -z "$TEST_CMD" ]]; then
787
+ TEST_PASSED=""
788
+ TEST_OUTPUT=""
789
+ return
790
+ fi
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
+
806
+ local test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
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
818
+ TEST_PASSED=true
819
+ TEST_OUTPUT="All tests passed (${test_mode} mode)."
820
+ else
821
+ TEST_PASSED=false
822
+ TEST_OUTPUT="$(tail -50 "$test_log")"
823
+ fi
824
+ }
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
+
874
+ # ─── Audit Agent ─────────────────────────────────────────────────────────────
875
+
876
+ run_audit_agent() {
877
+ if ! $AUDIT_AGENT_ENABLED; then
878
+ return
879
+ fi
880
+
881
+ local log_file="$LOG_DIR/iteration-${ITERATION}.log"
882
+ local audit_log="$LOG_DIR/audit-iter-${ITERATION}.log"
883
+
884
+ # Gather context: tail of implementer output + git diff
885
+ local impl_tail
886
+ impl_tail="$(tail -100 "$log_file" 2>/dev/null || echo "(no output)")"
887
+ local diff_stat
888
+ diff_stat="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 2>/dev/null || echo "(no changes)")"
889
+
890
+ local audit_prompt
891
+ read -r -d '' audit_prompt <<AUDIT_PROMPT || true
892
+ You are an independent code auditor reviewing an autonomous coding agent.
893
+
894
+ ## Goal the agent was working toward
895
+ ${GOAL}
896
+
897
+ ## Agent Output (last 100 lines)
898
+ ${impl_tail}
899
+
900
+ ## Changes Made (git diff --stat)
901
+ ${diff_stat}
902
+
903
+ ## Your Task
904
+ Critically review the work:
905
+ 1. Did the agent make meaningful progress toward the goal?
906
+ 2. Are there obvious bugs, logic errors, or security issues?
907
+ 3. Did the agent leave incomplete work (TODOs, placeholder code)?
908
+ 4. Are there any regressions or broken patterns?
909
+ 5. Is the code quality acceptable?
910
+
911
+ If the work is acceptable and moves toward the goal, output exactly: AUDIT_PASS
912
+ Otherwise, list the specific issues that need fixing.
913
+ AUDIT_PROMPT
914
+
915
+ echo -e " ${PURPLE}▸${RESET} Running audit agent..."
916
+
917
+ # Select audit model adaptively (haiku if success rate high, else sonnet)
918
+ local audit_model
919
+ audit_model="$(select_audit_model)"
920
+ local audit_flags=()
921
+ audit_flags+=("--model" "$audit_model")
922
+ if $SKIP_PERMISSIONS; then
923
+ audit_flags+=("--dangerously-skip-permissions")
924
+ fi
925
+
926
+ local exit_code=0
927
+ claude -p "$audit_prompt" "${audit_flags[@]}" > "$audit_log" 2>&1 || exit_code=$?
928
+
929
+ if grep -q "AUDIT_PASS" "$audit_log" 2>/dev/null; then
930
+ AUDIT_RESULT="pass"
931
+ echo -e " ${GREEN}✓${RESET} Audit: passed"
932
+ else
933
+ AUDIT_RESULT="$(grep -v '^$' "$audit_log" | tail -20 | head -10 2>/dev/null || echo "Audit returned no output")"
934
+ echo -e " ${YELLOW}⚠${RESET} Audit: issues found"
935
+ fi
936
+ }
937
+
938
+ # ─── Quality Gates ───────────────────────────────────────────────────────────
939
+
940
+ run_quality_gates() {
941
+ if ! $QUALITY_GATES_ENABLED; then
942
+ QUALITY_GATE_PASSED=true
943
+ return
944
+ fi
945
+
946
+ QUALITY_GATE_PASSED=true
947
+ local gate_failures=()
948
+
949
+ echo -e " ${PURPLE}▸${RESET} Running quality gates..."
950
+
951
+ # Gate 1: Tests pass (if TEST_CMD set)
952
+ if [[ -n "$TEST_CMD" ]] && [[ "$TEST_PASSED" == "false" ]]; then
953
+ gate_failures+=("tests failing")
954
+ fi
955
+
956
+ # Gate 2: No uncommitted changes
957
+ if ! git -C "$PROJECT_ROOT" diff --quiet 2>/dev/null || \
958
+ ! git -C "$PROJECT_ROOT" diff --cached --quiet 2>/dev/null; then
959
+ gate_failures+=("uncommitted changes present")
960
+ fi
961
+
962
+ # Gate 3: No TODO/FIXME/HACK/XXX in new code
963
+ local todo_count
964
+ todo_count="$(git -C "$PROJECT_ROOT" diff HEAD~1 2>/dev/null | grep -cE '^\+.*(TODO|FIXME|HACK|XXX)' || true)"
965
+ todo_count="${todo_count:-0}"
966
+ if [[ "${todo_count:-0}" -gt 0 ]]; then
967
+ gate_failures+=("${todo_count} TODO/FIXME/HACK/XXX markers in new code")
968
+ fi
969
+
970
+ # Gate 4: Definition of Done (if DOD_FILE set)
971
+ if [[ -n "$DOD_FILE" ]]; then
972
+ if ! check_definition_of_done; then
973
+ gate_failures+=("definition of done not satisfied")
974
+ fi
975
+ fi
976
+
977
+ if [[ ${#gate_failures[@]} -gt 0 ]]; then
978
+ QUALITY_GATE_PASSED=false
979
+ local failures_str
980
+ failures_str="$(printf ', %s' "${gate_failures[@]}")"
981
+ failures_str="${failures_str:2}" # trim leading ", "
982
+ echo -e " ${RED}✗${RESET} Quality gates: FAILED (${failures_str})"
983
+ else
984
+ echo -e " ${GREEN}✓${RESET} Quality gates: all passed"
985
+ fi
986
+ }
987
+
988
+ check_definition_of_done() {
989
+ if [[ ! -f "$DOD_FILE" ]]; then
990
+ warn "Definition of done file not found: $DOD_FILE"
991
+ return 1
992
+ fi
993
+
994
+ local dod_content
995
+ dod_content="$(cat "$DOD_FILE")"
996
+ local diff_content
997
+ diff_content="$(git -C "$PROJECT_ROOT" diff HEAD~1 2>/dev/null || echo "(no diff)")"
998
+
999
+ local dod_prompt
1000
+ read -r -d '' dod_prompt <<DOD_PROMPT || true
1001
+ You are evaluating whether code changes satisfy a Definition of Done checklist.
1002
+
1003
+ ## Definition of Done
1004
+ ${dod_content}
1005
+
1006
+ ## Changes Made (git diff)
1007
+ ${diff_content}
1008
+
1009
+ ## Your Task
1010
+ For each item in the Definition of Done, determine if the changes satisfy it.
1011
+ If ALL items are satisfied, output exactly: DOD_PASS
1012
+ Otherwise, list which items are NOT satisfied and why.
1013
+ DOD_PROMPT
1014
+
1015
+ local dod_log="$LOG_DIR/dod-iter-${ITERATION}.log"
1016
+ local dod_model
1017
+ dod_model="$(select_audit_model)"
1018
+ local dod_flags=()
1019
+ dod_flags+=("--model" "$dod_model")
1020
+ if $SKIP_PERMISSIONS; then
1021
+ dod_flags+=("--dangerously-skip-permissions")
1022
+ fi
1023
+
1024
+ claude -p "$dod_prompt" "${dod_flags[@]}" > "$dod_log" 2>&1 || true
1025
+
1026
+ if grep -q "DOD_PASS" "$dod_log" 2>/dev/null; then
1027
+ echo -e " ${GREEN}✓${RESET} Definition of Done: satisfied"
1028
+ return 0
1029
+ else
1030
+ echo -e " ${YELLOW}⚠${RESET} Definition of Done: not satisfied"
1031
+ return 1
1032
+ fi
1033
+ }
1034
+
1035
+ # ─── Guarded Completion ──────────────────────────────────────────────────────
1036
+
1037
+ guard_completion() {
1038
+ local log_file="$LOG_DIR/iteration-${ITERATION}.log"
1039
+
1040
+ # Check if LOOP_COMPLETE is in the log
1041
+ if ! grep -q "LOOP_COMPLETE" "$log_file" 2>/dev/null; then
1042
+ return 1 # No completion claim
1043
+ fi
1044
+
1045
+ echo -e " ${CYAN}▸${RESET} LOOP_COMPLETE detected — validating..."
1046
+
1047
+ local rejection_reasons=()
1048
+
1049
+ # Check quality gates
1050
+ if ! $QUALITY_GATE_PASSED; then
1051
+ rejection_reasons+=("quality gates failed")
1052
+ fi
1053
+
1054
+ # Check audit agent
1055
+ if $AUDIT_AGENT_ENABLED && [[ "$AUDIT_RESULT" != "pass" ]]; then
1056
+ rejection_reasons+=("audit agent found issues")
1057
+ fi
1058
+
1059
+ # Check tests
1060
+ if [[ -n "$TEST_CMD" ]] && [[ "$TEST_PASSED" == "false" ]]; then
1061
+ rejection_reasons+=("tests failing")
1062
+ fi
1063
+
1064
+ if [[ ${#rejection_reasons[@]} -gt 0 ]]; then
1065
+ local reasons_str
1066
+ reasons_str="$(printf ', %s' "${rejection_reasons[@]}")"
1067
+ reasons_str="${reasons_str:2}"
1068
+ echo -e " ${RED}✗${RESET} Completion REJECTED: ${reasons_str}"
1069
+ COMPLETION_REJECTED=true
1070
+ return 1
1071
+ fi
1072
+
1073
+ echo -e " ${GREEN}${BOLD}✓ LOOP_COMPLETE accepted — all gates passed!${RESET}"
1074
+ return 0
1075
+ }
1076
+
1077
+ # ─── Prompt Composition ──────────────────────────────────────────────────────
1078
+
1079
+ compose_prompt() {
1080
+ local recent_log
1081
+ # Get last 3 iteration summaries from log entries
1082
+ recent_log="$(echo "$LOG_ENTRIES" | tail -15)"
1083
+ if [[ -z "$recent_log" ]]; then
1084
+ recent_log="(first iteration — no previous progress)"
1085
+ fi
1086
+
1087
+ local git_log
1088
+ git_log="$(git_recent_log)"
1089
+
1090
+ local test_section
1091
+ if [[ -z "$TEST_CMD" ]]; then
1092
+ test_section="No test command configured."
1093
+ elif [[ -z "$TEST_PASSED" ]]; then
1094
+ test_section="No test results yet (first iteration). Test command: $TEST_CMD"
1095
+ elif $TEST_PASSED; then
1096
+ test_section="$TEST_OUTPUT"
1097
+ else
1098
+ test_section="TESTS FAILED — fix these before proceeding:
1099
+ $TEST_OUTPUT"
1100
+ fi
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
+
1117
+ # Build audit sections (captured before heredoc to avoid nested heredoc issues)
1118
+ local audit_section
1119
+ audit_section="$(compose_audit_section)"
1120
+ local audit_feedback_section
1121
+ audit_feedback_section="$(compose_audit_feedback_section)"
1122
+ local rejection_notice_section
1123
+ rejection_notice_section="$(compose_rejection_notice_section)"
1124
+
1125
+ # Memory context injection (failure patterns + past learnings)
1126
+ local memory_section=""
1127
+ if type memory_inject_context &>/dev/null 2>&1; then
1128
+ memory_section="$(memory_inject_context "build" 2>/dev/null || true)"
1129
+ elif [[ -f "$SCRIPT_DIR/sw-memory.sh" ]]; then
1130
+ memory_section="$("$SCRIPT_DIR/sw-memory.sh" inject build 2>/dev/null || true)"
1131
+ fi
1132
+
1133
+ # DORA baselines for context
1134
+ local dora_section=""
1135
+ if type memory_get_dora_baseline &>/dev/null 2>&1; then
1136
+ local dora_json
1137
+ dora_json="$(memory_get_dora_baseline 7 2>/dev/null || echo "{}")"
1138
+ local dora_total
1139
+ dora_total=$(echo "$dora_json" | jq -r '.total // 0' 2>/dev/null || echo "0")
1140
+ if [[ "$dora_total" -gt 0 ]]; then
1141
+ local dora_df dora_cfr
1142
+ dora_df=$(echo "$dora_json" | jq -r '.deploy_freq // 0' 2>/dev/null || echo "0")
1143
+ dora_cfr=$(echo "$dora_json" | jq -r '.cfr // 0' 2>/dev/null || echo "0")
1144
+ dora_section="## Performance Baselines (Last 7 Days)
1145
+ - Deploy frequency: ${dora_df}/week
1146
+ - Change failure rate: ${dora_cfr}%
1147
+ - Total pipeline runs: ${dora_total}"
1148
+ fi
1149
+ fi
1150
+
1151
+ # Append mid-loop memory refresh if available
1152
+ local memory_refresh_file="$LOG_DIR/memory-refresh-$(( ITERATION - 1 )).txt"
1153
+ if [[ -f "$memory_refresh_file" ]]; then
1154
+ memory_section="${memory_section}
1155
+
1156
+ ## Fresh Context (from iteration $(( ITERATION - 1 )) analysis)
1157
+ $(cat "$memory_refresh_file")"
1158
+ fi
1159
+
1160
+ # GitHub intelligence context (gated by availability)
1161
+ local intelligence_section=""
1162
+ if [[ "${NO_GITHUB:-}" != "true" ]]; then
1163
+ # File hotspots — top 5 most-changed files
1164
+ if type gh_file_change_frequency &>/dev/null 2>&1; then
1165
+ local hotspots
1166
+ hotspots=$(gh_file_change_frequency 2>/dev/null | head -5 || true)
1167
+ if [[ -n "$hotspots" ]]; then
1168
+ intelligence_section="${intelligence_section}
1169
+ ## File Hotspots (most frequently changed)
1170
+ ${hotspots}"
1171
+ fi
1172
+ fi
1173
+
1174
+ # CODEOWNERS context
1175
+ if type gh_codeowners &>/dev/null 2>&1; then
1176
+ local owners
1177
+ owners=$(gh_codeowners 2>/dev/null | head -10 || true)
1178
+ if [[ -n "$owners" ]]; then
1179
+ intelligence_section="${intelligence_section}
1180
+ ## Code Owners
1181
+ ${owners}"
1182
+ fi
1183
+ fi
1184
+
1185
+ # Active security alerts
1186
+ if type gh_security_alerts &>/dev/null 2>&1; then
1187
+ local alerts
1188
+ alerts=$(gh_security_alerts 2>/dev/null | head -5 || true)
1189
+ if [[ -n "$alerts" ]]; then
1190
+ intelligence_section="${intelligence_section}
1191
+ ## Active Security Alerts
1192
+ ${alerts}"
1193
+ fi
1194
+ fi
1195
+ fi
1196
+
1197
+ # Architecture rules (from intelligence layer)
1198
+ local repo_hash
1199
+ repo_hash=$(echo -n "$(pwd)" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
1200
+ local arch_file="${HOME}/.shipwright/memory/${repo_hash}/architecture.json"
1201
+ if [[ -f "$arch_file" ]]; then
1202
+ local arch_rules
1203
+ arch_rules=$(jq -r '.rules[]? // empty' "$arch_file" 2>/dev/null | head -10 || true)
1204
+ if [[ -n "$arch_rules" ]]; then
1205
+ intelligence_section="${intelligence_section}
1206
+ ## Architecture Rules
1207
+ ${arch_rules}"
1208
+ fi
1209
+ fi
1210
+
1211
+ # Coverage baseline
1212
+ local coverage_file="${HOME}/.shipwright/baselines/${repo_hash}/coverage.json"
1213
+ if [[ -f "$coverage_file" ]]; then
1214
+ local coverage_pct
1215
+ coverage_pct=$(jq -r '.coverage_percent // empty' "$coverage_file" 2>/dev/null || true)
1216
+ if [[ -n "$coverage_pct" ]]; then
1217
+ intelligence_section="${intelligence_section}
1218
+ ## Coverage Baseline
1219
+ Current coverage: ${coverage_pct}% — do not decrease this."
1220
+ fi
1221
+ fi
1222
+
1223
+ # Error classification from last failure
1224
+ local error_log=".claude/pipeline-artifacts/error-log.jsonl"
1225
+ if [[ -f "$error_log" ]]; then
1226
+ local last_error
1227
+ last_error=$(tail -1 "$error_log" 2>/dev/null | jq -r '"Type: \(.type), Exit: \(.exit_code), Error: \(.error | split("\n") | first)"' 2>/dev/null || true)
1228
+ if [[ -n "$last_error" ]]; then
1229
+ intelligence_section="${intelligence_section}
1230
+ ## Last Error Context
1231
+ ${last_error}"
1232
+ fi
1233
+ fi
1234
+
1235
+ # Stuckness detection — compare last 3 iteration outputs
1236
+ local stuckness_section=""
1237
+ stuckness_section="$(detect_stuckness)"
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
+
1249
+ cat <<PROMPT
1250
+ You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
1251
+
1252
+ ## Your Goal
1253
+ ${GOAL}
1254
+
1255
+ ## Current Progress
1256
+ ${recent_log}
1257
+
1258
+ ## Recent Git Activity
1259
+ ${git_log}
1260
+
1261
+ ## Test Results (Previous Iteration)
1262
+ ${test_section}
1263
+
1264
+ ${error_summary_section:+$error_summary_section
1265
+ }
1266
+ ${memory_section:+## Memory Context
1267
+ $memory_section
1268
+ }
1269
+ ${dora_section:+$dora_section
1270
+ }
1271
+ ${intelligence_section:+$intelligence_section
1272
+ }
1273
+ ${restart_section:+$restart_section
1274
+ }
1275
+ ## Instructions
1276
+ 1. Read the codebase and understand the current state
1277
+ 2. Identify the highest-priority remaining work toward the goal
1278
+ 3. Implement ONE meaningful chunk of progress
1279
+ 4. Run tests if a test command exists: ${TEST_CMD:-"(none)"}
1280
+ 5. Commit your work with a descriptive message
1281
+ 6. When the goal is FULLY achieved, output exactly: LOOP_COMPLETE
1282
+
1283
+ ${audit_section}
1284
+
1285
+ ${audit_feedback_section}
1286
+
1287
+ ${rejection_notice_section}
1288
+
1289
+ ${stuckness_section}
1290
+
1291
+ ## Rules
1292
+ - Focus on ONE task per iteration — do it well
1293
+ - Always commit with descriptive messages
1294
+ - If tests fail, fix them before ending
1295
+ - If stuck on the same issue for 2+ iterations, try a different approach
1296
+ - Do NOT output LOOP_COMPLETE unless the goal is genuinely achieved
1297
+ PROMPT
1298
+ }
1299
+
1300
+ # ─── Stuckness Detection ─────────────────────────────────────────────────────
1301
+ # Compares last 3 iteration log outputs for high overlap (>90% similar lines).
1302
+ detect_stuckness() {
1303
+ if [[ "$ITERATION" -lt 3 ]]; then
1304
+ return 0
1305
+ fi
1306
+
1307
+ local log1="$LOG_DIR/iteration-$(( ITERATION - 1 )).log"
1308
+ local log2="$LOG_DIR/iteration-$(( ITERATION - 2 )).log"
1309
+ local log3="$LOG_DIR/iteration-$(( ITERATION - 3 )).log"
1310
+
1311
+ # Need at least 2 previous logs
1312
+ if [[ ! -f "$log1" || ! -f "$log2" ]]; then
1313
+ return 0
1314
+ fi
1315
+
1316
+ # Compare last 50 lines of each (ignoring timestamps and blank lines)
1317
+ local lines1 lines2 common total overlap_pct
1318
+ lines1=$(tail -50 "$log1" 2>/dev/null | grep -v '^$' | sort || true)
1319
+ lines2=$(tail -50 "$log2" 2>/dev/null | grep -v '^$' | sort || true)
1320
+
1321
+ if [[ -z "$lines1" || -z "$lines2" ]]; then
1322
+ return 0
1323
+ fi
1324
+
1325
+ total=$(echo "$lines1" | wc -l | tr -d ' ')
1326
+ common=$(comm -12 <(echo "$lines1") <(echo "$lines2") 2>/dev/null | wc -l | tr -d ' ' || echo "0")
1327
+
1328
+ if [[ "$total" -gt 0 ]]; then
1329
+ overlap_pct=$(( common * 100 / total ))
1330
+ else
1331
+ overlap_pct=0
1332
+ fi
1333
+
1334
+ if [[ "$overlap_pct" -ge 90 ]]; then
1335
+ local diff_summary=""
1336
+ if [[ -f "$log3" ]]; then
1337
+ diff_summary=$(diff <(tail -30 "$log3" 2>/dev/null) <(tail -30 "$log1" 2>/dev/null) 2>/dev/null | head -10 || true)
1338
+ fi
1339
+
1340
+ # Gather memory-based alternative approaches
1341
+ local alternatives=""
1342
+ if type memory_inject_context &>/dev/null 2>&1; then
1343
+ alternatives=$(memory_inject_context "build" 2>/dev/null | grep -i "fix:" | head -3 || true)
1344
+ fi
1345
+
1346
+ cat <<STUCK_SECTION
1347
+ ## Stuckness Detected
1348
+ Your last ${CONSECUTIVE_FAILURES:-2}+ iterations produced very similar output (${overlap_pct}% overlap).
1349
+ You appear to be stuck on the same approach.
1350
+
1351
+ ${diff_summary:+Changes between recent iterations:
1352
+ $diff_summary
1353
+ }
1354
+ ${alternatives:+Consider these alternative approaches from past fixes:
1355
+ $alternatives
1356
+ }
1357
+ Try a fundamentally different approach:
1358
+ - Break the problem into smaller steps
1359
+ - Look for an entirely different implementation strategy
1360
+ - Check if there's a dependency or configuration issue blocking progress
1361
+ - Read error messages more carefully — the root cause may differ from your assumption
1362
+ STUCK_SECTION
1363
+ fi
1364
+ }
1365
+
1366
+ compose_audit_section() {
1367
+ if ! $AUDIT_ENABLED; then
1368
+ return
1369
+ fi
1370
+
1371
+ # Try to inject audit items from past review feedback in memory
1372
+ local memory_audit_items=""
1373
+ if [[ -f "$SCRIPT_DIR/sw-memory.sh" ]]; then
1374
+ local mem_dir_path
1375
+ mem_dir_path="$HOME/.shipwright/memory"
1376
+ # Look for review feedback in any repo memory
1377
+ local repo_hash_val
1378
+ repo_hash_val=$(git config --get remote.origin.url 2>/dev/null | shasum -a 256 2>/dev/null | cut -c1-12 || echo "")
1379
+ if [[ -n "$repo_hash_val" && -f "$mem_dir_path/$repo_hash_val/failures.json" ]]; then
1380
+ memory_audit_items=$(jq -r '.failures[] | select(.stage == "review" and .pattern != "") |
1381
+ "- Check for: \(.pattern[:100])"' \
1382
+ "$mem_dir_path/$repo_hash_val/failures.json" 2>/dev/null | head -5 || true)
1383
+ fi
1384
+ fi
1385
+
1386
+ echo "## Self-Audit Checklist"
1387
+ echo "Before declaring LOOP_COMPLETE, critically evaluate your own work:"
1388
+ echo "1. Does the implementation FULLY satisfy the goal, not just partially?"
1389
+ echo "2. Are there any edge cases you haven't handled?"
1390
+ echo "3. Did you leave any TODO, FIXME, HACK, or XXX comments in new code?"
1391
+ echo "4. Are all new functions/modules tested (if a test command exists)?"
1392
+ echo "5. Would a code reviewer approve this, or would they request changes?"
1393
+ echo "6. Is the code clean, well-structured, and following project conventions?"
1394
+ if [[ -n "$memory_audit_items" ]]; then
1395
+ echo ""
1396
+ echo "Common review findings from this repo's history:"
1397
+ echo "$memory_audit_items"
1398
+ fi
1399
+ echo ""
1400
+ echo "If ANY answer is \"no\", do NOT output LOOP_COMPLETE. Instead, fix the issues first."
1401
+ }
1402
+
1403
+ compose_audit_feedback_section() {
1404
+ if [[ -z "$AUDIT_RESULT" ]] || [[ "$AUDIT_RESULT" == "pass" ]]; then
1405
+ return
1406
+ fi
1407
+ cat <<AUDIT_FEEDBACK
1408
+ ## Audit Feedback (Previous Iteration)
1409
+ An independent audit of your last iteration found these issues:
1410
+ ${AUDIT_RESULT}
1411
+
1412
+ Address ALL audit findings before proceeding with new work.
1413
+ AUDIT_FEEDBACK
1414
+ }
1415
+
1416
+ compose_rejection_notice_section() {
1417
+ if ! $COMPLETION_REJECTED; then
1418
+ return
1419
+ fi
1420
+ COMPLETION_REJECTED=false
1421
+ cat <<'REJECTION'
1422
+ ## ⚠ Completion Rejected
1423
+ Your previous LOOP_COMPLETE was REJECTED because quality gates did not pass.
1424
+ Review the audit feedback and test results above, fix the issues, then try again.
1425
+ Do NOT output LOOP_COMPLETE until all quality checks pass.
1426
+ REJECTION
1427
+ }
1428
+
1429
+ compose_worker_prompt() {
1430
+ local agent_num="$1"
1431
+ local total_agents="$2"
1432
+
1433
+ local base_prompt
1434
+ base_prompt="$(compose_prompt)"
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
+
1467
+ cat <<PROMPT
1468
+ ${base_prompt}
1469
+
1470
+ ## Agent Identity
1471
+ You are Agent ${agent_num} of ${total_agents}. Other agents are working in parallel.
1472
+ Check git log to see what they've done — avoid duplicating their work.
1473
+ Focus on areas they haven't touched yet.
1474
+
1475
+ ${role_section}
1476
+ PROMPT
1477
+ }
1478
+
1479
+ # ─── Claude Execution ────────────────────────────────────────────────────────
1480
+
1481
+ build_claude_flags() {
1482
+ local flags=()
1483
+ flags+=("--model" "$MODEL")
1484
+
1485
+ if $SKIP_PERMISSIONS; then
1486
+ flags+=("--dangerously-skip-permissions")
1487
+ fi
1488
+
1489
+ if [[ -n "$MAX_TURNS" ]]; then
1490
+ flags+=("--max-turns" "$MAX_TURNS")
1491
+ fi
1492
+
1493
+ echo "${flags[*]}"
1494
+ }
1495
+
1496
+ run_claude_iteration() {
1497
+ local log_file="$LOG_DIR/iteration-${ITERATION}.log"
1498
+ local prompt
1499
+ prompt="$(compose_prompt)"
1500
+
1501
+ local flags
1502
+ flags="$(build_claude_flags)"
1503
+
1504
+ local iter_start
1505
+ iter_start="$(now_epoch)"
1506
+
1507
+ echo -e "\n${CYAN}${BOLD}▸${RESET} ${BOLD}Iteration ${ITERATION}/${MAX_ITERATIONS}${RESET} — Starting..."
1508
+
1509
+ # Run Claude headless (with timeout + PID capture for signal handling)
1510
+ local exit_code=0
1511
+ # shellcheck disable=SC2086
1512
+ if [[ -n "$TIMEOUT_CMD" ]]; then
1513
+ $TIMEOUT_CMD "$CLAUDE_TIMEOUT" claude -p "$prompt" $flags > "$log_file" 2>&1 &
1514
+ else
1515
+ claude -p "$prompt" $flags > "$log_file" 2>&1 &
1516
+ fi
1517
+ CHILD_PID=$!
1518
+ wait "$CHILD_PID" 2>/dev/null || exit_code=$?
1519
+ CHILD_PID=""
1520
+ if [[ "$exit_code" -eq 124 ]]; then
1521
+ warn "Claude CLI timed out after ${CLAUDE_TIMEOUT}s"
1522
+ fi
1523
+
1524
+ local iter_end
1525
+ iter_end="$(now_epoch)"
1526
+ local iter_duration=$(( iter_end - iter_start ))
1527
+
1528
+ echo -e " ${GREEN}✓${RESET} Claude session completed ($(format_duration "$iter_duration"), exit $exit_code)"
1529
+
1530
+ # Show verbose output if requested
1531
+ if $VERBOSE; then
1532
+ echo -e " ${DIM}─── Claude Output ───${RESET}"
1533
+ sed 's/^/ /' "$log_file" | head -100
1534
+ echo -e " ${DIM}─────────────────────${RESET}"
1535
+ fi
1536
+
1537
+ return $exit_code
1538
+ }
1539
+
1540
+ # ─── Iteration Summary Extraction ────────────────────────────────────────────
1541
+
1542
+ extract_summary() {
1543
+ local log_file="$1"
1544
+ # Grab last meaningful lines from Claude output, skipping empty lines
1545
+ local summary
1546
+ summary="$(grep -v '^$' "$log_file" | tail -5 | head -3 2>/dev/null || echo "(no output)")"
1547
+ # Truncate long lines
1548
+ echo "$summary" | cut -c1-120
1549
+ }
1550
+
1551
+ # ─── Display Helpers ─────────────────────────────────────────────────────────
1552
+
1553
+ show_banner() {
1554
+ echo ""
1555
+ echo -e "${CYAN}${BOLD}shipwright${RESET} ${DIM}v${VERSION}${RESET} — ${BOLD}Continuous Loop${RESET}"
1556
+ echo -e "${CYAN}═══════════════════════════════════════════════${RESET}"
1557
+ echo ""
1558
+ echo -e " ${BOLD}Goal:${RESET} $GOAL"
1559
+ local extend_info=""
1560
+ if $AUTO_EXTEND; then
1561
+ extend_info=" ${DIM}(auto-extend: +${EXTENSION_SIZE} x${MAX_EXTENSIONS})${RESET}"
1562
+ fi
1563
+ echo -e " ${BOLD}Model:${RESET} $MODEL ${DIM}|${RESET} ${BOLD}Max:${RESET} $MAX_ITERATIONS iterations${extend_info} ${DIM}|${RESET} ${BOLD}Test:${RESET} ${TEST_CMD:-"(none)"}"
1564
+ if [[ "$AGENTS" -gt 1 ]]; then
1565
+ echo -e " ${BOLD}Agents:${RESET} $AGENTS ${DIM}(parallel worktree mode)${RESET}"
1566
+ fi
1567
+ if $SKIP_PERMISSIONS; then
1568
+ echo -e " ${YELLOW}${BOLD}⚠${RESET} ${YELLOW}--dangerously-skip-permissions enabled${RESET}"
1569
+ fi
1570
+ if $AUDIT_ENABLED || $AUDIT_AGENT_ENABLED || $QUALITY_GATES_ENABLED; then
1571
+ echo -e " ${BOLD}Audit:${RESET} ${AUDIT_ENABLED:+self-audit }${AUDIT_AGENT_ENABLED:+audit-agent }${QUALITY_GATES_ENABLED:+quality-gates}${DIM}${DOD_FILE:+ | DoD: $DOD_FILE}${RESET}"
1572
+ fi
1573
+ echo ""
1574
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
1575
+ }
1576
+
1577
+ show_summary() {
1578
+ local end_epoch
1579
+ end_epoch="$(now_epoch)"
1580
+ local duration=$(( end_epoch - START_EPOCH ))
1581
+
1582
+ local status_display
1583
+ case "$STATUS" in
1584
+ complete) status_display="${GREEN}✓ Complete (LOOP_COMPLETE detected)${RESET}" ;;
1585
+ circuit_breaker) status_display="${RED}✗ Circuit breaker tripped${RESET}" ;;
1586
+ max_iterations) status_display="${YELLOW}⚠ Max iterations reached${RESET}" ;;
1587
+ interrupted) status_display="${YELLOW}⚠ Interrupted by user${RESET}" ;;
1588
+ error) status_display="${RED}✗ Error${RESET}" ;;
1589
+ *) status_display="${DIM}$STATUS${RESET}" ;;
1590
+ esac
1591
+
1592
+ local test_display
1593
+ if [[ -z "$TEST_CMD" ]]; then
1594
+ test_display="${DIM}No tests configured${RESET}"
1595
+ elif [[ "$TEST_PASSED" == "true" ]]; then
1596
+ test_display="${GREEN}All passing${RESET}"
1597
+ elif [[ "$TEST_PASSED" == "false" ]]; then
1598
+ test_display="${RED}Failing${RESET}"
1599
+ else
1600
+ test_display="${DIM}Not run${RESET}"
1601
+ fi
1602
+
1603
+ echo ""
1604
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
1605
+ local status_upper
1606
+ status_upper="$(echo "$STATUS" | tr '[:lower:]' '[:upper:]')"
1607
+ echo -e " ${BOLD}LOOP ${status_upper}${RESET}"
1608
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
1609
+ echo ""
1610
+ echo -e " ${BOLD}Goal:${RESET} $GOAL"
1611
+ echo -e " ${BOLD}Status:${RESET} $status_display"
1612
+ local ext_suffix=""
1613
+ [[ "$EXTENSION_COUNT" -gt 0 ]] && ext_suffix=" ${DIM}(${EXTENSION_COUNT} extensions)${RESET}"
1614
+ echo -e " ${BOLD}Iterations:${RESET} $ITERATION/$MAX_ITERATIONS${ext_suffix}"
1615
+ echo -e " ${BOLD}Duration:${RESET} $(format_duration "$duration")"
1616
+ echo -e " ${BOLD}Commits:${RESET} $TOTAL_COMMITS"
1617
+ echo -e " ${BOLD}Tests:${RESET} $test_display"
1618
+ echo ""
1619
+ echo -e " ${DIM}State: $STATE_FILE${RESET}"
1620
+ echo -e " ${DIM}Logs: $LOG_DIR/${RESET}"
1621
+ echo ""
1622
+ }
1623
+
1624
+ # ─── Signal Handling ──────────────────────────────────────────────────────────
1625
+
1626
+ CHILD_PID=""
1627
+
1628
+ cleanup() {
1629
+ echo ""
1630
+ warn "Loop interrupted at iteration $ITERATION"
1631
+
1632
+ # Kill any running Claude process
1633
+ if [[ -n "$CHILD_PID" ]] && kill -0 "$CHILD_PID" 2>/dev/null; then
1634
+ kill "$CHILD_PID" 2>/dev/null || true
1635
+ wait "$CHILD_PID" 2>/dev/null || true
1636
+ fi
1637
+
1638
+ # If multi-agent, kill worker panes
1639
+ if [[ "$AGENTS" -gt 1 ]]; then
1640
+ cleanup_multi_agent
1641
+ fi
1642
+
1643
+ STATUS="interrupted"
1644
+ write_state
1645
+
1646
+ # Save checkpoint on interruption
1647
+ "$SCRIPT_DIR/sw-checkpoint.sh" save \
1648
+ --stage "build" \
1649
+ --iteration "$ITERATION" \
1650
+ --git-sha "$(git rev-parse HEAD 2>/dev/null || echo unknown)" 2>/dev/null || true
1651
+
1652
+ # Clear heartbeat
1653
+ "$SCRIPT_DIR/sw-heartbeat.sh" clear "${PIPELINE_JOB_ID:-loop-$$}" 2>/dev/null || true
1654
+
1655
+ show_summary
1656
+ exit 130
1657
+ }
1658
+
1659
+ trap cleanup SIGINT SIGTERM
1660
+
1661
+ # ─── Multi-Agent: Worktree Setup ─────────────────────────────────────────────
1662
+
1663
+ setup_worktrees() {
1664
+ local branch_base="loop"
1665
+ mkdir -p "$WORKTREE_DIR"
1666
+
1667
+ for i in $(seq 1 "$AGENTS"); do
1668
+ local wt_path="$WORKTREE_DIR/agent-${i}"
1669
+ local branch_name="${branch_base}/agent-${i}"
1670
+
1671
+ if [[ -d "$wt_path" ]]; then
1672
+ info "Worktree agent-${i} already exists"
1673
+ continue
1674
+ fi
1675
+
1676
+ # Create branch if it doesn't exist
1677
+ if ! git -C "$PROJECT_ROOT" rev-parse --verify "$branch_name" &>/dev/null; then
1678
+ git -C "$PROJECT_ROOT" branch "$branch_name" HEAD 2>/dev/null || true
1679
+ fi
1680
+
1681
+ git -C "$PROJECT_ROOT" worktree add "$wt_path" "$branch_name" 2>/dev/null || {
1682
+ error "Failed to create worktree for agent-${i}"
1683
+ return 1
1684
+ }
1685
+
1686
+ success "Worktree: agent-${i} → $wt_path"
1687
+ done
1688
+ }
1689
+
1690
+ cleanup_worktrees() {
1691
+ for i in $(seq 1 "$AGENTS"); do
1692
+ local wt_path="$WORKTREE_DIR/agent-${i}"
1693
+ if [[ -d "$wt_path" ]]; then
1694
+ git -C "$PROJECT_ROOT" worktree remove --force "$wt_path" 2>/dev/null || true
1695
+ fi
1696
+ done
1697
+ rmdir "$WORKTREE_DIR" 2>/dev/null || true
1698
+ }
1699
+
1700
+ # ─── Multi-Agent: Worker Loop Script ─────────────────────────────────────────
1701
+
1702
+ generate_worker_script() {
1703
+ local agent_num="$1"
1704
+ local total_agents="$2"
1705
+ local wt_path="$WORKTREE_DIR/agent-${agent_num}"
1706
+ local worker_script="$LOG_DIR/worker-${agent_num}.sh"
1707
+
1708
+ local claude_flags
1709
+ claude_flags="$(build_claude_flags)"
1710
+
1711
+ cat > "$worker_script" <<'WORKEREOF'
1712
+ #!/usr/bin/env bash
1713
+ set -euo pipefail
1714
+
1715
+ AGENT_NUM="__AGENT_NUM__"
1716
+ TOTAL_AGENTS="__TOTAL_AGENTS__"
1717
+ WORK_DIR="__WORK_DIR__"
1718
+ LOG_DIR="__LOG_DIR__"
1719
+ MAX_ITERATIONS="__MAX_ITERATIONS__"
1720
+ GOAL="__GOAL__"
1721
+ TEST_CMD="__TEST_CMD__"
1722
+ CLAUDE_FLAGS="__CLAUDE_FLAGS__"
1723
+
1724
+ CYAN='\033[38;2;0;212;255m'
1725
+ GREEN='\033[38;2;74;222;128m'
1726
+ YELLOW='\033[38;2;250;204;21m'
1727
+ RED='\033[38;2;248;113;113m'
1728
+ DIM='\033[2m'
1729
+ BOLD='\033[1m'
1730
+ RESET='\033[0m'
1731
+
1732
+ cd "$WORK_DIR"
1733
+ ITERATION=0
1734
+ CONSECUTIVE_FAILURES=0
1735
+
1736
+ echo -e "${CYAN}${BOLD}▸${RESET} Agent ${AGENT_NUM}/${TOTAL_AGENTS} starting in ${WORK_DIR}"
1737
+
1738
+ while [[ "$ITERATION" -lt "$MAX_ITERATIONS" ]]; do
1739
+ ITERATION=$(( ITERATION + 1 ))
1740
+ echo -e "\n${CYAN}${BOLD}▸${RESET} Agent ${AGENT_NUM} — Iteration ${ITERATION}/${MAX_ITERATIONS}"
1741
+
1742
+ # Pull latest from other agents
1743
+ git fetch origin main 2>/dev/null && git merge origin/main --no-edit 2>/dev/null || true
1744
+
1745
+ # Build prompt
1746
+ GIT_LOG="$(git log --oneline -20 2>/dev/null || echo '(no commits)')"
1747
+ TEST_SECTION="No test results yet."
1748
+ if [[ -n "$TEST_CMD" ]]; then
1749
+ TEST_SECTION="Test command: $TEST_CMD"
1750
+ fi
1751
+
1752
+ PROMPT="$(cat <<PROMPT
1753
+ You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
1754
+
1755
+ ## Your Goal
1756
+ ${GOAL}
1757
+
1758
+ ## Recent Git Activity
1759
+ ${GIT_LOG}
1760
+
1761
+ ## Test Results
1762
+ ${TEST_SECTION}
1763
+
1764
+ ## Agent Identity
1765
+ You are Agent ${AGENT_NUM} of ${TOTAL_AGENTS}. Other agents are working in parallel.
1766
+ Check git log to see what they've done — avoid duplicating their work.
1767
+ Focus on areas they haven't touched yet.
1768
+
1769
+ ## Instructions
1770
+ 1. Read the codebase and understand the current state
1771
+ 2. Identify the highest-priority remaining work toward the goal
1772
+ 3. Implement ONE meaningful chunk of progress
1773
+ 4. Commit your work with a descriptive message
1774
+ 5. When the goal is FULLY achieved, output exactly: LOOP_COMPLETE
1775
+
1776
+ ## Rules
1777
+ - Focus on ONE task per iteration — do it well
1778
+ - Always commit with descriptive messages
1779
+ - If stuck on the same issue for 2+ iterations, try a different approach
1780
+ - Do NOT output LOOP_COMPLETE unless the goal is genuinely achieved
1781
+ PROMPT
1782
+ )"
1783
+
1784
+ # Run Claude
1785
+ LOG_FILE="$LOG_DIR/agent-${AGENT_NUM}-iter-${ITERATION}.log"
1786
+ # shellcheck disable=SC2086
1787
+ claude -p "$PROMPT" $CLAUDE_FLAGS > "$LOG_FILE" 2>&1 || true
1788
+
1789
+ echo -e " ${GREEN}✓${RESET} Claude session completed"
1790
+
1791
+ # Check completion
1792
+ if grep -q "LOOP_COMPLETE" "$LOG_FILE" 2>/dev/null; then
1793
+ echo -e " ${GREEN}${BOLD}✓ LOOP_COMPLETE detected!${RESET}"
1794
+ # Signal completion
1795
+ touch "$LOG_DIR/.agent-${AGENT_NUM}-complete"
1796
+ break
1797
+ fi
1798
+
1799
+ # Auto-commit
1800
+ git add -A 2>/dev/null || true
1801
+ if git commit -m "agent-${AGENT_NUM}: iteration ${ITERATION}" --no-verify 2>/dev/null; then
1802
+ git push origin "loop/agent-${AGENT_NUM}" 2>/dev/null || true
1803
+ echo -e " ${GREEN}✓${RESET} Committed and pushed"
1804
+ fi
1805
+
1806
+ # Circuit breaker: check for progress
1807
+ CHANGES="$(git diff --stat HEAD~1 2>/dev/null | tail -1 || echo '')"
1808
+ INSERTIONS="$(echo "$CHANGES" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)"
1809
+ if [[ "${INSERTIONS:-0}" -lt 5 ]]; then
1810
+ CONSECUTIVE_FAILURES=$(( CONSECUTIVE_FAILURES + 1 ))
1811
+ echo -e " ${YELLOW}⚠${RESET} Low progress (${CONSECUTIVE_FAILURES}/3)"
1812
+ else
1813
+ CONSECUTIVE_FAILURES=0
1814
+ fi
1815
+
1816
+ if [[ "$CONSECUTIVE_FAILURES" -ge 3 ]]; then
1817
+ echo -e " ${RED}✗${RESET} Circuit breaker — stopping agent ${AGENT_NUM}"
1818
+ break
1819
+ fi
1820
+
1821
+ sleep 2
1822
+ done
1823
+
1824
+ echo -e "\n${DIM}Agent ${AGENT_NUM} finished after ${ITERATION} iterations${RESET}"
1825
+ WORKEREOF
1826
+
1827
+ # Replace placeholders — use awk for all values to avoid sed injection
1828
+ # (sed breaks on & | \ in paths and test commands)
1829
+ sed_i "s|__AGENT_NUM__|${agent_num}|g" "$worker_script"
1830
+ sed_i "s|__TOTAL_AGENTS__|${total_agents}|g" "$worker_script"
1831
+ sed_i "s|__MAX_ITERATIONS__|${MAX_ITERATIONS}|g" "$worker_script"
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" \
1842
+ && mv "${worker_script}.tmp" "$worker_script"
1843
+ chmod +x "$worker_script"
1844
+ echo "$worker_script"
1845
+ }
1846
+
1847
+ # ─── Multi-Agent: Launch ─────────────────────────────────────────────────────
1848
+
1849
+ MULTI_WINDOW_NAME=""
1850
+
1851
+ launch_multi_agent() {
1852
+ info "Setting up multi-agent mode ($AGENTS agents)..."
1853
+
1854
+ # Setup worktrees
1855
+ setup_worktrees || { error "Failed to setup worktrees"; exit 1; }
1856
+
1857
+ # Create tmux window for workers
1858
+ MULTI_WINDOW_NAME="sw-loop-$(date +%s)"
1859
+ tmux new-window -n "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT"
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
+
1865
+ # First pane becomes monitor
1866
+ tmux send-keys -t "$monitor_pane_id" "printf '\\033]2;loop-monitor\\033\\\\'" Enter
1867
+ sleep 0.2
1868
+ tmux send-keys -t "$monitor_pane_id" "clear && echo 'Loop Monitor — watching agent logs...'" Enter
1869
+
1870
+ # Create worker panes
1871
+ for i in $(seq 1 "$AGENTS"); do
1872
+ local worker_script
1873
+ worker_script="$(generate_worker_script "$i" "$AGENTS")"
1874
+
1875
+ tmux split-window -t "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT"
1876
+ sleep 0.1
1877
+ tmux send-keys -t "$MULTI_WINDOW_NAME" "printf '\\033]2;agent-${i}\\033\\\\'" Enter
1878
+ sleep 0.1
1879
+ tmux send-keys -t "$MULTI_WINDOW_NAME" "bash '$worker_script'" Enter
1880
+ done
1881
+
1882
+ # Layout: monitor pane on top (35%), worker agents tile below
1883
+ tmux select-layout -t "$MULTI_WINDOW_NAME" main-vertical 2>/dev/null || true
1884
+ tmux resize-pane -t "$monitor_pane_id" -y 35% 2>/dev/null || true
1885
+
1886
+ # In the monitor pane, tail all agent logs
1887
+ tmux select-pane -t "$monitor_pane_id"
1888
+ sleep 0.5
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
1890
+
1891
+ success "Launched $AGENTS worker agents in window: $MULTI_WINDOW_NAME"
1892
+ echo ""
1893
+
1894
+ # Wait for completion
1895
+ info "Monitoring agents... (Ctrl-C to stop all)"
1896
+ wait_for_multi_completion
1897
+ }
1898
+
1899
+ wait_for_multi_completion() {
1900
+ while true; do
1901
+ # Check if any agent signaled completion
1902
+ for i in $(seq 1 "$AGENTS"); do
1903
+ if [[ -f "$LOG_DIR/.agent-${i}-complete" ]]; then
1904
+ success "Agent $i signaled LOOP_COMPLETE!"
1905
+ STATUS="complete"
1906
+ write_state
1907
+ return 0
1908
+ fi
1909
+ done
1910
+
1911
+ # Check if all worker panes are still running
1912
+ local running=0
1913
+ for i in $(seq 1 "$AGENTS"); do
1914
+ # Check if the worker log is still being written to
1915
+ local latest_log
1916
+ latest_log="$(ls -t "$LOG_DIR"/agent-"${i}"-iter-*.log 2>/dev/null | head -1)"
1917
+ if [[ -n "$latest_log" ]]; then
1918
+ local age
1919
+ age=$(( $(now_epoch) - $(stat -f %m "$latest_log" 2>/dev/null || echo 0) ))
1920
+ if [[ $age -lt 300 ]]; then # Active within 5 minutes
1921
+ running=$(( running + 1 ))
1922
+ fi
1923
+ fi
1924
+ done
1925
+
1926
+ if [[ $running -eq 0 ]]; then
1927
+ # Check if we have any logs at all (might still be starting)
1928
+ local total_logs
1929
+ total_logs="$(ls "$LOG_DIR"/agent-*-iter-*.log 2>/dev/null | wc -l | tr -d ' ')"
1930
+ if [[ "${total_logs:-0}" -gt 0 ]]; then
1931
+ warn "All agents appear to have stopped."
1932
+ STATUS="complete"
1933
+ write_state
1934
+ return 0
1935
+ fi
1936
+ fi
1937
+
1938
+ sleep 5
1939
+ done
1940
+ }
1941
+
1942
+ cleanup_multi_agent() {
1943
+ if [[ -n "$MULTI_WINDOW_NAME" ]]; then
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)
1951
+ sleep 1
1952
+ tmux kill-window -t "$MULTI_WINDOW_NAME" 2>/dev/null || true
1953
+ fi
1954
+
1955
+ # Clean up completion markers
1956
+ rm -f "$LOG_DIR"/.agent-*-complete 2>/dev/null || true
1957
+ }
1958
+
1959
+ # ─── Main: Single-Agent Loop ─────────────────────────────────────────────────
1960
+
1961
+ run_single_agent_loop() {
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
1966
+ resume_state
1967
+ else
1968
+ initialize_state
1969
+ fi
1970
+
1971
+ # Apply adaptive budget/model before showing banner
1972
+ apply_adaptive_budget
1973
+ MODEL="$(select_adaptive_model "build" "$MODEL")"
1974
+
1975
+ # Track applied memory fix patterns for outcome recording
1976
+ _applied_fix_pattern=""
1977
+
1978
+ show_banner
1979
+
1980
+ while true; do
1981
+ # Pre-checks (before incrementing — ITERATION tracks completed count)
1982
+ check_circuit_breaker || break
1983
+ check_max_iterations || break
1984
+ ITERATION=$(( ITERATION + 1 ))
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
+
2006
+ # Run Claude
2007
+ local exit_code=0
2008
+ run_claude_iteration || exit_code=$?
2009
+
2010
+ local log_file="$LOG_DIR/iteration-${ITERATION}.log"
2011
+
2012
+ # Mid-loop memory refresh — re-query with current error context after iteration 3
2013
+ if [[ "$ITERATION" -ge 3 ]] && type memory_inject_context &>/dev/null 2>&1; then
2014
+ local refresh_ctx
2015
+ refresh_ctx=$(tail -20 "$log_file" 2>/dev/null || true)
2016
+ if [[ -n "$refresh_ctx" ]]; then
2017
+ local refreshed_memory
2018
+ refreshed_memory=$(memory_inject_context "build" "$refresh_ctx" 2>/dev/null | head -5 || true)
2019
+ if [[ -n "$refreshed_memory" ]]; then
2020
+ # Append to next iteration's memory context
2021
+ local memory_refresh_file="$LOG_DIR/memory-refresh-${ITERATION}.txt"
2022
+ echo "$refreshed_memory" > "$memory_refresh_file"
2023
+ fi
2024
+ fi
2025
+ fi
2026
+
2027
+ # Auto-commit if Claude didn't
2028
+ local commits_before
2029
+ commits_before="$(git_commit_count)"
2030
+ git_auto_commit "$PROJECT_ROOT" || true
2031
+ local commits_after
2032
+ commits_after="$(git_commit_count)"
2033
+ local new_commits=$(( commits_after - commits_before ))
2034
+ TOTAL_COMMITS=$(( TOTAL_COMMITS + new_commits ))
2035
+
2036
+ # Git diff stats
2037
+ local diff_stat
2038
+ diff_stat="$(git_diff_stat)"
2039
+ if [[ -n "$diff_stat" ]]; then
2040
+ echo -e " ${GREEN}✓${RESET} Git: $diff_stat"
2041
+ fi
2042
+
2043
+ # Track velocity for adaptive extension budget
2044
+ track_iteration_velocity
2045
+
2046
+ # Test gate
2047
+ run_test_gate
2048
+ write_error_summary
2049
+ if [[ -n "$TEST_CMD" ]]; then
2050
+ if [[ "$TEST_PASSED" == "true" ]]; then
2051
+ echo -e " ${GREEN}✓${RESET} Tests: passed"
2052
+ else
2053
+ echo -e " ${RED}✗${RESET} Tests: failed"
2054
+ fi
2055
+ fi
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
+
2069
+ # Audit agent (reviews implementer's work)
2070
+ run_audit_agent
2071
+
2072
+ # Quality gates (automated checks)
2073
+ run_quality_gates
2074
+
2075
+ # Guarded completion (replaces naive grep check)
2076
+ if guard_completion; then
2077
+ STATUS="complete"
2078
+ write_state
2079
+ write_progress
2080
+ show_summary
2081
+ return 0
2082
+ fi
2083
+
2084
+ # Check progress (circuit breaker)
2085
+ if check_progress; then
2086
+ CONSECUTIVE_FAILURES=0
2087
+ echo -e " ${GREEN}✓${RESET} Progress detected — continuing"
2088
+ else
2089
+ CONSECUTIVE_FAILURES=$(( CONSECUTIVE_FAILURES + 1 ))
2090
+ echo -e " ${YELLOW}⚠${RESET} Low progress (${CONSECUTIVE_FAILURES}/${CIRCUIT_BREAKER_THRESHOLD} before circuit breaker)"
2091
+ fi
2092
+
2093
+ # Extract summary and update state
2094
+ local summary
2095
+ summary="$(extract_summary "$log_file")"
2096
+ append_log_entry "### Iteration $ITERATION ($(now_iso))
2097
+ $summary
2098
+ "
2099
+ write_state
2100
+ write_progress
2101
+
2102
+ # Update heartbeat
2103
+ "$SCRIPT_DIR/sw-heartbeat.sh" write "${PIPELINE_JOB_ID:-loop-$$}" \
2104
+ --pid $$ \
2105
+ --stage "build" \
2106
+ --iteration "$ITERATION" \
2107
+ --activity "Loop iteration $ITERATION" 2>/dev/null || true
2108
+
2109
+ # Human intervention: check for human message between iterations
2110
+ local human_msg_file="$STATE_DIR/pipeline-artifacts/human-message.txt"
2111
+ if [[ -f "$human_msg_file" ]]; then
2112
+ local human_msg
2113
+ human_msg="$(cat "$human_msg_file" 2>/dev/null || true)"
2114
+ if [[ -n "$human_msg" ]]; then
2115
+ echo -e " ${PURPLE}${BOLD}💬 Human message:${RESET} $human_msg"
2116
+ # Inject human message as additional context for next iteration
2117
+ GOAL="${GOAL}
2118
+
2119
+ HUMAN FEEDBACK (received after iteration $ITERATION): $human_msg"
2120
+ rm -f "$human_msg_file"
2121
+ fi
2122
+ fi
2123
+
2124
+ sleep 2
2125
+ done
2126
+
2127
+ # Write final state after loop exits
2128
+ write_state
2129
+ write_progress
2130
+ show_summary
2131
+ }
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
+
2200
+ # ─── Main: Entry Point ───────────────────────────────────────────────────────
2201
+
2202
+ main() {
2203
+ if [[ "$AGENTS" -gt 1 ]]; then
2204
+ if $RESUME; then
2205
+ resume_state
2206
+ else
2207
+ initialize_state
2208
+ fi
2209
+ show_banner
2210
+ launch_multi_agent
2211
+ show_summary
2212
+ else
2213
+ run_loop_with_restarts
2214
+ fi
2215
+ }
2216
+
2217
+ main