claude-evolve 1.8.5 → 1.8.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-run +36 -53
- package/bin/claude-evolve-worker +18 -3
- package/lib/evolution_csv.py +159 -21
- package/package.json +1 -1
package/bin/claude-evolve-run
CHANGED
|
@@ -295,8 +295,23 @@ get_csv_stats() {
|
|
|
295
295
|
echo "[DISPATCHER] Starting unified evolution engine"
|
|
296
296
|
echo "[DISPATCHER] Configuration: max_workers=$MAX_WORKERS, timeout=${timeout_seconds:-none}"
|
|
297
297
|
|
|
298
|
-
# Clean up any stuck 'running' statuses at startup
|
|
298
|
+
# Clean up any stuck 'running' statuses and duplicates at startup
|
|
299
299
|
if [[ -f "$FULL_CSV_PATH" ]]; then
|
|
300
|
+
echo "[DISPATCHER] Checking for duplicate candidates..."
|
|
301
|
+
"$PYTHON_CMD" -c "
|
|
302
|
+
import sys
|
|
303
|
+
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
304
|
+
from lib.evolution_csv import EvolutionCSV
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
308
|
+
dup_count = csv.remove_duplicate_candidates()
|
|
309
|
+
if dup_count == 0:
|
|
310
|
+
print('[DISPATCHER] No duplicate candidates found', file=sys.stderr)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print(f'[ERROR] Failed to remove duplicates: {e}', file=sys.stderr)
|
|
313
|
+
" 2>&1 || true
|
|
314
|
+
|
|
300
315
|
echo "[DISPATCHER] Resetting any stuck 'running' candidates to 'pending'..."
|
|
301
316
|
if "$SCRIPT_DIR/claude-evolve-edit" running pending >/dev/null 2>&1; then
|
|
302
317
|
echo "[DISPATCHER] Successfully reset stuck candidates"
|
|
@@ -583,44 +598,17 @@ while true; do
|
|
|
583
598
|
if [[ -f "$FULL_CSV_PATH" && ${#worker_pids[@]} -eq 0 ]]; then
|
|
584
599
|
echo "[DISPATCHER] Checking for stuck candidates (no active workers)..."
|
|
585
600
|
"$PYTHON_CMD" -c "
|
|
586
|
-
import csv
|
|
587
601
|
import sys
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
csv_file = '$FULL_CSV_PATH'
|
|
591
|
-
rows = []
|
|
592
|
-
|
|
593
|
-
with open(csv_file, 'r') as f:
|
|
594
|
-
rows = list(csv.reader(f))
|
|
602
|
+
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
603
|
+
from lib.evolution_csv import EvolutionCSV
|
|
595
604
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
candidate_id = rows[i][0] if rows[i] else ''
|
|
604
|
-
|
|
605
|
-
# Reset 'running' when no workers are active
|
|
606
|
-
if status == 'running':
|
|
607
|
-
print(f'[INFO] Resetting stuck running candidate: {candidate_id}', file=sys.stderr)
|
|
608
|
-
rows[i][4] = 'pending'
|
|
609
|
-
reset_count += 1
|
|
610
|
-
# Reset unknown statuses like 'ready'
|
|
611
|
-
elif status not in ['', 'pending', 'complete', 'failed', 'failed-ai-retry',
|
|
612
|
-
'failed-retry1', 'failed-retry2', 'failed-retry3', 'skipped',
|
|
613
|
-
'failed-parent-missing']:
|
|
614
|
-
print(f'[WARN] Resetting unknown status \"{status}\" to pending: {candidate_id}', file=sys.stderr)
|
|
615
|
-
rows[i][4] = 'pending'
|
|
616
|
-
reset_count += 1
|
|
617
|
-
|
|
618
|
-
if reset_count > 0:
|
|
619
|
-
with open(csv_file + '.tmp', 'w', newline='') as f:
|
|
620
|
-
csv.writer(f).writerows(rows)
|
|
621
|
-
Path(csv_file + '.tmp').rename(csv_file)
|
|
622
|
-
print(f'[INFO] Reset {reset_count} stuck/unknown candidates to pending', file=sys.stderr)
|
|
623
|
-
" || true
|
|
605
|
+
try:
|
|
606
|
+
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
607
|
+
reset_count = csv.reset_stuck_candidates()
|
|
608
|
+
except Exception as e:
|
|
609
|
+
print(f'[ERROR] Failed to reset stuck candidates: {e}', file=sys.stderr)
|
|
610
|
+
sys.exit(1)
|
|
611
|
+
"
|
|
624
612
|
fi
|
|
625
613
|
fi
|
|
626
614
|
|
|
@@ -642,22 +630,17 @@ if reset_count > 0:
|
|
|
642
630
|
# Before blocking, do final check for stuck work (immediate, not periodic)
|
|
643
631
|
echo "[DISPATCHER] Performing final verification for stuck candidates..."
|
|
644
632
|
stuck_work_count=$("$PYTHON_CMD" -c "
|
|
645
|
-
import
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
(status and status not in ['', 'pending', 'complete', 'failed', 'skipped',
|
|
657
|
-
'failed-ai-retry', 'failed-retry1', 'failed-retry2', 'failed-retry3',
|
|
658
|
-
'failed-parent-missing']):
|
|
659
|
-
stuck += 1
|
|
660
|
-
print(stuck)
|
|
633
|
+
import sys
|
|
634
|
+
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
635
|
+
from lib.evolution_csv import EvolutionCSV
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
639
|
+
stuck = csv.count_stuck_candidates()
|
|
640
|
+
print(stuck)
|
|
641
|
+
except Exception as e:
|
|
642
|
+
print(f'[ERROR] Failed to count stuck candidates: {e}', file=sys.stderr)
|
|
643
|
+
print('0') # Default to 0 on error
|
|
661
644
|
" 2>/dev/null || echo "0")
|
|
662
645
|
|
|
663
646
|
if [[ $stuck_work_count -gt 0 ]]; then
|
package/bin/claude-evolve-worker
CHANGED
|
@@ -297,13 +297,22 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
|
297
297
|
if [[ "$current_status" == "complete" ]]; then
|
|
298
298
|
echo "[WORKER-$$] Already evaluated - skipping"
|
|
299
299
|
# Reset status back to complete since get_next_pending_candidate() set it to running
|
|
300
|
-
"$PYTHON_CMD" -c "
|
|
300
|
+
if ! "$PYTHON_CMD" -c "
|
|
301
301
|
import sys
|
|
302
302
|
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
303
303
|
from lib.evolution_csv import EvolutionCSV
|
|
304
304
|
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
305
|
-
csv.update_candidate_status('$candidate_id', 'complete')
|
|
306
|
-
|
|
305
|
+
success = csv.update_candidate_status('$candidate_id', 'complete')
|
|
306
|
+
if not success:
|
|
307
|
+
print(f'ERROR: Failed to update status for $candidate_id', file=sys.stderr)
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
"; then
|
|
310
|
+
echo "[WORKER-$$] ERROR: Failed to reset status to complete for $candidate_id" >&2
|
|
311
|
+
# Don't clear CURRENT_CANDIDATE_ID so cleanup handler can try again
|
|
312
|
+
return 1
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
echo "[WORKER-$$] Status confirmed as complete"
|
|
307
316
|
# Clear CURRENT_CANDIDATE_ID before returning to prevent cleanup from interfering
|
|
308
317
|
CURRENT_CANDIDATE_ID=""
|
|
309
318
|
return 0
|
|
@@ -591,11 +600,17 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
|
591
600
|
if rows:
|
|
592
601
|
start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
|
|
593
602
|
status_count = {}
|
|
603
|
+
running_candidates = []
|
|
594
604
|
for row in rows[start_idx:]:
|
|
595
605
|
if len(row) > 4:
|
|
596
606
|
status = row[4].strip() or 'pending'
|
|
597
607
|
status_count[status] = status_count.get(status, 0) + 1
|
|
608
|
+
if status == 'running':
|
|
609
|
+
candidate_id = row[0].strip().strip('\"') if len(row) > 0 else '?'
|
|
610
|
+
running_candidates.append(candidate_id)
|
|
598
611
|
print(f'Status counts: {status_count}', file=sys.stderr)
|
|
612
|
+
if running_candidates:
|
|
613
|
+
print(f'Running candidates: {running_candidates}', file=sys.stderr)
|
|
599
614
|
" 2>&1 || true
|
|
600
615
|
|
|
601
616
|
# Try to claim a pending candidate
|
package/lib/evolution_csv.py
CHANGED
|
@@ -205,27 +205,33 @@ class EvolutionCSV:
|
|
|
205
205
|
rows = self._read_csv()
|
|
206
206
|
if not rows:
|
|
207
207
|
return False
|
|
208
|
-
|
|
208
|
+
|
|
209
209
|
updated = False
|
|
210
|
-
|
|
210
|
+
update_count = 0
|
|
211
|
+
|
|
211
212
|
# Skip header row if it exists
|
|
212
213
|
start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
|
|
213
|
-
|
|
214
|
+
|
|
215
|
+
# Update ALL matching rows (in case of duplicates)
|
|
214
216
|
for i in range(start_idx, len(rows)):
|
|
215
217
|
row = rows[i]
|
|
216
|
-
|
|
218
|
+
|
|
217
219
|
if self.is_valid_candidate_row(row) and row[0].strip().strip('"') == candidate_id.strip().strip('"'):
|
|
218
220
|
# Ensure row has at least 5 columns
|
|
219
221
|
while len(row) < 5:
|
|
220
222
|
row.append('')
|
|
221
|
-
|
|
223
|
+
|
|
222
224
|
row[4] = new_status
|
|
223
225
|
updated = True
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
update_count += 1
|
|
227
|
+
# Don't break - continue to update ALL instances (handles duplicates)
|
|
228
|
+
|
|
229
|
+
if update_count > 1:
|
|
230
|
+
print(f'[WARN] Updated {update_count} duplicate entries for candidate {candidate_id} to status {new_status}', file=sys.stderr)
|
|
231
|
+
|
|
226
232
|
if updated:
|
|
227
233
|
self._write_csv(rows)
|
|
228
|
-
|
|
234
|
+
|
|
229
235
|
return updated
|
|
230
236
|
|
|
231
237
|
def update_candidate_performance(self, candidate_id: str, performance: str) -> bool:
|
|
@@ -233,27 +239,33 @@ class EvolutionCSV:
|
|
|
233
239
|
rows = self._read_csv()
|
|
234
240
|
if not rows:
|
|
235
241
|
return False
|
|
236
|
-
|
|
242
|
+
|
|
237
243
|
updated = False
|
|
238
|
-
|
|
244
|
+
update_count = 0
|
|
245
|
+
|
|
239
246
|
# Skip header row if it exists
|
|
240
247
|
start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
|
|
241
|
-
|
|
248
|
+
|
|
249
|
+
# Update ALL matching rows (in case of duplicates)
|
|
242
250
|
for i in range(start_idx, len(rows)):
|
|
243
251
|
row = rows[i]
|
|
244
|
-
|
|
252
|
+
|
|
245
253
|
if self.is_valid_candidate_row(row) and row[0].strip().strip('"') == candidate_id.strip().strip('"'):
|
|
246
254
|
# Ensure row has at least 4 columns
|
|
247
255
|
while len(row) < 4:
|
|
248
256
|
row.append('')
|
|
249
|
-
|
|
257
|
+
|
|
250
258
|
row[3] = performance # Performance is column 4 (index 3)
|
|
251
259
|
updated = True
|
|
252
|
-
|
|
253
|
-
|
|
260
|
+
update_count += 1
|
|
261
|
+
# Don't break - continue to update ALL instances (handles duplicates)
|
|
262
|
+
|
|
263
|
+
if update_count > 1:
|
|
264
|
+
print(f'[WARN] Updated {update_count} duplicate entries for candidate {candidate_id} performance', file=sys.stderr)
|
|
265
|
+
|
|
254
266
|
if updated:
|
|
255
267
|
self._write_csv(rows)
|
|
256
|
-
|
|
268
|
+
|
|
257
269
|
return updated
|
|
258
270
|
|
|
259
271
|
def update_candidate_field(self, candidate_id: str, field_name: str, value: str) -> bool:
|
|
@@ -304,8 +316,10 @@ class EvolutionCSV:
|
|
|
304
316
|
|
|
305
317
|
# Update the candidate's field
|
|
306
318
|
updated = False
|
|
319
|
+
update_count = 0
|
|
307
320
|
start_idx = 1 if has_header else 0
|
|
308
|
-
|
|
321
|
+
|
|
322
|
+
# Update ALL matching rows (in case of duplicates)
|
|
309
323
|
for i in range(start_idx, len(rows)):
|
|
310
324
|
row = rows[i]
|
|
311
325
|
# Strip quotes from both stored ID and search ID for comparison
|
|
@@ -315,14 +329,18 @@ class EvolutionCSV:
|
|
|
315
329
|
# Ensure row has enough columns
|
|
316
330
|
while len(row) <= field_index:
|
|
317
331
|
row.append('')
|
|
318
|
-
|
|
332
|
+
|
|
319
333
|
row[field_index] = value
|
|
320
334
|
updated = True
|
|
321
|
-
|
|
322
|
-
|
|
335
|
+
update_count += 1
|
|
336
|
+
# Don't break - continue to update ALL instances (handles duplicates)
|
|
337
|
+
|
|
338
|
+
if update_count > 1:
|
|
339
|
+
print(f'[WARN] Updated {update_count} duplicate entries for candidate {candidate_id} field {field_name}', file=sys.stderr)
|
|
340
|
+
|
|
323
341
|
if updated:
|
|
324
342
|
self._write_csv(rows)
|
|
325
|
-
|
|
343
|
+
|
|
326
344
|
return updated
|
|
327
345
|
|
|
328
346
|
def get_candidate_info(self, candidate_id: str) -> Optional[Dict[str, str]]:
|
|
@@ -416,6 +434,126 @@ class EvolutionCSV:
|
|
|
416
434
|
|
|
417
435
|
return fixed_count
|
|
418
436
|
|
|
437
|
+
def reset_stuck_candidates(self) -> int:
|
|
438
|
+
"""
|
|
439
|
+
Reset 'running' candidates and unknown statuses to 'pending'.
|
|
440
|
+
Should only be called when no workers are active.
|
|
441
|
+
Returns the number of candidates reset.
|
|
442
|
+
"""
|
|
443
|
+
rows = self._read_csv()
|
|
444
|
+
if not rows:
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
reset_count = 0
|
|
448
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
449
|
+
start_idx = 1 if has_header else 0
|
|
450
|
+
|
|
451
|
+
valid_statuses = {'', 'pending', 'complete', 'failed', 'failed-ai-retry',
|
|
452
|
+
'failed-retry1', 'failed-retry2', 'failed-retry3', 'skipped',
|
|
453
|
+
'failed-parent-missing', 'running'}
|
|
454
|
+
|
|
455
|
+
# Track seen IDs to detect duplicates
|
|
456
|
+
seen_ids = {}
|
|
457
|
+
|
|
458
|
+
for i in range(start_idx, len(rows)):
|
|
459
|
+
if len(rows[i]) > 4:
|
|
460
|
+
status = rows[i][4].strip() if rows[i][4] else ''
|
|
461
|
+
candidate_id = rows[i][0].strip().strip('"') if rows[i] else ''
|
|
462
|
+
|
|
463
|
+
# Check for duplicate IDs
|
|
464
|
+
if candidate_id in seen_ids:
|
|
465
|
+
print(f'[WARN] Duplicate candidate ID found: {candidate_id} at rows {seen_ids[candidate_id]} and {i}', file=sys.stderr)
|
|
466
|
+
print(f'[WARN] Row {seen_ids[candidate_id]}: status={rows[seen_ids[candidate_id]][4] if len(rows[seen_ids[candidate_id]]) > 4 else "?"}', file=sys.stderr)
|
|
467
|
+
print(f'[WARN] Row {i}: status={status}', file=sys.stderr)
|
|
468
|
+
else:
|
|
469
|
+
seen_ids[candidate_id] = i
|
|
470
|
+
|
|
471
|
+
# Reset 'running' when no workers are active
|
|
472
|
+
if status == 'running':
|
|
473
|
+
print(f'[INFO] Resetting stuck running candidate: {candidate_id} (row {i})', file=sys.stderr)
|
|
474
|
+
rows[i][4] = 'pending'
|
|
475
|
+
reset_count += 1
|
|
476
|
+
# Reset unknown statuses
|
|
477
|
+
elif status not in valid_statuses:
|
|
478
|
+
print(f'[WARN] Resetting unknown status "{status}" to pending: {candidate_id}', file=sys.stderr)
|
|
479
|
+
rows[i][4] = 'pending'
|
|
480
|
+
reset_count += 1
|
|
481
|
+
|
|
482
|
+
if reset_count > 0:
|
|
483
|
+
self._write_csv(rows)
|
|
484
|
+
print(f'[INFO] Reset {reset_count} stuck/unknown candidates to pending', file=sys.stderr)
|
|
485
|
+
|
|
486
|
+
return reset_count
|
|
487
|
+
|
|
488
|
+
def count_stuck_candidates(self) -> int:
|
|
489
|
+
"""
|
|
490
|
+
Count candidates that are stuck (running or have unknown status).
|
|
491
|
+
Returns the number of stuck candidates.
|
|
492
|
+
"""
|
|
493
|
+
rows = self._read_csv()
|
|
494
|
+
if not rows:
|
|
495
|
+
return 0
|
|
496
|
+
|
|
497
|
+
stuck = 0
|
|
498
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
499
|
+
start_idx = 1 if has_header else 0
|
|
500
|
+
|
|
501
|
+
valid_statuses = {'', 'pending', 'complete', 'failed', 'failed-ai-retry',
|
|
502
|
+
'failed-retry1', 'failed-retry2', 'failed-retry3', 'skipped',
|
|
503
|
+
'failed-parent-missing'}
|
|
504
|
+
|
|
505
|
+
for i in range(start_idx, len(rows)):
|
|
506
|
+
if len(rows[i]) > 4:
|
|
507
|
+
status = rows[i][4].strip() if rows[i][4] else ''
|
|
508
|
+
# Count running and unknown statuses
|
|
509
|
+
if status == 'running' or (status and status not in valid_statuses):
|
|
510
|
+
stuck += 1
|
|
511
|
+
|
|
512
|
+
return stuck
|
|
513
|
+
|
|
514
|
+
def remove_duplicate_candidates(self) -> int:
|
|
515
|
+
"""
|
|
516
|
+
Remove duplicate candidate entries, keeping only the first occurrence.
|
|
517
|
+
Returns the number of duplicates removed.
|
|
518
|
+
"""
|
|
519
|
+
rows = self._read_csv()
|
|
520
|
+
if not rows:
|
|
521
|
+
return 0
|
|
522
|
+
|
|
523
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
524
|
+
start_idx = 1 if has_header else 0
|
|
525
|
+
|
|
526
|
+
# Track seen IDs and their row indices
|
|
527
|
+
seen_ids = {}
|
|
528
|
+
rows_to_remove = []
|
|
529
|
+
|
|
530
|
+
for i in range(start_idx, len(rows)):
|
|
531
|
+
if len(rows[i]) > 0:
|
|
532
|
+
candidate_id = rows[i][0].strip().strip('"')
|
|
533
|
+
|
|
534
|
+
if candidate_id in seen_ids:
|
|
535
|
+
# Duplicate found
|
|
536
|
+
first_row = seen_ids[candidate_id]
|
|
537
|
+
status_first = rows[first_row][4] if len(rows[first_row]) > 4 else ''
|
|
538
|
+
status_dup = rows[i][4] if len(rows[i]) > 4 else ''
|
|
539
|
+
|
|
540
|
+
print(f'[WARN] Removing duplicate candidate {candidate_id}:', file=sys.stderr)
|
|
541
|
+
print(f'[WARN] Keeping row {first_row}: status={status_first}', file=sys.stderr)
|
|
542
|
+
print(f'[WARN] Removing row {i}: status={status_dup}', file=sys.stderr)
|
|
543
|
+
rows_to_remove.append(i)
|
|
544
|
+
else:
|
|
545
|
+
seen_ids[candidate_id] = i
|
|
546
|
+
|
|
547
|
+
if rows_to_remove:
|
|
548
|
+
# Remove rows in reverse order to maintain indices
|
|
549
|
+
for idx in sorted(rows_to_remove, reverse=True):
|
|
550
|
+
del rows[idx]
|
|
551
|
+
|
|
552
|
+
self._write_csv(rows)
|
|
553
|
+
print(f'[INFO] Removed {len(rows_to_remove)} duplicate candidate(s)', file=sys.stderr)
|
|
554
|
+
|
|
555
|
+
return len(rows_to_remove)
|
|
556
|
+
|
|
419
557
|
def has_pending_work(self) -> bool:
|
|
420
558
|
"""Check if there are any pending candidates. Used by dispatcher."""
|
|
421
559
|
return self.count_pending_candidates() > 0
|