claude-evolve 1.5.4 → 1.5.8

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.
@@ -38,6 +38,7 @@ else
38
38
  load_config
39
39
  fi
40
40
 
41
+
41
42
  # Run the Python autostatus script
42
43
  exec "$PYTHON_CMD" -c '
43
44
  import os
@@ -118,9 +119,27 @@ class AutoStatus:
118
119
  "working_dir": os.path.dirname(self.csv_path)
119
120
  }
120
121
 
122
+ # Helpers
123
+ def parse_candidate_id(cid):
124
+ """Return (gen_num, seq_num) for ids like gen123-045; fall back to large numbers."""
125
+ try:
126
+ left, right = cid.split("-", 1)
127
+ gen_num = int(left[3:]) if left.startswith("gen") else 10**9
128
+ seq_num = int(right)
129
+ return gen_num, seq_num
130
+ except Exception:
131
+ return 10**9, 10**9
132
+
133
+ def is_earlier(cid_a, cid_b):
134
+ """True if cid_a is earlier than cid_b by generation, then sequence."""
135
+ ga, sa = parse_candidate_id(cid_a)
136
+ gb, sb = parse_candidate_id(cid_b)
137
+ return (ga, sa) < (gb, sb)
138
+
139
+
121
140
  # Process candidates by generation
122
- all_candidates = []
123
141
  stats_by_gen = {}
142
+ leader = None # Track overall leader with earliest-wins tie behavior
124
143
 
125
144
  for row in rows[1:]: # Skip header
126
145
  if len(row) >= 1 and row[0]: # Must have an ID
@@ -157,20 +176,24 @@ class AutoStatus:
157
176
  description = row[2] if len(row) > 2 else "No description"
158
177
  candidate_info = (candidate_id, description, score)
159
178
  stats_by_gen[gen]["candidates"].append(candidate_info)
160
- all_candidates.append(candidate_info)
179
+
180
+ # Update overall leader: highest raw score; ties -> earliest ID
181
+ if leader is None or score > leader[2] or (score == leader[2] and is_earlier(candidate_id, leader[0])):
182
+ leader = candidate_info
183
+
184
+ # Update generation best: highest raw score; ties -> earliest ID
185
+ if "best" not in stats_by_gen[gen]:
186
+ stats_by_gen[gen]["best"] = candidate_info
187
+ else:
188
+ best_id, _, best_score = stats_by_gen[gen]["best"]
189
+ if score > best_score or (score == best_score and is_earlier(candidate_id, best_id)):
190
+ stats_by_gen[gen]["best"] = candidate_info
161
191
  except ValueError:
162
192
  pass
163
193
 
164
- # Find the overall leader
165
- leader = None
166
- if all_candidates:
167
- leader = max(all_candidates, key=lambda x: x[2])
168
-
169
- # Find best performer in each generation
194
+ # Ensure every generation has a best field
170
195
  for gen in stats_by_gen:
171
- if stats_by_gen[gen]["candidates"]:
172
- stats_by_gen[gen]["best"] = max(stats_by_gen[gen]["candidates"], key=lambda x: x[2])
173
- else:
196
+ if "best" not in stats_by_gen[gen]:
174
197
  stats_by_gen[gen]["best"] = None
175
198
 
