claude-evolve 1.3.44 → 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.
@@ -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()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.3.44",
3
+ "version": "1.4.0",
4
4
  "bin": {
5
5
  "claude-evolve": "./bin/claude-evolve",
6
6
  "claude-evolve-main": "./bin/claude-evolve-main",