claude-evolve 1.8.51 → 1.9.0
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-ideate-py +15 -0
- package/bin/claude-evolve-run-py +15 -0
- package/bin/claude-evolve-worker-py +15 -0
- package/lib/__pycache__/ai_cli.cpython-314.pyc +0 -0
- package/lib/__pycache__/embedding.cpython-314.pyc +0 -0
- package/lib/__pycache__/evolution_csv.cpython-314.pyc +0 -0
- package/lib/__pycache__/evolve_ideate.cpython-314.pyc +0 -0
- package/lib/__pycache__/evolve_run.cpython-314.pyc +0 -0
- package/lib/__pycache__/evolve_worker.cpython-314.pyc +0 -0
- package/lib/ai_cli.py +196 -0
- package/lib/embedding.py +200 -0
- package/lib/evolution_csv.py +325 -0
- package/lib/evolve_ideate.py +509 -0
- package/lib/evolve_run.py +402 -0
- package/lib/evolve_worker.py +518 -0
- package/package.json +1 -1
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Main orchestrator for claude-evolve.
|
|
4
|
+
Manages worker processes and coordinates ideation.
|
|
5
|
+
|
|
6
|
+
AIDEV-NOTE: This is the Python port of bin/claude-evolve-run.
|
|
7
|
+
Exit codes:
|
|
8
|
+
0 - Success (evolution complete)
|
|
9
|
+
1 - Error
|
|
10
|
+
2 - Rate limit (workers should retry later)
|
|
11
|
+
3 - API exhausted
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import os
|
|
16
|
+
import signal
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import List, Optional, Set
|
|
23
|
+
|
|
24
|
+
# Add lib to path
|
|
25
|
+
SCRIPT_DIR = Path(__file__).parent
|
|
26
|
+
sys.path.insert(0, str(SCRIPT_DIR.parent))
|
|
27
|
+
|
|
28
|
+
from lib.evolution_csv import EvolutionCSV
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class RunConfig:
|
|
33
|
+
"""Configuration for the run orchestrator."""
|
|
34
|
+
csv_path: str
|
|
35
|
+
evolution_dir: str
|
|
36
|
+
max_workers: int = 4
|
|
37
|
+
auto_ideate: bool = True
|
|
38
|
+
worker_timeout: int = 600
|
|
39
|
+
poll_interval: int = 5
|
|
40
|
+
min_completed_for_ideation: int = 3
|
|
41
|
+
config_path: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class WorkerPool:
|
|
45
|
+
"""Manages worker subprocess pool."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, max_workers: int, worker_script: Path, config_path: Optional[str], timeout: int):
|
|
48
|
+
self.max_workers = max_workers
|
|
49
|
+
self.worker_script = worker_script
|
|
50
|
+
self.config_path = config_path
|
|
51
|
+
self.timeout = timeout
|
|
52
|
+
self.workers: dict[int, subprocess.Popen] = {} # pid -> process
|
|
53
|
+
|
|
54
|
+
def spawn_worker(self) -> Optional[int]:
|
|
55
|
+
"""Spawn a new worker. Returns pid or None if at capacity."""
|
|
56
|
+
if len(self.workers) >= self.max_workers:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
cmd = [sys.executable, str(self.worker_script)]
|
|
60
|
+
if self.config_path:
|
|
61
|
+
cmd.extend(['--config', self.config_path])
|
|
62
|
+
if self.timeout:
|
|
63
|
+
cmd.extend(['--timeout', str(self.timeout)])
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
proc = subprocess.Popen(
|
|
67
|
+
cmd,
|
|
68
|
+
stdout=subprocess.PIPE,
|
|
69
|
+
stderr=subprocess.STDOUT,
|
|
70
|
+
text=True
|
|
71
|
+
)
|
|
72
|
+
self.workers[proc.pid] = proc
|
|
73
|
+
print(f"[RUN] Spawned worker {proc.pid}", file=sys.stderr)
|
|
74
|
+
return proc.pid
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f"[RUN] Failed to spawn worker: {e}", file=sys.stderr)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def cleanup_finished(self) -> List[int]:
|
|
80
|
+
"""Clean up finished workers. Returns list of exit codes."""
|
|
81
|
+
exit_codes = []
|
|
82
|
+
finished_pids = []
|
|
83
|
+
|
|
84
|
+
for pid, proc in list(self.workers.items()):
|
|
85
|
+
ret = proc.poll()
|
|
86
|
+
if ret is not None:
|
|
87
|
+
finished_pids.append(pid)
|
|
88
|
+
exit_codes.append(ret)
|
|
89
|
+
|
|
90
|
+
# Log output
|
|
91
|
+
if proc.stdout:
|
|
92
|
+
output = proc.stdout.read()
|
|
93
|
+
if output:
|
|
94
|
+
for line in output.strip().split('\n'):
|
|
95
|
+
print(f"[WORKER-{pid}] {line}", file=sys.stderr)
|
|
96
|
+
|
|
97
|
+
print(f"[RUN] Worker {pid} exited with code {ret}", file=sys.stderr)
|
|
98
|
+
|
|
99
|
+
for pid in finished_pids:
|
|
100
|
+
del self.workers[pid]
|
|
101
|
+
|
|
102
|
+
return exit_codes
|
|
103
|
+
|
|
104
|
+
def shutdown(self, timeout: int = 10):
|
|
105
|
+
"""Shutdown all workers gracefully."""
|
|
106
|
+
if not self.workers:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
print(f"[RUN] Shutting down {len(self.workers)} workers...", file=sys.stderr)
|
|
110
|
+
|
|
111
|
+
# Send SIGTERM
|
|
112
|
+
for pid, proc in self.workers.items():
|
|
113
|
+
try:
|
|
114
|
+
proc.terminate()
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Wait for graceful shutdown
|
|
119
|
+
deadline = time.time() + timeout
|
|
120
|
+
while self.workers and time.time() < deadline:
|
|
121
|
+
self.cleanup_finished()
|
|
122
|
+
if self.workers:
|
|
123
|
+
time.sleep(0.5)
|
|
124
|
+
|
|
125
|
+
# Force kill remaining
|
|
126
|
+
for pid, proc in list(self.workers.items()):
|
|
127
|
+
try:
|
|
128
|
+
proc.kill()
|
|
129
|
+
print(f"[RUN] Force killed worker {pid}", file=sys.stderr)
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
self.workers.clear()
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def active_count(self) -> int:
|
|
137
|
+
return len(self.workers)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class EvolutionRunner:
|
|
141
|
+
"""Main evolution orchestrator."""
|
|
142
|
+
|
|
143
|
+
def __init__(self, config: RunConfig):
|
|
144
|
+
self.config = config
|
|
145
|
+
self.worker_script = SCRIPT_DIR / "evolve_worker.py"
|
|
146
|
+
self.ideate_script = SCRIPT_DIR / "evolve_ideate.py"
|
|
147
|
+
self.pool = WorkerPool(
|
|
148
|
+
max_workers=config.max_workers,
|
|
149
|
+
worker_script=self.worker_script,
|
|
150
|
+
config_path=config.config_path,
|
|
151
|
+
timeout=config.worker_timeout
|
|
152
|
+
)
|
|
153
|
+
self.api_limit_reached = False
|
|
154
|
+
self.shutdown_requested = False
|
|
155
|
+
self._setup_signal_handlers()
|
|
156
|
+
|
|
157
|
+
def _setup_signal_handlers(self):
|
|
158
|
+
"""Setup signal handlers for graceful shutdown."""
|
|
159
|
+
signal.signal(signal.SIGTERM, self._handle_signal)
|
|
160
|
+
signal.signal(signal.SIGINT, self._handle_signal)
|
|
161
|
+
|
|
162
|
+
def _handle_signal(self, signum, frame):
|
|
163
|
+
"""Handle termination signal."""
|
|
164
|
+
sig_name = signal.Signals(signum).name
|
|
165
|
+
print(f"\n[RUN] Received {sig_name}, shutting down...", file=sys.stderr)
|
|
166
|
+
self.shutdown_requested = True
|
|
167
|
+
self.pool.shutdown()
|
|
168
|
+
sys.exit(128 + signum)
|
|
169
|
+
|
|
170
|
+
def cleanup_csv(self):
|
|
171
|
+
"""Clean up CSV at startup."""
|
|
172
|
+
print("[RUN] Cleaning up CSV...", file=sys.stderr)
|
|
173
|
+
with EvolutionCSV(self.config.csv_path) as csv:
|
|
174
|
+
# Remove duplicates
|
|
175
|
+
removed = csv.remove_duplicate_candidates()
|
|
176
|
+
if removed:
|
|
177
|
+
print(f"[RUN] Removed {removed} duplicate candidates", file=sys.stderr)
|
|
178
|
+
|
|
179
|
+
# Reset stuck candidates
|
|
180
|
+
reset = csv.reset_stuck_candidates()
|
|
181
|
+
if reset:
|
|
182
|
+
print(f"[RUN] Reset {reset} stuck candidates", file=sys.stderr)
|
|
183
|
+
|
|
184
|
+
# Clean corrupted status fields
|
|
185
|
+
fixed = csv.cleanup_corrupted_status_fields()
|
|
186
|
+
if fixed:
|
|
187
|
+
print(f"[RUN] Fixed {fixed} corrupted status fields", file=sys.stderr)
|
|
188
|
+
|
|
189
|
+
def ensure_baseline(self):
|
|
190
|
+
"""Ensure baseline entry exists in CSV."""
|
|
191
|
+
with EvolutionCSV(self.config.csv_path) as csv:
|
|
192
|
+
info = csv.get_candidate_info('baseline-000')
|
|
193
|
+
if not info:
|
|
194
|
+
print("[RUN] Adding baseline-000 entry", file=sys.stderr)
|
|
195
|
+
csv.append_candidates([{
|
|
196
|
+
'id': 'baseline-000',
|
|
197
|
+
'basedOnId': '',
|
|
198
|
+
'description': 'Original algorithm.py performance',
|
|
199
|
+
'status': 'pending'
|
|
200
|
+
}])
|
|
201
|
+
|
|
202
|
+
def get_stats(self) -> dict:
|
|
203
|
+
"""Get CSV statistics."""
|
|
204
|
+
with EvolutionCSV(self.config.csv_path) as csv:
|
|
205
|
+
return csv.get_csv_stats()
|
|
206
|
+
|
|
207
|
+
def should_ideate(self, stats: dict) -> bool:
|
|
208
|
+
"""Check if we should run ideation."""
|
|
209
|
+
if not self.config.auto_ideate:
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
# Need minimum completed algorithms to learn from
|
|
213
|
+
if stats['complete'] < self.config.min_completed_for_ideation:
|
|
214
|
+
print(f"[RUN] Not enough completed ({stats['complete']} < {self.config.min_completed_for_ideation})", file=sys.stderr)
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
def run_ideation(self) -> bool:
|
|
220
|
+
"""Run ideation. Returns True on success."""
|
|
221
|
+
print("[RUN] Running ideation...", file=sys.stderr)
|
|
222
|
+
|
|
223
|
+
cmd = [sys.executable, str(self.ideate_script)]
|
|
224
|
+
if self.config.config_path:
|
|
225
|
+
cmd.extend(['--config', self.config.config_path])
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
result = subprocess.run(
|
|
229
|
+
cmd,
|
|
230
|
+
capture_output=True,
|
|
231
|
+
text=True,
|
|
232
|
+
cwd=self.config.evolution_dir
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Log output
|
|
236
|
+
if result.stdout:
|
|
237
|
+
for line in result.stdout.strip().split('\n'):
|
|
238
|
+
print(f"[IDEATE] {line}", file=sys.stderr)
|
|
239
|
+
if result.stderr:
|
|
240
|
+
for line in result.stderr.strip().split('\n'):
|
|
241
|
+
print(f"[IDEATE] {line}", file=sys.stderr)
|
|
242
|
+
|
|
243
|
+
return result.returncode == 0
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
print(f"[RUN] Ideation failed: {e}", file=sys.stderr)
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
def run(self) -> int:
|
|
250
|
+
"""
|
|
251
|
+
Main orchestration loop.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Exit code
|
|
255
|
+
"""
|
|
256
|
+
print("[RUN] Starting evolution run", file=sys.stderr)
|
|
257
|
+
print(f"[RUN] Max workers: {self.config.max_workers}", file=sys.stderr)
|
|
258
|
+
print(f"[RUN] Auto ideate: {self.config.auto_ideate}", file=sys.stderr)
|
|
259
|
+
|
|
260
|
+
# Startup cleanup
|
|
261
|
+
self.cleanup_csv()
|
|
262
|
+
self.ensure_baseline()
|
|
263
|
+
|
|
264
|
+
iteration = 0
|
|
265
|
+
|
|
266
|
+
while not self.shutdown_requested:
|
|
267
|
+
iteration += 1
|
|
268
|
+
|
|
269
|
+
# Clean up finished workers
|
|
270
|
+
exit_codes = self.pool.cleanup_finished()
|
|
271
|
+
|
|
272
|
+
# Check for API limit
|
|
273
|
+
if 2 in exit_codes or 3 in exit_codes:
|
|
274
|
+
print("[RUN] API limit reached, waiting 5 minutes...", file=sys.stderr)
|
|
275
|
+
self.api_limit_reached = True
|
|
276
|
+
time.sleep(300) # 5 minute wait
|
|
277
|
+
self.api_limit_reached = False
|
|
278
|
+
self.cleanup_csv() # Reset stuck candidates
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# Periodic cleanup (every 5 iterations)
|
|
282
|
+
if iteration % 5 == 0 and self.pool.active_count == 0:
|
|
283
|
+
with EvolutionCSV(self.config.csv_path) as csv:
|
|
284
|
+
csv.reset_stuck_candidates()
|
|
285
|
+
|
|
286
|
+
# Get stats
|
|
287
|
+
stats = self.get_stats()
|
|
288
|
+
print(f"[RUN] Stats: {stats['pending']} pending, {stats['complete']} complete, {stats['running']} running", file=sys.stderr)
|
|
289
|
+
|
|
290
|
+
# Check if we need ideation
|
|
291
|
+
if stats['pending'] == 0 and self.pool.active_count == 0:
|
|
292
|
+
# First reset any stuck candidates
|
|
293
|
+
with EvolutionCSV(self.config.csv_path) as csv:
|
|
294
|
+
csv.reset_stuck_candidates()
|
|
295
|
+
|
|
296
|
+
# Re-check stats after reset
|
|
297
|
+
stats = self.get_stats()
|
|
298
|
+
|
|
299
|
+
if stats['pending'] == 0:
|
|
300
|
+
if self.should_ideate(stats):
|
|
301
|
+
if self.run_ideation():
|
|
302
|
+
continue # Loop back to check for new work
|
|
303
|
+
else:
|
|
304
|
+
print("[RUN] Ideation failed, waiting...", file=sys.stderr)
|
|
305
|
+
time.sleep(30)
|
|
306
|
+
continue
|
|
307
|
+
else:
|
|
308
|
+
print("[RUN] Evolution complete!", file=sys.stderr)
|
|
309
|
+
break
|
|
310
|
+
|
|
311
|
+
# Spawn workers for pending work
|
|
312
|
+
while stats['pending'] > 0 and self.pool.active_count < self.config.max_workers:
|
|
313
|
+
pid = self.pool.spawn_worker()
|
|
314
|
+
if pid is None:
|
|
315
|
+
break
|
|
316
|
+
stats['pending'] -= 1 # Optimistic decrement
|
|
317
|
+
|
|
318
|
+
# Sleep before next iteration
|
|
319
|
+
time.sleep(self.config.poll_interval)
|
|
320
|
+
|
|
321
|
+
# Cleanup
|
|
322
|
+
self.pool.shutdown()
|
|
323
|
+
print("[RUN] Exiting", file=sys.stderr)
|
|
324
|
+
return 0
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def load_config(config_path: Optional[str] = None) -> RunConfig:
|
|
328
|
+
"""Load configuration from YAML."""
|
|
329
|
+
import yaml
|
|
330
|
+
|
|
331
|
+
# Find config
|
|
332
|
+
if config_path:
|
|
333
|
+
yaml_path = Path(config_path)
|
|
334
|
+
elif os.environ.get('CLAUDE_EVOLVE_CONFIG'):
|
|
335
|
+
yaml_path = Path(os.environ['CLAUDE_EVOLVE_CONFIG'])
|
|
336
|
+
else:
|
|
337
|
+
yaml_path = Path('evolution/config.yaml')
|
|
338
|
+
if not yaml_path.exists():
|
|
339
|
+
yaml_path = Path('config.yaml')
|
|
340
|
+
|
|
341
|
+
if not yaml_path.exists():
|
|
342
|
+
raise FileNotFoundError(f"Config not found: {yaml_path}")
|
|
343
|
+
|
|
344
|
+
with open(yaml_path) as f:
|
|
345
|
+
data = yaml.safe_load(f) or {}
|
|
346
|
+
|
|
347
|
+
base_dir = yaml_path.parent
|
|
348
|
+
|
|
349
|
+
def resolve(path: str) -> str:
|
|
350
|
+
p = Path(path)
|
|
351
|
+
if not p.is_absolute():
|
|
352
|
+
p = base_dir / p
|
|
353
|
+
return str(p.resolve())
|
|
354
|
+
|
|
355
|
+
parallel = data.get('parallel', {})
|
|
356
|
+
|
|
357
|
+
return RunConfig(
|
|
358
|
+
csv_path=resolve(data.get('csv_file', 'evolution.csv')),
|
|
359
|
+
evolution_dir=str(base_dir.resolve()),
|
|
360
|
+
max_workers=parallel.get('max_workers', 4),
|
|
361
|
+
auto_ideate=data.get('auto_ideate', True),
|
|
362
|
+
worker_timeout=data.get('timeout_seconds', 600),
|
|
363
|
+
poll_interval=parallel.get('poll_interval', 5),
|
|
364
|
+
min_completed_for_ideation=data.get('min_completed_for_ideation', 3),
|
|
365
|
+
config_path=str(yaml_path.resolve())
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def main():
|
|
370
|
+
parser = argparse.ArgumentParser(description='Claude Evolve Runner')
|
|
371
|
+
parser.add_argument('--config', help='Path to config.yaml')
|
|
372
|
+
parser.add_argument('--parallel', type=int, help='Max parallel workers')
|
|
373
|
+
parser.add_argument('--sequential', action='store_true', help='Run sequentially (1 worker)')
|
|
374
|
+
parser.add_argument('--timeout', type=int, help='Worker timeout in seconds')
|
|
375
|
+
args = parser.parse_args()
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
config = load_config(args.config)
|
|
379
|
+
|
|
380
|
+
if args.sequential:
|
|
381
|
+
config.max_workers = 1
|
|
382
|
+
elif args.parallel:
|
|
383
|
+
config.max_workers = args.parallel
|
|
384
|
+
|
|
385
|
+
if args.timeout:
|
|
386
|
+
config.worker_timeout = args.timeout
|
|
387
|
+
|
|
388
|
+
runner = EvolutionRunner(config)
|
|
389
|
+
sys.exit(runner.run())
|
|
390
|
+
|
|
391
|
+
except FileNotFoundError as e:
|
|
392
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
396
|
+
import traceback
|
|
397
|
+
traceback.print_exc()
|
|
398
|
+
sys.exit(1)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == '__main__':
|
|
402
|
+
main()
|