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.
- package/bin/claude-evolve-autostatus +192 -177
- package/bin/claude-evolve-edit +82 -6
- package/bin/claude-evolve-ideate +535 -218
- package/bin/claude-evolve-ideate.debug +907 -0
- package/bin/claude-evolve-run +49 -7
- package/bin/claude-evolve-worker +121 -21
- package/lib/__pycache__/evolution_csv.cpython-311.pyc +0 -0
- package/lib/__pycache__/evolution_csv.cpython-313.pyc +0 -0
- package/lib/evolution_csv.py +36 -2
- package/lib/validate_parent_ids.py +232 -0
- package/package.json +1 -1
|
@@ -1,9 +1,45 @@
|
|
|
1
|
-
#!/
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
78
|
+
print(f"\033[{row};{col}H", end="")
|
|
66
79
|
|
|
67
80
|
def clear_line(self):
|
|
68
81
|
"""Clear current line."""
|
|
69
|
-
print(
|
|
82
|
+
print("\033[2K", end="")
|
|
70
83
|
|
|
71
84
|
def hide_cursor(self):
|
|
72
85
|
"""Hide the cursor."""
|
|
73
|
-
print(
|
|
86
|
+
print("\033[?25l", end="")
|
|
74
87
|
|
|
75
88
|
def show_cursor(self):
|
|
76
89
|
"""Show the cursor."""
|
|
77
|
-
print(
|
|
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(
|
|
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,
|
|
89
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
#
|
|
221
|
+
# Leader
|
|
189
222
|
self.display.move_cursor(row, 1)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
#
|
|
205
|
-
|
|
206
|
-
|
|
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 =
|
|
225
|
-
|
|
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
|
-
#
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
261
|
+
gen_data = generations[gen]
|
|
262
|
+
stats_str = f"{gen_data['\''pending'\'']}/{gen_data['\''complete'\'']}/{gen_data['\''failed'\'']}/{gen_data['\''running'\'']}"
|
|
238
263
|
|
|
239
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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() ==
|
|
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
|
|
288
|
-
tty.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
'
|
package/bin/claude-evolve-edit
CHANGED
|
@@ -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 (
|
|
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
|
-
#
|
|
157
|
+
# Clear everything after description (keep id, basedOnId, description)
|
|
156
158
|
if len(row) >= 3:
|
|
157
|
-
|
|
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" "" "
|
|
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"
|