claude-evolve 1.4.12 → 1.5.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.
@@ -105,54 +105,79 @@ class AutoStatus:
105
105
 
106
106
  def get_status_data(self):
107
107
  """Get current status data from CSV."""
108
- # Read CSV data directly
108
+ # Read CSV data directly - using list reader to handle position-based access
109
109
  with open(self.csv_path, "r") as f:
110
- reader = csv.DictReader(f)
110
+ reader = csv.reader(f)
111
111
  rows = list(reader)
112
112
 
113
- # Count by status
114
- status_counts = {
115
- "pending": 0,
116
- "running": 0,
117
- "complete": 0,
118
- "failed": 0,
119
- "total": len(rows)
120
- }
113
+ if len(rows) <= 1:
114
+ return {
115
+ "leader": None,
116
+ "generations": {},
117
+ "csv_path": self.csv_path,
118
+ "working_dir": os.path.dirname(self.csv_path)
119
+ }
121
120
 
122
- # Collect performance values and recent candidates
123
- perf_values = []
124
- for row in rows:
125
- status = row.get("status", "unknown")
126
- if status in status_counts:
127
- status_counts[status] += 1
128
-
129
- # Collect performance for completed
130
- if status == "complete" and "performance" in row and row["performance"]:
131
- try:
132
- perf_values.append(float(row["performance"]))
133
- except ValueError:
134
- pass
121
+ # Process candidates by generation
122
+ all_candidates = []
123
+ stats_by_gen = {}
135
124
 
136
- # Calculate performance stats
137
- if perf_values:
138
- perf_stats = {
139
- "min": min(perf_values),
140
- "max": max(perf_values),
141
- "mean": sum(perf_values) / len(perf_values),
142
- "count": len(perf_values)
143
- }
144
- else:
145
- perf_stats = None
125
+ for row in rows[1:]: # Skip header
126
+ if len(row) >= 1 and row[0]: # Must have an ID
127
+ candidate_id = row[0]
128
+
129
+ # Extract generation (e.g., "gen03" from "gen03-001")
130
+ if "-" in candidate_id:
131
+ gen = candidate_id.split("-")[0]
132
+
133
+ # Get status and performance
134
+ status = row[4] if len(row) > 4 and row[4] else "pending"
135
+ performance = row[3] if len(row) > 3 and row[3] else ""
136
+
137
+ # Normalize failed-retry* to failed
138
+ if status.startswith("failed"):
139
+ status = "failed"
140
+
141
+ # Track by generation
142
+ if gen not in stats_by_gen:
143
+ stats_by_gen[gen] = {
144
+ "pending": 0, "complete": 0, "failed": 0, "running": 0,
145
+ "candidates": []
146
+ }
147
+
148
+ if status in stats_by_gen[gen]:
149
+ stats_by_gen[gen][status] += 1
150
+ else:
151
+ stats_by_gen[gen]["pending"] += 1
152
+
153
+ # Collect candidate info
154
+ if status == "complete" and performance:
155
+ try:
156
+ score = float(performance)
157
+ description = row[2] if len(row) > 2 else "No description"
158
+ candidate_info = (candidate_id, description, score)
159
+ stats_by_gen[gen]["candidates"].append(candidate_info)
160
+ all_candidates.append(candidate_info)
161
+ except ValueError:
162
+ pass
163
+
164
+ # Find the overall leader
165
+ leader = None
166
+ if all_candidates:
167
+ leader = max(all_candidates, key=lambda x: x[2])
146
168
 
147
- # Get recent candidates (last N that fit on screen)
148
- max_candidates = max(1, self.display.rows - 15) # Reserve space for header/stats
149
- recent = rows[-max_candidates:] if rows else []
169
+ # Find best performer in each generation
170
+ for gen in stats_by_gen:
171
+ if stats_by_gen[gen]["candidates"]:
172
+ stats_by_gen[gen]["best"] = max(stats_by_gen[gen]["candidates"], key=lambda x: x[2])
173
+ else:
174
+ stats_by_gen[gen]["best"] = None
150
175
 
151
176
  return {
152
- "counts": status_counts,
153
- "performance": perf_stats,
154
- "recent": recent,
155
- "csv_path": self.csv_path
177
+ "leader": leader,
178
+ "generations": stats_by_gen,
179
+ "csv_path": self.csv_path,
180
+ "working_dir": os.path.dirname(self.csv_path)
156
181
  }
157
182
 
158
183
  def format_duration(self, seconds):
@@ -186,97 +211,79 @@ class AutoStatus:
186
211
  print(f"\033[1;36m{header.center(self.display.cols)}\033[0m")
