claude-evolve 1.4.11 → 1.4.13

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.
@@ -1,9 +1,45 @@
1
- #!/usr/bin/env python3
2
- """
3
- Auto-updating status display for claude-evolve that fits to terminal size.
4
- Updates in real-time without flicker using ANSI escape sequences.
5
- """
1
+ #!/bin/bash
2
+ set -e
6
3
 
4
+ # Source configuration
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
6
+ source "$SCRIPT_DIR/../lib/config.sh"
7
+
8
+ # Parse arguments
9
+ while [[ $# -gt 0 ]]; do
10
+ case "$1" in
11
+ --working-dir)
12
+ if [[ -n ${2:-} ]]; then
13
+ export CLAUDE_EVOLVE_CONFIG="$2/config.yaml"
14
+ shift 2
15
+ else
16
+ echo "[ERROR] --working-dir requires a directory path" >&2
17
+ exit 1
18
+ fi
19
+ ;;
20
+ -h|--help)
21
+ echo "Usage: claude-evolve-autostatus [--working-dir DIR]"
22
+ echo ""
23
+ echo "Auto-updating status display that fits to terminal size."
24
+ echo "Press 'q' to quit while running."
25
+ exit 0
26
+ ;;
27
+ *)
28
+ echo "[ERROR] Unknown argument: $1" >&2
29
+ exit 1
30
+ ;;
31
+ esac
32
+ done
33
+
34
+ # Load config using the same logic as other commands
35
+ if [[ -n ${CLAUDE_EVOLVE_CONFIG:-} ]]; then
36
+ load_config "$CLAUDE_EVOLVE_CONFIG"
37
+ else
38
+ load_config
39
+ fi
40
+
41
+ # Run the Python autostatus script
42
+ exec "$PYTHON_CMD" -c '
7
43
  import os
8
44
  import sys
9
45
  import time
@@ -11,32 +47,9 @@ import termios
11
47
  import tty
12
48
  import select
13
49
  import signal
14
- import argparse
15
- import subprocess
50
+ import csv
16
51
  from datetime import datetime
17
52
 
18
- # Add parent directory to path for imports
19
- script_dir = os.path.dirname(os.path.abspath(__file__))
20
- parent_dir = os.path.join(script_dir, '..')
21
-
22
- # Try multiple paths to support both development and installed environments
23
- for path in [parent_dir, os.path.join(parent_dir, 'lib'), script_dir]:
24
- if path not in sys.path:
25
- sys.path.insert(0, path)
26
-
27
- try:
28
- from lib.config import Config
29
- from lib.evolution_csv import EvolutionCSV
30
- except ImportError:
31
- # Fallback for installed version where lib might be in a different location
32
- try:
33
- from config import Config
34
- from evolution_csv import EvolutionCSV
35
- except ImportError:
36
- print("Error: Could not import required modules. Please check installation.", file=sys.stderr)
37
- sys.exit(1)
38
-
39
-
40
53
  class TerminalDisplay:
41
54
  """Handles terminal display with ANSI escape sequences for flicker-free updates."""
42
55
 
@@ -47,7 +60,7 @@ class TerminalDisplay:
47
60
  def get_terminal_size(self):
48
61
  """Get current terminal size."""
49
62
  try:
50
- rows, cols = os.popen('stty size', 'r').read().split()
63
+ rows, cols = os.popen("stty size", "r").read().split()
51
64
  return int(rows), int(cols)
52
65
  except:
53
66
  return 24, 80 # Default fallback
@@ -58,90 +71,114 @@ class TerminalDisplay:
58
71
 
59
72
  def clear_screen(self):
60
73
  """Clear the entire screen."""
61
- print('\033[2J\033[H', end='')
74
+ print("\033[2J\033[H", end="")
62
75
 
63
76
  def move_cursor(self, row, col):
64
77
  """Move cursor to specific position."""
65
- print(f'\033[{row};{col}H', end='')
78
+ print(f"\033[{row};{col}H", end="")
66
79
 
67
80
  def clear_line(self):
68
81
  """Clear current line."""
69
- print('\033[2K', end='')
82
+ print("\033[2K", end="")
70
83
 
71
84
  def hide_cursor(self):
72
85
  """Hide the cursor."""
73
- print('\033[?25l', end='')
86
+ print("\033[?25l", end="")
74
87
 
75
88
  def show_cursor(self):
76
89
  """Show the cursor."""
77
- print('\033[?25h', end='')
90
+ print("\033[?25h", end="")
78
91
 
79
92
  def reset(self):
80
93
  """Reset terminal to normal state."""
81
94
  self.show_cursor()
82
- print('\033[0m', end='') # Reset colors
95
+ print("\033[0m", end="") # Reset colors
83
96
 
84
97
 
85
98
  class AutoStatus:
86
99
  """Auto-updating status display."""
87
100
 
