claude-evolve 1.4.12 → 1.5.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.
@@ -13,16 +13,23 @@ if [[ -n ${CLAUDE_EVOLVE_CONFIG:-} ]]; then
13
13
  else
14
14
  # Check if config.yaml exists in current directory
15
15
  if [[ -f "config.yaml" ]]; then
16
- export CLAUDE_EVOLVE_CONFIG="$(pwd)/config.yaml"
17
- load_config "$CLAUDE_EVOLVE_CONFIG"
16
+ # Don't export to avoid collision with parallel runs
17
+ CONFIG_FILE="$(pwd)/config.yaml"
18
+ load_config "$CONFIG_FILE"
18
19
  else
19
20
  load_config
20
21
  fi
21
22
  fi
22
23
 
23
- # Export the config path for workers if not already set
24
- if [[ -z ${CLAUDE_EVOLVE_CONFIG:-} ]] && [[ -f "config.yaml" ]]; then
25
- export CLAUDE_EVOLVE_CONFIG="$(pwd)/config.yaml"
24
+ # Store the config path for workers (don't export to avoid collision)
25
+ if [[ -n ${CLAUDE_EVOLVE_CONFIG:-} ]]; then
26
+ WORKER_CONFIG_PATH="$CLAUDE_EVOLVE_CONFIG"
27
+ elif [[ -n ${CONFIG_FILE:-} ]]; then
28
+ WORKER_CONFIG_PATH="$CONFIG_FILE"
29
+ elif [[ -f "config.yaml" ]]; then
30
+ WORKER_CONFIG_PATH="$(pwd)/config.yaml"
31
+ else
32
+ WORKER_CONFIG_PATH=""
26
33
  fi
27
34
 
28
35
  # Validate configuration
