shipwright-cli 1.7.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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +926 -0
  3. package/claude-code/CLAUDE.md.shipwright +125 -0
  4. package/claude-code/hooks/notify-idle.sh +35 -0
  5. package/claude-code/hooks/pre-compact-save.sh +57 -0
  6. package/claude-code/hooks/task-completed.sh +170 -0
  7. package/claude-code/hooks/teammate-idle.sh +68 -0
  8. package/claude-code/settings.json.template +184 -0
  9. package/completions/_shipwright +140 -0
  10. package/completions/shipwright.bash +89 -0
  11. package/completions/shipwright.fish +107 -0
  12. package/docs/KNOWN-ISSUES.md +199 -0
  13. package/docs/TIPS.md +331 -0
  14. package/docs/definition-of-done.example.md +16 -0
  15. package/docs/patterns/README.md +139 -0
  16. package/docs/patterns/audit-loop.md +149 -0
  17. package/docs/patterns/bug-hunt.md +183 -0
  18. package/docs/patterns/feature-implementation.md +159 -0
  19. package/docs/patterns/refactoring.md +183 -0
  20. package/docs/patterns/research-exploration.md +144 -0
  21. package/docs/patterns/test-generation.md +173 -0
  22. package/package.json +49 -0
  23. package/scripts/adapters/docker-deploy.sh +50 -0
  24. package/scripts/adapters/fly-deploy.sh +41 -0
  25. package/scripts/adapters/iterm2-adapter.sh +122 -0
  26. package/scripts/adapters/railway-deploy.sh +34 -0
  27. package/scripts/adapters/tmux-adapter.sh +87 -0
  28. package/scripts/adapters/vercel-deploy.sh +35 -0
  29. package/scripts/adapters/wezterm-adapter.sh +103 -0
  30. package/scripts/cct +242 -0
  31. package/scripts/cct-cleanup.sh +172 -0
  32. package/scripts/cct-cost.sh +590 -0
  33. package/scripts/cct-daemon.sh +3189 -0
  34. package/scripts/cct-doctor.sh +328 -0
  35. package/scripts/cct-fix.sh +478 -0
  36. package/scripts/cct-fleet.sh +904 -0
  37. package/scripts/cct-init.sh +282 -0
  38. package/scripts/cct-logs.sh +273 -0
  39. package/scripts/cct-loop.sh +1332 -0
  40. package/scripts/cct-memory.sh +1148 -0
  41. package/scripts/cct-pipeline.sh +3844 -0
  42. package/scripts/cct-prep.sh +1352 -0
  43. package/scripts/cct-ps.sh +168 -0
  44. package/scripts/cct-reaper.sh +390 -0
  45. package/scripts/cct-session.sh +284 -0
  46. package/scripts/cct-status.sh +169 -0
  47. package/scripts/cct-templates.sh +242 -0
  48. package/scripts/cct-upgrade.sh +422 -0
  49. package/scripts/cct-worktree.sh +405 -0
  50. package/scripts/postinstall.mjs +96 -0
  51. package/templates/pipelines/autonomous.json +71 -0
  52. package/templates/pipelines/cost-aware.json +95 -0
  53. package/templates/pipelines/deployed.json +79 -0
  54. package/templates/pipelines/enterprise.json +114 -0
  55. package/templates/pipelines/fast.json +63 -0
  56. package/templates/pipelines/full.json +104 -0
  57. package/templates/pipelines/hotfix.json +63 -0
  58. package/templates/pipelines/standard.json +91 -0
  59. package/tmux/claude-teams-overlay.conf +109 -0
  60. package/tmux/templates/architecture.json +19 -0
  61. package/tmux/templates/bug-fix.json +24 -0
  62. package/tmux/templates/code-review.json +24 -0
  63. package/tmux/templates/devops.json +19 -0
  64. package/tmux/templates/documentation.json +19 -0
  65. package/tmux/templates/exploration.json +19 -0
  66. package/tmux/templates/feature-dev.json +24 -0
  67. package/tmux/templates/full-stack.json +24 -0
  68. package/tmux/templates/migration.json +24 -0
  69. package/tmux/templates/refactor.json +19 -0
  70. package/tmux/templates/security-audit.json +24 -0
  71. package/tmux/templates/testing.json +24 -0
  72. package/tmux/tmux.conf +167 -0