88
- def __init__(self, working_dir=None):
89
- self.config = Config()
90
-
91
- # Load config using same mechanism as other commands
92
- # First check CLAUDE_EVOLVE_CONFIG env var
93
- config_env = os.environ.get('CLAUDE_EVOLVE_CONFIG')
94
- if config_env:
95
- self.config.load(config_env)
96
- else:
97
- # Load from working directory or current directory
98
- self.config.load(working_dir=working_dir)
99
-
101
+ def __init__(self, csv_path):
102
+ self.csv_path = csv_path
100
103
  self.display = TerminalDisplay()
101
104
  self.running = True
102
105
 
103
106
  def get_status_data(self):
104
107
  """Get current status data from CSV."""
105
- csv_path = self.config.resolve_path(self.config.data['csv_file'])
108
+ # Read CSV data directly - using list reader to handle position-based access
109
+ with open(self.csv_path, "r") as f:
110
+ reader = csv.reader(f)
111
+ rows = list(reader)
106
112
 
107
- with EvolutionCSV(csv_path) as csv:
108
- df = csv.df
109
-
110
- # Count by status
111
- status_counts = {
112
- 'pending': len(df[df['status'] == 'pending']),
113
- 'running': len(df[df['status'] == 'running']),
114
- 'complete': len(df[df['status'] == 'complete']),
115
- 'failed': len(df[df['status'] == 'failed']),
116
- 'total': len(df)
117
- }
118
-
119
- # Get performance stats for completed
120
- completed_df = df[df['status'] == 'complete']
121
- if not completed_df.empty and 'performance' in completed_df.columns:
122
- perf_values = completed_df['performance'].dropna()
123
- if not perf_values.empty:
124
- perf_stats = {
125
- 'min': perf_values.min(),
126
- 'max': perf_values.max(),
127
- 'mean': perf_values.mean(),
128
- 'count': len(perf_values)
129
- }
130
- else:
131
- perf_stats = None
132
- else:
133
- perf_stats = None
134
-
135
- # Get recent candidates (last N that fit on screen)
136
- max_candidates = max(1, self.display.rows - 15) # Reserve space for header/stats
137
- recent = df.tail(max_candidates)
138
-
113
+ if len(rows) <= 1:
139
114
  return {
140
- 'counts': status_counts,
141
- 'performance': perf_stats,
142
- 'recent': recent,
143
- 'csv_path': csv_path
115
+ "leader": None,
116
+ "generations": {},
117
+ "csv_path": self.csv_path,
118
+ "working_dir": os.path.dirname(self.csv_path)
144
119
  }
120
+
121
+ # Process candidates by generation
122
+ all_candidates = []
123
+ stats_by_gen = {}
124
+
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])
168
+
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
175
+
176
+ return {
177
+ "leader": leader,
178
+ "generations": stats_by_gen,
179
+ "csv_path": self.csv_path,
180
+ "working_dir": os.path.dirname(self.csv_path)
181
+ }
145
182
 
146
183
  def format_duration(self, seconds):
147
184
  """Format duration in human-readable form."""
@@ -174,94 +211,79 @@ class AutoStatus:
174
211
  print(f"\033[1;36m{header.center(self.display.cols)}\033[0m")
175
212
  row += 1
176
213
 
177
- # Timestamp
214
+ # Timestamp and working dir
178
215
  self.display.move_cursor(row, 1)
179
216
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
180
- print(f"Updated: {timestamp} | Press 'q' to quit")
181
- row += 2
182
-
183
- # File info
184
- self.display.move_cursor(row, 1)
185
- print(f"CSV: {data['csv_path']}")
217
+ working_dir = os.path.basename(data["working_dir"])
218
+ print(f"Last updated: {timestamp} | Working dir: {working_dir} | Press '\''q'\'' to quit")
186
219
  row += 2
187
220
 
188
- # Status summary
221
+ # Leader
189
222
  self.display.move_cursor(row, 1)
190
- print("\033[1mStatus Summary:\033[0m")
191
- row += 1
192
-
193
- counts = data['counts']
194
- status_line = (f" Total: {counts['total']} | "
195
- f"\033[33mPending: {counts['pending']}\033[0m | "
196
- f"\033[36mRunning: {counts['running']}\033[0m | "
197
- f"\033[32mComplete: {counts['complete']}\033[0m | "
198
- f"\033[31mFailed: {counts['failed']}\033[0m")
199
-
200
- self.display.move_cursor(row, 1)
201
- 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)")
202
232
  row += 2
203
233
 
204
- # Performance stats
205
- if data['performance']:
206
- self.display.move_cursor(row, 1)
207
- print("\033[1mPerformance Stats:\033[0m")
208
- row += 1
209
-
210
- perf = data['performance']
211
- self.display.move_cursor(row, 1)
212
- print(f" Min: {perf['min']:.4f} | Max: {perf['max']:.4f} | "
213
- f"Mean: {perf['mean']:.4f} | Count: {perf['count']}")
214
- row += 2
215
-
216
- # Recent candidates
217
- if not data['recent'].empty:
218
- self.display.move_cursor(row, 1)
219
- print("\033[1mRecent Candidates:\033[0m")
220
- row += 1
221
-
234
+ # Generation table
235
+ generations = data["generations"]
236
+ if generations:
222
237
  # Table header
