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