claude-evolve 1.3.0 → 1.3.2
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/bin/claude-evolve-run +12 -0
- package/bin/claude-evolve-run-parallel +129 -35
- package/bin/claude-evolve-worker +4 -2
- package/lib/config.sh +19 -4
- package/lib/csv-lock.sh +6 -2
- package/package.json +1 -1
package/bin/claude-evolve-run
CHANGED
|
@@ -29,6 +29,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
29
29
|
force_sequential="true"
|
|
30
30
|
shift
|
|
31
31
|
;;
|
|
32
|
+
--keep-awake|--caffeinate)
|
|
33
|
+
use_caffeinate="true"
|
|
34
|
+
shift
|
|
35
|
+
;;
|
|
32
36
|
--help)
|
|
33
37
|
cat <<EOF
|
|
34
38
|
claude-evolve run - Execute evolution candidates
|
|
@@ -40,6 +44,7 @@ OPTIONS:
|
|
|
40
44
|
--timeout <sec> Kill evaluator after specified seconds (default: no timeout)
|
|
41
45
|
--parallel Force parallel execution mode
|
|
42
46
|
--sequential Force sequential execution mode
|
|
47
|
+
--keep-awake Prevent system sleep during execution (macOS only)
|
|
43
48
|
--help Show this help message
|
|
44
49
|
|
|
45
50
|
DESCRIPTION:
|
|
@@ -59,6 +64,13 @@ EOF
|
|
|
59
64
|
esac
|
|
60
65
|
done
|
|
61
66
|
|
|
67
|
+
# Check if caffeinate should be used
|
|
68
|
+
if [[ "$use_caffeinate" == "true" ]] && command -v caffeinate >/dev/null 2>&1; then
|
|
69
|
+
echo "[INFO] Using caffeinate to prevent system sleep"
|
|
70
|
+
# Re-run this script with caffeinate
|
|
71
|
+
exec caffeinate -dims "$0" "$@"
|
|
72
|
+
fi
|
|
73
|
+
|
|
62
74
|
# Determine execution mode
|
|
63
75
|
use_parallel=false
|
|
64
76
|
if [[ "$force_parallel" == "true" ]]; then
|
|
@@ -44,8 +44,8 @@ fi
|
|
|
44
44
|
# Prepare logging
|
|
45
45
|
mkdir -p logs
|
|
46
46
|
|
|
47
|
-
# Track active workers
|
|
48
|
-
|
|
47
|
+
# Track active workers (using regular array for compatibility)
|
|
48
|
+
worker_pids=()
|
|
49
49
|
consecutive_failures=0
|
|
50
50
|
MAX_FAILURES=10
|
|
51
51
|
rate_limit_hit=false
|
|
@@ -57,7 +57,7 @@ count_pending() {
|
|
|
57
57
|
return
|
|
58
58
|
fi
|
|
59
59
|
|
|
60
|
-
echo "$csv_content" | awk -F',' 'NR>1 && $5 == "pending" { count++ } END { print count+0 }'
|
|
60
|
+
echo "$csv_content" | awk -F',' 'NR>1 && ($5 == "pending" || $5 == "") { count++ } END { print count+0 }'
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
# Start a worker
|
|
@@ -68,8 +68,16 @@ start_worker() {
|
|
|
68
68
|
echo "[DISPATCHER] Starting worker..."
|
|
69
69
|
$worker_cmd &
|
|
70
70
|
local pid=$!
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
|
|
72
|
+
# Verify worker started successfully
|
|
73
|
+
sleep 0.1
|
|
74
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
75
|
+
worker_pids+=($pid)
|
|
76
|
+
echo "[DISPATCHER] Worker $pid started"
|
|
77
|
+
else
|
|
78
|
+
echo "[ERROR] Worker failed to start" >&2
|
|
79
|
+
((consecutive_failures++))
|
|
80
|
+
fi
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
# Check worker exit status and handle accordingly
|
|
@@ -100,13 +108,17 @@ handle_worker_exit() {
|
|
|
100
108
|
esac
|
|
101
109
|
|
|
102
110
|
# Remove from tracking
|
|
103
|
-
|
|
111
|
+
local new_pids=()
|
|
112
|
+
for p in "${worker_pids[@]}"; do
|
|
113
|
+
[[ $p != "$pid" ]] && new_pids+=($p)
|
|
114
|
+
done
|
|
115
|
+
worker_pids=("${new_pids[@]}")
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
# Signal handler for graceful shutdown
|
|
107
119
|
shutdown_workers() {
|
|
108
120
|
echo "[DISPATCHER] Shutting down workers..."
|
|
109
|
-
for pid in "${
|
|
121
|
+
for pid in "${worker_pids[@]}"; do
|
|
110
122
|
if kill -0 "$pid" 2>/dev/null; then
|
|
111
123
|
echo "[DISPATCHER] Stopping worker $pid"
|
|
112
124
|
kill -TERM "$pid" 2>/dev/null || true
|
|
@@ -119,15 +131,17 @@ shutdown_workers() {
|
|
|
119
131
|
sleep 1
|
|
120
132
|
((timeout--))
|
|
121
133
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
local new_pids=()
|
|
135
|
+
for pid in "${worker_pids[@]}"; do
|
|
136
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
137
|
+
new_pids+=($pid)
|
|
125
138
|
fi
|
|
126
139
|
done
|
|
140
|
+
worker_pids=("${new_pids[@]}")
|
|
127
141
|
done
|
|
128
142
|
|
|
129
143
|
# Force kill remaining workers
|
|
130
|
-
for pid in "${
|
|
144
|
+
for pid in "${worker_pids[@]}"; do
|
|
131
145
|
if kill -0 "$pid" 2>/dev/null; then
|
|
132
146
|
echo "[DISPATCHER] Force killing worker $pid"
|
|
133
147
|
kill -KILL "$pid" 2>/dev/null || true
|
|
@@ -138,11 +152,75 @@ shutdown_workers() {
|
|
|
138
152
|
exit 0
|
|
139
153
|
}
|
|
140
154
|
|
|
141
|
-
|
|
155
|
+
# Better signal handling with logging
|
|
156
|
+
handle_signal() {
|
|
157
|
+
local signal="$1"
|
|
158
|
+
echo "[DISPATCHER] Received signal: $signal" >&2
|
|
159
|
+
echo "[DISPATCHER] Active workers: ${#worker_pids[@]}" >&2
|
|
160
|
+
|
|
161
|
+
# For expensive workers, give option to continue
|
|
162
|
+
if [[ ${#worker_pids[@]} -gt 0 ]]; then
|
|
163
|
+
echo "[DISPATCHER] Warning: ${#worker_pids[@]} expensive workers are still running!" >&2
|
|
164
|
+
echo "[DISPATCHER] Press Ctrl+C again within 5 seconds to force shutdown, or wait..." >&2
|
|
165
|
+
|
|
166
|
+
# Give 5 seconds to reconsider
|
|
167
|
+
local count=5
|
|
168
|
+
while [[ $count -gt 0 ]]; do
|
|
169
|
+
sleep 1
|
|
170
|
+
((count--))
|
|
171
|
+
# Check if we got another signal
|
|
172
|
+
if [[ -f /tmp/evolve-force-shutdown-$$ ]]; then
|
|
173
|
+
echo "[DISPATCHER] Force shutdown requested" >&2
|
|
174
|
+
rm -f /tmp/evolve-force-shutdown-$$
|
|
175
|
+
shutdown_workers
|
|
176
|
+
exit 1
|
|
177
|
+
fi
|
|
178
|
+
done
|
|
179
|
+
|
|
180
|
+
echo "[DISPATCHER] Continuing with active workers..." >&2
|
|
181
|
+
return
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
shutdown_workers
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Set up signal handlers
|
|
188
|
+
trap 'handle_signal INT' INT
|
|
189
|
+
trap 'handle_signal TERM' TERM
|
|
190
|
+
trap 'echo "[DISPATCHER] Exiting with code $?" >&2' EXIT
|
|
191
|
+
|
|
192
|
+
# Check for stuck "running" candidates from previous runs
|
|
193
|
+
check_stuck_candidates() {
|
|
194
|
+
if read_csv_with_lock csv_content; then
|
|
195
|
+
local stuck_count=$(echo "$csv_content" | awk -F',' 'NR>1 && $5 == "running" { count++ } END { print count+0 }')
|
|
196
|
+
if [[ $stuck_count -gt 0 ]]; then
|
|
197
|
+
echo "[DISPATCHER] Found $stuck_count candidates stuck in 'running' status"
|
|
198
|
+
echo "[DISPATCHER] Resetting them to 'pending' for retry..."
|
|
199
|
+
|
|
200
|
+
# Reset stuck candidates
|
|
201
|
+
if acquire_csv_lock; then
|
|
202
|
+
awk -F',' -v OFS=',' '
|
|
203
|
+
NR==1 { print }
|
|
204
|
+
NR>1 {
|
|
205
|
+
if ($5 == "running") $5 = "pending"
|
|
206
|
+
print
|
|
207
|
+
}
|
|
208
|
+
' "$FULL_CSV_PATH" > "${FULL_CSV_PATH}.tmp" && mv -f "${FULL_CSV_PATH}.tmp" "$FULL_CSV_PATH"
|
|
209
|
+
release_csv_lock
|
|
210
|
+
fi
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
}
|
|
142
214
|
|
|
143
215
|
# Main dispatcher loop
|
|
144
216
|
echo "[DISPATCHER] Starting main dispatch loop"
|
|
145
217
|
|
|
218
|
+
# Check for stuck candidates from previous runs
|
|
219
|
+
check_stuck_candidates
|
|
220
|
+
|
|
221
|
+
# Set error handling
|
|
222
|
+
set +e # Don't exit on error in the main loop
|
|
223
|
+
|
|
146
224
|
while true; do
|
|
147
225
|
# Check if too many consecutive failures
|
|
148
226
|
if [[ $consecutive_failures -ge $MAX_FAILURES ]]; then
|
|
@@ -158,29 +236,24 @@ while true; do
|
|
|
158
236
|
fi
|
|
159
237
|
|
|
160
238
|
# Count pending work
|
|
161
|
-
pending_count=$(count_pending)
|
|
239
|
+
pending_count=$(count_pending || echo "0")
|
|
162
240
|
active_workers=${#worker_pids[@]}
|
|
163
241
|
|
|
164
242
|
echo "[DISPATCHER] Status: $pending_count pending, $active_workers active workers"
|
|
165
243
|
|
|
166
|
-
#
|
|
244
|
+
# Debug: Show CSV status if no pending
|
|
245
|
+
if [[ $pending_count -eq 0 ]]; then
|
|
246
|
+
total_rows=$(read_csv_with_lock csv_content && echo "$csv_content" | wc -l | xargs)
|
|
247
|
+
complete_count=$(read_csv_with_lock csv_content && echo "$csv_content" | awk -F',' 'NR>1 && $5 == "complete" { count++ } END { print count+0 }')
|
|
248
|
+
echo "[DISPATCHER] CSV has $((total_rows-1)) total candidates, $complete_count complete"
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
# If no pending work and no active workers, we're done
|
|
167
252
|
if [[ $pending_count -eq 0 && $active_workers -eq 0 ]]; then
|
|
168
|
-
echo "[DISPATCHER] No
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
echo "[DISPATCHER] No ideate script found. Evolution complete."
|
|
173
|
-
break
|
|
174
|
-
fi
|
|
175
|
-
|
|
176
|
-
if ! "$ideate_script"; then
|
|
177
|
-
echo "[ERROR] Failed to generate new ideas" >&2
|
|
178
|
-
break
|
|
179
|
-
fi
|
|
180
|
-
|
|
181
|
-
# Recount after ideation
|
|
182
|
-
pending_count=$(count_pending)
|
|
183
|
-
echo "[DISPATCHER] Generated ideas. New pending count: $pending_count"
|
|
253
|
+
echo "[DISPATCHER] No pending candidates found. Evolution complete."
|
|
254
|
+
echo "[DISPATCHER] Run 'claude-evolve ideate' to generate more candidates."
|
|
255
|
+
echo "[DISPATCHER] Exiting main loop: no work remaining" >&2
|
|
256
|
+
break
|
|
184
257
|
fi
|
|
185
258
|
|
|
186
259
|
# Start workers if we have pending work and capacity
|
|
@@ -198,22 +271,43 @@ while true; do
|
|
|
198
271
|
|
|
199
272
|
# Wait for any worker to finish
|
|
200
273
|
if [[ $active_workers -gt 0 ]]; then
|
|
201
|
-
#
|
|
202
|
-
|
|
274
|
+
# Poll for finished workers (macOS compatible)
|
|
275
|
+
sleep 5 # Check every 5 seconds
|
|
203
276
|
|
|
204
277
|
# Check which workers have finished
|
|
205
|
-
for pid in "${
|
|
278
|
+
for pid in "${worker_pids[@]}"; do
|
|
206
279
|
if ! kill -0 "$pid" 2>/dev/null; then
|
|
207
280
|
# Get exit status
|
|
208
|
-
wait "$pid"
|
|
281
|
+
wait "$pid" 2>/dev/null
|
|
209
282
|
exit_code=$?
|
|
210
283
|
handle_worker_exit "$pid" "$exit_code"
|
|
211
284
|
fi
|
|
212
285
|
done
|
|
286
|
+
|
|
287
|
+
# Safety check - if we have workers but the array is corrupted
|
|
288
|
+
if [[ ${#worker_pids[@]} -eq 0 ]] && jobs -r | grep -q .; then
|
|
289
|
+
echo "[DISPATCHER] Warning: Lost track of workers but jobs still running!" >&2
|
|
290
|
+
echo "[DISPATCHER] Attempting to recover..." >&2
|
|
291
|
+
|
|
292
|
+
# Try to recover PIDs from jobs
|
|
293
|
+
while read -r job_info; do
|
|
294
|
+
if [[ $job_info =~ \[([0-9]+)\][[:space:]]+([0-9]+) ]]; then
|
|
295
|
+
local recovered_pid="${BASH_REMATCH[2]}"
|
|
296
|
+
echo "[DISPATCHER] Recovered worker PID: $recovered_pid" >&2
|
|
297
|
+
worker_pids+=($recovered_pid)
|
|
298
|
+
fi
|
|
299
|
+
done < <(jobs -l)
|
|
300
|
+
fi
|
|
213
301
|
else
|
|
214
302
|
# No active workers but we might be waiting for ideation or have pending work
|
|
215
303
|
sleep 1
|
|
216
304
|
fi
|
|
217
305
|
done
|
|
218
306
|
|
|
219
|
-
echo "[DISPATCHER] Evolution run complete"
|
|
307
|
+
echo "[DISPATCHER] Evolution run complete"
|
|
308
|
+
|
|
309
|
+
# Final cleanup check
|
|
310
|
+
if [[ ${#worker_pids[@]} -gt 0 ]]; then
|
|
311
|
+
echo "[DISPATCHER] Warning: ${#worker_pids[@]} workers still active at exit"
|
|
312
|
+
shutdown_workers
|
|
313
|
+
fi
|
package/bin/claude-evolve-worker
CHANGED
|
@@ -187,7 +187,8 @@ eval_exit_code=0
|
|
|
187
187
|
|
|
188
188
|
if [[ -n $timeout_seconds ]]; then
|
|
189
189
|
echo "[WORKER-$$] Evaluation timeout: ${timeout_seconds}s"
|
|
190
|
-
|
|
190
|
+
# For Modal compatibility, don't capture stderr
|
|
191
|
+
if eval_output=$(timeout "$timeout_seconds" "$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$output_file"); then
|
|
191
192
|
eval_exit_code=0
|
|
192
193
|
else
|
|
193
194
|
eval_exit_code=$?
|
|
@@ -198,7 +199,8 @@ if [[ -n $timeout_seconds ]]; then
|
|
|
198
199
|
fi
|
|
199
200
|
fi
|
|
200
201
|
else
|
|
201
|
-
|
|
202
|
+
# For Modal compatibility, don't capture stderr
|
|
203
|
+
if eval_output=$("$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$output_file"); then
|
|
202
204
|
eval_exit_code=0
|
|
203
205
|
else
|
|
204
206
|
eval_exit_code=$?
|
package/lib/config.sh
CHANGED
|
@@ -58,14 +58,29 @@ load_config() {
|
|
|
58
58
|
# Simple YAML parsing for key: value pairs and nested structures
|
|
59
59
|
local in_ideation_section=false
|
|
60
60
|
local in_parallel_section=false
|
|
61
|
-
while IFS='
|
|
61
|
+
while IFS='' read -r line; do
|
|
62
62
|
# Skip comments and empty lines
|
|
63
|
-
[[ $
|
|
63
|
+
[[ $line =~ ^[[:space:]]*# ]] || [[ -z $line ]] && continue
|
|
64
|
+
|
|
65
|
+
# Parse key:value from line
|
|
66
|
+
if [[ ! $line =~ ^([^:]+):(.*)$ ]]; then
|
|
67
|
+
continue
|
|
68
|
+
fi
|
|
69
|
+
key="${BASH_REMATCH[1]}"
|
|
70
|
+
value="${BASH_REMATCH[2]}"
|
|
71
|
+
|
|
72
|
+
# Check if key is indented (for nested sections)
|
|
73
|
+
local is_indented=false
|
|
74
|
+
[[ $key =~ ^[[:space:]]+ ]] && is_indented=true
|
|
75
|
+
|
|
64
76
|
|
|
65
77
|
# Remove leading/trailing whitespace
|
|
66
78
|
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
67
79
|
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
68
80
|
|
|
81
|
+
# Remove inline comments from value
|
|
82
|
+
value=$(echo "$value" | sed 's/[[:space:]]*#.*$//')
|
|
83
|
+
|
|
69
84
|
# Remove quotes from value
|
|
70
85
|
value=$(echo "$value" | sed 's/^"//;s/"$//')
|
|
71
86
|
|
|
@@ -78,8 +93,8 @@ load_config() {
|
|
|
78
93
|
in_parallel_section=true
|
|
79
94
|
in_ideation_section=false
|
|
80
95
|
continue
|
|
81
|
-
elif [[ $
|
|
82
|
-
#
|
|
96
|
+
elif [[ $is_indented == false ]] && [[ $in_ideation_section == true || $in_parallel_section == true ]]; then
|
|
97
|
+
# Non-indented key found while in a section, exit nested sections
|
|
83
98
|
in_ideation_section=false
|
|
84
99
|
in_parallel_section=false
|
|
85
100
|
fi
|
package/lib/csv-lock.sh
CHANGED
|
@@ -135,14 +135,18 @@ find_next_pending_with_lock() {
|
|
|
135
135
|
|
|
136
136
|
# Find oldest pending candidate and update to running
|
|
137
137
|
local candidate=$(awk -F',' '
|
|
138
|
-
NR>1 && $5 == "pending" { print $1; exit }
|
|
138
|
+
NR>1 && ($5 == "pending" || $5 == "") { print $1; exit }
|
|
139
139
|
' "$csv_file")
|
|
140
140
|
|
|
141
141
|
if [ -n "$candidate" ]; then
|
|
142
142
|
# Update status to running while we have the lock
|
|
143
143
|
awk -F',' -v OFS=',' -v id="$candidate" '
|
|
144
144
|
NR==1 || $1 != id { print }
|
|
145
|
-
$1 == id {
|
|
145
|
+
$1 == id {
|
|
146
|
+
# Preserve existing fields but set status to running
|
|
147
|
+
if ($5 == "" || $5 == "pending") $5 = "running"
|
|
148
|
+
print
|
|
149
|
+
}
|
|
146
150
|
' "$csv_file" > "${csv_file}.tmp" && mv -f "${csv_file}.tmp" "$csv_file"
|
|
147
151
|
fi
|
|
148
152
|
|