176
199
  return {
@@ -245,9 +268,17 @@ class AutoStatus:
245
268
  self.display.move_cursor(row, 1)
246
269
  print("-" * min(self.display.cols, len(header_fmt)))
247
270
  row += 1
271
+ # Sort generations numerically by extracting the number after "gen"
272
+ def gen_sort_key(gen_str):
273
+ """Extract numeric value from generation string for sorting."""
274
+ if gen_str.startswith("gen"):
275
+ try:
276
+ return int(gen_str[3:])
277
+ except ValueError:
278
+ return 999999 # Put non-numeric at end
279
+ return 999999
248
280
 
249
- # Sort generations numerically by extracting the number after 'gen'
250
- sorted_gens = sorted(generations.keys(), key=lambda g: int(g[3:]) if g.startswith("gen") and g[3:].isdigit() else 0)
281
+ sorted_gens = sorted(generations.keys(), key=gen_sort_key)
251
282
 
252
283
  # Calculate how many generations we can show
253
284
  available_rows = self.display.rows - row - 1 # Leave room at bottom
@@ -344,4 +375,4 @@ class AutoStatus:
344
375
  csv_path = "'"$FULL_CSV_PATH"'"
345
376
  auto_status = AutoStatus(csv_path)
346
377
  auto_status.run()
347
- '
378
+ '
@@ -18,6 +18,17 @@ else
18
18
  load_config
19
19
  fi
20
20
 
21
+ # Setup logging to file
22
+ if [[ -n "${FULL_EVOLUTION_DIR:-}" ]]; then
23
+ LOG_DIR="$FULL_EVOLUTION_DIR/logs"
24
+ mkdir -p "$LOG_DIR"
25
+ LOG_FILE="$LOG_DIR/ideate-$$-$(date +%Y%m%d-%H%M%S).log"
26
+
27
+ # Log to both terminal and file with timestamps
28
+ exec > >(while IFS= read -r line; do echo "$(date '+%Y-%m-%d %H:%M:%S'): $line"; done | tee -a "$LOG_FILE") 2>&1
29
+ echo "[IDEATE-$$] Logging to: $LOG_FILE"
30
+ fi
31
+
21
32
  # Helper function to call AI with limit check
22
33
  call_ai_with_limit_check() {
23
34
  local prompt="$1"
@@ -62,6 +73,7 @@ call_claude_with_limit_check() {
62
73
  }
63
74
 
64
75
  # Robust AI calling with fallbacks across all available models
76
+ # Returns 0 on success and echoes the successful model name to stdout
65
77
  call_ai_for_ideation() {
66
78
  local prompt="$1"
67
79
  local generation="${2:-01}"
@@ -119,8 +131,6 @@ call_ai_for_ideation() {
119
131
  ai_output=$(call_ai_model_configured "$model" "$prompt")
120
132
  local ai_exit_code=$?
121
133
 
122
- echo "[AI] $model completed with exit code $ai_exit_code" >&2
123
-
124
134
  # Check if the file was modified - this is ALL that matters
125
135
  if [[ -f "$temp_csv_file" ]]; then
126
136
  local new_csv_count
@@ -140,6 +150,8 @@ call_ai_for_ideation() {
140
150
  echo "[WARN] CSV format validation failed, using original" >&2
141
151
  fi
142
152
 
153
+ # Echo the successful model name for caller to capture
154
+ echo "$model"
143
155
  return 0
144
156
  else
145
157
  echo "[INFO] CSV unchanged after $model (exit code: $ai_exit_code)" >&2
@@ -210,7 +222,7 @@ fi
210
222
 
211
223
  # Ensure CSV exists
212
224
  if [[ ! -f "$FULL_CSV_PATH" ]]; then
213
- echo "id,basedOnId,description,performance,status" >"$FULL_CSV_PATH"
225
+ echo "id,basedOnId,description,performance,status,idea-LLM,run-LLM" >"$FULL_CSV_PATH"
214
226
  fi
215
227
 
216
228
  # Validate strategy configuration
@@ -306,6 +318,7 @@ validate_direct_csv_modification() {
306
318
  local temp_csv="$1"
307
319
  local expected_count="$2"
308
320
  local idea_type="$3"
321
+ local ai_model="${4:-}" # AI model that generated the ideas
309
322
 
310
323
  # Check if the file was actually modified
311
324
  if [[ ! -f "$temp_csv" ]]; then
@@ -369,6 +382,22 @@ validate_direct_csv_modification() {
369
382
  # Clean up temp file
370
383
  rm -f "$temp_csv"
371
384
 
385
+ # Update idea-LLM field for newly added rows if model is known
386
+ if [[ -n "$ai_model" ]]; then
387
+ echo "[INFO] Recording that $ai_model generated the ideas" >&2
388
+ # Get the IDs of the newly added rows (skip header line and strip quotes)
389
+ local new_ids
390
+ new_ids=$(tail -n $added_count "$FULL_CSV_PATH" | grep -v "^id," | cut -d',' -f1 | tr -d '"')
391
+
392
+ # Update each new row with the model that generated it
393
+ for id in $new_ids; do
394
+ if [[ -n "$id" && "$id" != "id" ]]; then
395
+ echo "[DEBUG] Updating $id with idea-LLM = $ai_model" >&2
396
+ "$PYTHON_CMD" "$SCRIPT_DIR/../lib/evolution_csv.py" "$FULL_CSV_PATH" field "$id" "idea-LLM" "$ai_model" || echo "[WARN] Failed to update $id" >&2
397
+ fi
398
+ done
399
+ fi
400
+
372
401
  # Release the lock
373
402
  release_csv_lock
374
403
 
@@ -462,6 +491,22 @@ validate_and_apply_csv_modification_old() {
462
491
  # Clean up temp file
463
492
  rm -f "$temp_csv"
464
493
 
494
+ # Update idea-LLM field for newly added rows if model is known
495
+ if [[ -n "$ai_model" ]]; then
496
+ echo "[INFO] Recording that $ai_model generated the ideas" >&2
497
+ # Get the IDs of the newly added rows (skip header line and strip quotes)
498
+ local new_ids
499
+ new_ids=$(tail -n $added_count "$FULL_CSV_PATH" | grep -v "^id," | cut -d',' -f1 | tr -d '"')
500
+
501
+ # Update each new row with the model that generated it
502
+ for id in $new_ids; do
503
+ if [[ -n "$id" && "$id" != "id" ]]; then
504
+ echo "[DEBUG] Updating $id with idea-LLM = $ai_model" >&2
505
+ "$PYTHON_CMD" "$SCRIPT_DIR/../lib/evolution_csv.py" "$FULL_CSV_PATH" field "$id" "idea-LLM" "$ai_model" || echo "[WARN] Failed to update $id" >&2
506
+ fi
507
+ done
508
+ fi
509
+
465
510
  # Release the lock
466
511
  release_csv_lock
467
512
 
@@ -937,7 +982,7 @@ CRITICAL: Do NOT use any git commands (git add, git commit, git reset, etc.). On
937
982
  echo "[DEBUG] AI response: $ai_response" >&2
938
983
 
939
984
  # Validate that the CSV file was actually modified
940
- if ! validate_direct_csv_modification "$temp_csv" "$count" "novel"; then
985
+ if ! validate_direct_csv_modification "$temp_csv" "$count" "novel" "$ai_response"; then
941
986
  rm -f "$temp_csv"
942
987
  return 1
943
988
  fi
@@ -1024,7 +1069,7 @@ CRITICAL: Do NOT use any git commands (git add, git commit, git reset, etc.). On
1024
1069
  echo "[DEBUG] AI response: $ai_response" >&2
1025
1070
 
1026
1071
  # Validate that the CSV file was actually modified
1027
- if ! validate_direct_csv_modification "$temp_csv" "$count" "hill-climbing"; then
1072
+ if ! validate_direct_csv_modification "$temp_csv" "$count" "hill-climbing" "$ai_response"; then
1028
1073
  rm -f "$temp_csv"
1029
1074
  return 1
1030
1075
  fi
@@ -1111,7 +1156,7 @@ CRITICAL: Do NOT use any git commands (git add, git commit, git reset, etc.). On
1111
1156
  echo "[DEBUG] AI response: $ai_response" >&2
1112
1157
 
1113
1158
  # Validate that the CSV file was actually modified
1114
- if ! validate_direct_csv_modification "$temp_csv" "$count" "structural"; then
1159
+ if ! validate_direct_csv_modification "$temp_csv" "$count" "structural" "$ai_response"; then
1115
1160
  rm -f "$temp_csv"
1116
1161
  return 1
1117
1162
  fi
@@ -1198,7 +1243,7 @@ CRITICAL: Do NOT use any git commands (git add, git commit, git reset, etc.). On
1198
1243
  echo "[DEBUG] AI response: $ai_response" >&2
1199
1244
 
1200
1245
  # Validate that the CSV file was actually modified
1201
- if ! validate_direct_csv_modification "$temp_csv" "$count" "crossover"; then
1246
+ if ! validate_direct_csv_modification "$temp_csv" "$count" "crossover" "$ai_response"; then
1202
1247
  rm -f "$temp_csv"
1203
1248
  return 1
1204
1249
  fi
@@ -1309,7 +1354,7 @@ CRITICAL: Do NOT use any git commands (git add, git commit, git reset, etc.). On
1309
1354
  echo "[DEBUG] AI response: $ai_response" >&2
1310
1355
 
1311
1356
  # Validate that the CSV file was actually modified
1312
- if ! validate_direct_csv_modification "$temp_csv" "$TOTAL_IDEAS" "mixed"; then
1357
+ if ! validate_direct_csv_modification "$temp_csv" "$TOTAL_IDEAS" "mixed" "$ai_response"; then
1313
1358
  rm -f "$temp_csv"
1314
1359
  return 1
1315
1360
  fi
@@ -0,0 +1,120 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ echo "[INFO] Migrating existing CSVs to add LLM tracking columns"
6
+ echo "=========================================================="
7
+
8
+ # Get script directory
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+
11
+ # Function to add LLM columns to a CSV file
12
+ migrate_csv() {
13
+ local csv_file="$1"
14
+
15
+ echo "[INFO] Processing: $csv_file"
16
+
17
+ # Check if file exists
18
+ if [[ ! -f "$csv_file" ]]; then
19
+ echo "[WARN] File not found: $csv_file"
20
+ return 1
21
+ fi
22
+
23
+ # Check if it has a header
24
+ local header
25
+ header=$(head -1 "$csv_file")
26
+
27
+ if [[ ! "$header" =~ ^id, ]]; then
28
+ echo "[WARN] No valid CSV header found in: $csv_file"
29
+ return 1
30
+ fi
31
+
32
+ # Check if LLM columns already exist
33
+ if echo "$header" | grep -q "idea-LLM" && echo "$header" | grep -q "run-LLM"; then
34
+ echo "[SKIP] Already has LLM columns: $csv_file"
35
+ return 0
36
+ fi
37
+
38
+ # Create backup
39
+ cp "$csv_file" "${csv_file}.bak-$(date +%Y%m%d-%H%M%S)"
40
+ echo "[INFO] Created backup: ${csv_file}.bak-$(date +%Y%m%d-%H%M%S)"
41
+
42
+ # Add LLM columns to header
43
+ local new_header="$header,idea-LLM,run-LLM"
44
+
45
+ # Create temporary file
46
+ local temp_file="${csv_file}.tmp.$$"
47
+
48
+ # Write new header
49
+ echo "$new_header" > "$temp_file"
50
+
51
+ # Copy data rows (if any) and add empty LLM columns
52
+ if [[ $(wc -l < "$csv_file") -gt 1 ]]; then
53
+ tail -n +2 "$csv_file" | while IFS= read -r line; do
54
+ echo "$line,," >> "$temp_file"
55
+ done
56
+ fi
57
+
58
+ # Replace original file
59
+ mv "$temp_file" "$csv_file"
60
+
61
+ echo "[SUCCESS] Added LLM columns to: $csv_file"
62
+ return 0
63
+ }
64
+
65
+ # Parse arguments
66
+ if [[ $# -eq 0 ]]; then
67
+ echo "Usage: claude-evolve-migrate-llm-columns <csv_file> [csv_file...]"
68
+ echo " OR: claude-evolve-migrate-llm-columns --all"
69
+ echo ""
70
+ echo "Adds idea-LLM and run-LLM columns to existing evolution.csv files"
71
+ echo ""
72
+ echo "Options:"
73
+ echo " --all Find and migrate all evolution.csv files in parent directories"
74
+ exit 1
75
+ fi
76
+
77
+ if [[ "$1" == "--all" ]]; then
78
+ # Find all evolution.csv files
79
+ echo "[INFO] Searching for evolution.csv files..."
80
+
81
+ # Look in parent directories for evolution CSVs
82
+ csv_files=()
83
+ while IFS= read -r -d '' file; do
84
+ csv_files+=("$file")
85
+ done < <(find .. -name "evolution.csv" -type f -print0 2>/dev/null)
86
+
87
+ if [[ ${#csv_files[@]} -eq 0 ]]; then
88
+ echo "[INFO] No evolution.csv files found"
89
+ exit 0
90
+ fi
91
+
92
+ echo "[INFO] Found ${#csv_files[@]} evolution.csv files"
93
+
94
+ # Migrate each file
95
+ success_count=0
96
+ for csv_file in "${csv_files[@]}"; do
97
+ if migrate_csv "$csv_file"; then
98
+ ((success_count++))
99
+ fi
100
+ done
101
+
102
+ echo ""
103
+ echo "=========================================================="
104
+ echo "[INFO] Migration complete: $success_count/${#csv_files[@]} files migrated"
105
+
106
+ else
107
+ # Migrate specific files
108
+ success_count=0
109
+ total_count=$#
110
+
111
+ for csv_file in "$@"; do
112
+ if migrate_csv "$csv_file"; then
113
+ ((success_count++))
114
+ fi
115
+ done
116
+
117
+ echo ""
118
+ echo "=========================================================="
119
+ echo "[INFO] Migration complete: $success_count/$total_count files migrated"
120
+ fi
@@ -37,7 +37,7 @@ done
37
37
  # Create CSV with header
38
38
  if [[ ! -f evolution/evolution.csv ]]; then
39
39
  echo "[INFO] Creating evolution.csv with header..."
40
- echo "id,basedOnId,description,performance,status" >evolution/evolution.csv
40
+ echo "id,basedOnId,description,performance,status,idea-LLM,run-LLM" >evolution/evolution.csv
41
41
  else
42
42
  echo "[INFO] evolution.csv already exists, skipping"
43
43
  fi
@@ -137,9 +137,25 @@ try:
137
137
  # Collect all candidates with scores and statuses
138
138
  all_candidates = []
139
139
  stats_by_gen = {}
140
+ winners_by_gen = {}
140
141
  total_stats = {'pending': 0, 'complete': 0, 'failed': 0, 'running': 0}
141
142
  retry_count = 0
142
143
 
144
+ def parse_candidate_id(cid):
145
+ try:
146
+ left, right = cid.split('-', 1)
147
+ gen_num = int(left[3:]) if left.startswith('gen') else 10**9
148
+ seq_num = int(right)
149
+ return gen_num, seq_num
150
+ except Exception:
151
+ return 10**9, 10**9
152
+
153
+ def is_earlier(a, b):
154
+ ga, sa = parse_candidate_id(a)
155
+ gb, sb = parse_candidate_id(b)
156
+ return (ga, sa) < (gb, sb)
157
+
158
+
143
159
  for row in rows[1:]:
144
160
  if len(row) >= 1 and row[0]: # Must have an ID
145
161
  candidate_id = row[0]
@@ -176,13 +192,26 @@ try:
176
192
  score = float(performance)
177
193
  description = row[2] if len(row) > 2 else 'No description'
178
194
  all_candidates.append((candidate_id, description, score))
195
+
196
+ # Track per-generation best with raw score; ties -> earlier ID
197
+ if gen not in winners_by_gen:
198
+ winners_by_gen[gen] = (candidate_id, description, score)
199
+ else:
200
+ cid, _, best_score = winners_by_gen[gen]
201
+ if score > best_score or (score == best_score and is_earlier(candidate_id, cid)):
202
+ winners_by_gen[gen] = (candidate_id, description, score)
179
203
  except ValueError:
180
204
  pass
181
205
 
182
- # Find the winner
206
+ # Find the winner using raw score; ties -> earliest ID
183
207
  winner = None
184
- if all_candidates:
185
- winner = max(all_candidates, key=lambda x: x[2])
208
+ for cid, desc, sc in all_candidates:
209
+ if winner is None:
210
+ winner = (cid, desc, sc)
211
+ else:
212
+ wc = winner[2]
213
+ if sc > wc or (sc == wc and is_earlier(cid, winner[0])):
214
+ winner = (cid, desc, sc)
186
215
 
187
216
 
188
217
  # Show winner only
@@ -250,9 +279,8 @@ try:
250
279
  data = stats_by_gen[gen]
251
280
  total = sum(data.values())
252
281
 
253
- # Find best performer in this generation
254
- gen_candidates = [c for c in all_candidates if c[0].startswith(gen + '-')]
255
- gen_best = max(gen_candidates, key=lambda x: x[2]) if gen_candidates else None
282
+ # Find best performer in this generation (using precomputed winners_by_gen)
283
+ gen_best = winners_by_gen.get(gen)
256
284
 
257
285
  status_str = f'{data[\"pending\"]}p {data[\"complete\"]}c {data[\"failed\"]}f {data[\"running\"]}r'
258
286
 
@@ -269,4 +297,4 @@ try:
269
297
  except Exception as e:
270
298
  print(f'Error reading evolution status: {e}')
271
299
  sys.exit(1)
272
- "
300
+ "
@@ -7,6 +7,17 @@ source "$SCRIPT_DIR/../lib/config.sh"
7
7
  source "$SCRIPT_DIR/../lib/csv-lock.sh"
8
8
  source "$SCRIPT_DIR/../lib/ai-cli.sh"
9
9
 
10
+ # Setup logging to file
11
+ if [[ -n "${FULL_EVOLUTION_DIR:-}" ]]; then
12
+ LOG_DIR="$FULL_EVOLUTION_DIR/logs"
13
+ mkdir -p "$LOG_DIR"
14
+ LOG_FILE="$LOG_DIR/worker-$$-$(date +%Y%m%d-%H%M%S).log"
15
+
16
+ # Log to both terminal and file with timestamps
17
+ exec > >(while IFS= read -r line; do echo "$(date '+%Y-%m-%d %H:%M:%S'): $line"; done | tee -a "$LOG_FILE") 2>&1
18
+ echo "[WORKER-$$] Logging to: $LOG_FILE"
19
+ fi
20
+
10
21
  # Track current candidate for cleanup
11
22
  CURRENT_CANDIDATE_ID=""
12
23
  TERMINATION_SIGNAL=""
@@ -62,6 +73,17 @@ call_ai_for_evolution() {
62
73
  local prompt="$1"
63
74
  local candidate_id="$2"
64
75
 
76
+ # Get target file path from worker context
77
+ local target_file="$FULL_OUTPUT_DIR/evolution_${candidate_id}.py"
78
+
79
+ # Capture file state before AI call
80
+ local file_hash_before=""
81
+ local file_mtime_before=""
82
+ if [[ -f "$target_file" ]]; then
83
+ file_hash_before=$(shasum -a 256 "$target_file" 2>/dev/null | cut -d' ' -f1)
84
+ file_mtime_before=$(stat -f %m "$target_file" 2>/dev/null || stat -c %Y "$target_file" 2>/dev/null)
85
+ fi
86
+
65
87
  # Extract generation and ID numbers for round-robin calculation
66
88
  local gen_num=0
67
89
  local id_num=0
@@ -85,14 +107,32 @@ call_ai_for_evolution() {
85
107
  exit 3
86
108
  fi
87
109
 
88
- if [[ $ai_exit_code -eq 0 ]]; then
89
- echo "[WORKER-$$] AI succeeded" >&2
110
+ # Check if the target file was actually modified
111
+ local file_was_modified=false
112
+ if [[ -f "$target_file" ]]; then
113
+ local file_hash_after
114
+ local file_mtime_after
115
+ file_hash_after=$(shasum -a 256 "$target_file" 2>/dev/null | cut -d' ' -f1)
116
+ file_mtime_after=$(stat -f %m "$target_file" 2>/dev/null || stat -c %Y "$target_file" 2>/dev/null)
117
+
118
+ if [[ "$file_hash_before" != "$file_hash_after" ]] || [[ "$file_mtime_before" != "$file_mtime_after" ]]; then
119
+ file_was_modified=true
120
+ fi
121
+ fi
122
+
123
+ # Success if file was modified OR exit code is 0 (for cases where file validation isn't applicable)
124
+ if [[ "$file_was_modified" == "true" ]] || [[ $ai_exit_code -eq 0 ]]; then
125
+ if [[ "$file_was_modified" == "true" ]]; then
126
+ echo "[WORKER-$$] AI successfully modified $target_file (exit code: $ai_exit_code)" >&2
127
+ else
128
+ echo "[WORKER-$$] AI succeeded with exit code 0" >&2
129
+ fi
90
130
  # Output the result for the worker to use
91
131
  echo "$ai_output"
92
132
  return 0
93
133
  fi
94
134
 
95
- echo "[WORKER-$$] All AI models failed" >&2
135
+ echo "[WORKER-$$] AI failed: exit code $ai_exit_code, no file changes detected" >&2
96
136
  return 1
97
137
  }
98
138
 
@@ -208,6 +248,18 @@ CRITICAL: Do NOT use any git commands (git add, git commit, git reset, etc.). On
208
248
 
209
249
  echo "[WORKER-$$] Evolution applied successfully"
210
250
 
251
+ # Record which AI model generated the code (regardless of evaluation outcome)
252
+ if [[ -n "${SUCCESSFUL_RUN_MODEL:-}" ]]; then
253
+ echo "[WORKER-$$] Recording that $SUCCESSFUL_RUN_MODEL generated the code" >&2
254
+ "$PYTHON_CMD" -c "
255
+ import sys
256
+ sys.path.insert(0, '$SCRIPT_DIR/..')
257
+ from lib.evolution_csv import EvolutionCSV
258
+ with EvolutionCSV('$FULL_CSV_PATH') as csv:
259
+ csv.update_candidate_field('$candidate_id', 'run-LLM', '$SUCCESSFUL_RUN_MODEL')
260
+ " || echo "[WORKER-$$] Warning: Failed to record run-LLM field" >&2
261
+ fi
262
+
211
263
  # Check if the generated Python file has syntax errors
212
264
  echo "[WORKER-$$] Checking Python syntax..." >&2
213
265
  if ! "$PYTHON_CMD" -m py_compile "$target_file" 2>&1; then
package/lib/ai-cli.sh CHANGED
@@ -12,36 +12,50 @@ call_ai_model_configured() {
12
12
  local model_name="$1"
13
13
  local prompt="$2"
14
14
 
15
+ # Record start time
16
+ local start_time=$(date +%s)
17
+
15
18
  # Build command directly based on model
16
19
  case "$model_name" in
17
20
  opus|sonnet)
18
21
  local ai_output
19
- ai_output=$(timeout 300 claude --dangerously-skip-permissions --model "$model_name" -p "$prompt" 2>&1)
22
+ ai_output=$(timeout 180 claude --dangerously-skip-permissions --model "$model_name" -p "$prompt" 2>&1)
20
23
  local ai_exit_code=$?
21
24
  ;;
22
- gpt-5)
25
+ gpt5high)
23
26
  local ai_output
24
- ai_output=$(timeout 300 codex exec -m gpt-5 --dangerously-bypass-approvals-and-sandbox "$prompt" 2>&1)
27
+ ai_output=$(timeout 420 codex exec --profile gpt5high --dangerously-bypass-approvals-and-sandbox "$prompt" 2>&1)
25
28
  local ai_exit_code=$?
26
29
  ;;
27
- o3)
30
+ o3high)
28
31
  local ai_output
29
- ai_output=$(timeout 300 codex exec -m o3 --dangerously-bypass-approvals-and-sandbox "$prompt" 2>&1)
32
+ ai_output=$(timeout 500 codex exec --profile o3high --dangerously-bypass-approvals-and-sandbox "$prompt" 2>&1)
30
33
  local ai_exit_code=$?
31
34
  ;;
32
35
  codex)
33
36
  local ai_output
34
- ai_output=$(timeout 300 codex exec --dangerously-bypass-approvals-and-sandbox "$prompt" 2>&1)
37
+ ai_output=$(timeout 420 codex exec --dangerously-bypass-approvals-and-sandbox "$prompt" 2>&1)
35
38
  local ai_exit_code=$?
36
39
  ;;
37
40
  gemini)
38
41
  # Debug: Show exact command
39
- echo "[DEBUG] Running: timeout 300 gemini -y -p <prompt>" >&2
42
+ echo "[DEBUG] Running: timeout 1200 gemini -y -p <prompt>" >&2
40
43
  echo "[DEBUG] Working directory: $(pwd)" >&2
41
44
  echo "[DEBUG] Files in current dir:" >&2
42
45
  ls -la temp-csv-*.csv 2>&1 | head -5 >&2
43
46
  local ai_output
44
- ai_output=$(timeout 300 gemini -y -p "$prompt" 2>&1)
47
+ # Gemini needs longer timeout as it streams output while working (20 minutes)
48
+ ai_output=$(timeout 1200 gemini -y -p "$prompt" 2>&1)
49
+ local ai_exit_code=$?
50
+ ;;
51
+ cursor-sonnet)
52
+ local ai_output
53
+ ai_output=$(timeout 180 cursor-agent sonnet -p "$prompt" 2>&1)
54
+ local ai_exit_code=$?
55
+ ;;
56
+ cursor-opus)
57
+ local ai_output
58
+ ai_output=$(timeout 300 cursor-agent opus -p "$prompt" 2>&1)
45
59
  local ai_exit_code=$?
46
60
  ;;
47
61
  *)
@@ -53,8 +67,12 @@ call_ai_model_configured() {
53
67
  # Debug: log model and prompt size
54
68
  echo "[DEBUG] Calling $model_name with prompt of ${#prompt} characters" >&2
55
69
 
56
- # Always log basic info
57
- echo "[AI] $model_name exit code: $ai_exit_code, output length: ${#ai_output} chars" >&2
70
+ # Calculate duration
71
+ local end_time=$(date +%s)
72
+ local duration=$((end_time - start_time))
73
+
74
+ # Always log basic info with timing
75
+ echo "[AI] $model_name exit code: $ai_exit_code, output length: ${#ai_output} chars, duration: ${duration}s" >&2
58
76
 
59
77
  # Show detailed output if verbose or if there was an error
60
78
  if [[ "${VERBOSE_AI_OUTPUT:-false}" == "true" ]] || [[ $ai_exit_code -ne 0 ]]; then
@@ -105,7 +123,7 @@ clean_ai_output() {
105
123
  local model_name="$2"
106
124
 
107
125
  # Handle codex-specific output format
108
- if [[ "$model_name" == "codex" || "$model_name" == "o3" || "$model_name" == "gpt-5" ]]; then
126
+ if [[ "$model_name" == "codex" || "$model_name" == "o3high" || "$model_name" == "gpt5high" ]]; then
109
127
  # Clean codex output - extract content between "codex" marker and "tokens used"
110
128
  if echo "$output" | grep -q "^\[.*\] codex$"; then
111
129
  # Extract content between "codex" line and "tokens used" line
@@ -196,11 +214,19 @@ call_ai_with_round_robin() {
196
214
  ai_output=$(call_ai_model_configured "$model" "$prompt")
197
215
  local ai_exit_code=$?
198
216
 
199
- # Just check exit code
200
- if [[ $ai_exit_code -eq 0 ]]; then
201
- # Clean output if needed
202
- ai_output=$(clean_ai_output "$ai_output" "$model")
203
- echo "[AI] $model returned exit code 0" >&2
217
+ # Clean output if needed
218
+ ai_output=$(clean_ai_output "$ai_output" "$model")
219
+
220
+ # Success if exit code is 0, or if it's just a timeout (124)
221
+ # Timeout doesn't mean the AI failed - it may have completed the task
222
+ if [[ $ai_exit_code -eq 0 ]] || [[ $ai_exit_code -eq 124 ]]; then
223
+ if [[ $ai_exit_code -eq 124 ]]; then
224
+ echo "[AI] $model timed out but continuing (exit code: 124)" >&2
225
+ else
226
+ echo "[AI] $model returned exit code 0" >&2
227
+ fi
228
+ # Export the successful model for tracking (used by worker)
229
+ export SUCCESSFUL_RUN_MODEL="$model"
204
230
  # Debug: log what AI returned on success
205
231
  if [[ "${DEBUG_AI_SUCCESS:-}" == "true" ]]; then
206
232
  echo "[AI] $model success output preview:" >&2
package/lib/config.sh CHANGED
@@ -54,8 +54,8 @@ DEFAULT_MAX_RETRIES=3
54
54
  DEFAULT_MEMORY_LIMIT_MB=12288
55
55
 
56
56
  # Default LLM CLI configuration - use simple variables instead of arrays
57
- DEFAULT_LLM_RUN="sonnet gpt-5 sonnet gpt-5"
58
- DEFAULT_LLM_IDEATE="gemini gpt-5 opus"
57
+ DEFAULT_LLM_RUN="sonnet cursor-sonnet"
58
+ DEFAULT_LLM_IDEATE="gemini opus gpt5high o3high cursor-opus"
59
59
 
60
60
  # Load configuration from config file
61
61
  load_config() {
@@ -98,12 +98,14 @@ load_config() {
98
98
  # Set LLM CLI defaults (compatibility for older bash)
99
99
  # Initialize associative array for LLM commands
100
100
  # Use simpler approach for compatibility
101
- LLM_CLI_gpt_5='codex exec -m gpt-5 --dangerously-bypass-approvals-and-sandbox "{{PROMPT}}"'
102
- LLM_CLI_o3='codex exec -m o3 --dangerously-bypass-approvals-and-sandbox "{{PROMPT}}"'
101
+ LLM_CLI_gpt5high='codex exec --profile gpt5high --dangerously-bypass-approvals-and-sandbox "{{PROMPT}}"'
102
+ LLM_CLI_o3high='codex exec --profile o3high --dangerously-bypass-approvals-and-sandbox "{{PROMPT}}"'
103
103
  LLM_CLI_codex='codex exec --dangerously-bypass-approvals-and-sandbox "{{PROMPT}}"'
104
104
  LLM_CLI_gemini='gemini -y -p "{{PROMPT}}"'
105
105
  LLM_CLI_opus='claude --dangerously-skip-permissions --model opus -p "{{PROMPT}}"'
106
106
  LLM_CLI_sonnet='claude --dangerously-skip-permissions --model sonnet -p "{{PROMPT}}"'
107
+ LLM_CLI_cursor_sonnet='cursor-agent sonnet -p "{{PROMPT}}"'
108
+ LLM_CLI_cursor_opus='cursor-agent opus -p "{{PROMPT}}"'
107
109
  LLM_RUN="$DEFAULT_LLM_RUN"
108
110
  LLM_IDEATE="$DEFAULT_LLM_IDEATE"
109
111
 
@@ -322,12 +324,13 @@ show_config() {
322
324
  echo " Memory limit: ${MEMORY_LIMIT_MB}MB"
323
325
  echo " LLM configuration:"
324
326
  # Show LLM configurations using dynamic variable names
325
- for model in gpt_5 o3 codex gemini opus sonnet; do
327
+ for model in gpt5high o3high codex gemini opus sonnet cursor_sonnet cursor_opus; do
326
328
  var_name="LLM_CLI_${model}"
327
- if [[ -n "${!var_name}" ]]; then
329
+ var_value=$(eval echo "\$$var_name")
330
+ if [[ -n "$var_value" ]]; then
328
331
  # Convert underscore back to dash for display
329
332
  display_name=$(echo "$model" | sed 's/_/-/g')
330
- echo " $display_name: ${!var_name}"
333
+ echo " $display_name: $var_value"
331
334
  fi
332
335
  done
333
336
  echo " LLM for run: $LLM_RUN"
@@ -199,7 +199,7 @@ class EvolutionCSV:
199
199
  for i in range(start_idx, len(rows)):
200
200
  row = rows[i]
201
201
 
202
- if self.is_valid_candidate_row(row) and row[0].strip() == candidate_id:
202
+ if self.is_valid_candidate_row(row) and row[0].strip().strip('"') == candidate_id.strip().strip('"'):
203
203
  # Ensure row has at least 5 columns
204
204
  while len(row) < 5:
205
205
  row.append('')
@@ -227,7 +227,7 @@ class EvolutionCSV:
227
227
  for i in range(start_idx, len(rows)):
228
228
  row = rows[i]
229
229
 
230
- if self.is_valid_candidate_row(row) and row[0].strip() == candidate_id:
230
+ if self.is_valid_candidate_row(row) and row[0].strip().strip('"') == candidate_id.strip().strip('"'):
231
231
  # Ensure row has at least 4 columns
232
232
  while len(row) < 4:
233
233
  row.append('')
@@ -263,10 +263,14 @@ class EvolutionCSV:
263
263
  field_index = i
264
264
  break
265
265
 
266
- # If field doesn't exist, add it to header
266
+ # If field doesn't exist, add it to header and extend all rows
267
267
  if field_index is None:
268
268
  field_index = len(header_row)
269
269
  header_row.append(field_name)
270
+ # Extend all data rows with empty values for the new column
271
+ for i in range(1, len(rows)):
272
+ while len(rows[i]) <= field_index:
273
+ rows[i].append('')
270
274
  else:
271
275
  # No header - we'll use predefined positions for known fields
272
276
  field_map = {
@@ -274,7 +278,9 @@ class EvolutionCSV:
274
278
  'basedonid': 1,
275
279
  'description': 2,
276
280
  'performance': 3,
277
- 'status': 4
281
+ 'status': 4,
282
+ 'idea-llm': 5,
283
+ 'run-llm': 6
278
284
  }
279
285
  field_index = field_map.get(field_name.lower())
280
286
  if field_index is None:
@@ -287,7 +293,10 @@ class EvolutionCSV:
287
293
 
288
294
  for i in range(start_idx, len(rows)):
289
295
  row = rows[i]
290
- if self.is_valid_candidate_row(row) and row[0].strip() == candidate_id:
296
+ # Strip quotes from both stored ID and search ID for comparison
297
+ stored_id = row[0].strip().strip('"') if len(row) > 0 else ''
298
+ search_id = candidate_id.strip().strip('"')
299
+ if self.is_valid_candidate_row(row) and stored_id == search_id:
291
300
  # Ensure row has enough columns
292
301
  while len(row) <= field_index:
293
302
  row.append('')
@@ -311,7 +320,7 @@ class EvolutionCSV:
311
320
  start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
312
321
 
313
322
  for row in rows[start_idx:]:
314
- if self.is_valid_candidate_row(row) and row[0].strip() == candidate_id:
323
+ if self.is_valid_candidate_row(row) and row[0].strip().strip('"') == candidate_id.strip().strip('"'):
315
324
  return {
316
325
  'id': row[0].strip() if len(row) > 0 else '',
317
326
  'basedOnId': row[1].strip() if len(row) > 1 else '',
@@ -344,7 +353,7 @@ class EvolutionCSV:
344
353
 
345
354
  for i in range(start_idx, len(rows)):
346
355
  row = rows[i]
347
- if self.is_valid_candidate_row(row) and row[0].strip() == candidate_id:
356
+ if self.is_valid_candidate_row(row) and row[0].strip().strip('"') == candidate_id.strip().strip('"'):
348
357
  deleted = True
349
358
  # Skip this row (delete it)
350
359
  continue
@@ -371,6 +380,7 @@ def main():
371
380
  print(" update <id> <status> - Update candidate status")
372
381
  print(" perf <id> <performance> - Update candidate performance")
373
382
  print(" info <id> - Get candidate info")
383
+ print(" field <id> <field> <val>- Update specific field")
374
384
  print(" check - Check if has pending work")
375
385
  sys.exit(1)
376
386
 
@@ -430,6 +440,17 @@ def main():
430
440
  has_work = csv_ops.has_pending_work()
431
441
  print("yes" if has_work else "no")
432
442
 
443
+ elif command == 'field' and len(sys.argv) >= 5:
444
+ candidate_id = sys.argv[3]
445
+ field_name = sys.argv[4]
446
+ value = sys.argv[5] if len(sys.argv) >= 6 else ''
447
+ success = csv_ops.update_candidate_field(candidate_id, field_name, value)
448
+ if success:
449
+ print(f"Updated {candidate_id} field {field_name} to {value}")
450
+ else:
451
+ print(f"Failed to update {candidate_id} field {field_name}")
452
+ sys.exit(1)
453
+
433
454
  else:
434
455
  print(f"Unknown command: {command}")
435
456
  sys.exit(1)
@@ -58,12 +58,19 @@ def monitor_memory_usage_native(process: subprocess.Popen, limit_mb: int) -> Opt
58
58
 
59
59
  if memory_mb > limit_mb:
60
60
  print(f"[MEMORY] Process exceeded {limit_mb}MB limit (using {memory_mb:.1f}MB), terminating", file=sys.stderr)
61
- # Kill the entire process group
61
+ # Kill the entire process group - fix race condition
62
+ try:
63
+ pgid = os.getpgid(process.pid)
64
+ os.killpg(pgid, signal.SIGTERM)
65
+ except ProcessLookupError:
66
+ return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
67
+
68
+ time.sleep(2) # Give it time to cleanup
69
+
62
70
  try:
63
- os.killpg(os.getpgid(process.pid), signal.SIGTERM)
64
- time.sleep(2) # Give it time to cleanup
65
71
  if process.poll() is None:
66
- os.killpg(os.getpgid(process.pid), signal.SIGKILL)
72
+ pgid = os.getpgid(process.pid)
73
+ os.killpg(pgid, signal.SIGKILL)
67
74
  except ProcessLookupError:
68
75
  pass
69
76
  return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
@@ -92,12 +99,19 @@ def monitor_memory_usage(process: subprocess.Popen, limit_mb: int) -> Optional[s
92
99
 
93
100
  if memory_mb > limit_mb:
94
101
  print(f"[MEMORY] Process exceeded {limit_mb}MB limit (using {memory_mb:.1f}MB), terminating", file=sys.stderr)
95
- # Kill the entire process group
102
+ # Kill the entire process group - fix race condition
103
+ try:
104
+ pgid = os.getpgid(process.pid)
105
+ os.killpg(pgid, signal.SIGTERM)
106
+ except ProcessLookupError:
107
+ return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
108
+
109
+ time.sleep(2) # Give it time to cleanup
110
+
96
111
  try:
97
- os.killpg(os.getpgid(process.pid), signal.SIGTERM)
98
- time.sleep(2) # Give it time to cleanup
99
112
  if process.poll() is None:
100
- os.killpg(os.getpgid(process.pid), signal.SIGKILL)
113
+ pgid = os.getpgid(process.pid)
114
+ os.killpg(pgid, signal.SIGKILL)
101
115
  except ProcessLookupError:
102
116
  pass
103
117
  return f"Memory limit exceeded: {memory_mb:.1f}MB > {limit_mb}MB"
@@ -112,6 +126,19 @@ def monitor_memory_usage(process: subprocess.Popen, limit_mb: int) -> Optional[s
112
126
 
113
127
  return None
114
128
 
129
+ def validate_memory_limit(limit_mb: int) -> bool:
130
+ """Validate memory limit against system resources."""
131
+ if limit_mb <= 0:
132
+ return True # 0 or negative means disabled
133
+
134
+ # Basic sanity checks
135
+ if limit_mb < 10:
136
+ print(f"[MEMORY] Warning: Memory limit {limit_mb}MB is very small", file=sys.stderr)
137
+ elif limit_mb > 64000:
138
+ print(f"[MEMORY] Warning: Memory limit {limit_mb}MB is very large", file=sys.stderr)
139
+
140
+ return True
141
+
115
142
  def main():
116
143
  if len(sys.argv) < 3:
117
144
  print("Usage: memory_limit_wrapper.py <memory_limit_mb> <command> [args...]", file=sys.stderr)
@@ -123,6 +150,9 @@ def main():
123
150
  print(f"Error: Invalid memory limit '{sys.argv[1]}' - must be integer MB", file=sys.stderr)
124
151
  sys.exit(1)
125
152
 
153
+ if not validate_memory_limit(memory_limit_mb):
154
+ sys.exit(1)
155
+
126
156
  command = sys.argv[2:]
127
157
 
128
158
  if memory_limit_mb <= 0:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.5.4",
3
+ "version": "1.5.8",
4
4
  "bin": {
5
5
  "claude-evolve": "./bin/claude-evolve",
6
6
  "claude-evolve-main": "./bin/claude-evolve-main",
@@ -47,9 +47,11 @@ auto_ideate: true
47
47
  max_retries: 3
48
48
 
49
49
  # Memory protection configuration
50
- # Memory limit in MB for evaluation processes (0 = no limit)
50
+ # Memory limit in MB for evaluation processes (0 = no limit)
51
51
  # This prevents runaway algorithms from consuming all system memory
52
- memory_limit_mb: 2048
52
+ # Default: 12GB (reasonable for ML workloads, adjust based on your system RAM)
53
+ # Recommendation: Set to ~50-75% of available system RAM
54
+ memory_limit_mb: 12288
53
55
 
54
56
  # Parallel execution configuration
55
57
  parallel:
@@ -70,5 +72,14 @@ llm_cli:
70
72
 
71
73
  # commented out because these change over time; if you want to fix them in a particular
72
74
  # configuration, uncomment them and set them
73
- #run: sonnet
74
- #ideate: gemini gpt-5 opus
75
+ #run: sonnet cursor-sonnet
76
+ #ideate: gemini opus gpt5high o3high cursor-opus
77
+
78
+ # Available models:
79
+ # - sonnet: Claude 3.5 Sonnet via Claude CLI
80
+ # - opus: Claude 3 Opus via Claude CLI
81
+ # - gemini: Gemini via Gemini CLI
82
+ # - gpt5high: GPT-5 via Codex CLI (high reasoning)
83
+ # - o3high: O3 via Codex CLI (high reasoning)
84
+ # - cursor-sonnet: Claude 3.5 Sonnet via Cursor Agent CLI
85
+ # - cursor-opus: Claude 3 Opus via Cursor Agent CLI