223
238
  self.display.move_cursor(row, 1)
224
- header_fmt = f"{'ID':>8} | {'Status':^10} | {'Performance':>11} | {'Description'}"
225
- 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")
226
243
  row += 1
227
244
 
228
245
  self.display.move_cursor(row, 1)
229
246
  print("-" * min(self.display.cols, len(header_fmt)))
230
247
  row += 1
231
248
 
232
- # Table rows
233
- for _, candidate in data['recent'].iterrows():
234
- 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:
235
259
  break
236
260
 
237
- 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'\'']}"
238
263
 
239
- # Color based on status
240
- status = candidate.get('status', 'unknown')
241
- if status == 'complete':
242
- color = '\033[32m' # Green
243
- elif status == 'running':
244
- color = '\033[36m' # Cyan
245
- elif status == 'failed':
246
- color = '\033[31m' # Red
247
- elif status == 'pending':
248
- color = '\033[33m' # Yellow
249
- else:
250
- color = '\033[0m' # Default
264
+ self.display.move_cursor(row, 1)
251
265
 
252
- # Format performance
253
- if status == 'complete' and 'performance' in candidate:
254
- perf = f"{candidate['performance']:.4f}"
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
+ )
255
282
  else:
256
- perf = "-"
257
-
258
- # Truncate description to fit
259
- desc = candidate.get('description', '')
260
- max_desc_len = self.display.cols - 35 # Account for other columns
261
- if len(desc) > max_desc_len:
262
- desc = desc[:max_desc_len-3] + "..."
283
+ line = "{:<10} | {:^20} | {:>10} | {:>8} | {}".format(
284
+ gen, stats_str, "-", "-", "No completed candidates"
285
+ )
263
286
 
264
- line = f"{candidate['id']:>8} | {color}{status:^10}\033[0m | {perf:>11} | {desc}"
265
287
  print(line[:self.display.cols])
266
288
  row += 1
267
289
 
@@ -273,7 +295,7 @@ class AutoStatus:
273
295
  """Check for keyboard input without blocking."""
274
296
  if select.select([sys.stdin], [], [], 0)[0]:
275
297
  char = sys.stdin.read(1)
276
- if char.lower() == 'q':
298
+ if char.lower() == "q":
277
299
  self.running = False
278
300
  return True
279
301
  return False
@@ -284,13 +306,21 @@ class AutoStatus:
284
306
  old_settings = termios.tcgetattr(sys.stdin)
285
307
 
286
308
  try:
287
- # Set terminal to raw mode for immediate input
288
- tty.setraw(sys.stdin.fileno())
309
+ # Set terminal to cbreak mode (allows Ctrl-C) instead of raw mode
310
+ tty.setcbreak(sys.stdin.fileno())
289
311
 
290
312
  self.display.hide_cursor()
291
313
 
292
314
  while self.running:
293
- self.render()
315
+ try:
316
+ self.render()
317
+ except Exception as e:
318
+ # Show error at bottom of screen
319
+ self.display.move_cursor(self.display.rows - 1, 1)
320
+ self.display.clear_line()
321
+ print(f"\033[31mError: {str(e)}\033[0m", end="")
322
+ sys.stdout.flush()
323
+ time.sleep(2) # Give time to read error
294
324
 
295
325
  # Check for input and wait
296
326
  for _ in range(10): # Check 10 times per second
@@ -299,7 +329,7 @@ class AutoStatus:
299
329
  time.sleep(0.1)
300
330
 
301
331
  except KeyboardInterrupt:
302
- pass
332
+ self.running = False
303
333
 
304
334
  finally:
305
335
  # Restore terminal settings
@@ -310,23 +340,8 @@ class AutoStatus:
310
340
  print("Exiting auto-status...")
311
341
 
312
342
 
313
- def main():
314
- """Main entry point."""
315
- parser = argparse.ArgumentParser(
316
- description="Auto-updating status display for claude-evolve that fits to terminal size.",
317
- epilog="Press 'q' to quit while running."
318
- )
319
- parser.add_argument(
320
- '--working-dir',
321
- help='Working directory containing claude-evolve.yaml config file'
322
- )
323
-
324
- args = parser.parse_args()
325
-
326
- # Run auto-status
327
- auto_status = AutoStatus(working_dir=args.working_dir)
328
- auto_status.run()
329
-
330
-
331
- if __name__ == '__main__':
332
- main()
343
+ # Main execution
344
+ csv_path = "'"$FULL_CSV_PATH"'"
345
+ auto_status = AutoStatus(csv_path)
346
+ auto_status.run()
347
+ '
@@ -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"