claude-evolve 1.5.3 → 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/claude-evolve-analyze +36 -12
- package/bin/claude-evolve-autostatus +35 -12
- package/bin/claude-evolve-edit +182 -17
- package/bin/claude-evolve-ideate +130 -40
- package/bin/claude-evolve-migrate-llm-columns +120 -0
- package/bin/claude-evolve-run +71 -1
- package/bin/claude-evolve-setup +1 -1
- package/bin/claude-evolve-status +35 -7
- package/bin/claude-evolve-worker +151 -42
- package/lib/__pycache__/evolution_csv.cpython-311.pyc +0 -0
- package/lib/__pycache__/evolution_csv.cpython-313.pyc +0 -0
- package/lib/ai-cli.sh +45 -14
- package/lib/config.sh +27 -18
- package/lib/csv_fixer.py +35 -0
- package/lib/evolution_csv.py +28 -7
- package/lib/memory_limit_wrapper.py +222 -0
- package/package.json +1 -1
- package/templates/config.yaml +21 -10
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=""
|
|
@@ -14,34 +25,11 @@ TERMINATION_SIGNAL=""
|
|
|
14
25
|
# Cleanup function to handle termination
|
|
15
26
|
cleanup_on_exit() {
|
|
16
27
|
if [[ -n "$CURRENT_CANDIDATE_ID" ]]; then
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import sys
|
|
23
|
-
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
24
|
-
from lib.evolution_csv import EvolutionCSV
|
|
25
|
-
try:
|
|
26
|
-
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
27
|
-
csv.update_candidate_status('$CURRENT_CANDIDATE_ID', 'failed')
|
|
28
|
-
except:
|
|
29
|
-
pass # Best effort cleanup
|
|
30
|
-
" 2>/dev/null || true
|
|
31
|
-
else
|
|
32
|
-
echo "[WORKER-$$] Interrupted, leaving $CURRENT_CANDIDATE_ID for retry" >&2
|
|
33
|
-
# Optionally reset to pending instead of leaving as running
|
|
34
|
-
"$PYTHON_CMD" -c "
|
|
35
|
-
import sys
|
|
36
|
-
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
37
|
-
from lib.evolution_csv import EvolutionCSV
|
|
38
|
-
try:
|
|
39
|
-
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
40
|
-
csv.update_candidate_status('$CURRENT_CANDIDATE_ID', 'pending')
|
|
41
|
-
except:
|
|
42
|
-
pass # Best effort cleanup
|
|
43
|
-
" 2>/dev/null || true
|
|
44
|
-
fi
|
|
28
|
+
echo "[WORKER-$$] Worker terminated while processing $CURRENT_CANDIDATE_ID" >&2
|
|
29
|
+
# If we're interrupted while processing, leave it as "running"
|
|
30
|
+
# This prevents other workers from picking it up in the same session
|
|
31
|
+
# A human can manually reset to pending if needed
|
|
32
|
+
echo "[WORKER-$$] Leaving $CURRENT_CANDIDATE_ID in current state" >&2
|
|
45
33
|
fi
|
|
46
34
|
}
|
|
47
35
|
|
|
@@ -85,6 +73,17 @@ call_ai_for_evolution() {
|
|
|
85
73
|
local prompt="$1"
|
|
86
74
|
local candidate_id="$2"
|
|
87
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
|
+
|
|
88
87
|
# Extract generation and ID numbers for round-robin calculation
|
|
89
88
|
local gen_num=0
|
|
90
89
|
local id_num=0
|
|
@@ -108,14 +107,32 @@ call_ai_for_evolution() {
|
|
|
108
107
|
exit 3
|
|
109
108
|
fi
|
|
110
109
|
|
|
111
|
-
if
|
|
112
|
-
|
|
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
|
|
113
130
|
# Output the result for the worker to use
|
|
114
131
|
echo "$ai_output"
|
|
115
132
|
return 0
|
|
116
133
|
fi
|
|
117
134
|
|
|
118
|
-
echo "[WORKER-$$]
|
|
135
|
+
echo "[WORKER-$$] AI failed: exit code $ai_exit_code, no file changes detected" >&2
|
|
119
136
|
return 1
|
|
120
137
|
}
|
|
121
138
|
|
|
@@ -135,6 +152,12 @@ process_candidate() {
|
|
|
135
152
|
echo "[WORKER-$$] Description: $description"
|
|
136
153
|
echo "[WORKER-$$] Based on ID: $parent_id"
|
|
137
154
|
|
|
155
|
+
# Treat "baseline-000" parent ID as empty/baseline
|
|
156
|
+
if [[ "$parent_id" == "baseline-000" ]]; then
|
|
157
|
+
parent_id=""
|
|
158
|
+
echo "[WORKER-$$] Parent ID 'baseline-000' treated as baseline (empty parent)"
|
|
159
|
+
fi
|
|
160
|
+
|
|
138
161
|
# Determine source algorithm
|
|
139
162
|
local source_file
|
|
140
163
|
if [[ -z "$parent_id" ]]; then
|
|
@@ -213,16 +236,38 @@ CRITICAL: Do NOT use any git commands (git add, git commit, git reset, etc.). On
|
|
|
213
236
|
|
|
214
237
|
# Try AI models with round-robin based on candidate ID
|
|
215
238
|
if ! call_ai_for_evolution "$evolution_prompt" "$candidate_id"; then
|
|
216
|
-
echo "[WORKER-$$] ERROR: All AI models failed to generate code" >&2
|
|
239
|
+
echo "[WORKER-$$] ERROR: All AI models failed to generate code - leaving as pending for retry" >&2
|
|
217
240
|
cd "$original_pwd"
|
|
218
241
|
rm -f "$target_file" # Clean up on failure
|
|
219
|
-
|
|
242
|
+
# Return with special code to indicate AI failure (should remain pending)
|
|
243
|
+
return 77
|
|
220
244
|
fi
|
|
221
245
|
|
|
222
246
|
# Restore working directory
|
|
223
247
|
cd "$original_pwd"
|
|
224
248
|
|
|
225
249
|
echo "[WORKER-$$] Evolution applied successfully"
|
|
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
|
+
|
|
263
|
+
# Check if the generated Python file has syntax errors
|
|
264
|
+
echo "[WORKER-$$] Checking Python syntax..." >&2
|
|
265
|
+
if ! "$PYTHON_CMD" -m py_compile "$target_file" 2>&1; then
|
|
266
|
+
echo "[WORKER-$$] ERROR: Generated Python file has syntax errors!" >&2
|
|
267
|
+
echo "[WORKER-$$] File: $target_file" >&2
|
|
268
|
+
# This is still an evaluation failure, not an AI failure
|
|
269
|
+
return 1
|
|
270
|
+
fi
|
|
226
271
|
fi
|
|
227
272
|
fi
|
|
228
273
|
|
|
@@ -239,11 +284,22 @@ CRITICAL: Do NOT use any git commands (git add, git commit, git reset, etc.). On
|
|
|
239
284
|
eval_arg=""
|
|
240
285
|
fi
|
|
241
286
|
local eval_cmd=("$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$eval_arg")
|
|
287
|
+
|
|
288
|
+
# Add memory limiting if configured
|
|
289
|
+
if [[ -n "$MEMORY_LIMIT_MB" ]] && [[ "$MEMORY_LIMIT_MB" -gt 0 ]]; then
|
|
290
|
+
eval_cmd=("$PYTHON_CMD" "$SCRIPT_DIR/../lib/memory_limit_wrapper.py" "$MEMORY_LIMIT_MB" "${eval_cmd[@]}")
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
# Add timeout if configured
|
|
242
294
|
[[ -n "$timeout_seconds" ]] && eval_cmd=(timeout "$timeout_seconds" "${eval_cmd[@]}")
|
|
243
295
|
|
|
244
296
|
# Run evaluation with tee to both display and capture output
|
|
245
297
|
# Use stdbuf to disable buffering for real-time output
|
|
246
|
-
|
|
298
|
+
# IMPORTANT: Use PIPESTATUS to get the exit code of the evaluation command, not tee
|
|
299
|
+
stdbuf -o0 -e0 "${eval_cmd[@]}" 2>&1 | tee "$eval_output_file" >&2
|
|
300
|
+
local eval_exit_code=${PIPESTATUS[0]} # Get exit code of first command in pipe
|
|
301
|
+
|
|
302
|
+
if [[ $eval_exit_code -eq 0 ]]; then
|
|
247
303
|
local eval_end=$(date +%s)
|
|
248
304
|
local eval_duration=$((eval_end - eval_start))
|
|
249
305
|
|
|
@@ -353,14 +409,19 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
|
353
409
|
echo "[WORKER-$$] Output: $eval_output" >&2
|
|
354
410
|
# rm -f "$eval_output_file" # Keep for debugging
|
|
355
411
|
echo "[WORKER-$$] Evaluation output saved to: $eval_output_file" >&2
|
|
412
|
+
# Clear CURRENT_CANDIDATE_ID before returning
|
|
413
|
+
CURRENT_CANDIDATE_ID=""
|
|
356
414
|
return 1
|
|
357
415
|
fi
|
|
358
416
|
|
|
359
417
|
# Clean up temp file (comment out to keep for debugging)
|
|
360
418
|
# rm -f "$eval_output_file"
|
|
361
419
|
echo "[WORKER-$$] Evaluation output saved to: $eval_output_file" >&2
|
|
420
|
+
|
|
421
|
+
# Clear CURRENT_CANDIDATE_ID on successful completion
|
|
422
|
+
CURRENT_CANDIDATE_ID=""
|
|
362
423
|
else
|
|
363
|
-
local exit_code
|
|
424
|
+
local exit_code=$eval_exit_code
|
|
364
425
|
# Read any output that was captured before failure
|
|
365
426
|
eval_output=$(<"$eval_output_file")
|
|
366
427
|
# rm -f "$eval_output_file" # Keep for debugging
|
|
@@ -370,22 +431,56 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
|
370
431
|
echo "[WORKER-$$] Output: $eval_output" >&2
|
|
371
432
|
|
|
372
433
|
# Mark as failed in CSV
|
|
373
|
-
"$
|
|
434
|
+
echo "[WORKER-$$] Marking $candidate_id as failed in CSV" >&2
|
|
435
|
+
if ! "$PYTHON_CMD" -c "
|
|
374
436
|
import sys
|
|
375
437
|
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
376
438
|
from lib.evolution_csv import EvolutionCSV
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
439
|
+
try:
|
|
440
|
+
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
441
|
+
success = csv.update_candidate_status('$candidate_id', 'failed')
|
|
442
|
+
if not success:
|
|
443
|
+
print(f'ERROR: Failed to update status for {candidate_id}', file=sys.stderr)
|
|
444
|
+
sys.exit(1)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
print(f'ERROR: Exception updating status: {e}', file=sys.stderr)
|
|
447
|
+
sys.exit(1)
|
|
448
|
+
" 2>&1; then
|
|
449
|
+
echo "[WORKER-$$] ERROR: Failed to update CSV status to failed" >&2
|
|
450
|
+
else
|
|
451
|
+
echo "[WORKER-$$] Successfully marked $candidate_id as failed" >&2
|
|
452
|
+
fi
|
|
380
453
|
|
|
454
|
+
# Clear CURRENT_CANDIDATE_ID before returning to prevent cleanup handler from resetting it
|
|
455
|
+
CURRENT_CANDIDATE_ID=""
|
|
381
456
|
return $exit_code
|
|
382
457
|
fi
|
|
383
458
|
}
|
|
384
459
|
|
|
460
|
+
# Don't reset running candidates on startup - they might be legitimately being processed by another worker
|
|
461
|
+
|
|
385
462
|
# Main worker loop
|
|
386
463
|
echo "[WORKER-$$] Worker started"
|
|
387
464
|
|
|
388
465
|
while true; do
|
|
466
|
+
# Debug: Show current status of all candidates
|
|
467
|
+
echo "[WORKER-$$] Current candidate statuses:" >&2
|
|
468
|
+
"$PYTHON_CMD" -c "
|
|
469
|
+
import sys
|
|
470
|
+
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
471
|
+
from lib.evolution_csv import EvolutionCSV
|
|
472
|
+
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
473
|
+
rows = csv._read_csv()
|
|
474
|
+
if rows:
|
|
475
|
+
start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
|
|
476
|
+
status_count = {}
|
|
477
|
+
for row in rows[start_idx:]:
|
|
478
|
+
if len(row) > 4:
|
|
479
|
+
status = row[4].strip() or 'pending'
|
|
480
|
+
status_count[status] = status_count.get(status, 0) + 1
|
|
481
|
+
print(f'Status counts: {status_count}', file=sys.stderr)
|
|
482
|
+
" 2>&1 || true
|
|
483
|
+
|
|
389
484
|
# Try to claim a pending candidate
|
|
390
485
|
candidate_info=$("$PYTHON_CMD" -c "
|
|
391
486
|
import sys
|
|
@@ -412,12 +507,26 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
|
412
507
|
# Set current candidate for cleanup
|
|
413
508
|
CURRENT_CANDIDATE_ID="$candidate_id"
|
|
414
509
|
|
|
415
|
-
# Process the candidate
|
|
416
|
-
|
|
510
|
+
# Process the candidate and capture exit code
|
|
511
|
+
process_candidate "$candidate_id" "$parent_id" "$description"
|
|
512
|
+
process_exit_code=$?
|
|
513
|
+
|
|
514
|
+
if [[ $process_exit_code -eq 0 ]]; then
|
|
417
515
|
echo "[WORKER-$$] Successfully processed $candidate_id"
|
|
516
|
+
elif [[ $process_exit_code -eq 77 ]]; then
|
|
517
|
+
# Special exit code 77 means AI failed to generate code
|
|
518
|
+
echo "[WORKER-$$] AI generation failed for $candidate_id - marking as failed-ai-retry"
|
|
519
|
+
# Mark with special status that indicates AI generation failed (not evaluation)
|
|
520
|
+
"$PYTHON_CMD" -c "
|
|
521
|
+
import sys
|
|
522
|
+
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
523
|
+
from lib.evolution_csv import EvolutionCSV
|
|
524
|
+
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
525
|
+
csv.update_candidate_status('$candidate_id', 'failed-ai-retry')
|
|
526
|
+
" 2>/dev/null || true
|
|
418
527
|
else
|
|
419
528
|
echo "[WORKER-$$] Failed to process $candidate_id"
|
|
420
|
-
#
|
|
529
|
+
# Other failures (evaluation errors, etc) mark as failed
|
|
421
530
|
"$PYTHON_CMD" -c "
|
|
422
531
|
import sys
|
|
423
532
|
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
Binary file
|
|
Binary file
|
package/lib/ai-cli.sh
CHANGED
|
@@ -12,31 +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)
|
|
23
|
+
local ai_exit_code=$?
|
|
24
|
+
;;
|
|
25
|
+
gpt5high)
|
|
26
|
+
local ai_output
|
|
27
|
+
ai_output=$(timeout 420 codex exec --profile gpt5high --dangerously-bypass-approvals-and-sandbox "$prompt" 2>&1)
|
|
20
28
|
local ai_exit_code=$?
|
|
21
29
|
;;
|
|
22
|
-
|
|
30
|
+
o3high)
|
|
23
31
|
local ai_output
|
|
24
|
-
ai_output=$(timeout
|
|
32
|
+
ai_output=$(timeout 500 codex exec --profile o3high --dangerously-bypass-approvals-and-sandbox "$prompt" 2>&1)
|
|
25
33
|
local ai_exit_code=$?
|
|
26
34
|
;;
|
|
27
35
|
codex)
|
|
28
36
|
local ai_output
|
|
29
|
-
ai_output=$(timeout
|
|
37
|
+
ai_output=$(timeout 420 codex exec --dangerously-bypass-approvals-and-sandbox "$prompt" 2>&1)
|
|
30
38
|
local ai_exit_code=$?
|
|
31
39
|
;;
|
|
32
40
|
gemini)
|
|
33
41
|
# Debug: Show exact command
|
|
34
|
-
echo "[DEBUG] Running: timeout
|
|
42
|
+
echo "[DEBUG] Running: timeout 1200 gemini -y -p <prompt>" >&2
|
|
35
43
|
echo "[DEBUG] Working directory: $(pwd)" >&2
|
|
36
44
|
echo "[DEBUG] Files in current dir:" >&2
|
|
37
45
|
ls -la temp-csv-*.csv 2>&1 | head -5 >&2
|
|
38
46
|
local ai_output
|
|
39
|
-
|
|
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)
|
|
40
59
|
local ai_exit_code=$?
|
|
41
60
|
;;
|
|
42
61
|
*)
|
|
@@ -48,8 +67,12 @@ call_ai_model_configured() {
|
|
|
48
67
|
# Debug: log model and prompt size
|
|
49
68
|
echo "[DEBUG] Calling $model_name with prompt of ${#prompt} characters" >&2
|
|
50
69
|
|
|
51
|
-
#
|
|
52
|
-
|
|
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
|
|
53
76
|
|
|
54
77
|
# Show detailed output if verbose or if there was an error
|
|
55
78
|
if [[ "${VERBOSE_AI_OUTPUT:-false}" == "true" ]] || [[ $ai_exit_code -ne 0 ]]; then
|
|
@@ -100,7 +123,7 @@ clean_ai_output() {
|
|
|
100
123
|
local model_name="$2"
|
|
101
124
|
|
|
102
125
|
# Handle codex-specific output format
|
|
103
|
-
if [[ "$model_name" == "codex" || "$model_name" == "
|
|
126
|
+
if [[ "$model_name" == "codex" || "$model_name" == "o3high" || "$model_name" == "gpt5high" ]]; then
|
|
104
127
|
# Clean codex output - extract content between "codex" marker and "tokens used"
|
|
105
128
|
if echo "$output" | grep -q "^\[.*\] codex$"; then
|
|
106
129
|
# Extract content between "codex" line and "tokens used" line
|
|
@@ -191,11 +214,19 @@ call_ai_with_round_robin() {
|
|
|
191
214
|
ai_output=$(call_ai_model_configured "$model" "$prompt")
|
|
192
215
|
local ai_exit_code=$?
|
|
193
216
|
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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"
|
|
199
230
|
# Debug: log what AI returned on success
|
|
200
231
|
if [[ "${DEBUG_AI_SUCCESS:-}" == "true" ]]; then
|
|
201
232
|
echo "[AI] $model success output preview:" >&2
|
package/lib/config.sh
CHANGED
|
@@ -49,17 +49,13 @@ DEFAULT_AUTO_IDEATE=true
|
|
|
49
49
|
# Default retry value
|
|
50
50
|
DEFAULT_MAX_RETRIES=3
|
|
51
51
|
|
|
52
|
-
# Default
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
DEFAULT_LLM_CLI_VALUES[3]='claude --dangerously-skip-permissions --model opus -p "{{PROMPT}}"'
|
|
60
|
-
DEFAULT_LLM_CLI_VALUES[4]='claude --dangerously-skip-permissions --model sonnet -p "{{PROMPT}}"'
|
|
61
|
-
DEFAULT_LLM_RUN="sonnet"
|
|
62
|
-
DEFAULT_LLM_IDEATE="gemini o3 opus"
|
|
52
|
+
# Default memory limit (in MB, 0 means no limit)
|
|
53
|
+
# Set to reasonable limit for ML workloads - about half of available system RAM
|
|
54
|
+
DEFAULT_MEMORY_LIMIT_MB=12288
|
|
55
|
+
|
|
56
|
+
# Default LLM CLI configuration - use simple variables instead of arrays
|
|
57
|
+
DEFAULT_LLM_RUN="sonnet cursor-sonnet"
|
|
58
|
+
DEFAULT_LLM_IDEATE="gemini opus gpt5high o3high cursor-opus"
|
|
63
59
|
|
|
64
60
|
# Load configuration from config file
|
|
65
61
|
load_config() {
|
|
@@ -96,14 +92,20 @@ load_config() {
|
|
|
96
92
|
# Set retry default
|
|
97
93
|
MAX_RETRIES="$DEFAULT_MAX_RETRIES"
|
|
98
94
|
|
|
95
|
+
# Set memory limit default
|
|
96
|
+
MEMORY_LIMIT_MB="$DEFAULT_MEMORY_LIMIT_MB"
|
|
97
|
+
|
|
99
98
|
# Set LLM CLI defaults (compatibility for older bash)
|
|
100
99
|
# Initialize associative array for LLM commands
|
|
101
100
|
# Use simpler approach for compatibility
|
|
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
|
|
|
@@ -202,12 +204,14 @@ load_config() {
|
|
|
202
204
|
# Model definition - key is model name, value is command template
|
|
203
205
|
# Remove single quotes from value if present
|
|
204
206
|
value=$(echo "$value" | sed "s/^'//;s/'$//")
|
|
207
|
+
# Convert dashes to underscores for bash variable names
|
|
208
|
+
var_key=$(echo "$key" | sed 's/-/_/g')
|
|
205
209
|
# Debug config loading
|
|
206
210
|
if [[ "${DEBUG_CONFIG:-}" == "true" ]]; then
|
|
207
|
-
echo "[CONFIG DEBUG] Setting LLM_CLI_${
|
|
211
|
+
echo "[CONFIG DEBUG] Setting LLM_CLI_${var_key} = '$value'" >&2
|
|
208
212
|
fi
|
|
209
213
|
# Use dynamic variable name for compatibility
|
|
210
|
-
eval "LLM_CLI_${
|
|
214
|
+
eval "LLM_CLI_${var_key}=\"$value\""
|
|
211
215
|
fi
|
|
212
216
|
else
|
|
213
217
|
# Handle top-level keys
|
|
@@ -221,6 +225,7 @@ load_config() {
|
|
|
221
225
|
python_cmd) PYTHON_CMD="$value" ;;
|
|
222
226
|
auto_ideate) AUTO_IDEATE="$value" ;;
|
|
223
227
|
max_retries) MAX_RETRIES="$value" ;;
|
|
228
|
+
memory_limit_mb) MEMORY_LIMIT_MB="$value" ;;
|
|
224
229
|
evolution_dir)
|
|
225
230
|
echo "[WARN] evolution_dir in config is ignored - automatically inferred from config file location" >&2
|
|
226
231
|
;;
|
|
@@ -316,14 +321,18 @@ show_config() {
|
|
|
316
321
|
echo " Lock timeout: $LOCK_TIMEOUT"
|
|
317
322
|
echo " Auto ideate: $AUTO_IDEATE"
|
|
318
323
|
echo " Max retries: $MAX_RETRIES"
|
|
324
|
+
echo " Memory limit: ${MEMORY_LIMIT_MB}MB"
|
|
319
325
|
echo " LLM configuration:"
|
|
320
326
|
# Show LLM configurations using dynamic variable names
|
|
321
|
-
for model in
|
|
327
|
+
for model in gpt5high o3high codex gemini opus sonnet cursor_sonnet cursor_opus; do
|
|
322
328
|
var_name="LLM_CLI_${model}"
|
|
323
|
-
|
|
324
|
-
|
|
329
|
+
var_value=$(eval echo "\$$var_name")
|
|
330
|
+
if [[ -n "$var_value" ]]; then
|
|
331
|
+
# Convert underscore back to dash for display
|
|
332
|
+
display_name=$(echo "$model" | sed 's/_/-/g')
|
|
333
|
+
echo " $display_name: $var_value"
|
|
325
334
|
fi
|
|
326
335
|
done
|
|
327
336
|
echo " LLM for run: $LLM_RUN"
|
|
328
337
|
echo " LLM for ideate: $LLM_IDEATE"
|
|
329
|
-
}
|
|
338
|
+
}
|
package/lib/csv_fixer.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CSV format fixer for claude-evolve
|
|
4
|
+
Ensures proper quoting of CSV fields, especially descriptions
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
def fix_csv_format(input_file, output_file):
|
|
11
|
+
"""
|
|
12
|
+
Read a CSV file and ensure all fields are properly quoted.
|
|
13
|
+
The csv module handles quoting automatically based on content.
|
|
14
|
+
"""
|
|
15
|
+
with open(input_file, 'r') as infile:
|
|
16
|
+
reader = csv.reader(infile)
|
|
17
|
+
rows = list(reader)
|
|
18
|
+
|
|
19
|
+
with open(output_file, 'w', newline='') as outfile:
|
|
20
|
+
writer = csv.writer(outfile, quoting=csv.QUOTE_NONNUMERIC)
|
|
21
|
+
|
|
22
|
+
# Write all rows - csv.writer handles quoting automatically
|
|
23
|
+
for row in rows:
|
|
24
|
+
writer.writerow(row)
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
if len(sys.argv) != 3:
|
|
28
|
+
print("Usage: csv_fixer.py <input_file> <output_file>", file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
fix_csv_format(sys.argv[1], sys.argv[2])
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"Error fixing CSV: {e}", file=sys.stderr)
|
|
35
|
+
sys.exit(1)
|
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)
|