claude-evolve 1.4.11 → 1.4.12
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 +147 -139
- 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,89 @@ 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
|
|
109
|
+
with open(self.csv_path, "r") as f:
|
|
110
|
+
reader = csv.DictReader(f)
|
|
111
|
+
rows = list(reader)
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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)
|
|
113
|
+
# Count by status
|
|
114
|
+
status_counts = {
|
|
115
|
+
"pending": 0,
|
|
116
|
+
"running": 0,
|
|
117
|
+
"complete": 0,
|
|
118
|
+
"failed": 0,
|
|
119
|
+
"total": len(rows)
|
|
120
|
+
}
|
|
121
|
+
|
|
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
|
|
138
128
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
135
|
+
|
|
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)
|
|
144
143
|
}
|
|
144
|
+
else:
|
|
145
|
+
perf_stats = None
|
|
146
|
+
|
|
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 []
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"counts": status_counts,
|
|
153
|
+
"performance": perf_stats,
|
|
154
|
+
"recent": recent,
|
|
155
|
+
"csv_path": self.csv_path
|
|
156
|
+
}
|
|
145
157
|
|
|
146
158
|
def format_duration(self, seconds):
|
|
147
159
|
"""Format duration in human-readable form."""
|
|
@@ -177,12 +189,12 @@ class AutoStatus:
|
|
|
177
189
|
# Timestamp
|
|
178
190
|
self.display.move_cursor(row, 1)
|
|
179
191
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
180
|
-
print(f"Updated: {timestamp} | Press 'q' to quit")
|
|
192
|
+
print(f"Updated: {timestamp} | Press '\''q'\'' to quit")
|
|
181
193
|
row += 2
|
|
182
194
|
|
|
183
195
|
# File info
|
|
184
196
|
self.display.move_cursor(row, 1)
|
|
185
|
-
print(f"CSV: {data['csv_path']}")
|
|
197
|
+
print(f"CSV: {data['\''csv_path'\'']}")
|
|
186
198
|
row += 2
|
|
187
199
|
|
|
188
200
|
# Status summary
|
|
@@ -190,38 +202,38 @@ class AutoStatus:
|
|
|
190
202
|
print("\033[1mStatus Summary:\033[0m")
|
|
191
203
|
row += 1
|
|
192
204
|
|
|
193
|
-
counts = data[
|
|
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")
|
|
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")
|
|
199
211
|
|
|
200
212
|
self.display.move_cursor(row, 1)
|
|
201
213
|
print(status_line)
|
|
202
214
|
row += 2
|
|
203
215
|
|
|
204
216
|
# Performance stats
|
|
205
|
-
if data[
|
|
217
|
+
if data["performance"]:
|
|
206
218
|
self.display.move_cursor(row, 1)
|
|
207
219
|
print("\033[1mPerformance Stats:\033[0m")
|
|
208
220
|
row += 1
|
|
209
221
|
|
|
210
|
-
perf = data[
|
|
222
|
+
perf = data["performance"]
|
|
211
223
|
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']}")
|
|
224
|
+
print(f" Min: {perf['\''min'\'']:.4f} | Max: {perf['\''max'\'']:.4f} | "
|
|
225
|
+
f"Mean: {perf['\''mean'\'']:.4f} | Count: {perf['\''count'\'']}")
|
|
214
226
|
row += 2
|
|
215
227
|
|
|
216
228
|
# Recent candidates
|
|
217
|
-
if
|
|
229
|
+
if data["recent"]:
|
|
218
230
|
self.display.move_cursor(row, 1)
|
|
219
231
|
print("\033[1mRecent Candidates:\033[0m")
|
|
220
232
|
row += 1
|
|
221
233
|
|
|
222
234
|
# Table header
|
|
223
235
|
self.display.move_cursor(row, 1)
|
|
224
|
-
header_fmt = f"{
|
|
236
|
+
header_fmt = f"{"ID":>8} | {"Status":^10} | {"Performance":>11} | {"Description"}"
|
|
225
237
|
print(header_fmt[:self.display.cols])
|
|
226
238
|
row += 1
|
|
227
239
|
|
|
@@ -230,38 +242,41 @@ class AutoStatus:
|
|
|
230
242
|
row += 1
|
|
231
243
|
|
|
232
244
|
# Table rows
|
|
233
|
-
for
|
|
245
|
+
for candidate in data["recent"]:
|
|
234
246
|
if row >= self.display.rows - 1: # Leave room for bottom
|
|
235
247
|
break
|
|
236
248
|
|
|
237
249
|
self.display.move_cursor(row, 1)
|
|
238
250
|
|
|
239
251
|
# Color based on status
|
|
240
|
-
status = candidate.get(
|
|
241
|
-
if status ==
|
|
242
|
-
color =
|
|
243
|
-
elif status ==
|
|
244
|
-
color =
|
|
245
|
-
elif status ==
|
|
246
|
-
color =
|
|
247
|
-
elif status ==
|
|
248
|
-
color =
|
|
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
|
|
249
261
|
else:
|
|
250
|
-
color =
|
|
262
|
+
color = "\033[0m" # Default
|
|
251
263
|
|
|
252
264
|
# Format performance
|
|
253
|
-
if status ==
|
|
254
|
-
|
|
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 = "-"
|
|
255
270
|
else:
|
|
256
271
|
perf = "-"
|
|
257
272
|
|
|
258
273
|
# Truncate description to fit
|
|
259
|
-
desc = candidate.get(
|
|
274
|
+
desc = candidate.get("description", "")
|
|
260
275
|
max_desc_len = self.display.cols - 35 # Account for other columns
|
|
261
276
|
if len(desc) > max_desc_len:
|
|
262
277
|
desc = desc[:max_desc_len-3] + "..."
|
|
263
278
|
|
|
264
|
-
line = f"{candidate['id']:>8} | {color}{status:^10}\033[0m | {perf:>11} | {desc}"
|
|
279
|
+
line = f"{candidate['\''id'\'']:>8} | {color}{status:^10}\033[0m | {perf:>11} | {desc}"
|
|
265
280
|
print(line[:self.display.cols])
|
|
266
281
|
row += 1
|
|
267
282
|
|
|
@@ -273,7 +288,7 @@ class AutoStatus:
|
|
|
273
288
|
"""Check for keyboard input without blocking."""
|
|
274
289
|
if select.select([sys.stdin], [], [], 0)[0]:
|
|
275
290
|
char = sys.stdin.read(1)
|
|
276
|
-
if char.lower() ==
|
|
291
|
+
if char.lower() == "q":
|
|
277
292
|
self.running = False
|
|
278
293
|
return True
|
|
279
294
|
return False
|
|
@@ -284,13 +299,21 @@ class AutoStatus:
|
|
|
284
299
|
old_settings = termios.tcgetattr(sys.stdin)
|
|
285
300
|
|
|
286
301
|
try:
|
|
287
|
-
# Set terminal to
|
|
288
|
-
tty.
|
|
302
|
+
# Set terminal to cbreak mode (allows Ctrl-C) instead of raw mode
|
|
303
|
+
tty.setcbreak(sys.stdin.fileno())
|
|
289
304
|
|
|
290
305
|
self.display.hide_cursor()
|
|
291
306
|
|
|
292
307
|
while self.running:
|
|
293
|
-
|
|
308
|
+
try:
|
|
309
|
+
self.render()
|
|
310
|
+
except Exception as e:
|
|
311
|
+
# Show error at bottom of screen
|
|
312
|
+
self.display.move_cursor(self.display.rows - 1, 1)
|
|
313
|
+
self.display.clear_line()
|
|
314
|
+
print(f"\033[31mError: {str(e)}\033[0m", end="")
|
|
315
|
+
sys.stdout.flush()
|
|
316
|
+
time.sleep(2) # Give time to read error
|
|
294
317
|
|
|
295
318
|
# Check for input and wait
|
|
296
319
|
for _ in range(10): # Check 10 times per second
|
|
@@ -299,7 +322,7 @@ class AutoStatus:
|
|
|
299
322
|
time.sleep(0.1)
|
|
300
323
|
|
|
301
324
|
except KeyboardInterrupt:
|
|
302
|
-
|
|
325
|
+
self.running = False
|
|
303
326
|
|
|
304
327
|
finally:
|
|
305
328
|
# Restore terminal settings
|
|
@@ -310,23 +333,8 @@ class AutoStatus:
|
|
|
310
333
|
print("Exiting auto-status...")
|
|
311
334
|
|
|
312
335
|
|
|
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()
|
|
336
|
+
# Main execution
|
|
337
|
+
csv_path = "'"$FULL_CSV_PATH"'"
|
|
338
|
+
auto_status = AutoStatus(csv_path)
|
|
339
|
+
auto_status.run()
|
|
340
|
+
'
|