claude-evolve 1.8.6 → 1.8.9

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.
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "🛑 Claude Evolve - Kill All Processes"
5
+ echo "======================================"
6
+ echo ""
7
+
8
+ # Find all claude-evolve related processes
9
+ # Patterns to match:
10
+ # - claude-evolve-* (but NOT this killall script)
11
+ # - evaluator.py
12
+ # - evolution_*.py
13
+ # - algorithm.py
14
+ # - backtest.py
15
+
16
+ echo "Searching for claude-evolve related processes..."
17
+ echo ""
18
+
19
+ # Get all matching processes with their PIDs
20
+ # Exclude this script itself and grep
21
+ PROCESSES=$(ps aux | grep -E '(claude-evolve-[^k]|evaluator\.py|evolution_.*\.py|algorithm\.py|backtest\.py)' | grep -v grep | grep -v 'claude-evolve-killall' || true)
22
+
23
+ if [[ -z "$PROCESSES" ]]; then
24
+ echo "✓ No claude-evolve processes found running."
25
+ exit 0
26
+ fi
27
+
28
+ echo "Found the following processes:"
29
+ echo "$PROCESSES" | awk '{printf " PID: %-8s CMD: %s\n", $2, substr($0, index($0,$11))}'
30
+ echo ""
31
+
32
+ # Extract PIDs
33
+ PIDS=$(echo "$PROCESSES" | awk '{print $2}')
34
+ PID_COUNT=$(echo "$PIDS" | wc -l | tr -d ' ')
35
+
36
+ echo "Found $PID_COUNT process(es) to terminate."
37
+ echo ""
38
+
39
+ # Ask for confirmation
40
+ read -p "Do you want to kill these processes? (y/N) " -n 1 -r
41
+ echo ""
42
+
43
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
44
+ echo "Aborted."
45
+ exit 0
46
+ fi
47
+
48
+ echo ""
49
+ echo "Terminating processes..."
50
+
51
+ # First try SIGTERM (graceful)
52
+ for PID in $PIDS; do
53
+ if kill -0 "$PID" 2>/dev/null; then
54
+ echo " Sending SIGTERM to PID $PID..."
55
+ kill -TERM "$PID" 2>/dev/null || echo " (already terminated)"
56
+ fi
57
+ done
58
+
59
+ # Wait a moment for graceful shutdown
60
+ sleep 2
61
+
62
+ # Check if any are still running and force kill if needed
63
+ REMAINING_PIDS=""
64
+ for PID in $PIDS; do
65
+ if kill -0 "$PID" 2>/dev/null; then
66
+ REMAINING_PIDS="$REMAINING_PIDS $PID"
67
+ fi
68
+ done
69
+
70
+ if [[ -n "$REMAINING_PIDS" ]]; then
71
+ echo ""
72
+ echo "Some processes did not terminate gracefully. Forcing with SIGKILL..."
73
+ for PID in $REMAINING_PIDS; do
74
+ echo " Sending SIGKILL to PID $PID..."
75
+ kill -9 "$PID" 2>/dev/null || echo " (already terminated)"
76
+ done
77
+ fi
78
+
79
+ echo ""
80
+ echo "✓ All claude-evolve processes terminated."
@@ -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"
@@ -600,11 +600,17 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
600
600
  if rows:
601
601
  start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
602
602
  status_count = {}
603
+ running_candidates = []
603
604
  for row in rows[start_idx:]:
604
605
  if len(row) > 4:
605
606
  status = row[4].strip() or 'pending'
606
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)
607
611
  print(f'Status counts: {status_count}', file=sys.stderr)
612
+ if running_candidates:
613
+ print(f'Running candidates: {running_candidates}', file=sys.stderr)
608
614
  " 2>&1 || true
609
615
 
610
616
  # Try to claim a pending candidate
@@ -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
- break
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
- break
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
- break
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]]:
@@ -434,14 +452,25 @@ class EvolutionCSV:
434
452
  'failed-retry1', 'failed-retry2', 'failed-retry3', 'skipped',
435
453
  'failed-parent-missing', 'running'}
436
454
 
455
+ # Track seen IDs to detect duplicates
456
+ seen_ids = {}
457
+
437
458
  for i in range(start_idx, len(rows)):
438
459
  if len(rows[i]) > 4:
439
460
  status = rows[i][4].strip() if rows[i][4] else ''
440
- candidate_id = rows[i][0] if rows[i] 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
441
470
 
442
471
  # Reset 'running' when no workers are active
443
472
  if status == 'running':
444
- print(f'[INFO] Resetting stuck running candidate: {candidate_id}', file=sys.stderr)
473
+ print(f'[INFO] Resetting stuck running candidate: {candidate_id} (row {i})', file=sys.stderr)
445
474
  rows[i][4] = 'pending'
446
475
  reset_count += 1
447
476
  # Reset unknown statuses
@@ -482,6 +511,49 @@ class EvolutionCSV:
482
511
 
483
512
  return stuck
484
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
+
485
557
  def has_pending_work(self) -> bool:
486
558
  """Check if there are any pending candidates. Used by dispatcher."""
487
559
  return self.count_pending_candidates() > 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.8.6",
3
+ "version": "1.8.9",
4
4
  "bin": {
5
5
  "claude-evolve": "./bin/claude-evolve",
6
6
  "claude-evolve-main": "./bin/claude-evolve-main",
@@ -9,7 +9,8 @@
9
9
  "claude-evolve-run": "./bin/claude-evolve-run",
10
10
  "claude-evolve-worker": "./bin/claude-evolve-worker",
11
11
  "claude-evolve-analyze": "./bin/claude-evolve-analyze",
12
- "claude-evolve-config": "./bin/claude-evolve-config"
12
+ "claude-evolve-config": "./bin/claude-evolve-config",
13
+ "claude-evolve-killall": "./bin/claude-evolve-killall"
13
14
  },
14
15
  "files": [
15
16
  "bin/",