claude-evolve 1.2.13 → 1.3.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/bin/claude-evolve-run +33 -2
- package/bin/claude-evolve-run-parallel +219 -0
- package/bin/claude-evolve-worker +259 -0
- package/lib/config.sh +30 -4
- package/lib/csv-lock.sh +151 -0
- package/package.json +3 -1
- package/templates/config.yaml +12 -1
package/bin/claude-evolve-run
CHANGED
|
@@ -21,6 +21,14 @@ while [[ $# -gt 0 ]]; do
|
|
|
21
21
|
timeout_seconds="$2"
|
|
22
22
|
shift 2
|
|
23
23
|
;;
|
|
24
|
+
--parallel)
|
|
25
|
+
force_parallel="true"
|
|
26
|
+
shift
|
|
27
|
+
;;
|
|
28
|
+
--sequential)
|
|
29
|
+
force_sequential="true"
|
|
30
|
+
shift
|
|
31
|
+
;;
|
|
24
32
|
--help)
|
|
25
33
|
cat <<EOF
|
|
26
34
|
claude-evolve run - Execute evolution candidates
|
|
@@ -30,6 +38,8 @@ USAGE:
|
|
|
30
38
|
|
|
31
39
|
OPTIONS:
|
|
32
40
|
--timeout <sec> Kill evaluator after specified seconds (default: no timeout)
|
|
41
|
+
--parallel Force parallel execution mode
|
|
42
|
+
--sequential Force sequential execution mode
|
|
33
43
|
--help Show this help message
|
|
34
44
|
|
|
35
45
|
DESCRIPTION:
|
|
@@ -49,8 +59,29 @@ EOF
|
|
|
49
59
|
esac
|
|
50
60
|
done
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
# Determine execution mode
|
|
63
|
+
use_parallel=false
|
|
64
|
+
if [[ "$force_parallel" == "true" ]]; then
|
|
65
|
+
use_parallel=true
|
|
66
|
+
echo "[INFO] Using parallel mode (forced via --parallel)"
|
|
67
|
+
elif [[ "$force_sequential" == "true" ]]; then
|
|
68
|
+
use_parallel=false
|
|
69
|
+
echo "[INFO] Using sequential mode (forced via --sequential)"
|
|
70
|
+
elif [[ "$PARALLEL_ENABLED" == "true" || "$PARALLEL_ENABLED" == "1" ]]; then
|
|
71
|
+
use_parallel=true
|
|
72
|
+
echo "[INFO] Using parallel mode (enabled in config)"
|
|
73
|
+
else
|
|
74
|
+
echo "[INFO] Using sequential mode (default)"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if [[ "$use_parallel" == "true" ]]; then
|
|
78
|
+
echo "[INFO] Starting parallel evolution run with up to $MAX_WORKERS workers"
|
|
79
|
+
exec "$SCRIPT_DIR/claude-evolve-run-parallel" ${timeout_seconds:+--timeout "$timeout_seconds"}
|
|
80
|
+
else
|
|
81
|
+
echo "[INFO] Starting continuous evolution run..."
|
|
82
|
+
echo "[INFO] Will continue running until no more pending candidates or 5 consecutive failures"
|
|
83
|
+
fi
|
|
84
|
+
|
|
54
85
|
[[ -n $timeout_seconds ]] && echo "[INFO] Using timeout: ${timeout_seconds} seconds per evaluation"
|
|
55
86
|
|
|
56
87
|
# Prepare logging directory
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Parallel evolution dispatcher - manages worker pool
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
# Load configuration
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
# shellcheck source=../lib/config.sh
|
|
9
|
+
source "$SCRIPT_DIR/../lib/config.sh"
|
|
10
|
+
# shellcheck source=../lib/csv-lock.sh
|
|
11
|
+
source "$SCRIPT_DIR/../lib/csv-lock.sh"
|
|
12
|
+
load_config
|
|
13
|
+
|
|
14
|
+
# Parse arguments
|
|
15
|
+
timeout_seconds=""
|
|
16
|
+
|
|
17
|
+
while [[ $# -gt 0 ]]; do
|
|
18
|
+
case $1 in
|
|
19
|
+
--timeout)
|
|
20
|
+
timeout_seconds="$2"
|
|
21
|
+
shift 2
|
|
22
|
+
;;
|
|
23
|
+
*)
|
|
24
|
+
echo "[ERROR] Unknown option: $1" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
echo "[DISPATCHER] Starting parallel evolution with $MAX_WORKERS max workers"
|
|
31
|
+
[[ -n $timeout_seconds ]] && echo "[DISPATCHER] Using timeout: ${timeout_seconds} seconds per worker"
|
|
32
|
+
|
|
33
|
+
# Validate workspace
|
|
34
|
+
if [[ ! -d "$FULL_EVOLUTION_DIR" ]]; then
|
|
35
|
+
echo "[ERROR] Evolution directory not found: $FULL_EVOLUTION_DIR" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [[ ! -f "$FULL_CSV_PATH" ]]; then
|
|
40
|
+
echo "[ERROR] CSV file not found: $FULL_CSV_PATH" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Prepare logging
|
|
45
|
+
mkdir -p logs
|
|
46
|
+
|
|
47
|
+
# Track active workers
|
|
48
|
+
declare -A worker_pids=()
|
|
49
|
+
consecutive_failures=0
|
|
50
|
+
MAX_FAILURES=10
|
|
51
|
+
rate_limit_hit=false
|
|
52
|
+
|
|
53
|
+
# Count pending candidates
|
|
54
|
+
count_pending() {
|
|
55
|
+
if ! read_csv_with_lock csv_content; then
|
|
56
|
+
echo "0"
|
|
57
|
+
return
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
echo "$csv_content" | awk -F',' 'NR>1 && $5 == "pending" { count++ } END { print count+0 }'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Start a worker
|
|
64
|
+
start_worker() {
|
|
65
|
+
local worker_cmd="$SCRIPT_DIR/claude-evolve-worker"
|
|
66
|
+
[[ -n $timeout_seconds ]] && worker_cmd="$worker_cmd --timeout $timeout_seconds"
|
|
67
|
+
|
|
68
|
+
echo "[DISPATCHER] Starting worker..."
|
|
69
|
+
$worker_cmd &
|
|
70
|
+
local pid=$!
|
|
71
|
+
worker_pids[$pid]=1
|
|
72
|
+
echo "[DISPATCHER] Worker $pid started"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Check worker exit status and handle accordingly
|
|
76
|
+
handle_worker_exit() {
|
|
77
|
+
local pid="$1"
|
|
78
|
+
local exit_code="$2"
|
|
79
|
+
|
|
80
|
+
case $exit_code in
|
|
81
|
+
0)
|
|
82
|
+
echo "[DISPATCHER] Worker $pid completed successfully"
|
|
83
|
+
consecutive_failures=0
|
|
84
|
+
;;
|
|
85
|
+
1)
|
|
86
|
+
echo "[DISPATCHER] Worker $pid failed"
|
|
87
|
+
((consecutive_failures++))
|
|
88
|
+
;;
|
|
89
|
+
2)
|
|
90
|
+
echo "[DISPATCHER] Worker $pid hit rate limit"
|
|
91
|
+
rate_limit_hit=true
|
|
92
|
+
;;
|
|
93
|
+
130)
|
|
94
|
+
echo "[DISPATCHER] Worker $pid interrupted"
|
|
95
|
+
;;
|
|
96
|
+
*)
|
|
97
|
+
echo "[DISPATCHER] Worker $pid exited with code $exit_code"
|
|
98
|
+
((consecutive_failures++))
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
|
|
102
|
+
# Remove from tracking
|
|
103
|
+
unset worker_pids[$pid]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Signal handler for graceful shutdown
|
|
107
|
+
shutdown_workers() {
|
|
108
|
+
echo "[DISPATCHER] Shutting down workers..."
|
|
109
|
+
for pid in "${!worker_pids[@]}"; do
|
|
110
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
111
|
+
echo "[DISPATCHER] Stopping worker $pid"
|
|
112
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
113
|
+
fi
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
# Wait for workers to exit
|
|
117
|
+
local timeout=10
|
|
118
|
+
while [[ ${#worker_pids[@]} -gt 0 && $timeout -gt 0 ]]; do
|
|
119
|
+
sleep 1
|
|
120
|
+
((timeout--))
|
|
121
|
+
|
|
122
|
+
for pid in "${!worker_pids[@]}"; do
|
|
123
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
124
|
+
unset worker_pids[$pid]
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
# Force kill remaining workers
|
|
130
|
+
for pid in "${!worker_pids[@]}"; do
|
|
131
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
132
|
+
echo "[DISPATCHER] Force killing worker $pid"
|
|
133
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
134
|
+
fi
|
|
135
|
+
done
|
|
136
|
+
|
|
137
|
+
echo "[DISPATCHER] Shutdown complete"
|
|
138
|
+
exit 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
trap shutdown_workers INT TERM
|
|
142
|
+
|
|
143
|
+
# Main dispatcher loop
|
|
144
|
+
echo "[DISPATCHER] Starting main dispatch loop"
|
|
145
|
+
|
|
146
|
+
while true; do
|
|
147
|
+
# Check if too many consecutive failures
|
|
148
|
+
if [[ $consecutive_failures -ge $MAX_FAILURES ]]; then
|
|
149
|
+
echo "[ERROR] Too many consecutive failures ($consecutive_failures). Stopping." >&2
|
|
150
|
+
break
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# Check if rate limit was hit
|
|
154
|
+
if [[ $rate_limit_hit == true ]]; then
|
|
155
|
+
echo "[DISPATCHER] Rate limit detected. Waiting 60 seconds before retrying..."
|
|
156
|
+
sleep 60
|
|
157
|
+
rate_limit_hit=false
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# Count pending work
|
|
161
|
+
pending_count=$(count_pending)
|
|
162
|
+
active_workers=${#worker_pids[@]}
|
|
163
|
+
|
|
164
|
+
echo "[DISPATCHER] Status: $pending_count pending, $active_workers active workers"
|
|
165
|
+
|
|
166
|
+
# If no pending work and no active workers, try to generate more ideas
|
|
167
|
+
if [[ $pending_count -eq 0 && $active_workers -eq 0 ]]; then
|
|
168
|
+
echo "[DISPATCHER] No work available. Generating new ideas..."
|
|
169
|
+
|
|
170
|
+
ideate_script="$SCRIPT_DIR/claude-evolve-ideate"
|
|
171
|
+
if [[ ! -f "$ideate_script" ]]; then
|
|
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"
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# Start workers if we have pending work and capacity
|
|
187
|
+
while [[ $pending_count -gt 0 && $active_workers -lt $MAX_WORKERS ]]; do
|
|
188
|
+
start_worker
|
|
189
|
+
active_workers=${#worker_pids[@]}
|
|
190
|
+
((pending_count--)) # Optimistically assume this will be picked up
|
|
191
|
+
done
|
|
192
|
+
|
|
193
|
+
# If no active workers and no pending work, we're done
|
|
194
|
+
if [[ $active_workers -eq 0 && $pending_count -eq 0 ]]; then
|
|
195
|
+
echo "[DISPATCHER] No active workers and no pending work. Evolution complete."
|
|
196
|
+
break
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# Wait for any worker to finish
|
|
200
|
+
if [[ $active_workers -gt 0 ]]; then
|
|
201
|
+
# Wait for any child process to exit
|
|
202
|
+
wait -n
|
|
203
|
+
|
|
204
|
+
# Check which workers have finished
|
|
205
|
+
for pid in "${!worker_pids[@]}"; do
|
|
206
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
207
|
+
# Get exit status
|
|
208
|
+
wait "$pid"
|
|
209
|
+
exit_code=$?
|
|
210
|
+
handle_worker_exit "$pid" "$exit_code"
|
|
211
|
+
fi
|
|
212
|
+
done
|
|
213
|
+
else
|
|
214
|
+
# No active workers but we might be waiting for ideation or have pending work
|
|
215
|
+
sleep 1
|
|
216
|
+
fi
|
|
217
|
+
done
|
|
218
|
+
|
|
219
|
+
echo "[DISPATCHER] Evolution run complete"
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Worker process for parallel evolution execution
|
|
3
|
+
# Processes a single evolution candidate and exits
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
# Load configuration
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
# shellcheck source=../lib/config.sh
|
|
10
|
+
source "$SCRIPT_DIR/../lib/config.sh"
|
|
11
|
+
# shellcheck source=../lib/csv-lock.sh
|
|
12
|
+
source "$SCRIPT_DIR/../lib/csv-lock.sh"
|
|
13
|
+
load_config
|
|
14
|
+
|
|
15
|
+
# Parse arguments
|
|
16
|
+
timeout_seconds=""
|
|
17
|
+
candidate_id=""
|
|
18
|
+
|
|
19
|
+
while [[ $# -gt 0 ]]; do
|
|
20
|
+
case $1 in
|
|
21
|
+
--timeout)
|
|
22
|
+
if [[ -z ${2:-} ]] || [[ ! $2 =~ ^[0-9]+$ ]] || [[ $2 -eq 0 ]]; then
|
|
23
|
+
echo "[ERROR] --timeout requires a positive integer (seconds)" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
timeout_seconds="$2"
|
|
27
|
+
shift 2
|
|
28
|
+
;;
|
|
29
|
+
--id)
|
|
30
|
+
candidate_id="$2"
|
|
31
|
+
shift 2
|
|
32
|
+
;;
|
|
33
|
+
*)
|
|
34
|
+
echo "[ERROR] Unknown option: $1" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
# If no ID provided, find next pending
|
|
41
|
+
if [[ -z $candidate_id ]]; then
|
|
42
|
+
candidate_id=$(find_next_pending_with_lock)
|
|
43
|
+
if [[ -z $candidate_id ]]; then
|
|
44
|
+
echo "[INFO] No pending candidates found"
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
else
|
|
48
|
+
# Mark specified candidate as running
|
|
49
|
+
update_csv_row_with_lock "$candidate_id" "status" "running"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "[WORKER-$$] Processing candidate ID: $candidate_id"
|
|
53
|
+
|
|
54
|
+
# Validate workspace
|
|
55
|
+
if [[ ! -d "$FULL_EVOLUTION_DIR" ]]; then
|
|
56
|
+
echo "[ERROR] Evolution directory not found: $FULL_EVOLUTION_DIR" >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Create log file for this run
|
|
61
|
+
mkdir -p logs
|
|
62
|
+
LOGFILE="logs/worker-${candidate_id}-$(date +%Y%m%d_%H%M%S).txt"
|
|
63
|
+
|
|
64
|
+
# Find candidate in CSV
|
|
65
|
+
row_data=""
|
|
66
|
+
if ! read_csv_with_lock csv_content; then
|
|
67
|
+
echo "[ERROR] Failed to read CSV" >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Extract candidate data
|
|
72
|
+
found=false
|
|
73
|
+
while IFS=, read -r csv_id csv_based_on csv_desc csv_perf csv_stat; do
|
|
74
|
+
if [[ $csv_id == "$candidate_id" ]]; then
|
|
75
|
+
id="$csv_id"
|
|
76
|
+
based_on_id="$csv_based_on"
|
|
77
|
+
description="$csv_desc"
|
|
78
|
+
performance="$csv_perf"
|
|
79
|
+
status="$csv_stat"
|
|
80
|
+
found=true
|
|
81
|
+
break
|
|
82
|
+
fi
|
|
83
|
+
done <<< "$csv_content"
|
|
84
|
+
|
|
85
|
+
if [[ $found == false ]]; then
|
|
86
|
+
echo "[ERROR] Candidate ID not found: $candidate_id" >&2
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Clean up description
|
|
91
|
+
description=${description#\"}
|
|
92
|
+
description=${description%\"}
|
|
93
|
+
|
|
94
|
+
echo "[WORKER-$$] Description: $description"
|
|
95
|
+
echo "[WORKER-$$] Based on ID: $based_on_id"
|
|
96
|
+
|
|
97
|
+
# Determine parent algorithm
|
|
98
|
+
if [[ -z $based_on_id || $based_on_id == "0" || $based_on_id == '""' ]]; then
|
|
99
|
+
parent_file="$FULL_ALGORITHM_PATH"
|
|
100
|
+
echo "[WORKER-$$] Using base algorithm"
|
|
101
|
+
else
|
|
102
|
+
# Handle both old and new format IDs
|
|
103
|
+
if [[ $based_on_id =~ ^[0-9]+$ ]]; then
|
|
104
|
+
parent_file="$FULL_OUTPUT_DIR/evolution_id${based_on_id}.py"
|
|
105
|
+
else
|
|
106
|
+
parent_file="$FULL_OUTPUT_DIR/evolution_${based_on_id}.py"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [[ ! -f $parent_file ]]; then
|
|
110
|
+
echo "[ERROR] Parent algorithm not found: $parent_file" >&2
|
|
111
|
+
update_csv_row_with_lock "$candidate_id" "status" "failed"
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Generate output file
|
|
117
|
+
if [[ $id =~ ^[0-9]+$ ]]; then
|
|
118
|
+
output_file="$FULL_OUTPUT_DIR/evolution_id${id}.py"
|
|
119
|
+
else
|
|
120
|
+
output_file="$FULL_OUTPUT_DIR/evolution_${id}.py"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Copy parent to output
|
|
124
|
+
cp "$parent_file" "$output_file"
|
|
125
|
+
echo "[WORKER-$$] Copied parent to: $output_file"
|
|
126
|
+
|
|
127
|
+
# Generate mutation (skip for baseline)
|
|
128
|
+
if [[ $id == "000" || $id == "0" || $id == "gen00-000" ]]; then
|
|
129
|
+
echo "[WORKER-$$] Baseline algorithm - skipping mutation"
|
|
130
|
+
else
|
|
131
|
+
# Check for claude CLI
|
|
132
|
+
claude_cmd="${CLAUDE_CMD:-claude}"
|
|
133
|
+
if ! command -v "$claude_cmd" >/dev/null 2>&1; then
|
|
134
|
+
echo "[ERROR] Claude CLI not found" >&2
|
|
135
|
+
update_csv_row_with_lock "$candidate_id" "status" "failed"
|
|
136
|
+
exit 1
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
CLAUDE_MODEL="sonnet"
|
|
140
|
+
echo "[WORKER-$$] Using Claude $CLAUDE_MODEL for mutation"
|
|
141
|
+
|
|
142
|
+
# Create mutation prompt
|
|
143
|
+
prompt="Edit the file $output_file to implement this specific change: $description
|
|
144
|
+
|
|
145
|
+
Requirements:
|
|
146
|
+
- Edit the file directly (don't just provide comments or suggestions)
|
|
147
|
+
- Maintain the same function signatures and interfaces
|
|
148
|
+
- Make the specific change described above
|
|
149
|
+
- Ensure the code runs without syntax errors
|
|
150
|
+
- Add proper error handling if needed
|
|
151
|
+
|
|
152
|
+
The file currently contains the parent algorithm. Modify it according to the description above."
|
|
153
|
+
|
|
154
|
+
# Log prompt
|
|
155
|
+
{
|
|
156
|
+
echo "=== WORKER $$ - MUTATION PROMPT ==="
|
|
157
|
+
echo "ID: $id"
|
|
158
|
+
echo "Timestamp: $(date)"
|
|
159
|
+
echo "$prompt"
|
|
160
|
+
echo
|
|
161
|
+
} >> "$LOGFILE"
|
|
162
|
+
|
|
163
|
+
# Call Claude
|
|
164
|
+
echo "[WORKER-$$] Calling Claude to apply mutation..."
|
|
165
|
+
claude_output=$(echo "$prompt" | "$claude_cmd" --dangerously-skip-permissions --model $CLAUDE_MODEL -p 2>&1 | tee -a "$LOGFILE")
|
|
166
|
+
claude_exit_code=${PIPESTATUS[1]}
|
|
167
|
+
|
|
168
|
+
# Check for rate limit
|
|
169
|
+
if echo "$claude_output" | grep -q "Claude AI usage limit reached"; then
|
|
170
|
+
echo "[ERROR] Claude API rate limit reached" >&2
|
|
171
|
+
# Reset to pending so it can be retried later
|
|
172
|
+
update_csv_row_with_lock "$candidate_id" "status" "pending"
|
|
173
|
+
exit 2 # Special exit code for rate limit
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
if [[ $claude_exit_code -ne 0 ]]; then
|
|
177
|
+
echo "[ERROR] Claude failed to mutate algorithm" >&2
|
|
178
|
+
update_csv_row_with_lock "$candidate_id" "status" "failed"
|
|
179
|
+
exit 1
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Run evaluator
|
|
184
|
+
echo "[WORKER-$$] Running evaluation..."
|
|
185
|
+
eval_output=""
|
|
186
|
+
eval_exit_code=0
|
|
187
|
+
|
|
188
|
+
if [[ -n $timeout_seconds ]]; then
|
|
189
|
+
echo "[WORKER-$$] Evaluation timeout: ${timeout_seconds}s"
|
|
190
|
+
if eval_output=$(timeout "$timeout_seconds" "$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$output_file" 2>&1); then
|
|
191
|
+
eval_exit_code=0
|
|
192
|
+
else
|
|
193
|
+
eval_exit_code=$?
|
|
194
|
+
if [[ $eval_exit_code -eq 124 ]]; then
|
|
195
|
+
echo "[ERROR] Evaluation timed out" >&2
|
|
196
|
+
update_csv_row_with_lock "$candidate_id" "status" "timeout"
|
|
197
|
+
exit 1
|
|
198
|
+
fi
|
|
199
|
+
fi
|
|
200
|
+
else
|
|
201
|
+
if eval_output=$("$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$output_file" 2>&1); then
|
|
202
|
+
eval_exit_code=0
|
|
203
|
+
else
|
|
204
|
+
eval_exit_code=$?
|
|
205
|
+
fi
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Log evaluator output
|
|
209
|
+
{
|
|
210
|
+
echo "=== WORKER $$ - EVALUATOR OUTPUT ==="
|
|
211
|
+
echo "Exit code: $eval_exit_code"
|
|
212
|
+
echo "$eval_output"
|
|
213
|
+
echo
|
|
214
|
+
} >> "$LOGFILE"
|
|
215
|
+
|
|
216
|
+
# Process results
|
|
217
|
+
if [[ $eval_exit_code -eq 0 ]]; then
|
|
218
|
+
# Extract score
|
|
219
|
+
if score=$(echo "$eval_output" | grep -o '"score"[[:space:]]*:[[:space:]]*[0-9.]*' | cut -d: -f2 | tr -d ' '); then
|
|
220
|
+
if [[ -n $score ]]; then
|
|
221
|
+
if (( $(echo "$score == 0" | bc -l) )); then
|
|
222
|
+
update_csv_row_with_lock "$candidate_id" "status" "failed"
|
|
223
|
+
update_csv_row_with_lock "$candidate_id" "performance" "$score"
|
|
224
|
+
echo "[WORKER-$$] ✗ Evaluation failed with score 0"
|
|
225
|
+
exit 1
|
|
226
|
+
else
|
|
227
|
+
update_csv_row_with_lock "$candidate_id" "performance" "$score"
|
|
228
|
+
update_csv_row_with_lock "$candidate_id" "status" "complete"
|
|
229
|
+
echo "[WORKER-$$] ✓ Evaluation complete, score: $score"
|
|
230
|
+
exit 0
|
|
231
|
+
fi
|
|
232
|
+
fi
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# Try "performance" field
|
|
236
|
+
if score=$(echo "$eval_output" | grep -o '"performance"[[:space:]]*:[[:space:]]*[0-9.]*' | cut -d: -f2 | tr -d ' '); then
|
|
237
|
+
if [[ -n $score ]]; then
|
|
238
|
+
if (( $(echo "$score == 0" | bc -l) )); then
|
|
239
|
+
update_csv_row_with_lock "$candidate_id" "status" "failed"
|
|
240
|
+
update_csv_row_with_lock "$candidate_id" "performance" "$score"
|
|
241
|
+
echo "[WORKER-$$] ✗ Evaluation failed with score 0"
|
|
242
|
+
exit 1
|
|
243
|
+
else
|
|
244
|
+
update_csv_row_with_lock "$candidate_id" "performance" "$score"
|
|
245
|
+
update_csv_row_with_lock "$candidate_id" "status" "complete"
|
|
246
|
+
echo "[WORKER-$$] ✓ Evaluation complete, score: $score"
|
|
247
|
+
exit 0
|
|
248
|
+
fi
|
|
249
|
+
fi
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
echo "[ERROR] No score found in evaluator output" >&2
|
|
253
|
+
update_csv_row_with_lock "$candidate_id" "status" "failed"
|
|
254
|
+
exit 1
|
|
255
|
+
else
|
|
256
|
+
echo "[ERROR] Evaluator failed with exit code $eval_exit_code" >&2
|
|
257
|
+
update_csv_row_with_lock "$candidate_id" "status" "failed"
|
|
258
|
+
exit 1
|
|
259
|
+
fi
|
package/lib/config.sh
CHANGED
|
@@ -19,6 +19,11 @@ DEFAULT_STRUCTURAL_MUTATION=3
|
|
|
19
19
|
DEFAULT_CROSSOVER_HYBRID=4
|
|
20
20
|
DEFAULT_NUM_ELITES=3
|
|
21
21
|
|
|
22
|
+
# Default parallel execution values
|
|
23
|
+
DEFAULT_PARALLEL_ENABLED=false
|
|
24
|
+
DEFAULT_MAX_WORKERS=4
|
|
25
|
+
DEFAULT_LOCK_TIMEOUT=30
|
|
26
|
+
|
|
22
27
|
# Load configuration from config file
|
|
23
28
|
load_config() {
|
|
24
29
|
# Set defaults first
|
|
@@ -38,6 +43,11 @@ load_config() {
|
|
|
38
43
|
STRUCTURAL_MUTATION="$DEFAULT_STRUCTURAL_MUTATION"
|
|
39
44
|
CROSSOVER_HYBRID="$DEFAULT_CROSSOVER_HYBRID"
|
|
40
45
|
NUM_ELITES="$DEFAULT_NUM_ELITES"
|
|
46
|
+
|
|
47
|
+
# Set parallel execution defaults
|
|
48
|
+
PARALLEL_ENABLED="$DEFAULT_PARALLEL_ENABLED"
|
|
49
|
+
MAX_WORKERS="$DEFAULT_MAX_WORKERS"
|
|
50
|
+
LOCK_TIMEOUT="$DEFAULT_LOCK_TIMEOUT"
|
|
41
51
|
|
|
42
52
|
# Single config file location: evolution/config.yaml
|
|
43
53
|
local config_file="evolution/config.yaml"
|
|
@@ -47,6 +57,7 @@ load_config() {
|
|
|
47
57
|
echo "[INFO] Loading configuration from: $config_file"
|
|
48
58
|
# Simple YAML parsing for key: value pairs and nested structures
|
|
49
59
|
local in_ideation_section=false
|
|
60
|
+
local in_parallel_section=false
|
|
50
61
|
while IFS=': ' read -r key value; do
|
|
51
62
|
# Skip comments and empty lines
|
|
52
63
|
[[ $key =~ ^[[:space:]]*# ]] || [[ -z $key ]] && continue
|
|
@@ -58,13 +69,19 @@ load_config() {
|
|
|
58
69
|
# Remove quotes from value
|
|
59
70
|
value=$(echo "$value" | sed 's/^"//;s/"$//')
|
|
60
71
|
|
|
61
|
-
# Handle nested
|
|
72
|
+
# Handle nested sections
|
|
62
73
|
if [[ $key == "ideation_strategies" ]]; then
|
|
63
74
|
in_ideation_section=true
|
|
75
|
+
in_parallel_section=false
|
|
76
|
+
continue
|
|
77
|
+
elif [[ $key == "parallel" ]]; then
|
|
78
|
+
in_parallel_section=true
|
|
79
|
+
in_ideation_section=false
|
|
64
80
|
continue
|
|
65
|
-
elif [[ $key =~ ^[a-z_]+ ]] && [[ $in_ideation_section == true ]]; then
|
|
66
|
-
# Top-level key found, exit
|
|
81
|
+
elif [[ $key =~ ^[a-z_]+ ]] && [[ $in_ideation_section == true || $in_parallel_section == true ]]; then
|
|
82
|
+
# Top-level key found, exit nested sections
|
|
67
83
|
in_ideation_section=false
|
|
84
|
+
in_parallel_section=false
|
|
68
85
|
fi
|
|
69
86
|
|
|
70
87
|
if [[ $in_ideation_section == true ]]; then
|
|
@@ -77,6 +94,13 @@ load_config() {
|
|
|
77
94
|
crossover_hybrid) CROSSOVER_HYBRID="$value" ;;
|
|
78
95
|
num_elites) NUM_ELITES="$value" ;;
|
|
79
96
|
esac
|
|
97
|
+
elif [[ $in_parallel_section == true ]]; then
|
|
98
|
+
# Handle indented keys in parallel section
|
|
99
|
+
case $key in
|
|
100
|
+
enabled) PARALLEL_ENABLED="$value" ;;
|
|
101
|
+
max_workers) MAX_WORKERS="$value" ;;
|
|
102
|
+
lock_timeout) LOCK_TIMEOUT="$value" ;;
|
|
103
|
+
esac
|
|
80
104
|
else
|
|
81
105
|
# Handle top-level keys
|
|
82
106
|
case $key in
|
|
@@ -149,6 +173,8 @@ show_config() {
|
|
|
149
173
|
echo " CSV file: $FULL_CSV_PATH"
|
|
150
174
|
echo " Output directory: $FULL_OUTPUT_DIR"
|
|
151
175
|
echo " Parent selection: $PARENT_SELECTION"
|
|
152
|
-
echo " Max ideas: $MAX_IDEAS"
|
|
153
176
|
echo " Python command: $PYTHON_CMD"
|
|
177
|
+
echo " Parallel enabled: $PARALLEL_ENABLED"
|
|
178
|
+
echo " Max workers: $MAX_WORKERS"
|
|
179
|
+
echo " Lock timeout: $LOCK_TIMEOUT"
|
|
154
180
|
}
|
package/lib/csv-lock.sh
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CSV locking functions for parallel execution
|
|
3
|
+
|
|
4
|
+
# Lock file location
|
|
5
|
+
CSV_LOCKFILE="${EVOLUTION_DIR:-evolution}/.evolution.csv.lock"
|
|
6
|
+
|
|
7
|
+
# Acquire exclusive lock on CSV file
|
|
8
|
+
# Usage: acquire_csv_lock [timeout_seconds]
|
|
9
|
+
acquire_csv_lock() {
|
|
10
|
+
local timeout="${1:-30}"
|
|
11
|
+
local lockdir="$(dirname "$CSV_LOCKFILE")"
|
|
12
|
+
|
|
13
|
+
# Ensure lock directory exists
|
|
14
|
+
mkdir -p "$lockdir"
|
|
15
|
+
|
|
16
|
+
# Try to acquire lock with timeout
|
|
17
|
+
if command -v flock >/dev/null 2>&1; then
|
|
18
|
+
# Use flock if available (Linux)
|
|
19
|
+
exec 200>"$CSV_LOCKFILE"
|
|
20
|
+
if ! flock -w "$timeout" -x 200; then
|
|
21
|
+
echo "ERROR: Failed to acquire CSV lock within $timeout seconds" >&2
|
|
22
|
+
return 1
|
|
23
|
+
fi
|
|
24
|
+
else
|
|
25
|
+
# Fallback for systems without flock (macOS)
|
|
26
|
+
local start_time=$(date +%s)
|
|
27
|
+
while ! (set -C; echo $$ > "$CSV_LOCKFILE") 2>/dev/null; do
|
|
28
|
+
local current_time=$(date +%s)
|
|
29
|
+
if [ $((current_time - start_time)) -ge $timeout ]; then
|
|
30
|
+
echo "ERROR: Failed to acquire CSV lock within $timeout seconds" >&2
|
|
31
|
+
return 1
|
|
32
|
+
fi
|
|
33
|
+
sleep 0.1
|
|
34
|
+
done
|
|
35
|
+
fi
|
|
36
|
+
return 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Release CSV lock
|
|
40
|
+
release_csv_lock() {
|
|
41
|
+
if command -v flock >/dev/null 2>&1; then
|
|
42
|
+
# Release flock
|
|
43
|
+
exec 200>&-
|
|
44
|
+
else
|
|
45
|
+
# Remove lock file
|
|
46
|
+
rm -f "$CSV_LOCKFILE"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Read CSV with lock
|
|
51
|
+
# Usage: read_csv_with_lock <variable_name>
|
|
52
|
+
read_csv_with_lock() {
|
|
53
|
+
local var_name="$1"
|
|
54
|
+
local csv_file="${EVOLUTION_DIR:-evolution}/evolution.csv"
|
|
55
|
+
|
|
56
|
+
if ! acquire_csv_lock; then
|
|
57
|
+
return 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Read CSV content
|
|
61
|
+
if [ -f "$csv_file" ]; then
|
|
62
|
+
eval "$var_name=\$(cat '$csv_file')"
|
|
63
|
+
else
|
|
64
|
+
eval "$var_name=''"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
release_csv_lock
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Write CSV with lock
|
|
72
|
+
# Usage: echo "content" | write_csv_with_lock
|
|
73
|
+
write_csv_with_lock() {
|
|
74
|
+
local csv_file="${EVOLUTION_DIR:-evolution}/evolution.csv"
|
|
75
|
+
local temp_file="${csv_file}.tmp.$$"
|
|
76
|
+
|
|
77
|
+
if ! acquire_csv_lock; then
|
|
78
|
+
return 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Write to temporary file first
|
|
82
|
+
cat > "$temp_file"
|
|
83
|
+
|
|
84
|
+
# Atomic move
|
|
85
|
+
mv -f "$temp_file" "$csv_file"
|
|
86
|
+
|
|
87
|
+
release_csv_lock
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Update single CSV row with lock
|
|
92
|
+
# Usage: update_csv_row_with_lock <id> <field> <value>
|
|
93
|
+
update_csv_row_with_lock() {
|
|
94
|
+
local target_id="$1"
|
|
95
|
+
local field="$2"
|
|
96
|
+
local value="$3"
|
|
97
|
+
local csv_file="${EVOLUTION_DIR:-evolution}/evolution.csv"
|
|
98
|
+
|
|
99
|
+
if ! acquire_csv_lock; then
|
|
100
|
+
return 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Determine field position
|
|
104
|
+
local field_pos
|
|
105
|
+
case "$field" in
|
|
106
|
+
"status") field_pos=5 ;;
|
|
107
|
+
"performance") field_pos=4 ;;
|
|
108
|
+
"description") field_pos=3 ;;
|
|
109
|
+
"basedOnId") field_pos=2 ;;
|
|
110
|
+
*)
|
|
111
|
+
echo "ERROR: Unknown field: $field" >&2
|
|
112
|
+
release_csv_lock
|
|
113
|
+
return 1
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
|
|
117
|
+
# Update CSV using awk
|
|
118
|
+
awk -F',' -v OFS=',' -v id="$target_id" -v pos="$field_pos" -v val="$value" '
|
|
119
|
+
NR==1 || $1 != id { print }
|
|
120
|
+
$1 == id { $pos = val; print }
|
|
121
|
+
' "$csv_file" > "${csv_file}.tmp" && mv -f "${csv_file}.tmp" "$csv_file"
|
|
122
|
+
|
|
123
|
+
release_csv_lock
|
|
124
|
+
return 0
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Find next pending candidate with lock
|
|
128
|
+
# Usage: next_pending=$(find_next_pending_with_lock)
|
|
129
|
+
find_next_pending_with_lock() {
|
|
130
|
+
local csv_file="${EVOLUTION_DIR:-evolution}/evolution.csv"
|
|
131
|
+
|
|
132
|
+
if ! acquire_csv_lock; then
|
|
133
|
+
return 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Find oldest pending candidate and update to running
|
|
137
|
+
local candidate=$(awk -F',' '
|
|
138
|
+
NR>1 && $5 == "pending" { print $1; exit }
|
|
139
|
+
' "$csv_file")
|
|
140
|
+
|
|
141
|
+
if [ -n "$candidate" ]; then
|
|
142
|
+
# Update status to running while we have the lock
|
|
143
|
+
awk -F',' -v OFS=',' -v id="$candidate" '
|
|
144
|
+
NR==1 || $1 != id { print }
|
|
145
|
+
$1 == id { $5 = "running"; print }
|
|
146
|
+
' "$csv_file" > "${csv_file}.tmp" && mv -f "${csv_file}.tmp" "$csv_file"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
release_csv_lock
|
|
150
|
+
echo "$candidate"
|
|
151
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-evolve",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"bin": {
|
|
5
5
|
"claude-evolve": "./bin/claude-evolve",
|
|
6
6
|
"claude-evolve-main": "./bin/claude-evolve-main",
|
|
7
7
|
"claude-evolve-setup": "./bin/claude-evolve-setup",
|
|
8
8
|
"claude-evolve-ideate": "./bin/claude-evolve-ideate",
|
|
9
9
|
"claude-evolve-run": "./bin/claude-evolve-run",
|
|
10
|
+
"claude-evolve-run-parallel": "./bin/claude-evolve-run-parallel",
|
|
11
|
+
"claude-evolve-worker": "./bin/claude-evolve-worker",
|
|
10
12
|
"claude-evolve-analyze": "./bin/claude-evolve-analyze",
|
|
11
13
|
"claude-evolve-config": "./bin/claude-evolve-config"
|
|
12
14
|
},
|
package/templates/config.yaml
CHANGED
|
@@ -35,4 +35,15 @@ ideation_strategies:
|
|
|
35
35
|
num_elites: 3
|
|
36
36
|
|
|
37
37
|
# Python command to use for evaluation
|
|
38
|
-
python_cmd: "python3"
|
|
38
|
+
python_cmd: "python3"
|
|
39
|
+
|
|
40
|
+
# Parallel execution configuration
|
|
41
|
+
parallel:
|
|
42
|
+
# Enable parallel execution of evolution candidates
|
|
43
|
+
enabled: false
|
|
44
|
+
|
|
45
|
+
# Maximum number of worker processes to run simultaneously
|
|
46
|
+
max_workers: 4
|
|
47
|
+
|
|
48
|
+
# Timeout in seconds when waiting for CSV locks
|
|
49
|
+
lock_timeout: 30
|