claude-evolve 1.3.37 → 1.3.38
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-analyze +67 -22
- package/bin/claude-evolve-ideate +41 -14
- package/bin/claude-evolve-main +2 -2
- package/bin/{claude-evolve-run-parallel → claude-evolve-run-parallel.OLD} +32 -5
- package/bin/claude-evolve-run-unified +342 -0
- package/bin/{claude-evolve-run → claude-evolve-run.OLD} +21 -15
- package/bin/claude-evolve-worker +10 -8
- package/lib/config.sh +1 -1
- package/lib/csv-lock.sh +80 -21
- package/lib/evolution_processor.py +77 -0
- package/package.json +1 -1
|
@@ -165,7 +165,7 @@ echo "Pending: $pending"
|
|
|
165
165
|
|
|
166
166
|
if [[ $count_with_performance -gt 0 ]]; then
|
|
167
167
|
avg_performance=$(echo "scale=4; $total_performance / $count_with_performance" | bc -l 2>/dev/null || echo "0")
|
|
168
|
-
echo "Average Performance: $avg_performance"
|
|
168
|
+
echo "Average Performance: $avg_performance" # Still showing mean for overall stats
|
|
169
169
|
else
|
|
170
170
|
echo "Average Performance: N/A"
|
|
171
171
|
fi
|
|
@@ -238,13 +238,24 @@ for gen in $(cut -d' ' -f1 "$gen_stats_file" | sort -u || echo ""); do
|
|
|
238
238
|
echo -n "$gen: $total_in_gen candidates"
|
|
239
239
|
|
|
240
240
|
if [[ "$completed_in_gen" -gt 0 ]]; then
|
|
241
|
-
# Calculate
|
|
242
|
-
|
|
241
|
+
# Calculate median performance for this generation
|
|
242
|
+
# AIDEV-NOTE: Changed from mean to median to be more robust to outliers
|
|
243
|
+
median="0"
|
|
243
244
|
if grep -q "^$gen completed" "$gen_stats_file"; then
|
|
244
|
-
|
|
245
|
+
median=$(grep "^$gen completed" "$gen_stats_file" | awk '{print $3}' | sort -n | awk '{
|
|
246
|
+
a[NR] = $0
|
|
247
|
+
}
|
|
248
|
+
END {
|
|
249
|
+
if (NR % 2) {
|
|
250
|
+
# Odd number of elements
|
|
251
|
+
print a[(NR + 1) / 2]
|
|
252
|
+
} else {
|
|
253
|
+
# Even number of elements - average of two middle values
|
|
254
|
+
printf "%.4f", (a[NR/2] + a[NR/2 + 1]) / 2.0
|
|
255
|
+
}
|
|
256
|
+
}' 2>/dev/null || echo "0")
|
|
245
257
|
fi
|
|
246
|
-
|
|
247
|
-
echo " ($completed_in_gen completed, avg: $avg)"
|
|
258
|
+
echo " ($completed_in_gen completed, median: $median)"
|
|
248
259
|
else
|
|
249
260
|
echo " (0 completed)"
|
|
250
261
|
fi
|
|
@@ -290,7 +301,7 @@ if command -v gnuplot >/dev/null 2>&1 && [[ $valid_performance_count -gt 0 ]]; t
|
|
|
290
301
|
gen_avg_file="/tmp/evolution_gen_avg_$$.dat"
|
|
291
302
|
|
|
292
303
|
echo "# Row ID Performance Generation" >"$data_file"
|
|
293
|
-
echo "# Generation
|
|
304
|
+
echo "# Generation MedianPerformance Color" >"$gen_avg_file"
|
|
294
305
|
|
|
295
306
|
# Get color by generation number (rotates through 7 colors)
|
|
296
307
|
get_gen_color() {
|
|
@@ -417,18 +428,31 @@ print(f'max_id=\"{max_id}\"')
|
|
|
417
428
|
max_gen_num=0
|
|
418
429
|
for gen in $(cut -d' ' -f1 "$gen_data_temp" | sort -u); do
|
|
419
430
|
if grep -q "^$gen " "$gen_data_temp"; then
|
|
420
|
-
# Calculate
|
|
421
|
-
|
|
431
|
+
# Calculate median for this generation
|
|
432
|
+
# AIDEV-NOTE: Changed from mean to median to be more robust to outliers
|
|
433
|
+
# Extract all performance values for this generation and sort them
|
|
434
|
+
median=$(grep "^$gen " "$gen_data_temp" | awk '{print $2}' | sort -n | awk '{
|
|
435
|
+
a[NR] = $0
|
|
436
|
+
}
|
|
437
|
+
END {
|
|
438
|
+
if (NR % 2) {
|
|
439
|
+
# Odd number of elements
|
|
440
|
+
print a[(NR + 1) / 2]
|
|
441
|
+
} else {
|
|
442
|
+
# Even number of elements - average of two middle values
|
|
443
|
+
print (a[NR/2] + a[NR/2 + 1]) / 2.0
|
|
444
|
+
}
|
|
445
|
+
}' 2>/dev/null || echo "0")
|
|
422
446
|
count=$(grep -c "^$gen " "$gen_data_temp")
|
|
423
447
|
if [[ $count -gt 0 ]]; then
|
|
424
|
-
avg=$
|
|
448
|
+
avg=$median # Using median instead of mean
|
|
425
449
|
gen_num=$(echo "$gen" | sed 's/gen0*//')
|
|
426
450
|
# Track max generation number
|
|
427
451
|
if [[ $gen_num =~ ^[0-9]+$ ]] && [[ $gen_num -gt $max_gen_num ]]; then
|
|
428
452
|
max_gen_num=$gen_num
|
|
429
453
|
fi
|
|
430
454
|
color=$(get_gen_color "$gen_num")
|
|
431
|
-
echo "$gen_index \"Gen$gen_num\" $avg \"$color\"" >>"$gen_avg_file"
|
|
455
|
+
echo "$gen_index \"Gen$gen_num\" $avg \"$color\"" >>"$gen_avg_file" # avg is now median
|
|
432
456
|
((gen_index++))
|
|
433
457
|
fi
|
|
434
458
|
fi
|
|
@@ -449,6 +473,19 @@ print(f'max_id=\"{max_id}\"')
|
|
|
449
473
|
# cat "$data_file"
|
|
450
474
|
# echo "DEBUG: max_gen_num=$max_gen_num"
|
|
451
475
|
|
|
476
|
+
# Calculate total data points for dynamic sizing
|
|
477
|
+
total_data_points=$(awk 'END {print NR-1}' "$data_file") # Subtract header row
|
|
478
|
+
|
|
479
|
+
# AIDEV-NOTE: Dynamic dot sizing based on data point count
|
|
480
|
+
# Use significantly larger dots when there are fewer data points for better visibility
|
|
481
|
+
if [[ $total_data_points -lt 35 ]]; then
|
|
482
|
+
regular_dot_size="1.8"
|
|
483
|
+
winner_dot_size="3.0"
|
|
484
|
+
else
|
|
485
|
+
regular_dot_size="0.6"
|
|
486
|
+
winner_dot_size="1.5"
|
|
487
|
+
fi
|
|
488
|
+
|
|
452
489
|
# Plot all algorithms in order of completion, colored by generation
|
|
453
490
|
plot_cmd=""
|
|
454
491
|
gen_plots_added=0
|
|
@@ -462,7 +499,7 @@ print(f'max_id=\"{max_id}\"')
|
|
|
462
499
|
if [[ $gen_plots_added -gt 0 ]]; then
|
|
463
500
|
plot_cmd="$plot_cmd, \\"$'\n'
|
|
464
501
|
fi
|
|
465
|
-
plot_cmd="${plot_cmd} \"$data_file\" using (\$4==$gen_num?\$1:1/0):3 with
|
|
502
|
+
plot_cmd="${plot_cmd} \"$data_file\" using (\$4==$gen_num?\$1:1/0):3 with points linecolor rgb \"$color\" pointsize $regular_dot_size title \"Gen $gen_num\""
|
|
466
503
|
((gen_plots_added++))
|
|
467
504
|
fi
|
|
468
505
|
done
|
|
@@ -472,15 +509,15 @@ print(f'max_id=\"{max_id}\"')
|
|
|
472
509
|
if [[ $gen_plots_added -gt 0 ]]; then
|
|
473
510
|
plot_cmd="$plot_cmd, \\"$'\n'
|
|
474
511
|
fi
|
|
475
|
-
plot_cmd="${plot_cmd} \"$winner_file\" using 1:3 with points pointtype 7 pointsize
|
|
512
|
+
plot_cmd="${plot_cmd} \"$winner_file\" using 1:3 with points pointtype 7 pointsize $winner_dot_size linecolor rgb \"gold\" title \"Best ($max_id)\""
|
|
476
513
|
fi
|
|
477
514
|
|
|
478
515
|
# Fallback if no generation-specific plots
|
|
479
516
|
if [[ $gen_plots_added -eq 0 ]]; then
|
|
480
|
-
plot_cmd="\"$data_file\" using 1:3 with
|
|
517
|
+
plot_cmd="\"$data_file\" using 1:3 with points linecolor rgb \"#1f77b4\" pointsize $regular_dot_size title \"Evolution Progress\""
|
|
481
518
|
if [[ -n $max_id && -s "$winner_file" ]]; then
|
|
482
519
|
plot_cmd="$plot_cmd, \\"$'\n'
|
|
483
|
-
plot_cmd="${plot_cmd} \"$winner_file\" using 1:3 with points pointtype 7 pointsize
|
|
520
|
+
plot_cmd="${plot_cmd} \"$winner_file\" using 1:3 with points pointtype 7 pointsize $winner_dot_size linecolor rgb \"gold\" title \"Best ($max_id)\""
|
|
484
521
|
fi
|
|
485
522
|
fi
|
|
486
523
|
|
|
@@ -506,21 +543,25 @@ set output "$output_file"
|
|
|
506
543
|
set multiplot layout 2,1 margins 0.08,0.82,0.15,0.95 spacing 0.1,0.15
|
|
507
544
|
|
|
508
545
|
#=================== TOP PLOT: Performance Over Time ===================
|
|
546
|
+
# AIDEV-NOTE: Removed x-axis to eliminate tick overlap and formatting issues
|
|
509
547
|
set title "Algorithm Evolution Performance Over Time" font ",14"
|
|
510
|
-
|
|
548
|
+
unset xlabel
|
|
511
549
|
set ylabel "Performance Score"
|
|
512
|
-
set grid
|
|
550
|
+
set grid y # Only show horizontal grid lines
|
|
513
551
|
set key outside right
|
|
514
|
-
|
|
515
|
-
|
|
552
|
+
|
|
553
|
+
# AIDEV-NOTE: Remove x-axis entirely to avoid tick problems with large datasets
|
|
554
|
+
unset xtics
|
|
555
|
+
set autoscale
|
|
556
|
+
set yrange [*:*] # Auto-scale y-axis only
|
|
516
557
|
|
|
517
558
|
# Define colors for generations
|
|
518
559
|
plot $plot_cmd
|
|
519
560
|
|
|
520
|
-
#=================== BOTTOM PLOT: Generation
|
|
521
|
-
set title "
|
|
561
|
+
#=================== BOTTOM PLOT: Generation Medians ===================
|
|
562
|
+
set title "Median Performance by Generation" font ",14"
|
|
522
563
|
set xlabel "Generation"
|
|
523
|
-
set ylabel "
|
|
564
|
+
set ylabel "Median Performance"
|
|
524
565
|
set style fill solid 0.8
|
|
525
566
|
set boxwidth 0.6
|
|
526
567
|
unset key
|
|
@@ -529,6 +570,10 @@ set grid y
|
|
|
529
570
|
# Set custom x-axis labels
|
|
530
571
|
set xtics ($xtics_labels)
|
|
531
572
|
|
|
573
|
+
# Auto-scale for generation plot too
|
|
574
|
+
set autoscale
|
|
575
|
+
set yrange [*:*]
|
|
576
|
+
|
|
532
577
|
plot "$gen_avg_file" using 1:3 with boxes linecolor rgb "#4CAF50" notitle
|
|
533
578
|
|
|
534
579
|
unset multiplot
|
package/bin/claude-evolve-ideate
CHANGED
|
@@ -14,14 +14,40 @@ else
|
|
|
14
14
|
load_config
|
|
15
15
|
fi
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# Function to determine which model to use based on generation
|
|
18
|
+
get_model_for_generation() {
|
|
19
|
+
local generation="$1"
|
|
20
|
+
local gen_num
|
|
21
|
+
|
|
22
|
+
# Extract numeric part of generation (e.g., "05" from gen05)
|
|
23
|
+
if [[ $generation =~ ^0*([0-9]+)$ ]]; then
|
|
24
|
+
gen_num=$((10#${BASH_REMATCH[1]}))
|
|
25
|
+
else
|
|
26
|
+
gen_num=1 # Default for malformed input
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Alternate between models by generation
|
|
30
|
+
if (( gen_num % 2 == 1 )); then
|
|
31
|
+
echo "opus" # Odd generations: use Opus for exploration
|
|
32
|
+
else
|
|
33
|
+
echo "o3-pro" # Even generations: use o3-pro for refinement
|
|
34
|
+
fi
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Helper function to call AI model (alternating based on generation)
|
|
18
38
|
call_ai_with_limit_check() {
|
|
19
39
|
local prompt="$1"
|
|
20
|
-
local
|
|
40
|
+
local generation="${2:-01}" # Default to generation 01 if not provided
|
|
41
|
+
|
|
42
|
+
# Determine which model to use for this generation
|
|
43
|
+
local preferred_model
|
|
44
|
+
preferred_model=$(get_model_for_generation "$generation")
|
|
45
|
+
|
|
46
|
+
echo "[INFO] Generation $generation: Using $preferred_model" >&2
|
|
21
47
|
|
|
22
|
-
#
|
|
23
|
-
if command -v codex >/dev/null 2>&1; then
|
|
24
|
-
echo "[INFO] Using codex o3-pro for ideation
|
|
48
|
+
# Try preferred model first
|
|
49
|
+
if [[ "$preferred_model" == "o3-pro" ]] && command -v codex >/dev/null 2>&1; then
|
|
50
|
+
echo "[INFO] Using codex o3-pro for ideation" >&2
|
|
25
51
|
|
|
26
52
|
# Call codex with o3-pro model using -q flag and --full-auto
|
|
27
53
|
local ai_output
|
|
@@ -32,16 +58,17 @@ call_ai_with_limit_check() {
|
|
|
32
58
|
echo "$ai_output"
|
|
33
59
|
return 0
|
|
34
60
|
else
|
|
35
|
-
echo "[WARN] Codex failed, falling back to Claude" >&2
|
|
61
|
+
echo "[WARN] Codex o3-pro failed, falling back to Claude Opus" >&2
|
|
62
|
+
preferred_model="opus"
|
|
36
63
|
fi
|
|
37
64
|
fi
|
|
38
65
|
|
|
39
|
-
#
|
|
40
|
-
echo "[INFO] Using Claude $
|
|
66
|
+
# Use Claude with preferred model (or fallback)
|
|
67
|
+
echo "[INFO] Using Claude $preferred_model for ideation" >&2
|
|
41
68
|
|
|
42
69
|
# Call Claude and capture output
|
|
43
70
|
local claude_output
|
|
44
|
-
claude_output=$(echo "$prompt" | claude --dangerously-skip-permissions --model "$
|
|
71
|
+
claude_output=$(echo "$prompt" | claude --dangerously-skip-permissions --model "$preferred_model" -p 2>&1)
|
|
45
72
|
local claude_exit_code=$?
|
|
46
73
|
|
|
47
74
|
# Check for usage limit
|
|
@@ -309,7 +336,7 @@ Example descriptions:
|
|
|
309
336
|
Add exactly $count rows to the CSV file now."
|
|
310
337
|
|
|
311
338
|
echo "[INFO] Generating $count novel exploration ideas..."
|
|
312
|
-
if ! call_ai_with_limit_check "$prompt" "
|
|
339
|
+
if ! call_ai_with_limit_check "$prompt" "$CURRENT_GENERATION"; then
|
|
313
340
|
echo "[WARN] AI failed to generate novel ideas" >&2
|
|
314
341
|
return 1
|
|
315
342
|
fi
|
|
@@ -372,7 +399,7 @@ Example descriptions:
|
|
|
372
399
|
Add exactly $count parameter tuning rows to the CSV file now."
|
|
373
400
|
|
|
374
401
|
echo "[INFO] Generating $count hill climbing ideas..."
|
|
375
|
-
if ! call_ai_with_limit_check "$prompt" "
|
|
402
|
+
if ! call_ai_with_limit_check "$prompt" "$CURRENT_GENERATION"; then
|
|
376
403
|
echo "[WARN] AI failed to generate hill climbing ideas" >&2
|
|
377
404
|
return 1
|
|
378
405
|
fi
|
|
@@ -435,7 +462,7 @@ Example descriptions:
|
|
|
435
462
|
Add exactly $count structural modification rows to the CSV file now."
|
|
436
463
|
|
|
437
464
|
echo "[INFO] Generating $count structural mutation ideas..."
|
|
438
|
-
if ! call_ai_with_limit_check "$prompt" "
|
|
465
|
+
if ! call_ai_with_limit_check "$prompt" "$CURRENT_GENERATION"; then
|
|
439
466
|
echo "[WARN] AI failed to generate structural mutation ideas" >&2
|
|
440
467
|
return 1
|
|
441
468
|
fi
|
|
@@ -498,7 +525,7 @@ Example descriptions:
|
|
|
498
525
|
Add exactly $count hybrid combination rows to the CSV file now."
|
|
499
526
|
|
|
500
527
|
echo "[INFO] Generating $count crossover hybrid ideas..."
|
|
501
|
-
if ! call_ai_with_limit_check "$prompt" "
|
|
528
|
+
if ! call_ai_with_limit_check "$prompt" "$CURRENT_GENERATION"; then
|
|
502
529
|
echo "[WARN] AI failed to generate crossover ideas" >&2
|
|
503
530
|
return 1
|
|
504
531
|
fi
|
|
@@ -567,7 +594,7 @@ CRITICAL CSV FORMAT RULES:
|
|
|
567
594
|
Add exactly $TOTAL_IDEAS algorithm variation rows to the CSV file now."
|
|
568
595
|
|
|
569
596
|
echo "[INFO] Generating $TOTAL_IDEAS ideas (legacy mode)..."
|
|
570
|
-
if ! call_ai_with_limit_check "$prompt" "
|
|
597
|
+
if ! call_ai_with_limit_check "$prompt" "$CURRENT_GENERATION"; then
|
|
571
598
|
echo "[WARN] AI failed to generate ideas" >&2
|
|
572
599
|
return 1
|
|
573
600
|
fi
|
package/bin/claude-evolve-main
CHANGED
|
@@ -142,7 +142,7 @@ if [[ $# -eq 0 ]]; then
|
|
|
142
142
|
case $choice in
|
|
143
143
|
1) exec "$SCRIPT_DIR/claude-evolve-setup" ;;
|
|
144
144
|
2) exec "$SCRIPT_DIR/claude-evolve-ideate" ;;
|
|
145
|
-
3) exec "$SCRIPT_DIR/claude-evolve-run" ;;
|
|
145
|
+
3) exec "$SCRIPT_DIR/claude-evolve-run-unified" ;;
|
|
146
146
|
4) exec "$SCRIPT_DIR/claude-evolve-analyze" ;;
|
|
147
147
|
5) exec "$SCRIPT_DIR/claude-evolve-config" ;;
|
|
148
148
|
6) show_help ;;
|
|
@@ -174,7 +174,7 @@ ideate)
|
|
|
174
174
|
;;
|
|
175
175
|
run)
|
|
176
176
|
shift
|
|
177
|
-
exec "$SCRIPT_DIR/claude-evolve-run" "$@"
|
|
177
|
+
exec "$SCRIPT_DIR/claude-evolve-run-unified" "$@"
|
|
178
178
|
;;
|
|
179
179
|
analyze)
|
|
180
180
|
shift
|
|
@@ -297,12 +297,39 @@ print(count)
|
|
|
297
297
|
echo "[DISPATCHER] CSV has $((total_rows-1)) total candidates, $complete_count complete"
|
|
298
298
|
fi
|
|
299
299
|
|
|
300
|
-
# If no pending work and no active workers,
|
|
300
|
+
# If no pending work and no active workers, check for auto-ideation
|
|
301
301
|
if [[ $pending_count -eq 0 && $active_workers -eq 0 ]]; then
|
|
302
|
-
echo "[DISPATCHER] No pending candidates found.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
echo "[DISPATCHER] No pending candidates found."
|
|
303
|
+
|
|
304
|
+
# Check if auto ideation is enabled
|
|
305
|
+
echo "[DEBUG] AUTO_IDEATE value: '$AUTO_IDEATE'"
|
|
306
|
+
if [[ "$AUTO_IDEATE" == "true" || "$AUTO_IDEATE" == "1" ]]; then
|
|
307
|
+
echo "[DISPATCHER] Auto ideation is enabled. Generating new ideas..."
|
|
308
|
+
|
|
309
|
+
# Check if claude-evolve-ideate exists
|
|
310
|
+
ideate_script="$SCRIPT_DIR/claude-evolve-ideate"
|
|
311
|
+
if [[ ! -f "$ideate_script" ]]; then
|
|
312
|
+
echo "[ERROR] claude-evolve-ideate script not found: $ideate_script" >&2
|
|
313
|
+
echo "[DISPATCHER] Evolution complete - no way to generate more ideas."
|
|
314
|
+
break
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
# Generate new ideas using the multi-strategy approach
|
|
318
|
+
echo "[DISPATCHER] Calling claude-evolve-ideate to generate new candidates..."
|
|
319
|
+
if ! "$ideate_script"; then
|
|
320
|
+
echo "[ERROR] Failed to generate new ideas" >&2
|
|
321
|
+
echo "[DISPATCHER] Evolution complete - ideation failed."
|
|
322
|
+
break
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
echo "[DISPATCHER] New ideas generated successfully. Continuing evolution..."
|
|
326
|
+
continue # Go back to start of loop to find the new candidates
|
|
327
|
+
else
|
|
328
|
+
echo "[DISPATCHER] Auto ideation is disabled. Evolution complete."
|
|
329
|
+
echo "[DISPATCHER] Run 'claude-evolve ideate' to generate more candidates."
|
|
330
|
+
echo "[DISPATCHER] Exiting main loop: no work remaining" >&2
|
|
331
|
+
break
|
|
332
|
+
fi
|
|
306
333
|
fi
|
|
307
334
|
|
|
308
335
|
# Start workers if we have pending work and capacity
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
# Load configuration
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
# shellcheck source=../lib/config.sh
|
|
8
|
+
source "$SCRIPT_DIR/../lib/config.sh"
|
|
9
|
+
|
|
10
|
+
# Use CLAUDE_EVOLVE_CONFIG if set, otherwise default
|
|
11
|
+
if [[ -n ${CLAUDE_EVOLVE_CONFIG:-} ]]; then
|
|
12
|
+
load_config "$CLAUDE_EVOLVE_CONFIG"
|
|
13
|
+
else
|
|
14
|
+
load_config
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Validate configuration
|
|
18
|
+
if ! validate_config; then
|
|
19
|
+
echo "[ERROR] Configuration validation failed" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# AIDEV-NOTE: Unified execution engine that handles both sequential and parallel modes
|
|
24
|
+
# Sequential mode is just parallel mode with max_workers=1
|
|
25
|
+
|
|
26
|
+
# Default values
|
|
27
|
+
timeout_seconds=""
|
|
28
|
+
force_parallel=""
|
|
29
|
+
force_sequential=""
|
|
30
|
+
use_caffeinate="false"
|
|
31
|
+
|
|
32
|
+
# Parse command line arguments
|
|
33
|
+
while [[ $# -gt 0 ]]; do
|
|
34
|
+
case $1 in
|
|
35
|
+
--timeout)
|
|
36
|
+
if [[ -z ${2:-} ]] || [[ ! $2 =~ ^[0-9]+$ ]] || [[ $2 -eq 0 ]]; then
|
|
37
|
+
echo "[ERROR] --timeout requires a positive integer (seconds)" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
timeout_seconds="$2"
|
|
41
|
+
shift 2
|
|
42
|
+
;;
|
|
43
|
+
--parallel)
|
|
44
|
+
force_parallel="true"
|
|
45
|
+
shift
|
|
46
|
+
;;
|
|
47
|
+
--sequential)
|
|
48
|
+
force_sequential="true"
|
|
49
|
+
shift
|
|
50
|
+
;;
|
|
51
|
+
--keep-awake|--caffeinate)
|
|
52
|
+
use_caffeinate="true"
|
|
53
|
+
shift
|
|
54
|
+
;;
|
|
55
|
+
--help)
|
|
56
|
+
cat <<EOF
|
|
57
|
+
claude-evolve run - Execute evolution candidates
|
|
58
|
+
|
|
59
|
+
USAGE:
|
|
60
|
+
claude-evolve run [OPTIONS]
|
|
61
|
+
|
|
62
|
+
OPTIONS:
|
|
63
|
+
--timeout N Timeout in seconds for each evaluation
|
|
64
|
+
--parallel Force parallel execution mode
|
|
65
|
+
--sequential Force sequential execution mode (max_workers=1)
|
|
66
|
+
--keep-awake Keep system awake during execution (macOS only)
|
|
67
|
+
--caffeinate Alias for --keep-awake
|
|
68
|
+
--help Show this help message
|
|
69
|
+
|
|
70
|
+
DESCRIPTION:
|
|
71
|
+
Continuously processes evolution candidates by:
|
|
72
|
+
1. Finding pending candidates in CSV
|
|
73
|
+
2. Calling Claude to mutate algorithms
|
|
74
|
+
3. Updating CSV with performance score and completion status
|
|
75
|
+
4. Auto-generating new ideas when no pending candidates remain
|
|
76
|
+
|
|
77
|
+
Use --timeout to prevent runaway evaluations from blocking progress.
|
|
78
|
+
EOF
|
|
79
|
+
exit 0
|
|
80
|
+
;;
|
|
81
|
+
*)
|
|
82
|
+
echo "[ERROR] Unknown option: $1" >&2
|
|
83
|
+
exit 1
|
|
84
|
+
;;
|
|
85
|
+
esac
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
# Check if caffeinate should be used
|
|
89
|
+
if [[ "$use_caffeinate" == "true" ]] && command -v caffeinate >/dev/null 2>&1; then
|
|
90
|
+
echo "[INFO] Using caffeinate to prevent system sleep"
|
|
91
|
+
# Re-run this script with caffeinate
|
|
92
|
+
exec caffeinate -dims "$0" "$@"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Determine execution mode and worker count
|
|
96
|
+
if [[ "$force_sequential" == "true" ]]; then
|
|
97
|
+
MAX_WORKERS=1
|
|
98
|
+
echo "[INFO] Using sequential mode (forced via --sequential, max_workers=1)"
|
|
99
|
+
elif [[ "$force_parallel" == "true" ]]; then
|
|
100
|
+
echo "[INFO] Using parallel mode (forced via --parallel, max_workers=$MAX_WORKERS)"
|
|
101
|
+
elif [[ "$PARALLEL_ENABLED" == "true" || "$PARALLEL_ENABLED" == "1" ]]; then
|
|
102
|
+
echo "[INFO] Using parallel mode (enabled in config, max_workers=$MAX_WORKERS)"
|
|
103
|
+
else
|
|
104
|
+
MAX_WORKERS=1
|
|
105
|
+
echo "[INFO] Using sequential mode (default, max_workers=1)"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Start unified execution engine
|
|
109
|
+
echo "[INFO] Starting evolution run with up to $MAX_WORKERS workers"
|
|
110
|
+
|
|
111
|
+
# Rest of the logic is the same as claude-evolve-run-parallel but with dynamic MAX_WORKERS
|
|
112
|
+
# and the updated auto-ideation logic we just added
|
|
113
|
+
|
|
114
|
+
# Worker management
|
|
115
|
+
declare -a worker_pids=()
|
|
116
|
+
|
|
117
|
+
# Graceful shutdown function
|
|
118
|
+
shutdown_workers() {
|
|
119
|
+
if [[ ${#worker_pids[@]} -eq 0 ]]; then
|
|
120
|
+
echo "[DISPATCHER] No workers to shutdown"
|
|
121
|
+
return 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
echo "[DISPATCHER] Shutting down workers..."
|
|
125
|
+
for pid in "${worker_pids[@]}"; do
|
|
126
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
127
|
+
echo "[DISPATCHER] Stopping worker $pid"
|
|
128
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
|
|
132
|
+
# Wait for workers to exit
|
|
133
|
+
local timeout=10
|
|
134
|
+
while [[ ${#worker_pids[@]} -gt 0 && $timeout -gt 0 ]]; do
|
|
135
|
+
sleep 1
|
|
136
|
+
((timeout--))
|
|
137
|
+
|
|
138
|
+
local new_pids=()
|
|
139
|
+
for pid in "${worker_pids[@]}"; do
|
|
140
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
141
|
+
new_pids+=($pid)
|
|
142
|
+
fi
|
|
143
|
+
done
|
|
144
|
+
worker_pids=("${new_pids[@]}")
|
|
145
|
+
done
|
|
146
|
+
|
|
147
|
+
# Force kill remaining workers
|
|
148
|
+
for pid in "${worker_pids[@]}"; do
|
|
149
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
150
|
+
echo "[DISPATCHER] Force killing worker $pid"
|
|
151
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
152
|
+
fi
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
echo "[DISPATCHER] Shutdown complete"
|
|
156
|
+
exit 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Signal handling - graceful shutdown with force option
|
|
160
|
+
handle_signal() {
|
|
161
|
+
local signal="$1"
|
|
162
|
+
echo "[DISPATCHER] Received signal: $signal" >&2
|
|
163
|
+
echo "[DISPATCHER] Active workers: ${#worker_pids[@]}" >&2
|
|
164
|
+
|
|
165
|
+
# For expensive workers, give option to force shutdown
|
|
166
|
+
if [[ ${#worker_pids[@]} -gt 0 ]]; then
|
|
167
|
+
echo "[DISPATCHER] Warning: ${#worker_pids[@]} expensive workers are still running!" >&2
|
|
168
|
+
echo "[DISPATCHER] Press Ctrl+C again to force shutdown immediately, or wait for graceful shutdown..." >&2
|
|
169
|
+
|
|
170
|
+
# Give a few seconds for force shutdown option
|
|
171
|
+
local count=3
|
|
172
|
+
while [[ $count -gt 0 && "$force_shutdown_requested" != "true" ]]; do
|
|
173
|
+
sleep 1
|
|
174
|
+
((count--))
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
if [[ "$force_shutdown_requested" == "true" ]]; then
|
|
178
|
+
echo "[DISPATCHER] Force shutdown in progress..." >&2
|
|
179
|
+
return # shutdown_workers already called
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
echo "[DISPATCHER] Proceeding with graceful shutdown..." >&2
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
shutdown_workers
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Track signals for force shutdown
|
|
189
|
+
signal_count=0
|
|
190
|
+
force_shutdown_requested=false
|
|
191
|
+
|
|
192
|
+
# Immediate signal handler for force shutdown detection
|
|
193
|
+
signal_handler() {
|
|
194
|
+
((signal_count++))
|
|
195
|
+
|
|
196
|
+
if [[ $signal_count -eq 1 ]]; then
|
|
197
|
+
# First signal - start graceful shutdown
|
|
198
|
+
handle_signal "SIGINT"
|
|
199
|
+
else
|
|
200
|
+
# Second+ signal - force shutdown immediately
|
|
201
|
+
echo "[DISPATCHER] Force shutdown requested!" >&2
|
|
202
|
+
force_shutdown_requested=true
|
|
203
|
+
shutdown_workers
|
|
204
|
+
fi
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Set up signal handlers
|
|
208
|
+
trap 'signal_handler' INT
|
|
209
|
+
trap 'handle_signal SIGTERM' TERM
|
|
210
|
+
|
|
211
|
+
# Function to start a worker
|
|
212
|
+
start_worker() {
|
|
213
|
+
local worker_script="$SCRIPT_DIR/claude-evolve-worker"
|
|
214
|
+
if [[ ! -f "$worker_script" ]]; then
|
|
215
|
+
echo "[ERROR] Worker script not found: $worker_script" >&2
|
|
216
|
+
exit 1
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
local worker_args=()
|
|
220
|
+
[[ -n $timeout_seconds ]] && worker_args+=(--timeout "$timeout_seconds")
|
|
221
|
+
|
|
222
|
+
echo "[DISPATCHER] Starting worker..."
|
|
223
|
+
"$worker_script" "${worker_args[@]}" &
|
|
224
|
+
local worker_pid=$!
|
|
225
|
+
worker_pids+=($worker_pid)
|
|
226
|
+
echo "[DISPATCHER] Worker started with PID: $worker_pid"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Function to clean up finished workers
|
|
230
|
+
cleanup_workers() {
|
|
231
|
+
local new_pids=()
|
|
232
|
+
for pid in "${worker_pids[@]}"; do
|
|
233
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
234
|
+
new_pids+=($pid)
|
|
235
|
+
else
|
|
236
|
+
# Worker finished
|
|
237
|
+
if wait "$pid" 2>/dev/null; then
|
|
238
|
+
echo "[DISPATCHER] Worker $pid completed successfully"
|
|
239
|
+
else
|
|
240
|
+
local exit_code=$?
|
|
241
|
+
if [[ $exit_code -eq 2 ]]; then
|
|
242
|
+
echo "[DISPATCHER] Worker $pid hit rate limit, will retry later"
|
|
243
|
+
else
|
|
244
|
+
echo "[DISPATCHER] Worker $pid failed with exit code $exit_code"
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
247
|
+
fi
|
|
248
|
+
done
|
|
249
|
+
worker_pids=("${new_pids[@]}")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Function to count pending candidates
|
|
253
|
+
count_pending_candidates() {
|
|
254
|
+
"$PYTHON_CMD" "$SCRIPT_DIR/../lib/csv_helper.py" find_pending "$FULL_CSV_PATH" >/dev/null 2>&1
|
|
255
|
+
echo $? # 0 if found, 1 if not found
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Function to get CSV stats
|
|
259
|
+
get_csv_stats() {
|
|
260
|
+
if [[ ! -f "$FULL_CSV_PATH" ]]; then
|
|
261
|
+
echo "0 0 0"
|
|
262
|
+
return
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
local total_rows complete_count pending_count
|
|
266
|
+
total_rows=$(wc -l < "$FULL_CSV_PATH" | tr -d '[:space:]')
|
|
267
|
+
complete_count=$(grep -c ',complete' "$FULL_CSV_PATH" || echo "0")
|
|
268
|
+
|
|
269
|
+
# Count pending: empty status or "pending"
|
|
270
|
+
# Handle potential Windows line endings by stripping carriage returns
|
|
271
|
+
pending_count=$(awk -F, 'NR>1 {gsub(/\r/, "", $5); if($5=="" || $5=="pending") count++} END {print count+0}' "$FULL_CSV_PATH")
|
|
272
|
+
|
|
273
|
+
echo "$total_rows $complete_count $pending_count"
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
echo "[DISPATCHER] Starting unified evolution engine"
|
|
277
|
+
echo "[DISPATCHER] Configuration: max_workers=$MAX_WORKERS, timeout=${timeout_seconds:-none}"
|
|
278
|
+
|
|
279
|
+
# Main dispatch loop
|
|
280
|
+
while true; do
|
|
281
|
+
# Clean up finished workers
|
|
282
|
+
cleanup_workers
|
|
283
|
+
|
|
284
|
+
# Get current status
|
|
285
|
+
read -r total_rows complete_count pending_count <<< "$(get_csv_stats)"
|
|
286
|
+
active_workers=${#worker_pids[@]}
|
|
287
|
+
|
|
288
|
+
# Status reporting
|
|
289
|
+
if [[ $total_rows -gt 1 ]]; then
|
|
290
|
+
echo "[DISPATCHER] Status: $pending_count pending, $active_workers active workers"
|
|
291
|
+
echo "[DISPATCHER] CSV has $((total_rows-1)) total candidates, $complete_count complete"
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
# If no pending work and no active workers, check for auto-ideation
|
|
295
|
+
if [[ $pending_count -eq 0 && $active_workers -eq 0 ]]; then
|
|
296
|
+
echo "[DISPATCHER] No pending candidates found."
|
|
297
|
+
|
|
298
|
+
# Check if auto ideation is enabled
|
|
299
|
+
if [[ "$AUTO_IDEATE" == "true" || "$AUTO_IDEATE" == "1" ]]; then
|
|
300
|
+
echo "[DISPATCHER] Auto ideation is enabled. Generating new ideas..."
|
|
301
|
+
|
|
302
|
+
# Check if claude-evolve-ideate exists
|
|
303
|
+
ideate_script="$SCRIPT_DIR/claude-evolve-ideate"
|
|
304
|
+
if [[ ! -f "$ideate_script" ]]; then
|
|
305
|
+
echo "[ERROR] claude-evolve-ideate script not found: $ideate_script" >&2
|
|
306
|
+
echo "[DISPATCHER] Evolution complete - no way to generate more ideas."
|
|
307
|
+
break
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
# Generate new ideas using the multi-strategy approach
|
|
311
|
+
echo "[DISPATCHER] Calling claude-evolve-ideate to generate new candidates..."
|
|
312
|
+
if ! "$ideate_script"; then
|
|
313
|
+
echo "[ERROR] Failed to generate new ideas" >&2
|
|
314
|
+
echo "[DISPATCHER] Evolution complete - ideation failed."
|
|
315
|
+
break
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
echo "[DISPATCHER] New ideas generated successfully. Continuing evolution..."
|
|
319
|
+
continue # Go back to start of loop to find the new candidates
|
|
320
|
+
else
|
|
321
|
+
echo "[DISPATCHER] Auto ideation is disabled. Evolution complete."
|
|
322
|
+
echo "[DISPATCHER] Run 'claude-evolve ideate' to generate more candidates."
|
|
323
|
+
echo "[DISPATCHER] Exiting main loop: no work remaining" >&2
|
|
324
|
+
break
|
|
325
|
+
fi
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
# Start workers if we have pending work and capacity
|
|
329
|
+
while [[ $pending_count -gt 0 && $active_workers -lt $MAX_WORKERS ]]; do
|
|
330
|
+
start_worker
|
|
331
|
+
active_workers=${#worker_pids[@]}
|
|
332
|
+
((pending_count--)) # Optimistically assume this will be picked up
|
|
333
|
+
done
|
|
334
|
+
|
|
335
|
+
# Brief pause to avoid busy waiting
|
|
336
|
+
sleep 2
|
|
337
|
+
done
|
|
338
|
+
|
|
339
|
+
# Clean shutdown
|
|
340
|
+
shutdown_workers
|
|
341
|
+
echo "[DISPATCHER] Evolution run complete"
|
|
342
|
+
echo "[DISPATCHER] Exiting with code 0"
|
|
@@ -233,6 +233,7 @@ while true; do
|
|
|
233
233
|
echo "[INFO] No more pending candidates found."
|
|
234
234
|
|
|
235
235
|
# Check if auto ideation is enabled
|
|
236
|
+
echo "[DEBUG] AUTO_IDEATE value: '$AUTO_IDEATE'"
|
|
236
237
|
if [[ "$AUTO_IDEATE" == "true" || "$AUTO_IDEATE" == "1" ]]; then
|
|
237
238
|
echo "[INFO] Auto ideation is enabled. Generating new ideas..."
|
|
238
239
|
|
|
@@ -267,7 +268,7 @@ while true; do
|
|
|
267
268
|
eval "$("$PYTHON_CMD" "$SCRIPT_DIR/../lib/csv_helper.py" get_row "$FULL_CSV_PATH" "$row_num")"
|
|
268
269
|
|
|
269
270
|
# Variables are now set: id, basedOnId, description, performance, status
|
|
270
|
-
|
|
271
|
+
# based_on_id is already set correctly by csv_helper.py
|
|
271
272
|
|
|
272
273
|
# Check if ID is empty
|
|
273
274
|
if [[ -z $id ]]; then
|
|
@@ -282,18 +283,18 @@ echo "[INFO] Based on ID: $based_on_id"
|
|
|
282
283
|
# Set interrupt handler - just exit without updating CSV status
|
|
283
284
|
trap 'echo "[INFO] Evolution interrupted"; exit 130' INT
|
|
284
285
|
|
|
285
|
-
#
|
|
286
|
+
# AIDEV-NOTE: Using common evolution processor logic to determine parent/output files
|
|
287
|
+
# and check if processing should be skipped (handles self-parent detection)
|
|
288
|
+
|
|
289
|
+
# Determine parent algorithm path
|
|
286
290
|
if [[ -z $based_on_id || $based_on_id == "0" || $based_on_id == '""' ]]; then
|
|
287
|
-
# Empty or zero basedonID means use the base algorithm
|
|
288
291
|
parent_file="$FULL_ALGORITHM_PATH"
|
|
289
292
|
echo "[INFO] Using base algorithm (basedonID is empty or 0)"
|
|
290
293
|
else
|
|
291
294
|
# Handle both old format (numeric) and new format (genXX-XXX)
|
|
292
295
|
if [[ $based_on_id =~ ^[0-9]+$ ]]; then
|
|
293
|
-
# Old numeric format
|
|
294
296
|
parent_file="$FULL_OUTPUT_DIR/evolution_id${based_on_id}.py"
|
|
295
297
|
else
|
|
296
|
-
# New generation format
|
|
297
298
|
parent_file="$FULL_OUTPUT_DIR/evolution_${based_on_id}.py"
|
|
298
299
|
fi
|
|
299
300
|
|
|
@@ -310,20 +311,24 @@ fi
|
|
|
310
311
|
|
|
311
312
|
echo "[INFO] Using parent algorithm: $parent_file"
|
|
312
313
|
|
|
313
|
-
# Generate
|
|
314
|
-
# Handle both old format (numeric) and new format (genXX-XXX)
|
|
314
|
+
# Generate output file path
|
|
315
315
|
if [[ $id =~ ^[0-9]+$ ]]; then
|
|
316
|
-
# Old numeric format
|
|
317
316
|
output_file="$FULL_OUTPUT_DIR/evolution_id${id}.py"
|
|
318
317
|
else
|
|
319
|
-
# New generation format
|
|
320
318
|
output_file="$FULL_OUTPUT_DIR/evolution_${id}.py"
|
|
321
319
|
fi
|
|
322
320
|
echo "[INFO] Generating algorithm mutation..."
|
|
323
321
|
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
|
|
322
|
+
# Check if processing should be skipped using common logic
|
|
323
|
+
eval "$("$PYTHON_CMD" "$SCRIPT_DIR/../lib/evolution_processor.py" "$id" "$based_on_id" "$FULL_OUTPUT_DIR" "$ROOT_DIR" "$parent_file" "$output_file")"
|
|
324
|
+
|
|
325
|
+
# Handle copy operation
|
|
326
|
+
if [[ "$skip_copy" == "True" ]]; then
|
|
327
|
+
echo "[INFO] ⚠️ Skipping copy - $reason"
|
|
328
|
+
else
|
|
329
|
+
cp "$parent_file" "$output_file"
|
|
330
|
+
echo "[INFO] Copied parent algorithm to: $output_file"
|
|
331
|
+
fi
|
|
327
332
|
|
|
328
333
|
# Check for claude CLI
|
|
329
334
|
claude_cmd="${CLAUDE_CMD:-claude}"
|
|
@@ -354,9 +359,10 @@ Requirements:
|
|
|
354
359
|
|
|
355
360
|
The file currently contains the parent algorithm. Modify it according to the description above while adhering to all guidelines from the CLAUDE.md files."
|
|
356
361
|
|
|
357
|
-
#
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
# AIDEV-NOTE: Using common evolution processor logic for Claude processing decisions
|
|
363
|
+
# Handle Claude mutation based on skip flags
|
|
364
|
+
if [[ "$skip_claude" == "True" ]]; then
|
|
365
|
+
echo "[INFO] ⚠️ Skipping Claude processing - $reason"
|
|
360
366
|
else
|
|
361
367
|
echo "[INFO] Calling Claude $CLAUDE_MODEL to apply mutation..."
|
|
362
368
|
echo "[INFO] Claude will edit: $output_file"
|
package/bin/claude-evolve-worker
CHANGED
|
@@ -110,6 +110,7 @@ fi
|
|
|
110
110
|
echo "[WORKER-$$] Description: $description"
|
|
111
111
|
echo "[WORKER-$$] Based on ID: $based_on_id"
|
|
112
112
|
|
|
113
|
+
# AIDEV-NOTE: Using common evolution processor logic for consistent handling
|
|
113
114
|
# Determine parent algorithm
|
|
114
115
|
if [[ -z $based_on_id || $based_on_id == "0" || $based_on_id == '""' ]]; then
|
|
115
116
|
parent_file="$FULL_ALGORITHM_PATH"
|
|
@@ -136,19 +137,20 @@ else
|
|
|
136
137
|
output_file="$FULL_OUTPUT_DIR/evolution_${id}.py"
|
|
137
138
|
fi
|
|
138
139
|
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
# Check if processing should be skipped using common logic
|
|
141
|
+
eval "$("$PYTHON_CMD" "$SCRIPT_DIR/../lib/evolution_processor.py" "$id" "$based_on_id" "$FULL_OUTPUT_DIR" "$ROOT_DIR" "$parent_file" "$output_file")"
|
|
142
|
+
|
|
143
|
+
# Handle copy operation
|
|
144
|
+
if [[ "$skip_copy" == "True" ]]; then
|
|
145
|
+
echo "[WORKER-$$] ⚠️ Skipping copy - $reason"
|
|
142
146
|
else
|
|
143
147
|
cp "$parent_file" "$output_file"
|
|
144
148
|
echo "[WORKER-$$] Copied parent to: $output_file"
|
|
145
149
|
fi
|
|
146
150
|
|
|
147
|
-
#
|
|
148
|
-
if [[
|
|
149
|
-
echo "[WORKER-$$]
|
|
150
|
-
elif [[ "$parent_file" == "$output_file" ]]; then
|
|
151
|
-
echo "[WORKER-$$] ⚠️ Self-parent detected - skipping mutation to preserve existing code"
|
|
151
|
+
# Handle Claude mutation based on skip flags
|
|
152
|
+
if [[ "$skip_claude" == "True" ]]; then
|
|
153
|
+
echo "[WORKER-$$] ⚠️ Skipping Claude processing - $reason"
|
|
152
154
|
else
|
|
153
155
|
# Check for claude CLI
|
|
154
156
|
claude_cmd="${CLAUDE_CMD:-claude}"
|
package/lib/config.sh
CHANGED
package/lib/csv-lock.sh
CHANGED
|
@@ -4,36 +4,95 @@
|
|
|
4
4
|
# Lock file location
|
|
5
5
|
CSV_LOCKFILE="${EVOLUTION_DIR:-evolution}/.evolution.csv.lock"
|
|
6
6
|
|
|
7
|
-
# Acquire exclusive lock on CSV file
|
|
7
|
+
# Acquire exclusive lock on CSV file with automatic stale lock cleanup
|
|
8
8
|
# Usage: acquire_csv_lock [timeout_seconds]
|
|
9
9
|
acquire_csv_lock() {
|
|
10
|
-
local timeout="${1:-
|
|
10
|
+
local timeout="${1:-${LOCK_TIMEOUT:-10}}" # Reduced default timeout
|
|
11
11
|
local lockdir="$(dirname "$CSV_LOCKFILE")"
|
|
12
12
|
|
|
13
13
|
# Ensure lock directory exists
|
|
14
14
|
mkdir -p "$lockdir"
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
16
|
+
# AIDEV-NOTE: Robust locking with automatic stale lock detection and cleanup
|
|
17
|
+
# CSV operations should be fast (<100ms), so long timeouts indicate problems
|
|
18
|
+
|
|
19
|
+
# Clean up stale locks first
|
|
20
|
+
cleanup_stale_locks
|
|
21
|
+
|
|
22
|
+
# Try to acquire lock with short timeout and fast retry
|
|
23
|
+
local end_time=$(($(date +%s) + timeout))
|
|
24
|
+
local sleep_time=0.01 # Start with 10ms sleep
|
|
25
|
+
|
|
26
|
+
while [ $(date +%s) -lt $end_time ]; do
|
|
27
|
+
if command -v flock >/dev/null 2>&1; then
|
|
28
|
+
# Use flock if available (Linux) - this should be instant
|
|
29
|
+
exec 200>"$CSV_LOCKFILE"
|
|
30
|
+
if flock -n -x 200; then
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
else
|
|
34
|
+
# Fallback for systems without flock (macOS)
|
|
35
|
+
if (set -C; echo $$ > "$CSV_LOCKFILE") 2>/dev/null; then
|
|
36
|
+
return 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Check if existing lock is stale and clean it up
|
|
40
|
+
if [ -f "$CSV_LOCKFILE" ]; then
|
|
41
|
+
local lock_pid=$(cat "$CSV_LOCKFILE" 2>/dev/null)
|
|
42
|
+
if [[ "$lock_pid" =~ ^[0-9]+$ ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
|
|
43
|
+
echo "[DEBUG] Removing stale lock from dead process $lock_pid" >&2
|
|
44
|
+
rm -f "$CSV_LOCKFILE"
|
|
45
|
+
continue # Try again immediately
|
|
46
|
+
fi
|
|
32
47
|
fi
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Brief sleep with exponential backoff (cap at 100ms)
|
|
51
|
+
sleep "$sleep_time"
|
|
52
|
+
sleep_time=$(echo "$sleep_time * 1.5" | bc -l 2>/dev/null | head -c 10)
|
|
53
|
+
if (( $(echo "$sleep_time > 0.1" | bc -l 2>/dev/null || echo 0) )); then
|
|
54
|
+
sleep_time=0.1
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
echo "ERROR: Failed to acquire CSV lock within $timeout seconds" >&2
|
|
59
|
+
echo "ERROR: This indicates a serious problem - CSV operations should be fast" >&2
|
|
60
|
+
|
|
61
|
+
# As a last resort, if lock is very old, break it
|
|
62
|
+
if [ -f "$CSV_LOCKFILE" ]; then
|
|
63
|
+
local lock_age=$(($(date +%s) - $(stat -f %m "$CSV_LOCKFILE" 2>/dev/null || stat -c %Y "$CSV_LOCKFILE" 2>/dev/null || echo $(date +%s))))
|
|
64
|
+
if [ $lock_age -gt 60 ]; then # Lock older than 1 minute is definitely stale
|
|
65
|
+
echo "[WARN] Breaking very old lock file (${lock_age}s old)" >&2
|
|
66
|
+
rm -f "$CSV_LOCKFILE"
|
|
67
|
+
return 1 # Still return error to trigger retry
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
return 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Clean up stale lock files
|
|
75
|
+
cleanup_stale_locks() {
|
|
76
|
+
if [ ! -f "$CSV_LOCKFILE" ]; then
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Check file age - any lock older than 10 seconds is definitely stale
|
|
81
|
+
local lock_age=$(($(date +%s) - $(stat -f %m "$CSV_LOCKFILE" 2>/dev/null || stat -c %Y "$CSV_LOCKFILE" 2>/dev/null || echo $(date +%s))))
|
|
82
|
+
if [ $lock_age -gt 10 ]; then
|
|
83
|
+
echo "[DEBUG] Removing stale lock file (${lock_age}s old)" >&2
|
|
84
|
+
rm -f "$CSV_LOCKFILE"
|
|
85
|
+
return 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Check if process is still alive (macOS fallback mode only)
|
|
89
|
+
if ! command -v flock >/dev/null 2>&1; then
|
|
90
|
+
local lock_pid=$(cat "$CSV_LOCKFILE" 2>/dev/null)
|
|
91
|
+
if [[ "$lock_pid" =~ ^[0-9]+$ ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
|
|
92
|
+
echo "[DEBUG] Removing lock from dead process $lock_pid" >&2
|
|
93
|
+
rm -f "$CSV_LOCKFILE"
|
|
94
|
+
fi
|
|
35
95
|
fi
|
|
36
|
-
return 0
|
|
37
96
|
}
|
|
38
97
|
|
|
39
98
|
# Release CSV lock
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Evolution processor - common logic for processing evolution candidates.
|
|
4
|
+
Used by both sequential (claude-evolve-run) and parallel (claude-evolve-worker) modes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import subprocess
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
def should_skip_processing(id_val, based_on_id, parent_file, output_file):
|
|
14
|
+
"""
|
|
15
|
+
Determine if evolution processing should be skipped.
|
|
16
|
+
|
|
17
|
+
Simple rule: If file exists, skip everything. This handles all edge cases cleanly.
|
|
18
|
+
|
|
19
|
+
Returns tuple: (skip_copy, skip_claude, reason)
|
|
20
|
+
"""
|
|
21
|
+
# Baseline algorithm check
|
|
22
|
+
if id_val in ["000", "0", "gen00-000"]:
|
|
23
|
+
return True, True, "Baseline algorithm - no processing needed"
|
|
24
|
+
|
|
25
|
+
# File existence check - if file exists, skip both copy and Claude
|
|
26
|
+
# This automatically handles self-parent cases and re-runs
|
|
27
|
+
if os.path.exists(output_file):
|
|
28
|
+
return True, True, "File already exists - skipping all processing"
|
|
29
|
+
|
|
30
|
+
# File doesn't exist - proceed with copy and Claude
|
|
31
|
+
return False, False, None
|
|
32
|
+
|
|
33
|
+
def get_parent_file_path(based_on_id, output_dir, root_dir):
|
|
34
|
+
"""Get the parent file path based on based_on_id."""
|
|
35
|
+
if not based_on_id or based_on_id in ["0", '""']:
|
|
36
|
+
# Use base algorithm
|
|
37
|
+
return os.path.join(root_dir, "algorithm.py")
|
|
38
|
+
|
|
39
|
+
# Handle both old format (numeric) and new format (genXX-XXX)
|
|
40
|
+
if re.match(r'^[0-9]+$', based_on_id):
|
|
41
|
+
# Old numeric format
|
|
42
|
+
return os.path.join(output_dir, f"evolution_id{based_on_id}.py")
|
|
43
|
+
else:
|
|
44
|
+
# New generation format
|
|
45
|
+
return os.path.join(output_dir, f"evolution_{based_on_id}.py")
|
|
46
|
+
|
|
47
|
+
def get_output_file_path(id_val, output_dir):
|
|
48
|
+
"""Get the output file path based on id."""
|
|
49
|
+
# Handle both old format (numeric) and new format (genXX-XXX)
|
|
50
|
+
if re.match(r'^[0-9]+$', id_val):
|
|
51
|
+
# Old numeric format
|
|
52
|
+
return os.path.join(output_dir, f"evolution_id{id_val}.py")
|
|
53
|
+
else:
|
|
54
|
+
# New generation format
|
|
55
|
+
return os.path.join(output_dir, f"evolution_{id_val}.py")
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
"""Main entry point for standalone testing."""
|
|
59
|
+
if len(sys.argv) < 7:
|
|
60
|
+
print("Usage: evolution_processor.py <id> <based_on_id> <output_dir> <root_dir> <parent_file> <output_file>")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
id_val = sys.argv[1]
|
|
64
|
+
based_on_id = sys.argv[2]
|
|
65
|
+
output_dir = sys.argv[3]
|
|
66
|
+
root_dir = sys.argv[4]
|
|
67
|
+
parent_file = sys.argv[5]
|
|
68
|
+
output_file = sys.argv[6]
|
|
69
|
+
|
|
70
|
+
skip_copy, skip_claude, reason = should_skip_processing(id_val, based_on_id, parent_file, output_file)
|
|
71
|
+
|
|
72
|
+
print(f"skip_copy={skip_copy}")
|
|
73
|
+
print(f"skip_claude={skip_claude}")
|
|
74
|
+
print(f'reason="{reason or ""}"')
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|