@@ -229,6 +236,7 @@ start_worker() {
229
236
 
230
237
  local worker_args=()
231
238
  [[ -n $timeout_seconds ]] && worker_args+=(--timeout "$timeout_seconds")
239
+ [[ -n $WORKER_CONFIG_PATH ]] && worker_args+=(--config "$WORKER_CONFIG_PATH")
232
240
 
233
241
  echo "[DISPATCHER] Starting worker..."
234
242
  "$worker_script" "${worker_args[@]}" &
@@ -252,6 +260,12 @@ cleanup_workers() {
252
260
  if [[ $exit_code -eq 2 ]]; then
253
261
  echo "[DISPATCHER] Worker $pid hit rate limit, will retry later"
254
262
  # Rate limits don't count as consecutive failures
263
+ elif [[ $exit_code -eq 3 ]]; then
264
+ echo "[DISPATCHER] Worker $pid hit API usage limit - stopping all processing" >&2
265
+ echo "[DISPATCHER] Cannot continue evolution run due to API limits" >&2
266
+ echo "[DISPATCHER] Please wait for limits to reset before restarting" >&2
267
+ # Set a flag to stop the main loop
268
+ api_limit_reached=true
255
269
  else
256
270
  echo "[DISPATCHER] Worker $pid failed with exit code $exit_code"
257
271
  # With retry mechanism, failures are normal - just keep processing
@@ -290,6 +304,16 @@ get_csv_stats() {
290
304
  echo "[DISPATCHER] Starting unified evolution engine"
291
305
  echo "[DISPATCHER] Configuration: max_workers=$MAX_WORKERS, timeout=${timeout_seconds:-none}"
292
306
 
307
+ # Clean up any stuck 'running' statuses at startup
308
+ if [[ -f "$FULL_CSV_PATH" ]]; then
309
+ echo "[DISPATCHER] Resetting any stuck 'running' candidates to 'pending'..."
310
+ if "$SCRIPT_DIR/claude-evolve-edit" running pending >/dev/null 2>&1; then
311
+ echo "[DISPATCHER] Successfully reset stuck candidates"
312
+ else
313
+ echo "[DISPATCHER] No stuck candidates found or edit command not available"
314
+ fi
315
+ fi
316
+
293
317
  # Validate CSV and clean up stuck statuses and duplicates
294
318
  if [[ -f "$FULL_CSV_PATH" ]]; then
295
319
  echo "[DISPATCHER] Validating CSV and cleaning up..."
@@ -451,11 +475,20 @@ ensure_baseline_entry
451
475
  # With retry mechanism, we don't need consecutive failure tracking
452
476
  # Failures are handled gracefully through the retry system
453
477
 
478
+ # Flag to track API limit status
479
+ api_limit_reached=false
480
+
454
481
  # Main dispatch loop
455
482
  while true; do
456
483
  # Clean up finished workers
457
484
  cleanup_workers
458
485
 
486
+ # Check if API limit was reached
487
+ if [[ "$api_limit_reached" == "true" ]]; then
488
+ echo "[DISPATCHER] Stopping evolution run due to API usage limits" >&2
489
+ break
490
+ fi
491
+
459
492
  # Get current status
460
493
  csv_stats=$(get_csv_stats "$FULL_CSV_PATH")
461
494
  read -r total_rows complete_count pending_count <<< "$csv_stats"
@@ -514,5 +547,14 @@ done
514
547
 
515
548
  # Clean shutdown
516
549
  shutdown_workers
517
- echo "[DISPATCHER] Evolution run complete"
518
- echo "[DISPATCHER] Exiting with code 0"
550
+
551
+ # Final status message
552
+ if [[ "$api_limit_reached" == "true" ]]; then
553
+ echo "[DISPATCHER] Evolution run stopped due to API usage limits"
554
+ echo "[DISPATCHER] Wait for limits to reset, then run 'claude-evolve run' again"
555
+ echo "[DISPATCHER] Exiting with code 1 (API limits reached)"
556
+ exit 1
557
+ else
558
+ echo "[DISPATCHER] Evolution run complete"
559
+ echo "[DISPATCHER] Exiting with code 0"
560
+ fi
@@ -6,21 +6,63 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
6
6
  source "$SCRIPT_DIR/../lib/config.sh"
7
7
  source "$SCRIPT_DIR/../lib/csv-lock.sh"
8
8
 
9
- # Load config using the same logic as dispatcher
10
- if [[ -n ${CLAUDE_EVOLVE_CONFIG:-} ]]; then
11
- load_config "$CLAUDE_EVOLVE_CONFIG"
12
- else
13
- load_config
14
- fi
9
+ # Track current candidate for cleanup
10
+ CURRENT_CANDIDATE_ID=""
11
+ TERMINATION_SIGNAL=""
12
+
13
+ # Cleanup function to handle termination
14
+ cleanup_on_exit() {
15
+ if [[ -n "$CURRENT_CANDIDATE_ID" ]]; then
16
+ # Only mark as failed if it was a timeout (SIGTERM from timeout command)
17
+ # For user interruption (Ctrl-C) or kill, leave it for retry
18
+ if [[ "$TERMINATION_SIGNAL" == "TERM" ]]; then
19
+ echo "[WORKER-$$] Timeout detected, marking $CURRENT_CANDIDATE_ID as failed" >&2
20
+ "$PYTHON_CMD" -c "
21
+ import sys
22
+ sys.path.insert(0, '$SCRIPT_DIR/..')
23
+ from lib.evolution_csv import EvolutionCSV
24
+ try:
25
+ with EvolutionCSV('$FULL_CSV_PATH') as csv:
26
+ csv.update_candidate_status('$CURRENT_CANDIDATE_ID', 'failed')
27
+ except:
28
+ pass # Best effort cleanup
29
+ " 2>/dev/null || true
30
+ else
31
+ echo "[WORKER-$$] Interrupted, leaving $CURRENT_CANDIDATE_ID for retry" >&2
32
+ # Optionally reset to pending instead of leaving as running
33
+ "$PYTHON_CMD" -c "
34
+ import sys
35
+ sys.path.insert(0, '$SCRIPT_DIR/..')
36
+ from lib.evolution_csv import EvolutionCSV
37
+ try:
38
+ with EvolutionCSV('$FULL_CSV_PATH') as csv:
39
+ csv.update_candidate_status('$CURRENT_CANDIDATE_ID', 'pending')
40
+ except:
41
+ pass # Best effort cleanup
42
+ " 2>/dev/null || true
43
+ fi
44
+ fi
45
+ }
46
+
47
+ # Set up signal handlers
48
+ trap 'TERMINATION_SIGNAL="TERM"; cleanup_on_exit' TERM
49
+ trap 'TERMINATION_SIGNAL="INT"; cleanup_on_exit' INT
50
+ trap 'TERMINATION_SIGNAL="HUP"; cleanup_on_exit' HUP
51
+ trap 'cleanup_on_exit' EXIT
15
52
 
16
- # Parse arguments
53
+ # Parse arguments first to get config path
17
54
  timeout_seconds=""
55
+ config_path=""
18
56
  while [[ $# -gt 0 ]]; do
19
57
  case "$1" in
20
58
  --timeout)
21
59
  timeout_seconds="$2"
22
60
  shift 2
23
61
  ;;
62
+ --config)
63
+ config_path="$2"
64
+ shift 2
65
+ ;;
24
66
  *)
25
67
  echo "[ERROR] Unknown argument: $1" >&2
26
68
  exit 1
@@ -28,6 +70,141 @@ while [[ $# -gt 0 ]]; do
28
70
  esac
29
71
  done
30
72
 
73
+ # Load config using the provided path, environment variable, or default
74
+ if [[ -n $config_path ]]; then
75
+ load_config "$config_path"
76
+ elif [[ -n ${CLAUDE_EVOLVE_CONFIG:-} ]]; then
77
+ load_config "$CLAUDE_EVOLVE_CONFIG"
78
+ else
79
+ load_config
80
+ fi
81
+
82
+ # AI round-robin with fallback function for code evolution
83
+ call_ai_for_evolution() {
84
+ local prompt="$1"
85
+ local candidate_id="$2"
86
+
87
+ # Extract generation and ID numbers for round-robin calculation
88
+ local gen_num=0
89
+ local id_num=0
90
+ if [[ $candidate_id =~ ^gen([0-9]+)-([0-9]+)$ ]]; then
91
+ gen_num=$((10#${BASH_REMATCH[1]}))
92
+ id_num=$((10#${BASH_REMATCH[2]}))
93
+ fi
94
+
95
+ # Calculate hash for round-robin (combine generation and ID)
96
+ local hash_value=$((gen_num * 1000 + id_num))
97
+
98
+ # Check which AI tools are available
99
+ local available_models=()
100
+ available_models+=("claude") # Claude Sonnet always available
101
+ if command -v gemini >/dev/null 2>&1; then
102
+ available_models+=("gemini")
103
+ fi
104
+ if command -v codex >/dev/null 2>&1; then
105
+ available_models+=("codex")
106
+ fi
107
+
108
+ # Create ordered list based on round-robin for this candidate
109
+ local num_models=${#available_models[@]}
110
+ local start_index=$((hash_value % num_models))
111
+ local models=()
112
+
113
+ # Add models in round-robin order starting from the calculated index
114
+ for ((i=0; i<num_models; i++)); do
115
+ local idx=$(((start_index + i) % num_models))
116
+ models+=("${available_models[$idx]}")
117
+ done
118
+
119
+ echo "[WORKER-$$] Model order for $candidate_id (round-robin): ${models[*]}" >&2
120
+
121
+ # Try each model in the ordered sequence
122
+ for model in "${models[@]}"; do
123
+ echo "[WORKER-$$] Attempting code evolution with $model" >&2
124
+ local ai_output
125
+ local ai_exit_code
126
+
127
+ case "$model" in
128
+ "claude")
129
+ ai_output=$(echo "$prompt" | claude --dangerously-skip-permissions -p 2>&1)
130
+ ai_exit_code=$?
131
+
132
+ # Check for usage limits
133
+ if echo "$ai_output" | grep -q "Claude AI usage limit reached"; then
134
+ echo "[WORKER-$$] Claude AI usage limit reached - trying next model" >&2
135
+ continue
136
+ fi
137
+
138
+ if [[ $ai_exit_code -eq 0 ]]; then
139
+ echo "[WORKER-$$] Claude Sonnet succeeded" >&2
140
+ return 0
141
+ fi
142
+ ;;
143
+
144
+ "gemini")
145
+ ai_output=$(gemini -y -p "$prompt" 2>&1)
146
+ ai_exit_code=$?
147
+
148
+ # Check for authentication messages or valid response
149
+ if [[ $ai_exit_code -eq 0 ]]; then
150
+ if ! echo "$ai_output" | grep -q "Attempting to authenticate\|Authenticating\|Loading\|Initializing"; then
151
+ if [[ -n "$ai_output" ]] && [[ $(echo "$ai_output" | wc -l) -ge 2 ]]; then
152
+ echo "[WORKER-$$] Gemini succeeded" >&2
153
+ return 0
154
+ fi
155
+ fi
156
+ fi
157
+ ;;
158
+
159
+ "codex")
160
+ ai_output=$(echo "$prompt" | codex exec --full-auto 2>&1)
161
+ ai_exit_code=$?
162
+
163
+ if [[ $ai_exit_code -eq 0 ]]; then
164
+ # Clean codex output if it's JSON
165
+ if echo "$ai_output" | grep -q '"content"'; then
166
+ ai_output=$(echo "$ai_output" | python3 -c "
167
+ import sys
168
+ import json
169
+ try:
170
+ data = json.load(sys.stdin)
171
+ if 'content' in data:
172
+ print(data['content'])
173
+ elif 'response' in data:
174
+ print(data['response'])
175
+ elif 'text' in data:
176
+ print(data['text'])
177
+ else:
178
+ print(json.dumps(data))
179
+ except:
180
+ print(sys.stdin.read())
181
+ " 2>/dev/null || echo "$ai_output")
182
+ fi
183
+
184
+ if [[ -n "$ai_output" ]] && ! echo "$ai_output" | grep -q "error\|failed\|exception"; then
185
+ echo "[WORKER-$$] Codex succeeded" >&2
186
+ return 0
187
+ fi
188
+ fi
189
+ ;;
190
+ esac
191
+
192
+ echo "[WORKER-$$] $model failed (exit code $ai_exit_code), trying next model..." >&2
193
+ if [[ -n "$ai_output" ]]; then
194
+ echo "[WORKER-$$] $model error: $(echo "$ai_output" | head -5)" >&2
195
+ fi
196
+ done
197
+
198
+ # All models in round-robin failed, check for API limit exit
199
+ if echo "${ai_output:-}" | grep -q "Claude AI usage limit reached"; then
200
+ echo "[WORKER-$$] ERROR: All AI models unavailable - Claude hit usage limit" >&2
201
+ exit 3
202
+ fi
203
+
204
+ echo "[WORKER-$$] All AI models failed for code evolution" >&2
205
+ return 1
206
+ }
207
+
31
208
  # Validate paths
32
209
  if [[ ! -f "$FULL_CSV_PATH" ]]; then
33
210
  echo "[WORKER-$$] CSV file not found: $FULL_CSV_PATH" >&2
@@ -58,11 +235,21 @@ process_candidate() {
58
235
  fi
59
236
  fi
60
237
 
61
- # Target file for evolution
238
+ # Check if this is a baseline candidate (no parent and specific ID pattern)
239
+ local is_baseline=false
240
+ if [[ -z "$parent_id" ]] && [[ "$candidate_id" =~ ^(baseline|baseline-000|000|0|gen00-000)$ ]]; then
241
+ is_baseline=true
242
+ echo "[WORKER-$$] Detected baseline candidate - will run algorithm.py directly"
243
+ fi
244
+
245
+ # Target file for evolution (not used for baseline)
62
246
  local target_file="$FULL_OUTPUT_DIR/evolution_${candidate_id}.py"
63
247
 
64
248
  # Check if processing should be skipped
65
- if [[ -f "$target_file" ]]; then
249
+ if [[ "$is_baseline" == "true" ]]; then
250
+ # For baseline, skip all file operations
251
+ echo "[WORKER-$$] Baseline candidate - skipping file operations"
252
+ elif [[ -f "$target_file" ]]; then
66
253
  echo "[WORKER-$$] � Skipping copy - File already exists - skipping all processing"
67
254
  echo "[WORKER-$$] � Skipping Claude processing - File already exists - skipping all processing"
68
255
 
@@ -92,30 +279,50 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
92
279
  echo "[WORKER-$$] Copying $source_file to $target_file"
93
280
  cp "$source_file" "$target_file"
94
281
 
95
- # Apply evolution using Claude
96
- echo "[WORKER-$$] Applying evolution with Claude..."
97
- local evolution_prompt="Modify the algorithm in $target_file based on this description: $description
282
+ # Apply evolution using AI
283
+ echo "[WORKER-$$] Applying evolution..."
284
+
285
+ # Use relative path for AI prompt
286
+ local target_basename=$(basename "$target_file")
287
+ local evolution_prompt="Modify the algorithm in $target_basename based on this description: $description
98
288
 
99
289
  The modification should be substantial and follow the description exactly. Make sure the algorithm still follows all interface requirements and can run properly.
100
290
 
101
291
  Important: Make meaningful changes that match the description. Don't just add comments or make trivial adjustments."
102
292
 
103
- if ! echo "$evolution_prompt" | claude --dangerously-skip-permissions -p 2>&1; then
104
- echo "[WORKER-$$] ERROR: Claude evolution failed" >&2
105
- rm -f "$target_file" # Clean up on failure
106
- return 1
293
+ if [[ "$is_baseline" != "true" ]]; then
294
+ # Change to evolution directory so AI can access files
295
+ local original_pwd=$(pwd)
296
+ cd "$FULL_EVOLUTION_DIR"
297
+
298
+ # Try AI models with round-robin based on candidate ID
299
+ if ! call_ai_for_evolution "$evolution_prompt" "$candidate_id"; then
300
+ echo "[WORKER-$$] ERROR: All AI models failed to generate code" >&2
301
+ cd "$original_pwd"
302
+ rm -f "$target_file" # Clean up on failure
303
+ return 1
304
+ fi
305
+
306
+ # Restore working directory
307
+ cd "$original_pwd"
308
+
309
+ echo "[WORKER-$$] Evolution applied successfully"
107
310
  fi
108
-
109
- echo "[WORKER-$$] Evolution applied successfully"
110
311
  fi
111
312
 
112
313
  # Run evaluation
113
314
  echo "[WORKER-$$] Evaluating algorithm..."
114
- local eval_output_file="/tmp/claude-evolve-eval-$$-$candidate_id.out"
315
+ local eval_output_file="$FULL_EVOLUTION_DIR/temp-eval-$$-$candidate_id.out"
115
316
  local eval_start=$(date +%s)
116
317
 
117
318
  # Prepare evaluation command
118
- local eval_cmd=("$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$candidate_id")
319
+ # For baseline, pass "baseline" or empty string to evaluator to use algorithm.py
320
+ local eval_arg="$candidate_id"
321
+ if [[ "$is_baseline" == "true" ]]; then
322
+ # Evaluator should interpret this as "use algorithm.py directly"
323
+ eval_arg=""
324
+ fi
325
+ local eval_cmd=("$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$eval_arg")
119
326
  [[ -n "$timeout_seconds" ]] && eval_cmd=(timeout "$timeout_seconds" "${eval_cmd[@]}")
120
327
 
121
328
  # Run evaluation with tee to both display and capture output
@@ -228,17 +435,20 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
228
435
  else
229
436
  echo "[WORKER-$$] ERROR: No score found in evaluation output" >&2
230
437
  echo "[WORKER-$$] Output: $eval_output" >&2
231
- rm -f "$eval_output_file"
438
+ # rm -f "$eval_output_file" # Keep for debugging
439
+ echo "[WORKER-$$] Evaluation output saved to: $eval_output_file" >&2
232
440
  return 1
233
441
  fi
234
442
 
235
- # Clean up temp file
236
- rm -f "$eval_output_file"
443
+ # Clean up temp file (comment out to keep for debugging)
444
+ # rm -f "$eval_output_file"
445
+ echo "[WORKER-$$] Evaluation output saved to: $eval_output_file" >&2
237
446
  else
238
447
  local exit_code=$?
239
448
  # Read any output that was captured before failure
240
449
  eval_output=$(<"$eval_output_file")
241
- rm -f "$eval_output_file"
450
+ # rm -f "$eval_output_file" # Keep for debugging
451
+ echo "[WORKER-$$] Evaluation output saved to: $eval_output_file" >&2
242
452
 
243
453
  echo "[WORKER-$$] ERROR: Evaluation failed with exit code $exit_code" >&2
244
454
  echo "[WORKER-$$] Output: $eval_output" >&2
@@ -272,7 +482,7 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
272
482
  # Get full candidate info
273
483
  candidate = csv.get_candidate_info(candidate_id)
274
484
  if candidate:
275
- print(f'{candidate[\"id\"]}|{candidate.get(\"parent_id\", \"\")}|{candidate[\"description\"]}')
485
+ print(f'{candidate[\"id\"]}|{candidate.get(\"basedOnId\", \"\")}|{candidate[\"description\"]}')
276
486
  ")
277
487
 
278
488
  if [[ -z "$candidate_info" ]]; then
@@ -283,12 +493,26 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
283
493
  # Parse candidate info
284
494
  IFS='|' read -r candidate_id parent_id description <<< "$candidate_info"
285
495
 
496
+ # Set current candidate for cleanup
497
+ CURRENT_CANDIDATE_ID="$candidate_id"
498
+
286
499
  # Process the candidate
287
500
  if process_candidate "$candidate_id" "$parent_id" "$description"; then
288
501
  echo "[WORKER-$$] Successfully processed $candidate_id"
289
502
  else
290
503
  echo "[WORKER-$$] Failed to process $candidate_id"
504
+ # Ensure status is set to failed (might already be done in process_candidate)
505
+ "$PYTHON_CMD" -c "
506
+ import sys
507
+ sys.path.insert(0, '$SCRIPT_DIR/..')
508
+ from lib.evolution_csv import EvolutionCSV
509
+ with EvolutionCSV('$FULL_CSV_PATH') as csv:
510
+ csv.update_candidate_status('$candidate_id', 'failed')
511
+ " 2>/dev/null || true
291
512
  fi
513
+
514
+ # Clear current candidate
515
+ CURRENT_CANDIDATE_ID=""
292
516
  done
293
517
 
294
518
  echo "[WORKER-$$] No more pending candidates, worker exiting"
@@ -121,8 +121,9 @@ class EvolutionCSV:
121
121
  # Check status field (5th column, index 4)
122
122
  status = row[4].strip().lower() if row[4] else ''
123
123
 
124
- # Blank, missing, "pending", or "running" all mean pending
125
- if not status or status in ['pending', 'running']:
124
+ # Only blank, missing, or "pending" mean pending
125
+ # "running" should NOT be considered pending to avoid duplicate processing
126
+ if not status or status == 'pending':
126
127
  return True
127
128
 
128
129
  # Check for retry statuses
@@ -321,6 +322,39 @@ class EvolutionCSV:
321
322
 
322
323
  return None
323
324
 
325
+ def delete_candidate(self, candidate_id: str) -> bool:
326
+ """Delete a candidate from the CSV file."""
327
+ rows = self._read_csv()
328
+ if not rows:
329
+ return False
330
+
331
+ # Check if we have a header row
332
+ has_header = rows and rows[0] and rows[0][0].lower() == 'id'
333
+
334
+ # Find and remove the candidate
335
+ deleted = False
336
+ new_rows = []
337
+
338
+ # Keep header if it exists
339
+ if has_header:
340
+ new_rows.append(rows[0])
341
+ start_idx = 1
342
+ else:
343
+ start_idx = 0
344
+
345
+ for i in range(start_idx, len(rows)):
346
+ row = rows[i]
347
+ if self.is_valid_candidate_row(row) and row[0].strip() == candidate_id:
348
+ deleted = True
349
+ # Skip this row (delete it)
350
+ continue
351
+ new_rows.append(row)
352
+
353
+ if deleted:
354
+ self._write_csv(new_rows)
355
+
356
+ return deleted
357
+
324
358
  def has_pending_work(self) -> bool:
325
359
  """Check if there are any pending candidates. Used by dispatcher."""
326
360
  return self.count_pending_candidates() > 0