187
212
  row += 1
188
213
 
189
- # Timestamp
214
+ # Timestamp and working dir
190
215
  self.display.move_cursor(row, 1)
191
216
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
192
- print(f"Updated: {timestamp} | Press '\''q'\'' to quit")
217
+ working_dir = os.path.basename(data["working_dir"])
218
+ print(f"Last updated: {timestamp} | Working dir: {working_dir} | Press '\''q'\'' to quit")
193
219
  row += 2
194
220
 
195
- # File info
196
- self.display.move_cursor(row, 1)
197
- print(f"CSV: {data['\''csv_path'\'']}")
198
- row += 2
199
-
200
- # Status summary
201
- self.display.move_cursor(row, 1)
202
- print("\033[1mStatus Summary:\033[0m")
203
- row += 1
204
-
205
- counts = data["counts"]
206
- status_line = (f" Total: {counts['\''total'\'']} | "
207
- f"\033[33mPending: {counts['\''pending'\'']}\033[0m | "
208
- f"\033[36mRunning: {counts['\''running'\'']}\033[0m | "
209
- f"\033[32mComplete: {counts['\''complete'\'']}\033[0m | "
210
- f"\033[31mFailed: {counts['\''failed'\'']}\033[0m")
211
-
221
+ # Leader
212
222
  self.display.move_cursor(row, 1)
213
- print(status_line)
223
+ if data["leader"]:
224
+ leader_id, leader_desc, leader_score = data["leader"]
225
+ # Truncate description for leader
226
+ max_desc_len = self.display.cols - 30
227
+ if len(leader_desc) > max_desc_len:
228
+ leader_desc = leader_desc[:max_desc_len-3] + "..."
229
+ print(f"\033[1;32mLeader:\033[0m {leader_id} | {leader_score:.4f} | {leader_desc}")
230
+ else:
231
+ print("\033[1;32mLeader:\033[0m None (no completed candidates)")
214
232
  row += 2
215
233
 
216
- # Performance stats
217
- if data["performance"]:
218
- self.display.move_cursor(row, 1)
219
- print("\033[1mPerformance Stats:\033[0m")
220
- row += 1
221
-
222
- perf = data["performance"]
223
- self.display.move_cursor(row, 1)
224
- print(f" Min: {perf['\''min'\'']:.4f} | Max: {perf['\''max'\'']:.4f} | "
225
- f"Mean: {perf['\''mean'\'']:.4f} | Count: {perf['\''count'\'']}")
226
- row += 2
227
-
228
- # Recent candidates
229
- if data["recent"]:
230
- self.display.move_cursor(row, 1)
231
- print("\033[1mRecent Candidates:\033[0m")
232
- row += 1
233
-
234
+ # Generation table
235
+ generations = data["generations"]
236
+ if generations:
234
237
  # Table header
235
238
  self.display.move_cursor(row, 1)
236
- header_fmt = f"{"ID":>8} | {"Status":^10} | {"Performance":>11} | {"Description"}"
237
- print(header_fmt[:self.display.cols])
239
+ header_fmt = "{:<10} | {:^20} | {:>10} | {:>8} | {}".format(
240
+ "Generation", "Stats (p/c/f/r)", "Top ID", "Score", "Description"
241
+ )
242
+ print("\033[1m" + header_fmt[:self.display.cols] + "\033[0m")
238
243
  row += 1
239
244
 
240
245
  self.display.move_cursor(row, 1)
241
246
  print("-" * min(self.display.cols, len(header_fmt)))
242
247
  row += 1
243
248
 
244
- # Table rows
245
- for candidate in data["recent"]:
246
- if row >= self.display.rows - 1: # Leave room for bottom
249
+ # Sort generations
250
+ sorted_gens = sorted(generations.keys())
251
+
252
+ # Calculate how many generations we can show
253
+ available_rows = self.display.rows - row - 1 # Leave room at bottom
254
+ start_idx = max(0, len(sorted_gens) - available_rows)
255
+
256
+ # Show generations (most recent at bottom)
257
+ for gen in sorted_gens[start_idx:]:
258
+ if row >= self.display.rows - 1:
247
259
  break
248
260
 
249
- self.display.move_cursor(row, 1)
261
+ gen_data = generations[gen]
262
+ stats_str = f"{gen_data['\''pending'\'']}/{gen_data['\''complete'\'']}/{gen_data['\''failed'\'']}/{gen_data['\''running'\'']}"
250
263
 