@@ -0,0 +1,1332 @@
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
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+
14
+ # ─── Colors (matches cct theme) ──────────────────────────────────────────────
15
+ CYAN='\033[38;2;0;212;255m'
16
+ PURPLE='\033[38;2;124;58;237m'
17
+ BLUE='\033[38;2;0;102;255m'
18
+ GREEN='\033[38;2;74;222;128m'
19
+ YELLOW='\033[38;2;250;204;21m'
20
+ RED='\033[38;2;248;113;113m'
21
+ DIM='\033[2m'
22
+ BOLD='\033[1m'
23
+ RESET='\033[0m'
24
+
25
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
26
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
27
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
28
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
29
+
30
+ # ─── Defaults ─────────────────────────────────────────────────────────────────
31
+ GOAL=""
32
+ MAX_ITERATIONS=20
33
+ TEST_CMD=""
34
+ MODEL="opus"
35
+ AGENTS=1
36
+ USE_WORKTREE=false
37
+ SKIP_PERMISSIONS=false
38
+ MAX_TURNS=""
39
+ RESUME=false
40
+ VERBOSE=false
41
+ MAX_ITERATIONS_EXPLICIT=false
42
+ VERSION="1.7.0"
43
+
44
+ # ─── Audit & Quality Gate Defaults ───────────────────────────────────────────
45
+ AUDIT_ENABLED=false
46
+ AUDIT_AGENT_ENABLED=false
47
+ DOD_FILE=""
48
+ QUALITY_GATES_ENABLED=false
49
+ AUDIT_RESULT=""
50
+ COMPLETION_REJECTED=false
51
+ QUALITY_GATE_PASSED=true
52
+
53
+ # ─── Parse Arguments ──────────────────────────────────────────────────────────
54
+ show_help() {
55
+ echo -e "${CYAN}${BOLD}shipwright${RESET} ${DIM}v${VERSION}${RESET} — ${BOLD}Continuous Loop${RESET}"
56
+ echo ""
57
+ echo -e "${BOLD}USAGE${RESET}"
58
+ echo -e " ${CYAN}shipwright loop${RESET} \"<goal>\" [options]"
59
+ echo ""
60
+ echo -e "${BOLD}OPTIONS${RESET}"
61
+ echo -e " ${CYAN}--max-iterations${RESET} N Max loop iterations (default: 20)"
62
+ echo -e " ${CYAN}--test-cmd${RESET} \"cmd\" Test command to run between iterations"
63
+ echo -e " ${CYAN}--model${RESET} MODEL Claude model to use (default: opus)"
64
+ echo -e " ${CYAN}--agents${RESET} N Number of parallel agents (default: 1)"
65
+ echo -e " ${CYAN}--worktree${RESET} Use git worktrees for isolation (auto if agents > 1)"
66
+ echo -e " ${CYAN}--skip-permissions${RESET} Pass --dangerously-skip-permissions to Claude"
67
+ echo -e " ${CYAN}--max-turns${RESET} N Max API turns per Claude session"
68
+ echo -e " ${CYAN}--resume${RESET} Resume from existing .claude/loop-state.md"
69
+ echo -e " ${CYAN}--verbose${RESET} Show full Claude output (default: summary)"
70
+ echo -e " ${CYAN}--help${RESET} Show this help"
71
+ echo ""
72
+ echo -e "${BOLD}AUDIT & QUALITY${RESET}"
73
+ echo -e " ${CYAN}--audit${RESET} Inject self-audit checklist into agent prompt"
74
+ echo -e " ${CYAN}--audit-agent${RESET} Run separate auditor agent (haiku) after each iteration"
75
+ echo -e " ${CYAN}--quality-gates${RESET} Enable automated quality gates before accepting completion"
76
+ echo -e " ${CYAN}--definition-of-done${RESET} FILE DoD checklist file — evaluated by AI against git diff"
77
+ echo ""
78
+ echo -e "${BOLD}EXAMPLES${RESET}"
79
+ echo -e " ${DIM}shipwright loop \"Build user auth with JWT\"${RESET}"
80
+ echo -e " ${DIM}shipwright loop \"Add payment processing\" --test-cmd \"npm test\" --max-iterations 30${RESET}"
81
+ echo -e " ${DIM}shipwright loop \"Refactor the database layer\" --agents 3 --model sonnet${RESET}"
82
+ echo -e " ${DIM}shipwright loop \"Fix all lint errors\" --skip-permissions --verbose${RESET}"
83
+ echo -e " ${DIM}shipwright loop \"Add auth\" --audit --audit-agent --quality-gates${RESET}"
84
+ echo -e " ${DIM}shipwright loop \"Ship feature\" --quality-gates --definition-of-done dod.md${RESET}"
85
+ echo ""
86
+ echo -e "${BOLD}CIRCUIT BREAKER${RESET}"
87
+ echo -e " The loop automatically stops if:"
88
+ echo -e " ${DIM}• 3 consecutive iterations with < 5 lines changed${RESET}"
89
+ echo -e " ${DIM}• Claude outputs LOOP_COMPLETE (validated by quality gates if enabled)${RESET}"
90
+ echo -e " ${DIM}• Max iterations reached${RESET}"
91
+ echo -e " ${DIM}• Ctrl-C (graceful shutdown with summary)${RESET}"
92
+ echo ""
93
+ echo -e "${BOLD}STATE & LOGS${RESET}"
94
+ echo -e " ${DIM}State file: .claude/loop-state.md${RESET}"
95
+ echo -e " ${DIM}Logs dir: .claude/loop-logs/${RESET}"
96
+ echo -e " ${DIM}Resume: shipwright loop --resume${RESET}"
97
+ }
98
+
99
+ while [[ $# -gt 0 ]]; do
100
+ case "$1" in
101
+ --max-iterations)
102
+ MAX_ITERATIONS="${2:-}"
103
+ MAX_ITERATIONS_EXPLICIT=true
104
+ [[ -z "$MAX_ITERATIONS" ]] && { error "Missing value for --max-iterations"; exit 1; }
105
+ shift 2
106
+ ;;
107
+ --max-iterations=*) MAX_ITERATIONS="${1#--max-iterations=}"; MAX_ITERATIONS_EXPLICIT=true; shift ;;
108
+ --test-cmd)
109
+ TEST_CMD="${2:-}"
110
+ [[ -z "$TEST_CMD" ]] && { error "Missing value for --test-cmd"; exit 1; }
111
+ shift 2
112
+ ;;
113
+ --test-cmd=*) TEST_CMD="${1#--test-cmd=}"; shift ;;
114
+ --model)
115
+ MODEL="${2:-}"
116
+ [[ -z "$MODEL" ]] && { error "Missing value for --model"; exit 1; }
117
+ shift 2
118
+ ;;
119
+ --model=*) MODEL="${1#--model=}"; shift ;;
120
+ --agents)
121
+ AGENTS="${2:-}"
122
+ [[ -z "$AGENTS" ]] && { error "Missing value for --agents"; exit 1; }
123
+ shift 2
124
+ ;;
125
+ --agents=*) AGENTS="${1#--agents=}"; shift ;;
126
+ --worktree) USE_WORKTREE=true; shift ;;
127
+ --skip-permissions) SKIP_PERMISSIONS=true; shift ;;
128
+ --max-turns)
129
+ MAX_TURNS="${2:-}"
130
+ [[ -z "$MAX_TURNS" ]] && { error "Missing value for --max-turns"; exit 1; }
131
+ shift 2
132
+ ;;
133
+ --max-turns=*) MAX_TURNS="${1#--max-turns=}"; shift ;;
134
+ --resume) RESUME=true; shift ;;
135
+ --verbose) VERBOSE=true; shift ;;
136
+ --audit) AUDIT_ENABLED=true; shift ;;
137
+ --audit-agent) AUDIT_AGENT_ENABLED=true; shift ;;
138
+ --definition-of-done)
139
+ DOD_FILE="${2:-}"
140
+ [[ -z "$DOD_FILE" ]] && { error "Missing value for --definition-of-done"; exit 1; }
141
+ shift 2
142
+ ;;
143
+ --definition-of-done=*) DOD_FILE="${1#--definition-of-done=}"; shift ;;
144
+ --quality-gates) QUALITY_GATES_ENABLED=true; shift ;;
145
+ --help|-h)
146
+ show_help
147
+ exit 0
148
+ ;;
149
+ -*)
150
+ error "Unknown option: $1"
151
+ echo ""
152
+ show_help
153
+ exit 1
154
+ ;;
155
+ *)
156
+ # Positional: goal
157
+ if [[ -z "$GOAL" ]]; then
158
+ GOAL="$1"
159
+ else
160
+ error "Unexpected argument: $1"
161
+ exit 1
162
+ fi
163
+ shift
164
+ ;;
165
+ esac
166
+ done
167
+
168
+ # Auto-enable worktree for multi-agent
169
+ if [[ "$AGENTS" -gt 1 ]]; then
170
+ USE_WORKTREE=true
171
+ fi
172
+
173
+ # ─── Validate Inputs ─────────────────────────────────────────────────────────
174
+
175
+ if ! $RESUME && [[ -z "$GOAL" ]]; then
176
+ error "Missing goal. Usage: shipwright loop \"<goal>\" [options]"
177
+ echo ""
178
+ echo -e " ${DIM}shipwright loop \"Build user auth with JWT\"${RESET}"
179
+ echo -e " ${DIM}shipwright loop --resume${RESET}"
180
+ exit 1
181
+ fi
182
+
183
+ if ! command -v claude &>/dev/null; then
184
+ error "Claude Code CLI not found. Install it first:"
185
+ echo -e " ${DIM}npm install -g @anthropic-ai/claude-code${RESET}"
186
+ exit 1
187
+ fi
188
+
189
+ if ! git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then
190
+ error "Not inside a git repository. The loop requires git for progress tracking."
191
+ exit 1
192
+ fi
193
+
194
+ if [[ "$AGENTS" -gt 1 ]]; then
195
+ if ! command -v tmux &>/dev/null; then
196
+ error "tmux is required for multi-agent mode."
197
+ echo -e " ${DIM}brew install tmux${RESET} (macOS)"
198
+ exit 1
199
+ fi
200
+ if [[ -z "${TMUX:-}" ]]; then
201
+ error "Multi-agent mode requires running inside tmux."
202
+ echo -e " ${DIM}tmux new -s work${RESET}"
203
+ exit 1
204
+ fi
205
+ fi
206
+
207
+ # ─── Directory Setup ─────────────────────────────────────────────────────────
208
+
209
+ PROJECT_ROOT="$(git rev-parse --show-toplevel)"
210
+ STATE_DIR="$PROJECT_ROOT/.claude"
211
+ STATE_FILE="$STATE_DIR/loop-state.md"
212
+ LOG_DIR="$STATE_DIR/loop-logs"
213
+ WORKTREE_DIR="$PROJECT_ROOT/.worktrees"
214
+
215
+ mkdir -p "$STATE_DIR" "$LOG_DIR"
216
+
217
+ # ─── Timing Helpers ───────────────────────────────────────────────────────────
218
+
219
+ now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
220
+ now_epoch() { date +%s; }
221
+
222
+ format_duration() {
223
+ local secs="$1"
224
+ local mins=$(( secs / 60 ))
225
+ local remaining_secs=$(( secs % 60 ))
226
+ if [[ $mins -gt 0 ]]; then
227
+ printf "%dm %ds" "$mins" "$remaining_secs"
228
+ else
229
+ printf "%ds" "$remaining_secs"
230
+ fi
231
+ }
232
+
233
+ # ─── State Management ────────────────────────────────────────────────────────
234
+
235
+ ITERATION=0
236
+ CONSECUTIVE_FAILURES=0
237
+ TOTAL_COMMITS=0
238
+ START_EPOCH=""
239
+ STATUS="running"
240
+ TEST_PASSED=""
241
+ TEST_OUTPUT=""
242
+ LOG_ENTRIES=""
243
+
244
+ initialize_state() {
245
+ ITERATION=0
246
+ CONSECUTIVE_FAILURES=0
247
+ TOTAL_COMMITS=0
248
+ START_EPOCH="$(now_epoch)"
249
+ STATUS="running"
250
+ LOG_ENTRIES=""
251
+
252
+ write_state
253
+ }
254
+
255
+ resume_state() {
256
+ if [[ ! -f "$STATE_FILE" ]]; then
257
+ error "No state file found at $STATE_FILE"
258
+ echo -e " Start a new loop instead: ${DIM}shipwright loop \"<goal>\"${RESET}"
259
+ exit 1
260
+ fi
261
+
262
+ info "Resuming from $STATE_FILE"
263
+
264
+ # Save CLI values before parsing state (CLI takes precedence)
265
+ local cli_max_iterations="$MAX_ITERATIONS"
266
+
267
+ # Parse YAML front matter
268
+ local in_frontmatter=false
269
+ while IFS= read -r line; do
270
+ if [[ "$line" == "---" ]]; then
271
+ if $in_frontmatter; then
272
+ break
273
+ else
274
+ in_frontmatter=true
275
+ continue
276
+ fi
277
+ fi
278
+ if $in_frontmatter; then
279
+ case "$line" in
280
+ goal:*) [[ -z "$GOAL" ]] && GOAL="$(echo "${line#goal:}" | sed 's/^ *"//;s/" *$//')" ;;
281
+ iteration:*) ITERATION="$(echo "${line#iteration:}" | tr -d ' ')" ;;
282
+ max_iterations:*) MAX_ITERATIONS="$(echo "${line#max_iterations:}" | tr -d ' ')" ;;
283
+ status:*) STATUS="$(echo "${line#status:}" | tr -d ' ')" ;;
284
+ test_cmd:*) [[ -z "$TEST_CMD" ]] && TEST_CMD="$(echo "${line#test_cmd:}" | sed 's/^ *"//;s/" *$//')" ;;
285
+ model:*) MODEL="$(echo "${line#model:}" | tr -d ' ')" ;;
286
+ agents:*) AGENTS="$(echo "${line#agents:}" | tr -d ' ')" ;;
287
+ consecutive_failures:*) CONSECUTIVE_FAILURES="$(echo "${line#consecutive_failures:}" | tr -d ' ')" ;;
288
+ total_commits:*) TOTAL_COMMITS="$(echo "${line#total_commits:}" | tr -d ' ')" ;;
289
+ audit_enabled:*) AUDIT_ENABLED="$(echo "${line#audit_enabled:}" | tr -d ' ')" ;;
290
+ audit_agent_enabled:*) AUDIT_AGENT_ENABLED="$(echo "${line#audit_agent_enabled:}" | tr -d ' ')" ;;
291
+ quality_gates_enabled:*) QUALITY_GATES_ENABLED="$(echo "${line#quality_gates_enabled:}" | tr -d ' ')" ;;
292
+ dod_file:*) DOD_FILE="$(echo "${line#dod_file:}" | sed 's/^ *"//;s/" *$//')" ;;
293
+ esac
294
+ fi
295
+ done < "$STATE_FILE"
296
+
297
+ # CLI --max-iterations overrides state file
298
+ if $MAX_ITERATIONS_EXPLICIT; then
299
+ MAX_ITERATIONS="$cli_max_iterations"
300
+ fi
301
+
302
+ # Extract the log section (everything after ## Log)
303
+ LOG_ENTRIES="$(sed -n '/^## Log$/,$ { /^## Log$/d; p; }' "$STATE_FILE" 2>/dev/null || true)"
304
+
305
+ if [[ -z "$GOAL" ]]; then
306
+ error "Could not parse goal from state file."
307
+ exit 1
308
+ fi
309
+
310
+ if [[ "$STATUS" == "complete" ]]; then
311
+ warn "Previous loop completed. Start a new one or edit the state file."
312
+ exit 0
313
+ fi
314
+
315
+ # Reset circuit breaker on resume
316
+ CONSECUTIVE_FAILURES=0
317
+ START_EPOCH="$(now_epoch)"
318
+ STATUS="running"
319
+
320
+ # If we hit max iterations before, warn user to extend
321
+ if [[ "$ITERATION" -ge "$MAX_ITERATIONS" ]] && ! $MAX_ITERATIONS_EXPLICIT; then
322
+ warn "Previous run stopped at iteration $ITERATION/$MAX_ITERATIONS."
323
+ echo -e " Extend with: ${DIM}shipwright loop --resume --max-iterations $(( MAX_ITERATIONS + 10 ))${RESET}"
324
+ exit 0
325
+ fi
326
+
327
+ success "Resumed: iteration $ITERATION/$MAX_ITERATIONS"
328
+ }
329
+
330
+ write_state() {
331
+ cat > "$STATE_FILE" <<EOF
332
+ ---
333
+ goal: "$GOAL"
334
+ iteration: $ITERATION
335
+ max_iterations: $MAX_ITERATIONS
336
+ status: $STATUS
337
+ test_cmd: "$TEST_CMD"
338
+ model: $MODEL
339
+ agents: $AGENTS
340
+ started_at: $(now_iso)
341
+ last_iteration_at: $(now_iso)
342
+ consecutive_failures: $CONSECUTIVE_FAILURES
343
+ total_commits: $TOTAL_COMMITS
344
+ audit_enabled: $AUDIT_ENABLED
345
+ audit_agent_enabled: $AUDIT_AGENT_ENABLED
346
+ quality_gates_enabled: $QUALITY_GATES_ENABLED
347
+ dod_file: "$DOD_FILE"
348
+ ---
349
+
350
+ ## Log
351
+ $LOG_ENTRIES
352
+ EOF
353
+ }
354
+
355
+ append_log_entry() {
356
+ local entry="$1"
357
+ if [[ -n "$LOG_ENTRIES" ]]; then
358
+ LOG_ENTRIES="${LOG_ENTRIES}
359
+ ${entry}"
360
+ else
361
+ LOG_ENTRIES="$entry"
362
+ fi
363
+ }
364
+
365
+ # ─── Git Helpers ──────────────────────────────────────────────────────────────
366
+
367
+ git_commit_count() {
368
+ git -C "$PROJECT_ROOT" rev-list --count HEAD 2>/dev/null || echo 0
369
+ }
370
+
371
+ git_recent_log() {
372
+ git -C "$PROJECT_ROOT" log --oneline -20 2>/dev/null || echo "(no commits)"
373
+ }
374
+
375
+ git_diff_stat() {
376
+ git -C "$PROJECT_ROOT" diff --stat HEAD~1 2>/dev/null | tail -1 || echo ""
377
+ }
378
+
379
+ git_auto_commit() {
380
+ local work_dir="${1:-$PROJECT_ROOT}"
381
+ # Only commit if there are changes
382
+ if git -C "$work_dir" diff --quiet && git -C "$work_dir" diff --cached --quiet; then
383
+ # Check for untracked files
384
+ local untracked
385
+ untracked="$(git -C "$work_dir" ls-files --others --exclude-standard | head -1)"
386
+ if [[ -z "$untracked" ]]; then
387
+ return 1 # Nothing to commit
388
+ fi
389
+ fi
390
+
391
+ git -C "$work_dir" add -A 2>/dev/null || true
392
+ git -C "$work_dir" commit -m "loop: iteration $ITERATION — autonomous progress" --no-verify 2>/dev/null || return 1
393
+ return 0
394
+ }
395
+
396
+ # ─── Progress & Circuit Breaker ───────────────────────────────────────────────
397
+
398
+ check_progress() {
399
+ local changes
400
+ changes="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 2>/dev/null | tail -1 || echo "")"
401
+ local insertions
402
+ insertions="$(echo "$changes" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)"
403
+ if [[ "${insertions:-0}" -lt 5 ]]; then
404
+ return 1 # No meaningful progress
405
+ fi
406
+ return 0
407
+ }
408
+
409
+ check_completion() {
410
+ local log_file="$1"
411
+ grep -q "LOOP_COMPLETE" "$log_file" 2>/dev/null
412
+ }
413
+
414
+ check_circuit_breaker() {
415
+ if [[ "$CONSECUTIVE_FAILURES" -ge 3 ]]; then
416
+ error "Circuit breaker tripped: 3 consecutive iterations with no meaningful progress."
417
+ STATUS="circuit_breaker"
418
+ return 1
419
+ fi
420
+ return 0
421
+ }
422
+
423
+ check_max_iterations() {
424
+ if [[ "$ITERATION" -gt "$MAX_ITERATIONS" ]]; then
425
+ warn "Max iterations ($MAX_ITERATIONS) reached."
426
+ STATUS="max_iterations"
427
+ return 1
428
+ fi
429
+ return 0
430
+ }
431
+
432
+ # ─── Test Gate ────────────────────────────────────────────────────────────────
433
+
434
+ run_test_gate() {
435
+ if [[ -z "$TEST_CMD" ]]; then
436
+ TEST_PASSED=""
437
+ TEST_OUTPUT=""
438
+ return
439
+ fi
440
+
441
+ local test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
442
+ if eval "$TEST_CMD" > "$test_log" 2>&1; then
443
+ TEST_PASSED=true
444
+ TEST_OUTPUT="All tests passed."
445
+ else
446
+ TEST_PASSED=false
447
+ TEST_OUTPUT="$(tail -50 "$test_log")"
448
+ fi
449
+ }
450
+
451
+ # ─── Audit Agent ─────────────────────────────────────────────────────────────
452
+
453
+ run_audit_agent() {
454
+ if ! $AUDIT_AGENT_ENABLED; then
455
+ return
456
+ fi
457
+
458
+ local log_file="$LOG_DIR/iteration-${ITERATION}.log"
459
+ local audit_log="$LOG_DIR/audit-iter-${ITERATION}.log"
460
+
461
+ # Gather context: tail of implementer output + git diff
462
+ local impl_tail
463
+ impl_tail="$(tail -100 "$log_file" 2>/dev/null || echo "(no output)")"
464
+ local diff_stat
465
+ diff_stat="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 2>/dev/null || echo "(no changes)")"
466
+
467
+ local audit_prompt
468
+ read -r -d '' audit_prompt <<AUDIT_PROMPT || true
469
+ You are an independent code auditor reviewing an autonomous coding agent.
470
+
471
+ ## Goal the agent was working toward
472
+ ${GOAL}
473
+
474
+ ## Agent Output (last 100 lines)
475
+ ${impl_tail}
476
+
477
+ ## Changes Made (git diff --stat)
478
+ ${diff_stat}
479
+
480
+ ## Your Task
481
+ Critically review the work:
482
+ 1. Did the agent make meaningful progress toward the goal?
483
+ 2. Are there obvious bugs, logic errors, or security issues?
484
+ 3. Did the agent leave incomplete work (TODOs, placeholder code)?
485
+ 4. Are there any regressions or broken patterns?
486
+ 5. Is the code quality acceptable?
487
+
488
+ If the work is acceptable and moves toward the goal, output exactly: AUDIT_PASS
489
+ Otherwise, list the specific issues that need fixing.
490
+ AUDIT_PROMPT
491
+
492
+ echo -e " ${PURPLE}▸${RESET} Running audit agent..."
493
+
494
+ # Build flags with haiku model override for fast/cheap audit
495
+ local audit_flags=()
496
+ audit_flags+=("--model" "haiku")
497
+ if $SKIP_PERMISSIONS; then
498
+ audit_flags+=("--dangerously-skip-permissions")
499
+ fi
500
+
501
+ local exit_code=0
502
+ claude -p "$audit_prompt" "${audit_flags[@]}" > "$audit_log" 2>&1 || exit_code=$?
503
+
504
+ if grep -q "AUDIT_PASS" "$audit_log" 2>/dev/null; then
505
+ AUDIT_RESULT="pass"
506
+ echo -e " ${GREEN}✓${RESET} Audit: passed"
507
+ else
508
+ AUDIT_RESULT="$(grep -v '^$' "$audit_log" | tail -20 | head -10 2>/dev/null || echo "Audit returned no output")"
509
+ echo -e " ${YELLOW}⚠${RESET} Audit: issues found"
510
+ fi
511
+ }
512
+
513
+ # ─── Quality Gates ───────────────────────────────────────────────────────────
514
+
515
+ run_quality_gates() {
516
+ if ! $QUALITY_GATES_ENABLED; then
517
+ QUALITY_GATE_PASSED=true
518
+ return
519
+ fi
520
+
521
+ QUALITY_GATE_PASSED=true
522
+ local gate_failures=()
523
+
524
+ echo -e " ${PURPLE}▸${RESET} Running quality gates..."
525
+
526
+ # Gate 1: Tests pass (if TEST_CMD set)
527
+ if [[ -n "$TEST_CMD" ]] && [[ "$TEST_PASSED" == "false" ]]; then
528
+ gate_failures+=("tests failing")
529
+ fi
530
+
531
+ # Gate 2: No uncommitted changes
532
+ if ! git -C "$PROJECT_ROOT" diff --quiet 2>/dev/null || \
533
+ ! git -C "$PROJECT_ROOT" diff --cached --quiet 2>/dev/null; then
534
+ gate_failures+=("uncommitted changes present")
535
+ fi
536
+
537
+ # Gate 3: No TODO/FIXME/HACK/XXX in new code
538
+ local todo_count
539
+ todo_count="$(git -C "$PROJECT_ROOT" diff HEAD~1 2>/dev/null | grep -cE '^\+.*(TODO|FIXME|HACK|XXX)' || true)"
540
+ todo_count="${todo_count:-0}"
541
+ if [[ "${todo_count:-0}" -gt 0 ]]; then
542
+ gate_failures+=("${todo_count} TODO/FIXME/HACK/XXX markers in new code")
543
+ fi
544
+
545
+ # Gate 4: Definition of Done (if DOD_FILE set)
546
+ if [[ -n "$DOD_FILE" ]]; then
547
+ if ! check_definition_of_done; then
548
+ gate_failures+=("definition of done not satisfied")
549
+ fi
550
+ fi
551
+
552
+ if [[ ${#gate_failures[@]} -gt 0 ]]; then
553
+ QUALITY_GATE_PASSED=false
554
+ local failures_str
555
+ failures_str="$(printf ', %s' "${gate_failures[@]}")"
556
+ failures_str="${failures_str:2}" # trim leading ", "
557
+ echo -e " ${RED}✗${RESET} Quality gates: FAILED (${failures_str})"
558
+ else
559
+ echo -e " ${GREEN}✓${RESET} Quality gates: all passed"
560
+ fi
561
+ }
562
+
563
+ check_definition_of_done() {
564
+ if [[ ! -f "$DOD_FILE" ]]; then
565
+ warn "Definition of done file not found: $DOD_FILE"
566
+ return 1
567
+ fi
568
+
569
+ local dod_content
570
+ dod_content="$(cat "$DOD_FILE")"
571
+ local diff_content
572
+ diff_content="$(git -C "$PROJECT_ROOT" diff HEAD~1 2>/dev/null || echo "(no diff)")"
573
+
574
+ local dod_prompt
575
+ read -r -d '' dod_prompt <<DOD_PROMPT || true
576
+ You are evaluating whether code changes satisfy a Definition of Done checklist.
577
+
578
+ ## Definition of Done
579
+ ${dod_content}
580
+
581
+ ## Changes Made (git diff)
582
+ ${diff_content}
583
+
584
+ ## Your Task
585
+ For each item in the Definition of Done, determine if the changes satisfy it.
586
+ If ALL items are satisfied, output exactly: DOD_PASS
587
+ Otherwise, list which items are NOT satisfied and why.
588
+ DOD_PROMPT
589
+
590
+ local dod_log="$LOG_DIR/dod-iter-${ITERATION}.log"
591
+ local dod_flags=()
592
+ dod_flags+=("--model" "haiku")
593
+ if $SKIP_PERMISSIONS; then
594
+ dod_flags+=("--dangerously-skip-permissions")
595
+ fi
596
+
597
+ claude -p "$dod_prompt" "${dod_flags[@]}" > "$dod_log" 2>&1 || true
598
+
599
+ if grep -q "DOD_PASS" "$dod_log" 2>/dev/null; then
600
+ echo -e " ${GREEN}✓${RESET} Definition of Done: satisfied"
601
+ return 0
602
+ else
603
+ echo -e " ${YELLOW}⚠${RESET} Definition of Done: not satisfied"
604
+ return 1
605
+ fi
606
+ }
607
+
608
+ # ─── Guarded Completion ──────────────────────────────────────────────────────
609
+
610
+ guard_completion() {
611
+ local log_file="$LOG_DIR/iteration-${ITERATION}.log"
612
+
613
+ # Check if LOOP_COMPLETE is in the log
614
+ if ! grep -q "LOOP_COMPLETE" "$log_file" 2>/dev/null; then
615
+ return 1 # No completion claim
616
+ fi
617
+
618
+ echo -e " ${CYAN}▸${RESET} LOOP_COMPLETE detected — validating..."
619
+
620
+ local rejection_reasons=()
621
+
622
+ # Check quality gates
623
+ if ! $QUALITY_GATE_PASSED; then
624
+ rejection_reasons+=("quality gates failed")
625
+ fi
626
+
627
+ # Check audit agent
628
+ if $AUDIT_AGENT_ENABLED && [[ "$AUDIT_RESULT" != "pass" ]]; then
629
+ rejection_reasons+=("audit agent found issues")
630
+ fi
631
+
632
+ # Check tests
633
+ if [[ -n "$TEST_CMD" ]] && [[ "$TEST_PASSED" == "false" ]]; then
634
+ rejection_reasons+=("tests failing")
635
+ fi
636
+
637
+ if [[ ${#rejection_reasons[@]} -gt 0 ]]; then
638
+ local reasons_str
639
+ reasons_str="$(printf ', %s' "${rejection_reasons[@]}")"
640
+ reasons_str="${reasons_str:2}"
641
+ echo -e " ${RED}✗${RESET} Completion REJECTED: ${reasons_str}"
642
+ COMPLETION_REJECTED=true
643
+ return 1
644
+ fi
645
+
646
+ echo -e " ${GREEN}${BOLD}✓ LOOP_COMPLETE accepted — all gates passed!${RESET}"
647
+ return 0
648
+ }
649
+
650
+ # ─── Prompt Composition ──────────────────────────────────────────────────────
651
+
652
+ compose_prompt() {
653
+ local recent_log
654
+ # Get last 3 iteration summaries from log entries
655
+ recent_log="$(echo "$LOG_ENTRIES" | tail -15)"
656
+ if [[ -z "$recent_log" ]]; then
657
+ recent_log="(first iteration — no previous progress)"
658
+ fi
659
+
660
+ local git_log
661
+ git_log="$(git_recent_log)"
662
+
663
+ local test_section
664
+ if [[ -z "$TEST_CMD" ]]; then
665
+ test_section="No test command configured."
666
+ elif [[ -z "$TEST_PASSED" ]]; then
667
+ test_section="No test results yet (first iteration). Test command: $TEST_CMD"
668
+ elif $TEST_PASSED; then
669
+ test_section="$TEST_OUTPUT"
670
+ else
671
+ test_section="TESTS FAILED — fix these before proceeding:
672
+ $TEST_OUTPUT"
673
+ fi
674
+
675
+ # Build audit sections (captured before heredoc to avoid nested heredoc issues)
676
+ local audit_section
677
+ audit_section="$(compose_audit_section)"
678
+ local audit_feedback_section
679
+ audit_feedback_section="$(compose_audit_feedback_section)"
680
+ local rejection_notice_section
681
+ rejection_notice_section="$(compose_rejection_notice_section)"
682
+
683
+ cat <<PROMPT
684
+ You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
685
+
686
+ ## Your Goal
687
+ ${GOAL}
688
+
689
+ ## Current Progress
690
+ ${recent_log}
691
+
692
+ ## Recent Git Activity
693
+ ${git_log}
694
+
695
+ ## Test Results (Previous Iteration)
696
+ ${test_section}
697
+
698
+ ## Instructions
699
+ 1. Read the codebase and understand the current state
700
+ 2. Identify the highest-priority remaining work toward the goal
701
+ 3. Implement ONE meaningful chunk of progress
702
+ 4. Run tests if a test command exists: ${TEST_CMD:-"(none)"}
703
+ 5. Commit your work with a descriptive message
704
+ 6. When the goal is FULLY achieved, output exactly: LOOP_COMPLETE
705
+
706
+ ${audit_section}
707
+
708
+ ${audit_feedback_section}
709
+
710
+ ${rejection_notice_section}
711
+
712
+ ## Rules
713
+ - Focus on ONE task per iteration — do it well
714
+ - Always commit with descriptive messages
715
+ - If tests fail, fix them before ending
716
+ - If stuck on the same issue for 2+ iterations, try a different approach
717
+ - Do NOT output LOOP_COMPLETE unless the goal is genuinely achieved
718
+ PROMPT
719
+ }
720
+
721
+ compose_audit_section() {
722
+ if ! $AUDIT_ENABLED; then
723
+ return
724
+ fi
725
+ cat <<'AUDIT_SECTION'
726
+ ## Self-Audit Checklist
727
+ Before declaring LOOP_COMPLETE, critically evaluate your own work:
728
+ 1. Does the implementation FULLY satisfy the goal, not just partially?
729
+ 2. Are there any edge cases you haven't handled?
730
+ 3. Did you leave any TODO, FIXME, HACK, or XXX comments in new code?
731
+ 4. Are all new functions/modules tested (if a test command exists)?
732
+ 5. Would a code reviewer approve this, or would they request changes?
733
+ 6. Is the code clean, well-structured, and following project conventions?
734
+
735
+ If ANY answer is "no", do NOT output LOOP_COMPLETE. Instead, fix the issues first.
736
+ AUDIT_SECTION
737
+ }
738
+
739
+ compose_audit_feedback_section() {
740
+ if [[ -z "$AUDIT_RESULT" ]] || [[ "$AUDIT_RESULT" == "pass" ]]; then
741
+ return
742
+ fi
743
+ cat <<AUDIT_FEEDBACK
744
+ ## Audit Feedback (Previous Iteration)
745
+ An independent audit of your last iteration found these issues:
746
+ ${AUDIT_RESULT}
747
+
748
+ Address ALL audit findings before proceeding with new work.
749
+ AUDIT_FEEDBACK
750
+ }
751
+
752
+ compose_rejection_notice_section() {
753
+ if ! $COMPLETION_REJECTED; then
754
+ return
755
+ fi
756
+ COMPLETION_REJECTED=false
757
+ cat <<'REJECTION'
758
+ ## ⚠ Completion Rejected
759
+ Your previous LOOP_COMPLETE was REJECTED because quality gates did not pass.
760
+ Review the audit feedback and test results above, fix the issues, then try again.
761
+ Do NOT output LOOP_COMPLETE until all quality checks pass.
762
+ REJECTION
763
+ }
764
+
765
+ compose_worker_prompt() {
766
+ local agent_num="$1"
767
+ local total_agents="$2"
768
+
769
+ local base_prompt
770
+ base_prompt="$(compose_prompt)"
771
+
772
+ cat <<PROMPT
773
+ ${base_prompt}
774
+
775
+ ## Agent Identity
776
+ You are Agent ${agent_num} of ${total_agents}. Other agents are working in parallel.
777
+ Check git log to see what they've done — avoid duplicating their work.
778
+ Focus on areas they haven't touched yet.
779
+ PROMPT
780
+ }
781
+
782
+ # ─── Claude Execution ────────────────────────────────────────────────────────
783
+
784
+ build_claude_flags() {
785
+ local flags=()
786
+ flags+=("--model" "$MODEL")
787
+
788
+ if $SKIP_PERMISSIONS; then
789
+ flags+=("--dangerously-skip-permissions")
790
+ fi
791
+
792
+ if [[ -n "$MAX_TURNS" ]]; then
793
+ flags+=("--max-turns" "$MAX_TURNS")
794
+ fi
795
+
796
+ echo "${flags[*]}"
797
+ }
798
+
799
+ run_claude_iteration() {
800
+ local log_file="$LOG_DIR/iteration-${ITERATION}.log"
801
+ local prompt
802
+ prompt="$(compose_prompt)"
803
+
804
+ local flags
805
+ flags="$(build_claude_flags)"
806
+
807
+ local iter_start
808
+ iter_start="$(now_epoch)"
809
+
810
+ echo -e "\n${CYAN}${BOLD}▸${RESET} ${BOLD}Iteration ${ITERATION}/${MAX_ITERATIONS}${RESET} — Starting..."
811
+
812
+ # Run Claude headless
813
+ local exit_code=0
814
+ # shellcheck disable=SC2086
815
+ claude -p "$prompt" $flags > "$log_file" 2>&1 || exit_code=$?
816
+
817
+ local iter_end
818
+ iter_end="$(now_epoch)"
819
+ local iter_duration=$(( iter_end - iter_start ))
820
+
821
+ echo -e " ${GREEN}✓${RESET} Claude session completed ($(format_duration "$iter_duration"), exit $exit_code)"
822
+
823
+ # Show verbose output if requested
824
+ if $VERBOSE; then
825
+ echo -e " ${DIM}─── Claude Output ───${RESET}"
826
+ sed 's/^/ /' "$log_file" | head -100
827
+ echo -e " ${DIM}─────────────────────${RESET}"
828
+ fi
829
+
830
+ return $exit_code
831
+ }
832
+
833
+ # ─── Iteration Summary Extraction ────────────────────────────────────────────
834
+
835
+ extract_summary() {
836
+ local log_file="$1"
837
+ # Grab last meaningful lines from Claude output, skipping empty lines
838
+ local summary
839
+ summary="$(grep -v '^$' "$log_file" | tail -5 | head -3 2>/dev/null || echo "(no output)")"
840
+ # Truncate long lines
841
+ echo "$summary" | cut -c1-120
842
+ }
843
+
844
+ # ─── Display Helpers ─────────────────────────────────────────────────────────
845
+
846
+ show_banner() {
847
+ echo ""
848
+ echo -e "${CYAN}${BOLD}shipwright${RESET} ${DIM}v${VERSION}${RESET} — ${BOLD}Continuous Loop${RESET}"
849
+ echo -e "${CYAN}═══════════════════════════════════════════════${RESET}"
850
+ echo ""
851
+ echo -e " ${BOLD}Goal:${RESET} $GOAL"
852
+ echo -e " ${BOLD}Model:${RESET} $MODEL ${DIM}|${RESET} ${BOLD}Max:${RESET} $MAX_ITERATIONS iterations ${DIM}|${RESET} ${BOLD}Test:${RESET} ${TEST_CMD:-"(none)"}"
853
+ if [[ "$AGENTS" -gt 1 ]]; then
854
+ echo -e " ${BOLD}Agents:${RESET} $AGENTS ${DIM}(parallel worktree mode)${RESET}"
855
+ fi
856
+ if $SKIP_PERMISSIONS; then
857
+ echo -e " ${YELLOW}${BOLD}⚠${RESET} ${YELLOW}--dangerously-skip-permissions enabled${RESET}"
858
+ fi
859
+ if $AUDIT_ENABLED || $AUDIT_AGENT_ENABLED || $QUALITY_GATES_ENABLED; then
860
+ 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}"
861
+ fi
862
+ echo ""
863
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
864
+ }
865
+
866
+ show_summary() {
867
+ local end_epoch
868
+ end_epoch="$(now_epoch)"
869
+ local duration=$(( end_epoch - START_EPOCH ))
870
+
871
+ local status_display
872
+ case "$STATUS" in
873
+ complete) status_display="${GREEN}✓ Complete (LOOP_COMPLETE detected)${RESET}" ;;
874
+ circuit_breaker) status_display="${RED}✗ Circuit breaker tripped${RESET}" ;;
875
+ max_iterations) status_display="${YELLOW}⚠ Max iterations reached${RESET}" ;;
876
+ interrupted) status_display="${YELLOW}⚠ Interrupted by user${RESET}" ;;
877
+ error) status_display="${RED}✗ Error${RESET}" ;;
878
+ *) status_display="${DIM}$STATUS${RESET}" ;;
879
+ esac
880
+
881
+ local test_display
882
+ if [[ -z "$TEST_CMD" ]]; then
883
+ test_display="${DIM}No tests configured${RESET}"
884
+ elif [[ "$TEST_PASSED" == "true" ]]; then
885
+ test_display="${GREEN}All passing${RESET}"
886
+ elif [[ "$TEST_PASSED" == "false" ]]; then
887
+ test_display="${RED}Failing${RESET}"
888
+ else
889
+ test_display="${DIM}Not run${RESET}"
890
+ fi
891
+
892
+ echo ""
893
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
894
+ local status_upper
895
+ status_upper="$(echo "$STATUS" | tr '[:lower:]' '[:upper:]')"
896
+ echo -e " ${BOLD}LOOP ${status_upper}${RESET}"
897
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
898
+ echo ""
899
+ echo -e " ${BOLD}Goal:${RESET} $GOAL"
900
+ echo -e " ${BOLD}Status:${RESET} $status_display"
901
+ echo -e " ${BOLD}Iterations:${RESET} $ITERATION/$MAX_ITERATIONS"
902
+ echo -e " ${BOLD}Duration:${RESET} $(format_duration "$duration")"
903
+ echo -e " ${BOLD}Commits:${RESET} $TOTAL_COMMITS"
904
+ echo -e " ${BOLD}Tests:${RESET} $test_display"
905
+ echo ""
906
+ echo -e " ${DIM}State: $STATE_FILE${RESET}"
907
+ echo -e " ${DIM}Logs: $LOG_DIR/${RESET}"
908
+ echo ""
909
+ }
910
+
911
+ # ─── Signal Handling ──────────────────────────────────────────────────────────
912
+
913
+ CHILD_PID=""
914
+
915
+ cleanup() {
916
+ echo ""
917
+ warn "Loop interrupted at iteration $ITERATION"
918
+
919
+ # Kill any running Claude process
920
+ if [[ -n "$CHILD_PID" ]] && kill -0 "$CHILD_PID" 2>/dev/null; then
921
+ kill "$CHILD_PID" 2>/dev/null || true
922
+ wait "$CHILD_PID" 2>/dev/null || true
923
+ fi
924
+
925
+ # If multi-agent, kill worker panes
926
+ if [[ "$AGENTS" -gt 1 ]]; then
927
+ cleanup_multi_agent
928
+ fi
929
+
930
+ STATUS="interrupted"
931
+ write_state
932
+ show_summary
933
+ exit 130
934
+ }
935
+
936
+ trap cleanup SIGINT SIGTERM
937
+
938
+ # ─── Multi-Agent: Worktree Setup ─────────────────────────────────────────────
939
+
940
+ setup_worktrees() {
941
+ local branch_base="loop"
942
+ mkdir -p "$WORKTREE_DIR"
943
+
944
+ for i in $(seq 1 "$AGENTS"); do
945
+ local wt_path="$WORKTREE_DIR/agent-${i}"
946
+ local branch_name="${branch_base}/agent-${i}"
947
+
948
+ if [[ -d "$wt_path" ]]; then
949
+ info "Worktree agent-${i} already exists"
950
+ continue
951
+ fi
952
+
953
+ # Create branch if it doesn't exist
954
+ if ! git -C "$PROJECT_ROOT" rev-parse --verify "$branch_name" &>/dev/null; then
955
+ git -C "$PROJECT_ROOT" branch "$branch_name" HEAD 2>/dev/null || true
956
+ fi
957
+
958
+ git -C "$PROJECT_ROOT" worktree add "$wt_path" "$branch_name" 2>/dev/null || {
959
+ error "Failed to create worktree for agent-${i}"
960
+ return 1
961
+ }
962
+
963
+ success "Worktree: agent-${i} → $wt_path"
964
+ done
965
+ }
966
+
967
+ cleanup_worktrees() {
968
+ for i in $(seq 1 "$AGENTS"); do
969
+ local wt_path="$WORKTREE_DIR/agent-${i}"
970
+ if [[ -d "$wt_path" ]]; then
971
+ git -C "$PROJECT_ROOT" worktree remove --force "$wt_path" 2>/dev/null || true
972
+ fi
973
+ done
974
+ rmdir "$WORKTREE_DIR" 2>/dev/null || true
975
+ }
976
+
977
+ # ─── Multi-Agent: Worker Loop Script ─────────────────────────────────────────
978
+
979
+ generate_worker_script() {
980
+ local agent_num="$1"
981
+ local total_agents="$2"
982
+ local wt_path="$WORKTREE_DIR/agent-${agent_num}"
983
+ local worker_script="$LOG_DIR/worker-${agent_num}.sh"
984
+
985
+ local claude_flags
986
+ claude_flags="$(build_claude_flags)"
987
+
988
+ cat > "$worker_script" <<'WORKEREOF'
989
+ #!/usr/bin/env bash
990
+ set -euo pipefail
991
+
992
+ AGENT_NUM="__AGENT_NUM__"
993
+ TOTAL_AGENTS="__TOTAL_AGENTS__"
994
+ WORK_DIR="__WORK_DIR__"
995
+ LOG_DIR="__LOG_DIR__"
996
+ MAX_ITERATIONS="__MAX_ITERATIONS__"
997
+ GOAL="__GOAL__"
998
+ TEST_CMD="__TEST_CMD__"
999
+ CLAUDE_FLAGS="__CLAUDE_FLAGS__"
1000
+
1001
+ CYAN='\033[38;2;0;212;255m'
1002
+ GREEN='\033[38;2;74;222;128m'
1003
+ YELLOW='\033[38;2;250;204;21m'
1004
+ RED='\033[38;2;248;113;113m'
1005
+ DIM='\033[2m'
1006
+ BOLD='\033[1m'
1007
+ RESET='\033[0m'
1008
+
1009
+ cd "$WORK_DIR"
1010
+ ITERATION=0
1011
+ CONSECUTIVE_FAILURES=0
1012
+
1013
+ echo -e "${CYAN}${BOLD}▸${RESET} Agent ${AGENT_NUM}/${TOTAL_AGENTS} starting in ${WORK_DIR}"
1014
+
1015
+ while [[ "$ITERATION" -lt "$MAX_ITERATIONS" ]]; do
1016
+ ITERATION=$(( ITERATION + 1 ))
1017
+ echo -e "\n${CYAN}${BOLD}▸${RESET} Agent ${AGENT_NUM} — Iteration ${ITERATION}/${MAX_ITERATIONS}"
1018
+
1019
+ # Pull latest from other agents
1020
+ git fetch origin main 2>/dev/null && git merge origin/main --no-edit 2>/dev/null || true
1021
+
1022
+ # Build prompt
1023
+ GIT_LOG="$(git log --oneline -20 2>/dev/null || echo '(no commits)')"
1024
+ TEST_SECTION="No test results yet."
1025
+ if [[ -n "$TEST_CMD" ]]; then
1026
+ TEST_SECTION="Test command: $TEST_CMD"
1027
+ fi
1028
+
1029
+ PROMPT="$(cat <<PROMPT
1030
+ You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
1031
+
1032
+ ## Your Goal
1033
+ ${GOAL}
1034
+
1035
+ ## Recent Git Activity
1036
+ ${GIT_LOG}
1037
+
1038
+ ## Test Results
1039
+ ${TEST_SECTION}
1040
+
1041
+ ## Agent Identity
1042
+ You are Agent ${AGENT_NUM} of ${TOTAL_AGENTS}. Other agents are working in parallel.
1043
+ Check git log to see what they've done — avoid duplicating their work.
1044
+ Focus on areas they haven't touched yet.
1045
+
1046
+ ## Instructions
1047
+ 1. Read the codebase and understand the current state
1048
+ 2. Identify the highest-priority remaining work toward the goal
1049
+ 3. Implement ONE meaningful chunk of progress
1050
+ 4. Commit your work with a descriptive message
1051
+ 5. When the goal is FULLY achieved, output exactly: LOOP_COMPLETE
1052
+
1053
+ ## Rules
1054
+ - Focus on ONE task per iteration — do it well
1055
+ - Always commit with descriptive messages
1056
+ - If stuck on the same issue for 2+ iterations, try a different approach
1057
+ - Do NOT output LOOP_COMPLETE unless the goal is genuinely achieved
1058
+ PROMPT
1059
+ )"
1060
+
1061
+ # Run Claude
1062
+ LOG_FILE="$LOG_DIR/agent-${AGENT_NUM}-iter-${ITERATION}.log"
1063
+ # shellcheck disable=SC2086
1064
+ claude -p "$PROMPT" $CLAUDE_FLAGS > "$LOG_FILE" 2>&1 || true
1065
+
1066
+ echo -e " ${GREEN}✓${RESET} Claude session completed"
1067
+
1068
+ # Check completion
1069
+ if grep -q "LOOP_COMPLETE" "$LOG_FILE" 2>/dev/null; then
1070
+ echo -e " ${GREEN}${BOLD}✓ LOOP_COMPLETE detected!${RESET}"
1071
+ # Signal completion
1072
+ touch "$LOG_DIR/.agent-${AGENT_NUM}-complete"
1073
+ break
1074
+ fi
1075
+
1076
+ # Auto-commit
1077
+ git add -A 2>/dev/null || true
1078
+ if git commit -m "agent-${AGENT_NUM}: iteration ${ITERATION}" --no-verify 2>/dev/null; then
1079
+ git push origin "loop/agent-${AGENT_NUM}" 2>/dev/null || true
1080
+ echo -e " ${GREEN}✓${RESET} Committed and pushed"
1081
+ fi
1082
+
1083
+ # Circuit breaker: check for progress
1084
+ CHANGES="$(git diff --stat HEAD~1 2>/dev/null | tail -1 || echo '')"
1085
+ INSERTIONS="$(echo "$CHANGES" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)"
1086
+ if [[ "${INSERTIONS:-0}" -lt 5 ]]; then
1087
+ CONSECUTIVE_FAILURES=$(( CONSECUTIVE_FAILURES + 1 ))
1088
+ echo -e " ${YELLOW}⚠${RESET} Low progress (${CONSECUTIVE_FAILURES}/3)"
1089
+ else
1090
+ CONSECUTIVE_FAILURES=0
1091
+ fi
1092
+
1093
+ if [[ "$CONSECUTIVE_FAILURES" -ge 3 ]]; then
1094
+ echo -e " ${RED}✗${RESET} Circuit breaker — stopping agent ${AGENT_NUM}"
1095
+ break
1096
+ fi
1097
+
1098
+ sleep 2
1099
+ done
1100
+
1101
+ echo -e "\n${DIM}Agent ${AGENT_NUM} finished after ${ITERATION} iterations${RESET}"
1102
+ WORKEREOF
1103
+
1104
+ # Replace placeholders
1105
+ sed -i '' "s|__AGENT_NUM__|${agent_num}|g" "$worker_script"
1106
+ sed -i '' "s|__TOTAL_AGENTS__|${total_agents}|g" "$worker_script"
1107
+ sed -i '' "s|__WORK_DIR__|${wt_path}|g" "$worker_script"
1108
+ sed -i '' "s|__LOG_DIR__|${LOG_DIR}|g" "$worker_script"
1109
+ sed -i '' "s|__MAX_ITERATIONS__|${MAX_ITERATIONS}|g" "$worker_script"
1110
+ sed -i '' "s|__TEST_CMD__|${TEST_CMD}|g" "$worker_script"
1111
+ sed -i '' "s|__CLAUDE_FLAGS__|${claude_flags}|g" "$worker_script"
1112
+ # Goal needs special handling for sed (may contain special chars)
1113
+ # Use awk for safe string replacement without python
1114
+ awk -v goal="$GOAL" '{gsub(/__GOAL__/, goal); print}' "$worker_script" > "${worker_script}.tmp" \
1115
+ && mv "${worker_script}.tmp" "$worker_script"
1116
+ chmod +x "$worker_script"
1117
+ echo "$worker_script"
1118
+ }
1119
+
1120
+ # ─── Multi-Agent: Launch ─────────────────────────────────────────────────────
1121
+
1122
+ MULTI_WINDOW_NAME=""
1123
+
1124
+ launch_multi_agent() {
1125
+ info "Setting up multi-agent mode ($AGENTS agents)..."
1126
+
1127
+ # Setup worktrees
1128
+ setup_worktrees || { error "Failed to setup worktrees"; exit 1; }
1129
+
1130
+ # Create tmux window for workers
1131
+ MULTI_WINDOW_NAME="cct-loop-$(date +%s)"
1132
+ tmux new-window -n "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT"
1133
+
1134
+ # First pane becomes monitor
1135
+ tmux send-keys -t "$MULTI_WINDOW_NAME" "printf '\\033]2;loop-monitor\\033\\\\'" Enter
1136
+ sleep 0.2
1137
+ tmux send-keys -t "$MULTI_WINDOW_NAME" "clear && echo 'Loop Monitor — watching agent logs...'" Enter
1138
+
1139
+ # Create worker panes
1140
+ for i in $(seq 1 "$AGENTS"); do
1141
+ local worker_script
1142
+ worker_script="$(generate_worker_script "$i" "$AGENTS")"
1143
+
1144
+ tmux split-window -t "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT"
1145
+ sleep 0.1
1146
+ tmux send-keys -t "$MULTI_WINDOW_NAME" "printf '\\033]2;agent-${i}\\033\\\\'" Enter
1147
+ sleep 0.1
1148
+ tmux send-keys -t "$MULTI_WINDOW_NAME" "bash '$worker_script'" Enter
1149
+ done
1150
+
1151
+ # Layout: monitor pane on top (35%), worker agents tile below
1152
+ tmux select-layout -t "$MULTI_WINDOW_NAME" main-vertical 2>/dev/null || true
1153
+ tmux resize-pane -t "$MULTI_WINDOW_NAME.0" -y 35% 2>/dev/null || true
1154
+
1155
+ # In the monitor pane, tail all agent logs
1156
+ tmux select-pane -t "$MULTI_WINDOW_NAME.0"
1157
+ sleep 0.5
1158
+ 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
1159
+
1160
+ success "Launched $AGENTS worker agents in window: $MULTI_WINDOW_NAME"
1161
+ echo ""
1162
+
1163
+ # Wait for completion
1164
+ info "Monitoring agents... (Ctrl-C to stop all)"
1165
+ wait_for_multi_completion
1166
+ }
1167
+
1168
+ wait_for_multi_completion() {
1169
+ while true; do
1170
+ # Check if any agent signaled completion
1171
+ for i in $(seq 1 "$AGENTS"); do
1172
+ if [[ -f "$LOG_DIR/.agent-${i}-complete" ]]; then
1173
+ success "Agent $i signaled LOOP_COMPLETE!"
1174
+ STATUS="complete"
1175
+ write_state
1176
+ return 0
1177
+ fi
1178
+ done
1179
+
1180
+ # Check if all worker panes are still running
1181
+ local running=0
1182
+ for i in $(seq 1 "$AGENTS"); do
1183
+ # Check if the worker log is still being written to
1184
+ local latest_log
1185
+ latest_log="$(ls -t "$LOG_DIR"/agent-"${i}"-iter-*.log 2>/dev/null | head -1)"
1186
+ if [[ -n "$latest_log" ]]; then
1187
+ local age
1188
+ age=$(( $(now_epoch) - $(stat -f %m "$latest_log" 2>/dev/null || echo 0) ))
1189
+ if [[ $age -lt 300 ]]; then # Active within 5 minutes
1190
+ running=$(( running + 1 ))
1191
+ fi
1192
+ fi
1193
+ done
1194
+
1195
+ if [[ $running -eq 0 ]]; then
1196
+ # Check if we have any logs at all (might still be starting)
1197
+ local total_logs
1198
+ total_logs="$(ls "$LOG_DIR"/agent-*-iter-*.log 2>/dev/null | wc -l | tr -d ' ')"
1199
+ if [[ "${total_logs:-0}" -gt 0 ]]; then
1200
+ warn "All agents appear to have stopped."
1201
+ STATUS="complete"
1202
+ write_state
1203
+ return 0
1204
+ fi
1205
+ fi
1206
+
1207
+ sleep 5
1208
+ done
1209
+ }
1210
+
1211
+ cleanup_multi_agent() {
1212
+ if [[ -n "$MULTI_WINDOW_NAME" ]]; then
1213
+ # Send Ctrl-C to all panes in the worker window
1214
+ local pane_count
1215
+ pane_count="$(tmux list-panes -t "$MULTI_WINDOW_NAME" 2>/dev/null | wc -l | tr -d ' ')"
1216
+ for i in $(seq 0 $(( pane_count - 1 ))); do
1217
+ tmux send-keys -t "$MULTI_WINDOW_NAME.$i" C-c 2>/dev/null || true
1218
+ done
1219
+ sleep 1
1220
+ tmux kill-window -t "$MULTI_WINDOW_NAME" 2>/dev/null || true
1221
+ fi
1222
+
1223
+ # Clean up completion markers
1224
+ rm -f "$LOG_DIR"/.agent-*-complete 2>/dev/null || true
1225
+ }
1226
+
1227
+ # ─── Main: Single-Agent Loop ─────────────────────────────────────────────────
1228
+
1229
+ run_single_agent_loop() {
1230
+ if $RESUME; then
1231
+ resume_state
1232
+ else
1233
+ initialize_state
1234
+ fi
1235
+
1236
+ show_banner
1237
+
1238
+ while true; do
1239
+ # Pre-checks (before incrementing — ITERATION tracks completed count)
1240
+ check_circuit_breaker || break
1241
+ ITERATION=$(( ITERATION + 1 ))
1242
+ check_max_iterations || { ITERATION=$(( ITERATION - 1 )); break; }
1243
+
1244
+ # Run Claude
1245
+ local exit_code=0
1246
+ run_claude_iteration || exit_code=$?
1247
+
1248
+ local log_file="$LOG_DIR/iteration-${ITERATION}.log"
1249
+
1250
+ # Auto-commit if Claude didn't
1251
+ local commits_before
1252
+ commits_before="$(git_commit_count)"
1253
+ git_auto_commit "$PROJECT_ROOT" || true
1254
+ local commits_after
1255
+ commits_after="$(git_commit_count)"
1256
+ local new_commits=$(( commits_after - commits_before ))
1257
+ TOTAL_COMMITS=$(( TOTAL_COMMITS + new_commits ))
1258
+
1259
+ # Git diff stats
1260
+ local diff_stat
1261
+ diff_stat="$(git_diff_stat)"
1262
+ if [[ -n "$diff_stat" ]]; then
1263
+ echo -e " ${GREEN}✓${RESET} Git: $diff_stat"
1264
+ fi
1265
+
1266
+ # Test gate
1267
+ run_test_gate
1268
+ if [[ -n "$TEST_CMD" ]]; then
1269
+ if [[ "$TEST_PASSED" == "true" ]]; then
1270
+ echo -e " ${GREEN}✓${RESET} Tests: passed"
1271
+ else
1272
+ echo -e " ${RED}✗${RESET} Tests: failed"
1273
+ fi
1274
+ fi
1275
+
1276
+ # Audit agent (reviews implementer's work)
1277
+ run_audit_agent
1278
+
1279
+ # Quality gates (automated checks)
1280
+ run_quality_gates
1281
+
1282
+ # Guarded completion (replaces naive grep check)
1283
+ if guard_completion; then
1284
+ STATUS="complete"
1285
+ write_state
1286
+ show_summary
1287
+ return 0
1288
+ fi
1289
+
1290
+ # Check progress (circuit breaker)
1291
+ if check_progress; then
1292
+ CONSECUTIVE_FAILURES=0
1293
+ echo -e " ${GREEN}✓${RESET} Progress detected — continuing"
1294
+ else
1295
+ CONSECUTIVE_FAILURES=$(( CONSECUTIVE_FAILURES + 1 ))
1296
+ echo -e " ${YELLOW}⚠${RESET} Low progress (${CONSECUTIVE_FAILURES}/3 before circuit breaker)"
1297
+ fi
1298
+
1299
+ # Extract summary and update state
1300
+ local summary
1301
+ summary="$(extract_summary "$log_file")"
1302
+ append_log_entry "### Iteration $ITERATION ($(now_iso))
1303
+ $summary
1304
+ "
1305
+ write_state
1306
+
1307
+ sleep 2
1308
+ done
1309
+
1310
+ # Write final state after loop exits
1311
+ write_state
1312
+ show_summary
1313
+ }
1314
+
1315
+ # ─── Main: Entry Point ───────────────────────────────────────────────────────
1316
+
1317
+ main() {
1318
+ if [[ "$AGENTS" -gt 1 ]]; then
1319
+ if $RESUME; then
1320
+ resume_state
1321
+ else
1322
+ initialize_state
1323
+ fi
1324
+ show_banner
1325
+ launch_multi_agent
1326
+ show_summary
1327
+ else
1328
+ run_single_agent_loop
1329
+ fi
1330
+ }
1331
+
1332
+ main