claude-evolve 1.3.43 → 1.4.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-analyze +29 -13
- package/bin/claude-evolve-clean-invalid +117 -0
- package/bin/claude-evolve-cleanup-duplicates +131 -0
- package/bin/claude-evolve-ideate +433 -310
- package/bin/claude-evolve-run +79 -30
- package/bin/claude-evolve-status +23 -0
- package/bin/claude-evolve-worker +24 -24
- package/lib/__pycache__/evolution_csv.cpython-311.pyc +0 -0
- package/lib/__pycache__/evolution_csv.cpython-313.pyc +0 -0
- package/lib/config.sh +3 -0
- package/lib/csv_helper_robust.py +121 -0
- package/lib/evolution_csv.py +349 -0
- package/package.json +1 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Unified CSV operations for claude-evolve system.
|
|
4
|
+
This module provides all CSV functionality to ensure dispatcher and worker
|
|
5
|
+
use identical logic for determining pending work and updating candidates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
import fcntl
|
|
13
|
+
import time
|
|
14
|
+
from typing import List, Tuple, Optional, Dict, Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EvolutionCSV:
|
|
18
|
+
"""Unified CSV operations for evolution system."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, csv_path: str, lock_timeout: int = 10):
|
|
21
|
+
"""Initialize with CSV file path and lock timeout."""
|
|
22
|
+
self.csv_path = csv_path
|
|
23
|
+
self.lock_timeout = lock_timeout
|
|
24
|
+
self.lock_file = None
|
|
25
|
+
|
|
26
|
+
def __enter__(self):
|
|
27
|
+
"""Context manager entry - acquire lock."""
|
|
28
|
+
self._acquire_lock()
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
32
|
+
"""Context manager exit - release lock."""
|
|
33
|
+
self._release_lock()
|
|
34
|
+
|
|
35
|
+
def _acquire_lock(self):
|
|
36
|
+
"""Acquire exclusive lock on CSV file."""
|
|
37
|
+
# Use same lock path as bash implementation for consistency
|
|
38
|
+
csv_dir = os.path.dirname(self.csv_path)
|
|
39
|
+
lock_path = os.path.join(csv_dir, ".evolution.csv.lock")
|
|
40
|
+
end_time = time.time() + self.lock_timeout
|
|
41
|
+
|
|
42
|
+
while time.time() < end_time:
|
|
43
|
+
try:
|
|
44
|
+
self.lock_file = open(lock_path, 'w')
|
|
45
|
+
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
46
|
+
self.lock_file.write(str(os.getpid()))
|
|
47
|
+
self.lock_file.flush()
|
|
48
|
+
return
|
|
49
|
+
except (IOError, OSError):
|
|
50
|
+
if self.lock_file:
|
|
51
|
+
self.lock_file.close()
|
|
52
|
+
self.lock_file = None
|
|
53
|
+
time.sleep(0.01)
|
|
54
|
+
|
|
55
|
+
raise RuntimeError(f"Failed to acquire CSV lock within {self.lock_timeout} seconds")
|
|
56
|
+
|
|
57
|
+
def _release_lock(self):
|
|
58
|
+
"""Release CSV lock."""
|
|
59
|
+
if self.lock_file:
|
|
60
|
+
try:
|
|
61
|
+
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN)
|
|
62
|
+
self.lock_file.close()
|
|
63
|
+
# Use same lock path as bash implementation
|
|
64
|
+
csv_dir = os.path.dirname(self.csv_path)
|
|
65
|
+
lock_path = os.path.join(csv_dir, ".evolution.csv.lock")
|
|
66
|
+
os.unlink(lock_path)
|
|
67
|
+
except (IOError, OSError):
|
|
68
|
+
pass
|
|
69
|
+
finally:
|
|
70
|
+
self.lock_file = None
|
|
71
|
+
|
|
72
|
+
def _read_csv(self) -> List[List[str]]:
|
|
73
|
+
"""Read CSV file and return all rows."""
|
|
74
|
+
if not os.path.exists(self.csv_path):
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
with open(self.csv_path, 'r', newline='') as f:
|
|
78
|
+
reader = csv.reader(f)
|
|
79
|
+
return list(reader)
|
|
80
|
+
|
|
81
|
+
def _write_csv(self, rows: List[List[str]]):
|
|
82
|
+
"""Write rows to CSV file atomically."""
|
|
83
|
+
temp_path = f"{self.csv_path}.tmp.{os.getpid()}"
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(temp_path, 'w', newline='') as f:
|
|
87
|
+
writer = csv.writer(f)
|
|
88
|
+
writer.writerows(rows)
|
|
89
|
+
|
|
90
|
+
# Atomic move
|
|
91
|
+
os.rename(temp_path, self.csv_path)
|
|
92
|
+
except Exception:
|
|
93
|
+
# Cleanup temp file on error
|
|
94
|
+
if os.path.exists(temp_path):
|
|
95
|
+
os.unlink(temp_path)
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
def is_valid_candidate_row(self, row: List[str]) -> bool:
|
|
99
|
+
"""Check if a row represents a valid candidate."""
|
|
100
|
+
if not row:
|
|
101
|
+
return False
|
|
102
|
+
if len(row) == 0:
|
|
103
|
+
return False
|
|
104
|
+
# First column should have a non-empty ID
|
|
105
|
+
if not row[0] or row[0].strip() == '':
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def is_pending_candidate(self, row: List[str]) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
UNIFIED LOGIC: Check if a candidate row is pending (needs processing).
|
|
112
|
+
This is the single source of truth for both dispatcher and worker.
|
|
113
|
+
"""
|
|
114
|
+
if not self.is_valid_candidate_row(row):
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
# Must have at least 5 columns to check status
|
|
118
|
+
if len(row) < 5:
|
|
119
|
+
return True # Incomplete row is pending
|
|
120
|
+
|
|
121
|
+
# Check status field (5th column, index 4)
|
|
122
|
+
status = row[4].strip().lower() if row[4] else ''
|
|
123
|
+
|
|
124
|
+
# Blank, missing, "pending", or "running" all mean pending
|
|
125
|
+
if not status or status in ['pending', 'running']:
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
# Check for retry statuses
|
|
129
|
+
if status.startswith('failed-retry'):
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def get_pending_candidates(self) -> List[Tuple[str, str]]:
|
|
135
|
+
"""Get list of pending candidate IDs and their current status."""
|
|
136
|
+
rows = self._read_csv()
|
|
137
|
+
pending = []
|
|
138
|
+
|
|
139
|
+
# Skip header row if it exists
|
|
140
|
+
start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
|
|
141
|
+
|
|
142
|
+
for row in rows[start_idx:]:
|
|
143
|
+
if self.is_pending_candidate(row):
|
|
144
|
+
candidate_id = row[0].strip()
|
|
145
|
+
current_status = row[4].strip() if len(row) > 4 else ''
|
|
146
|
+
pending.append((candidate_id, current_status))
|
|
147
|
+
|
|
148
|
+
return pending
|
|
149
|
+
|
|
150
|
+
def count_pending_candidates(self) -> int:
|
|
151
|
+
"""Count number of pending candidates."""
|
|
152
|
+
return len(self.get_pending_candidates())
|
|
153
|
+
|
|
154
|
+
def get_next_pending_candidate(self) -> Optional[Tuple[str, str]]:
|
|
155
|
+
"""
|
|
156
|
+
Get the next pending candidate and mark it as 'running'.
|
|
157
|
+
Returns (candidate_id, original_status) or None if no pending work.
|
|
158
|
+
"""
|
|
159
|
+
rows = self._read_csv()
|
|
160
|
+
if not rows:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
# Skip header row if it exists
|
|
164
|
+
start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
|
|
165
|
+
|
|
166
|
+
for i in range(start_idx, len(rows)):
|
|
167
|
+
row = rows[i]
|
|
168
|
+
|
|
169
|
+
if self.is_pending_candidate(row):
|
|
170
|
+
candidate_id = row[0].strip()
|
|
171
|
+
original_status = row[4].strip() if len(row) > 4 else ''
|
|
172
|
+
|
|
173
|
+
# Ensure row has at least 5 columns
|
|
174
|
+
while len(row) < 5:
|
|
175
|
+
row.append('')
|
|
176
|
+
|
|
177
|
+
# Mark as running
|
|
178
|
+
row[4] = 'running'
|
|
179
|
+
|
|
180
|
+
# Write back to CSV
|
|
181
|
+
self._write_csv(rows)
|
|
182
|
+
|
|
183
|
+
return (candidate_id, original_status)
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def update_candidate_status(self, candidate_id: str, new_status: str) -> bool:
|
|
188
|
+
"""Update the status of a specific candidate."""
|
|
189
|
+
rows = self._read_csv()
|
|
190
|
+
if not rows:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
updated = False
|
|
194
|
+
|
|
195
|
+
# Skip header row if it exists
|
|
196
|
+
start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
|
|
197
|
+
|
|
198
|
+
for i in range(start_idx, len(rows)):
|
|
199
|
+
row = rows[i]
|
|
200
|
+
|
|
201
|
+
if self.is_valid_candidate_row(row) and row[0].strip() == candidate_id:
|
|
202
|
+
# Ensure row has at least 5 columns
|
|
203
|
+
while len(row) < 5:
|
|
204
|
+
row.append('')
|
|
205
|
+
|
|
206
|
+
row[4] = new_status
|
|
207
|
+
updated = True
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
if updated:
|
|
211
|
+
self._write_csv(rows)
|
|
212
|
+
|
|
213
|
+
return updated
|
|
214
|
+
|
|
215
|
+
def update_candidate_performance(self, candidate_id: str, performance: str) -> bool:
|
|
216
|
+
"""Update the performance of a specific candidate."""
|
|
217
|
+
rows = self._read_csv()
|
|
218
|
+
if not rows:
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
updated = False
|
|
222
|
+
|
|
223
|
+
# Skip header row if it exists
|
|
224
|
+
start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
|
|
225
|
+
|
|
226
|
+
for i in range(start_idx, len(rows)):
|
|
227
|
+
row = rows[i]
|
|
228
|
+
|
|
229
|
+
if self.is_valid_candidate_row(row) and row[0].strip() == candidate_id:
|
|
230
|
+
# Ensure row has at least 4 columns
|
|
231
|
+
while len(row) < 4:
|
|
232
|
+
row.append('')
|
|
233
|
+
|
|
234
|
+
row[3] = performance # Performance is column 4 (index 3)
|
|
235
|
+
updated = True
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
if updated:
|
|
239
|
+
self._write_csv(rows)
|
|
240
|
+
|
|
241
|
+
return updated
|
|
242
|
+
|
|
243
|
+
def get_candidate_info(self, candidate_id: str) -> Optional[Dict[str, str]]:
|
|
244
|
+
"""Get information about a specific candidate."""
|
|
245
|
+
rows = self._read_csv()
|
|
246
|
+
if not rows:
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
# Skip header row if it exists
|
|
250
|
+
start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
|
|
251
|
+
|
|
252
|
+
for row in rows[start_idx:]:
|
|
253
|
+
if self.is_valid_candidate_row(row) and row[0].strip() == candidate_id:
|
|
254
|
+
return {
|
|
255
|
+
'id': row[0].strip() if len(row) > 0 else '',
|
|
256
|
+
'basedOnId': row[1].strip() if len(row) > 1 else '',
|
|
257
|
+
'description': row[2].strip() if len(row) > 2 else '',
|
|
258
|
+
'performance': row[3].strip() if len(row) > 3 else '',
|
|
259
|
+
'status': row[4].strip() if len(row) > 4 else ''
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def has_pending_work(self) -> bool:
|
|
265
|
+
"""Check if there are any pending candidates. Used by dispatcher."""
|
|
266
|
+
return self.count_pending_candidates() > 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def main():
|
|
270
|
+
"""Command line interface for testing."""
|
|
271
|
+
if len(sys.argv) < 3:
|
|
272
|
+
print("Usage: evolution_csv.py <csv_file> <command> [args...]")
|
|
273
|
+
print("Commands:")
|
|
274
|
+
print(" list - List all pending candidates")
|
|
275
|
+
print(" count - Count pending candidates")
|
|
276
|
+
print(" next - Get next pending candidate")
|
|
277
|
+
print(" update <id> <status> - Update candidate status")
|
|
278
|
+
print(" perf <id> <performance> - Update candidate performance")
|
|
279
|
+
print(" info <id> - Get candidate info")
|
|
280
|
+
print(" check - Check if has pending work")
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
csv_file = sys.argv[1]
|
|
284
|
+
command = sys.argv[2]
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
with EvolutionCSV(csv_file) as csv_ops:
|
|
288
|
+
if command == 'list':
|
|
289
|
+
pending = csv_ops.get_pending_candidates()
|
|
290
|
+
for candidate_id, status in pending:
|
|
291
|
+
print(f"{candidate_id}|{status}")
|
|
292
|
+
|
|
293
|
+
elif command == 'count':
|
|
294
|
+
count = csv_ops.count_pending_candidates()
|
|
295
|
+
print(count)
|
|
296
|
+
|
|
297
|
+
elif command == 'next':
|
|
298
|
+
result = csv_ops.get_next_pending_candidate()
|
|
299
|
+
if result:
|
|
300
|
+
candidate_id, original_status = result
|
|
301
|
+
print(f"{candidate_id}|{original_status}")
|
|
302
|
+
else:
|
|
303
|
+
print("")
|
|
304
|
+
|
|
305
|
+
elif command == 'update' and len(sys.argv) >= 5:
|
|
306
|
+
candidate_id = sys.argv[3]
|
|
307
|
+
new_status = sys.argv[4]
|
|
308
|
+
success = csv_ops.update_candidate_status(candidate_id, new_status)
|
|
309
|
+
if success:
|
|
310
|
+
print(f"Updated {candidate_id} to {new_status}")
|
|
311
|
+
else:
|
|
312
|
+
print(f"Failed to update {candidate_id}")
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
|
|
315
|
+
elif command == 'perf' and len(sys.argv) >= 5:
|
|
316
|
+
candidate_id = sys.argv[3]
|
|
317
|
+
performance = sys.argv[4]
|
|
318
|
+
success = csv_ops.update_candidate_performance(candidate_id, performance)
|
|
319
|
+
if success:
|
|
320
|
+
print(f"Updated {candidate_id} performance to {performance}")
|
|
321
|
+
else:
|
|
322
|
+
print(f"Failed to update {candidate_id} performance")
|
|
323
|
+
sys.exit(1)
|
|
324
|
+
|
|
325
|
+
elif command == 'info' and len(sys.argv) >= 4:
|
|
326
|
+
candidate_id = sys.argv[3]
|
|
327
|
+
info = csv_ops.get_candidate_info(candidate_id)
|
|
328
|
+
if info:
|
|
329
|
+
for key, value in info.items():
|
|
330
|
+
print(f"{key}: {value}")
|
|
331
|
+
else:
|
|
332
|
+
print(f"Candidate {candidate_id} not found")
|
|
333
|
+
sys.exit(1)
|
|
334
|
+
|
|
335
|
+
elif command == 'check':
|
|
336
|
+
has_work = csv_ops.has_pending_work()
|
|
337
|
+
print("yes" if has_work else "no")
|
|
338
|
+
|
|
339
|
+
else:
|
|
340
|
+
print(f"Unknown command: {command}")
|
|
341
|
+
sys.exit(1)
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
if __name__ == '__main__':
|
|
349
|
+
main()
|