251
- # Color based on status
252
- status = candidate.get("status", "unknown")
253
- if status == "complete":
254
- color = "\033[32m" # Green
255
- elif status == "running":
256
- color = "\033[36m" # Cyan
257
- elif status == "failed":
258
- color = "\033[31m" # Red
259
- elif status == "pending":
260
- color = "\033[33m" # Yellow
261
- else:
262
- color = "\033[0m" # Default
264
+ self.display.move_cursor(row, 1)
263
265
 
264
- # Format performance
265
- if status == "complete" and "performance" in candidate and candidate["performance"]:
266
- try:
267
- perf = f"{float(candidate['\''performance'\'']):.4f}"
268
- except ValueError:
269
- perf = "-"
266
+ if gen_data["best"]:
267
+ best_id, best_desc, best_score = gen_data["best"]
268
+ # Truncate description
269
+ max_desc_len = self.display.cols - 55
270
+ if len(best_desc) > max_desc_len:
271
+ best_desc = best_desc[:max_desc_len-3] + "..."
272
+
273
+ # Highlight if this is the overall leader
274
+ if data["leader"] and best_id == data["leader"][0]:
275
+ line = "{:<10} | {:^20} | \033[32m{:>10}\033[0m | {:>8.4f} | {}".format(
276
+ gen, stats_str, best_id, best_score, best_desc
277
+ )
278
+ else:
279
+ line = "{:<10} | {:^20} | {:>10} | {:>8.4f} | {}".format(
280
+ gen, stats_str, best_id, best_score, best_desc
281
+ )
270
282
  else:
271
- perf = "-"
272
-
273
- # Truncate description to fit
274
- desc = candidate.get("description", "")
275
- max_desc_len = self.display.cols - 35 # Account for other columns
276
- if len(desc) > max_desc_len:
277
- desc = desc[:max_desc_len-3] + "..."
283
+ line = "{:<10} | {:^20} | {:>10} | {:>8} | {}".format(
284
+ gen, stats_str, "-", "-", "No completed candidates"
285
+ )
278
286
 
279
- line = f"{candidate['\''id'\'']:>8} | {color}{status:^10}\033[0m | {perf:>11} | {desc}"
280
287
  print(line[:self.display.cols])
281
288
  row += 1
282
289
 
@@ -33,11 +33,12 @@ SELECTORS:
33
33
  ACTIONS:
34
34
  failed Mark candidates as failed (keeps scores)
35
35
  complete Mark candidates as complete (keeps scores)
36
- pending Mark candidates as pending (keeps scores)
36
+ pending Mark candidates as pending (clears all data)
37
37
  failed-retry1 Mark candidates for retry attempt 1 (bug fixing)
38
38
  failed-retry2 Mark candidates for retry attempt 2 (bug fixing)
39
39
  failed-retry3 Mark candidates for retry attempt 3 (bug fixing)
40
40
  reboot Reset completely (delete .py files, clear scores, set pending)
41
+ delete Delete candidates from CSV and remove .py files (asks confirmation)
41
42
 
42
43
  EXAMPLES:
43
44
  claude-evolve edit gen03 failed # Mark all gen03 as failed
@@ -46,6 +47,7 @@ EXAMPLES:
46
47
  claude-evolve edit complete failed # Mark all complete as failed for re-run
47
48
  claude-evolve edit all pending # Mark everything as pending for re-run
48
49
  claude-evolve edit gen02 reboot # Full reset of gen02 (delete files + clear data)
50
+ claude-evolve edit gen02 delete # Delete gen02 from CSV and remove .py files
49
51
 
50
52
  DESCRIPTION:
51
53
  This command helps manage evolution runs when you need to re-evaluate candidates.
@@ -77,9 +79,9 @@ fi
77
79
 
78
80
  # Validate action
79
81
  case "$ACTION" in
80
- failed|complete|pending|failed-retry1|failed-retry2|failed-retry3|reboot) ;;
82
+ failed|complete|pending|failed-retry1|failed-retry2|failed-retry3|reboot|delete) ;;
81
83
  *)
82
- echo "[ERROR] Action must be one of: failed, complete, pending, failed-retry1, failed-retry2, failed-retry3, reboot" >&2
84
+ echo "[ERROR] Action must be one of: failed, complete, pending, failed-retry1, failed-retry2, failed-retry3, reboot, delete" >&2
83
85
  exit 1
84
86
  ;;
85
87
  esac
@@ -152,9 +154,10 @@ try:
152
154
 
153
155
  if matches:
154
156
  if clear_scores:
155
- # Reboot: clear everything after description (keep id, basedOnId, description)
157
+ # Clear everything after description (keep id, basedOnId, description)
156
158
  if len(row) >= 3:
