-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathloop.sh
More file actions
executable file
·328 lines (279 loc) · 10.3 KB
/
loop.sh
File metadata and controls
executable file
·328 lines (279 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
#!/bin/bash
set -euo pipefail
# Orchestration loop for autonomous agent execution
# Implements iteration control, state persistence, process monitoring,
# and structured logging for continuous development workflows.
# Load configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/loop.config.sh"
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck source=loop.config.sh
source "$CONFIG_FILE"
else
echo "Error: Configuration file not found: $CONFIG_FILE"
exit 1
fi
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <goal>"
echo ""
echo "Example: $0 'Complete integration tests'"
echo ""
echo "Configuration can be overridden via environment variables."
echo "See loop.config.sh for available options."
exit 1
fi
CURRENT_GOAL="$*"
# Build opencode command with commit reminder
OPENCODE_GOAL="Follow execution protocol in $PROMPT_FILE to achieve: $CURRENT_GOAL. IMPORTANT: You MUST commit all changes at the end of this iteration using git."
OPENCODE_COMMAND="run --print-logs --agent $OPENCODE_AGENT \"$OPENCODE_GOAL\" -f $PROMPT_FILE"
# Colors for TUI-lite experience
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
NC='\033[0m' # No Color
mkdir -p "$LOG_DIR"
# Resource management: Check disk space
check_disk_space() {
if command -v df &>/dev/null; then
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS uses -g for gigabytes
AVAILABLE_GB=$(df -g "$LOG_DIR" | awk 'NR==2 {print int($4)}')
else
# Linux uses -BG
AVAILABLE_GB=$(df -BG "$LOG_DIR" | awk 'NR==2 {gsub(/G/,"",$4); print $4}')
fi
if ((AVAILABLE_GB < MIN_DISK_SPACE_GB)); then
echo -e "${RED}❌ Error: Insufficient disk space (${AVAILABLE_GB}GB < ${MIN_DISK_SPACE_GB}GB required).${NC}"
exit 1
fi
fi
}
# Resource management: Clean old logs
cleanup_old_logs() {
if [[ "$LOG_RETENTION_DAYS" -gt 0 ]] && command -v find &>/dev/null; then
echo -e "${BLUE}🧹 Cleaning logs older than ${LOG_RETENTION_DAYS} days...${NC}"
find "$LOG_DIR" -name "iteration_*.log" -mtime +"$LOG_RETENTION_DAYS" -delete 2>/dev/null || true
fi
}
# Load or initialize state
load_state() {
if [[ -f "$STATE_FILE" ]]; then
LAST_ITERATION=$(jq -r '.last_iteration // 0' "$STATE_FILE" 2>/dev/null || echo 0)
LAST_TASK=$(jq -r '.last_task // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
echo -e "${BLUE}📂 Resuming from iteration $((LAST_ITERATION + 1)) (last task: $LAST_TASK)${NC}"
else
LAST_ITERATION=0
LAST_TASK="none"
echo -e "${BLUE}🆕 Starting fresh (no previous state)${NC}"
fi
}
# Save state after each iteration
save_state() {
local iteration=$1
local task=$2
mkdir -p "$(dirname "$STATE_FILE")"
jq -n \
--arg iter "$iteration" \
--arg task "$task" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{last_iteration: ($iter | tonumber), last_task: $task, updated_at: $timestamp}' \
>"$STATE_FILE"
}
# Detect uncommitted changes
check_git_status() {
if git rev-parse --git-dir >/dev/null 2>&1; then
if ! git diff --quiet || ! git diff --cached --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then
return 1 # Has changes
fi
fi
return 0 # Clean or not a git repo
}
# Process activity monitoring using ps with CPU sampling over time
monitor_process_activity() {
local pid=$1
local timeout=$2
local last_activity=$(date +%s)
while kill -0 "$pid" 2>/dev/null; do
local NOW=$(date +%s)
# Sample CPU usage over the last ACTIVITY_CHECK_INTERVAL seconds
local samples=()
local sample_interval=1
local num_samples=$ACTIVITY_CHECK_INTERVAL
for ((j = 0; j < num_samples; j++)); do
local CPU_USAGE
if [[ "$OSTYPE" == "darwin"* ]]; then
CPU_USAGE=$(ps -p "$pid" -o %cpu= 2>/dev/null | awk '{print $1}' || echo 0)
else
CPU_USAGE=$(ps -p "$pid" -o %cpu= 2>/dev/null | awk '{print $1}' || echo 0)
fi
samples+=("$CPU_USAGE")
sleep "$sample_interval"
done
# Compute average CPU usage
local sum=0
for s in "${samples[@]}"; do
sum=$(awk "BEGIN {print $sum + $s}")
done
local avg
avg=$(awk "BEGIN {print $sum / ${#samples[@]}}")
# If average CPU > 0.1%, consider the process active
if awk "BEGIN {exit !($avg > 0.1)}"; then
last_activity=$NOW
fi
# Check timeout
if ((NOW - last_activity > timeout)); then
echo -e "\n${RED}❌ Timeout: No process activity for ${timeout}s. Killing process...${NC}"
kill -9 "$pid" 2>/dev/null || true
return 1
fi
done
return 0
}
# Pre-flight checks
check_disk_space
cleanup_old_logs
if ! command -v opencode &>/dev/null; then
echo -e "${RED}❌ Error: 'opencode' command not found.${NC}"
exit 1
fi
if [[ ! -f "$PROMPT_FILE" ]]; then
echo -e "${RED}❌ Error: Prompt file '$PROMPT_FILE' not found.${NC}"
exit 1
fi
echo -e "${BLUE}🔄 Starting orchestration loop (max iterations: $MAX_ITERATIONS)${NC}"
echo -e "${BLUE}🎯 Goal: $CURRENT_GOAL${NC}"
echo -e "${BLUE}📝 Prompt: $PROMPT_FILE${NC}"
echo -e "${BLUE}📂 Logs: $LOG_DIR/${NC}"
# Load state and determine starting iteration
load_state
START_ITERATION=$((LAST_ITERATION + 1))
# Cleanup on interrupt
trap 'cleanup_on_interrupt' SIGINT
cleanup_on_interrupt() {
echo -e "\n${YELLOW}🛑 Loop interrupted by user. Cleaning up...${NC}"
if [ -n "$CURRENT_PID" ] && kill -0 "$CURRENT_PID" 2>/dev/null; then
echo -e "${RED}Killing opencode process (PID: $CURRENT_PID)...${NC}"
kill -9 "$CURRENT_PID" 2>/dev/null || true
fi
END_TIME=$(date +%s)
TOTAL_DURATION=$((END_TIME - START_TIME))
output_stats
exit 1
}
output_stats() {
if ((COMPLETED_ITERS > 0)); then
AVG_DURATION=$((SUM_DURATION / COMPLETED_ITERS))
echo -e "${BLUE}📊 Stats:${NC}"
echo -e " - Iterations: $COMPLETED_ITERS"
echo -e " - Total time: ${TOTAL_DURATION}s"
echo -e " - Avg time: ${AVG_DURATION}s"
echo -e " - Min time: ${MIN_DURATION}s"
echo -e " - Max time: ${MAX_DURATION}s"
fi
}
START_TIME=$(date +%s)
SUM_DURATION=0
MIN_DURATION=999999
MAX_DURATION=0
COMPLETED_ITERS=0
CURRENT_PID=""
TASK_REPEAT_DETECTOR=()
for ((i = START_ITERATION; i <= MAX_ITERATIONS; i++)); do
ITER_START=$(date +%s)
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
LOG_FILE="$LOG_DIR/iteration_${i}_${TIMESTAMP}.log"
echo -e "\n${YELLOW}--- Iteration $i / $MAX_ITERATIONS ---${NC}" | tee -a "$LOG_FILE"
# Check for uncommitted changes from previous iteration
if ! check_git_status; then
echo -e "${YELLOW}⚠️ Warning: Uncommitted changes detected from previous iteration${NC}" | tee -a "$LOG_FILE"
fi
# Run opencode with stdbuf for line buffering
(
stdbuf -oL -eL "$OPENCODE_BIN" $OPENCODE_COMMAND 2>&1
) | tee -a "$LOG_FILE" | grep -v INFO &
PID=$!
CURRENT_PID=$PID
# Monitor process activity instead of log file modification
set +e
monitor_process_activity "$PID" "$TIMEOUT"
MONITOR_EXIT=$?
set -e
if [ $MONITOR_EXIT -ne 0 ]; then
echo -e "${RED}❌ Error: Iteration timed out (no activity for ${TIMEOUT}s).${NC}" | tee -a "$LOG_FILE"
save_state "$i" "TIMEOUT"
exit 1
fi
set +e
wait $PID
EXIT_CODE=$?
set -e
CURRENT_PID=""
if [ $EXIT_CODE -eq 0 ]; then
# Extract task identifier for loop detection
CURRENT_TASK=$(grep -o "NEXT_STEPS\.md.*" "$LOG_FILE" | head -1 || echo "unknown")
# Check for completion signals
if grep -q "<promise>DONE</promise>" "$LOG_FILE"; then
echo -e "${GREEN}✅ Success: ALL TASKS DONE signal received.${NC}" | tee -a "$LOG_FILE"
save_state "$i" "DONE"
break
elif grep -q "<promise>BLOCKED</promise>" "$LOG_FILE"; then
BLOCKER_MSG=$(grep -A 3 "<promise>BLOCKED</promise>" "$LOG_FILE" | tail -2 || echo "No details provided")
echo -e "${RED}🚫 BLOCKED: Human intervention required.${NC}" | tee -a "$LOG_FILE"
echo -e "${YELLOW}Blocker: $BLOCKER_MSG${NC}" | tee -a "$LOG_FILE"
save_state "$i" "BLOCKED"
exit 2
elif grep -q "<promise>NEXT_TASK</promise>" "$LOG_FILE"; then
echo -e "${BLUE}⏭️ Task complete. NEXT_TASK signal received. Continuing...${NC}" | tee -a "$LOG_FILE"
# Detect infinite loops (same task repeated 3+ times)
TASK_REPEAT_DETECTOR+=("$CURRENT_TASK")
if [ ${#TASK_REPEAT_DETECTOR[@]} -ge 3 ]; then
RECENT_TASKS="${TASK_REPEAT_DETECTOR[@]: -3}"
if [[ "$RECENT_TASKS" == *"$CURRENT_TASK"*"$CURRENT_TASK"*"$CURRENT_TASK"* ]]; then
echo -e "${RED}❌ Error: Infinite loop detected (same task repeated 3+ times).${NC}" | tee -a "$LOG_FILE"
echo -e "${YELLOW}Task: $CURRENT_TASK${NC}" | tee -a "$LOG_FILE"
save_state "$i" "INFINITE_LOOP"
exit 3
fi
fi
else
echo -e "${YELLOW}⚠️ Iteration finished without explicit signal. Continuing...${NC}" | tee -a "$LOG_FILE"
fi
# Verify files were updated
if check_git_status; then
echo -e "${YELLOW}⚠️ Warning: No changes detected in working tree${NC}" | tee -a "$LOG_FILE"
fi
# Verify commit happened
if ! check_git_status; then
echo -e "${MAGENTA}⚠️ Uncommitted changes remain - agent should commit before next iteration${NC}" | tee -a "$LOG_FILE"
fi
else
echo -e "${RED}❌ Error: opencode failed with exit code $EXIT_CODE. Check $LOG_FILE${NC}" | tee -a "$LOG_FILE"
save_state "$i" "ERROR_$EXIT_CODE"
exit 1
fi
# Safety check: if the log file is just the header we added
if [[ $(wc -l <"$LOG_FILE") -le 1 ]]; then
echo -e "${RED}❌ Error: Empty log file (or only header). opencode may have crashed.${NC}" | tee -a "$LOG_FILE"
save_state "$i" "CRASH"
exit 1
fi
ITER_END=$(date +%s)
DURATION=$((ITER_END - ITER_START))
SUM_DURATION=$((SUM_DURATION + DURATION))
((COMPLETED_ITERS++))
((DURATION < MIN_DURATION)) && MIN_DURATION=$DURATION
((DURATION > MAX_DURATION)) && MAX_DURATION=$DURATION
save_state "$i" "$CURRENT_TASK"
echo -e "${GREEN}Iteration $i complete (${DURATION}s). Continuing...${NC}"
done
END_TIME=$(date +%s)
TOTAL_DURATION=$((END_TIME - START_TIME))
echo -e "\n${BLUE}🏁 Loop finished in ${TOTAL_DURATION}s.${NC}"
output_stats
if ((i > MAX_ITERATIONS)); then
echo -e "${YELLOW}🛑 Reached maximum iterations ($MAX_ITERATIONS).${NC}" | tee -a "$LOG_DIR/final_status.log"
else
echo -e "${GREEN}✅ Project signaled completion or was stopped manually.${NC}"
fi