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.
- package/bin/claude-evolve-autostatus +45 -14
- package/bin/claude-evolve-ideate +53 -8
- package/bin/claude-evolve-migrate-llm-columns +120 -0
- package/bin/claude-evolve-setup +1 -1
- package/bin/claude-evolve-status +35 -7
- package/bin/claude-evolve-worker +55 -3
- package/lib/__pycache__/evolution_csv.cpython-310.pyc +0 -0
- package/lib/ai-cli.sh +42 -16
- package/lib/config.sh +10 -7
- package/lib/evolution_csv.py +28 -7
- package/lib/memory_limit_wrapper.py +38 -8
- package/package.json +1 -1
- package/templates/config.yaml +15 -4
- package/lib/__pycache__/evolution_csv.cpython-311.pyc +0 -0
- package/lib/__pycache__/evolution_csv.cpython-313.pyc +0 -0
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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]
|
|
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
|
-
|
|
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
|
+
'
|
package/bin/claude-evolve-ideate
CHANGED
|
@@ -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
|
package/bin/claude-evolve-setup
CHANGED
|
@@ -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
|
package/bin/claude-evolve-status
CHANGED
|
@@ -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
|
-
|
|
185
|
-
winner
|
|
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
|
-
|
|
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
|
+
"
|
package/bin/claude-evolve-worker
CHANGED
|
@@ -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
|
|
89
|
-
|
|
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-$$]
|
|
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
|
|
Binary file
|
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
|
|
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
|
-
|
|
25
|
+
gpt5high)
|
|
23
26
|
local ai_output
|
|
24
|
-
ai_output=$(timeout
|
|
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
|
-
|
|
30
|
+
o3high)
|
|
28
31
|
local ai_output
|
|
29
|
-
ai_output=$(timeout
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
57
|
-
|
|
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" == "
|
|
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
|
-
#
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
58
|
-
DEFAULT_LLM_IDEATE="gemini
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
327
|
+
for model in gpt5high o3high codex gemini opus sonnet cursor_sonnet cursor_opus; do
|
|
326
328
|
var_name="LLM_CLI_${model}"
|
|
327
|
-
|
|
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: $
|
|
333
|
+
echo " $display_name: $var_value"
|
|
331
334
|
fi
|
|
332
335
|
done
|
|
333
336
|
echo " LLM for run: $LLM_RUN"
|
package/lib/evolution_csv.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
package/templates/config.yaml
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
Binary file
|
|
Binary file
|