157
- rows[i] = [row[0], row[1], row[2], '', ''] # id, basedOnId, description, empty performance, empty status
159
+ # Keep first 3 columns, then add empty performance and the new status
160
+ rows[i] = [row[0], row[1], row[2], '', new_status]
158
161
  updated_count += 1
159
162
  else:
160
163
  # Just update status (preserve other fields)
@@ -277,6 +280,66 @@ except Exception as e:
277
280
  echo "[INFO] Deleted $deleted_count evolution files"
278
281
  }
279
282
 
283
+ # Function to delete candidates from CSV
284
+ delete_candidates_from_csv() {
285
+ local selector="$1"
286
+
287
+ echo "[INFO] Deleting candidates matching '$selector' from CSV..."
288
+
289
+ "$PYTHON_CMD" -c "
290
+ import sys
291
+ sys.path.insert(0, '$SCRIPT_DIR/..')
292
+ from lib.evolution_csv import EvolutionCSV
293
+ import re
294
+
295
+ selector = '$selector'
296
+ deleted_count = 0
297
+
298
+ with EvolutionCSV('$FULL_CSV_PATH') as csv:
299
+ # Read CSV directly to get all candidates
300
+ import csv as csv_module
301
+ candidates_to_delete = []
302
+
303
+ with open('$FULL_CSV_PATH', 'r') as f:
304
+ reader = csv_module.reader(f)
305
+ rows = list(reader)
306
+ has_header = rows and rows[0] and rows[0][0].lower() == 'id'
307
+ start_idx = 1 if has_header else 0
308
+
309
+ for row in rows[start_idx:]:
310
+ if not row or not row[0].strip():
311
+ continue
312
+
313
+ candidate_id = row[0].strip()
314
+ current_status = row[4].strip() if len(row) > 4 else ''
315
+
316
+ matches = False
317
+ if selector == 'all':
318
+ matches = True
319
+ elif selector.startswith('gen') and re.match(r'^gen\\d+$', selector):
320
+ # Generation selector (e.g., gen01, gen02)
321
+ gen_pattern = f'^{selector}-'
322
+ matches = re.match(gen_pattern, candidate_id) is not None
323
+ elif selector == 'pending':
324
+ matches = current_status == '' or current_status == 'pending'
325
+ elif selector == 'failed':
326
+ matches = current_status.startswith('failed')
327
+ else:
328
+ matches = current_status == selector
329
+
330
+ if matches:
331
+ candidates_to_delete.append(candidate_id)
332
+
333
+ # Delete candidates
334
+ for candidate_id in candidates_to_delete:
335
+ csv.delete_candidate(candidate_id)
336
+ deleted_count += 1
337
+ print(f'[INFO] Deleted from CSV: {candidate_id}')
338
+
339
+ print(f'[INFO] Deleted {deleted_count} candidates from CSV')
340
+ "
341
+ }
342
+
280
343
  # Main execution
281
344
  echo "[INFO] Processing '$SELECTOR' with action: $ACTION"
282
345
 
@@ -288,7 +351,7 @@ case "$ACTION" in
288
351
  update_candidates_status "$SELECTOR" "complete" "false"
289
352
  ;;
290
353
  pending)
291
- update_candidates_status "$SELECTOR" "" "false" # Empty status means pending
354
+ update_candidates_status "$SELECTOR" "pending" "true" # Clear all data and set to pending
292
355
  ;;
293
356
  failed-retry1)
294
357
  update_candidates_status "$SELECTOR" "failed-retry1" "false"
@@ -305,6 +368,19 @@ case "$ACTION" in
305
368
  update_candidates_status "$SELECTOR" "" "true" # Clear scores and set pending
306
369
  echo "[INFO] Reboot complete: files deleted, scores cleared, status set to pending"
307
370
  ;;
371
+ delete)
372
+ # Ask for confirmation
373
+ read -p "[WARNING] This will permanently delete candidates matching '$SELECTOR' from CSV and remove their .py files. Are you sure? (yes/no): " confirmation
374
+ if [[ "$confirmation" != "yes" ]]; then
375
+ echo "[INFO] Delete operation cancelled"
376
+ exit 0
377
+ fi
378
+
379
+ echo "[INFO] Performing delete of '$SELECTOR'..."
380
+ delete_evolution_files "$SELECTOR"
381
+ delete_candidates_from_csv "$SELECTOR"
382
+ echo "[INFO] Delete complete: candidates removed from CSV and files deleted"
383
+ ;;
308
384
  esac
309
385
 
310
386
  echo "[INFO] Edit operation complete"