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.
- package/LICENSE +21 -0
- package/README.md +926 -0
- package/claude-code/CLAUDE.md.shipwright +125 -0
- package/claude-code/hooks/notify-idle.sh +35 -0
- package/claude-code/hooks/pre-compact-save.sh +57 -0
- package/claude-code/hooks/task-completed.sh +170 -0
- package/claude-code/hooks/teammate-idle.sh +68 -0
- package/claude-code/settings.json.template +184 -0
- package/completions/_shipwright +140 -0
- package/completions/shipwright.bash +89 -0
- package/completions/shipwright.fish +107 -0
- package/docs/KNOWN-ISSUES.md +199 -0
- package/docs/TIPS.md +331 -0
- package/docs/definition-of-done.example.md +16 -0
- package/docs/patterns/README.md +139 -0
- package/docs/patterns/audit-loop.md +149 -0
- package/docs/patterns/bug-hunt.md +183 -0
- package/docs/patterns/feature-implementation.md +159 -0
- package/docs/patterns/refactoring.md +183 -0
- package/docs/patterns/research-exploration.md +144 -0
- package/docs/patterns/test-generation.md +173 -0
- package/package.json +49 -0
- package/scripts/adapters/docker-deploy.sh +50 -0
- package/scripts/adapters/fly-deploy.sh +41 -0
- package/scripts/adapters/iterm2-adapter.sh +122 -0
- package/scripts/adapters/railway-deploy.sh +34 -0
- package/scripts/adapters/tmux-adapter.sh +87 -0
- package/scripts/adapters/vercel-deploy.sh +35 -0
- package/scripts/adapters/wezterm-adapter.sh +103 -0
- package/scripts/cct +242 -0
- package/scripts/cct-cleanup.sh +172 -0
- package/scripts/cct-cost.sh +590 -0
- package/scripts/cct-daemon.sh +3189 -0
- package/scripts/cct-doctor.sh +328 -0
- package/scripts/cct-fix.sh +478 -0
- package/scripts/cct-fleet.sh +904 -0
- package/scripts/cct-init.sh +282 -0
- package/scripts/cct-logs.sh +273 -0
- package/scripts/cct-loop.sh +1332 -0
- package/scripts/cct-memory.sh +1148 -0
- package/scripts/cct-pipeline.sh +3844 -0
- package/scripts/cct-prep.sh +1352 -0
- package/scripts/cct-ps.sh +168 -0
- package/scripts/cct-reaper.sh +390 -0
- package/scripts/cct-session.sh +284 -0
- package/scripts/cct-status.sh +169 -0
- package/scripts/cct-templates.sh +242 -0
- package/scripts/cct-upgrade.sh +422 -0
- package/scripts/cct-worktree.sh +405 -0
- package/scripts/postinstall.mjs +96 -0
- package/templates/pipelines/autonomous.json +71 -0
- package/templates/pipelines/cost-aware.json +95 -0
- package/templates/pipelines/deployed.json +79 -0
- package/templates/pipelines/enterprise.json +114 -0
- package/templates/pipelines/fast.json +63 -0
- package/templates/pipelines/full.json +104 -0
- package/templates/pipelines/hotfix.json +63 -0
- package/templates/pipelines/standard.json +91 -0
- package/tmux/claude-teams-overlay.conf +109 -0
- package/tmux/templates/architecture.json +19 -0
- package/tmux/templates/bug-fix.json +24 -0
- package/tmux/templates/code-review.json +24 -0
- package/tmux/templates/devops.json +19 -0
- package/tmux/templates/documentation.json +19 -0
- package/tmux/templates/exploration.json +19 -0
- package/tmux/templates/feature-dev.json +24 -0
- package/tmux/templates/full-stack.json +24 -0
- package/tmux/templates/migration.json +24 -0
- package/tmux/templates/refactor.json +19 -0
- package/tmux/templates/security-audit.json +24 -0
- package/tmux/templates/testing.json +24 -0
- 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
|