claude-evolve 1.4.10 → 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.
@@ -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,18 +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
- sys.path.insert(0, os.path.join(script_dir, '..'))
21
-
22
- from lib.config import Config
23
- from lib.evolution_csv import EvolutionCSV
24
-
25
-
26
53
  class TerminalDisplay:
27
54
  """Handles terminal display with ANSI escape sequences for flicker-free updates."""
28
55
 
@@ -33,7 +60,7 @@ class TerminalDisplay:
33
60
  def get_terminal_size(self):
34
61
  """Get current terminal size."""
35
62
  try:
36
- rows, cols = os.popen('stty size', 'r').read().split()
63
+ rows, cols = os.popen("stty size", "r").read().split()
37
64
  return int(rows), int(cols)
38
65
  except:
39
66
  return 24, 80 # Default fallback
@@ -44,90 +71,89 @@ class TerminalDisplay:
44
71
 
45
72
  def clear_screen(self):
46
73
  """Clear the entire screen."""
47
- print('\033[2J\033[H', end='')
74
+ print("\033[2J\033[H", end="")
48
75
 
49
76
  def move_cursor(self, row, col):
50
77
  """Move cursor to specific position."""
51
- print(f'\033[{row};{col}H', end='')
78
+ print(f"\033[{row};{col}H", end="")
52
79
 
53
80
  def clear_line(self):
54
81
  """Clear current line."""
55
- print('\033[2K', end='')
82
+ print("\033[2K", end="")
56
83
 
57
84
  def hide_cursor(self):
58
85
  """Hide the cursor."""
59
- print('\033[?25l', end='')
86
+ print("\033[?25l", end="")
60
87
 
61
88
  def show_cursor(self):
62
89
  """Show the cursor."""
63
- print('\033[?25h', end='')
90
+ print("\033[?25h", end="")
64
91
 
65
92
  def reset(self):
66
93
  """Reset terminal to normal state."""
67
94
  self.show_cursor()
68
- print('\033[0m', end='') # Reset colors
95
+ print("\033[0m", end="") # Reset colors
69
96
 
70
97
 
71
98
  class AutoStatus:
72
99
  """Auto-updating status display."""
73
100
 
74
- def __init__(self, working_dir=None):
75
- self.config = Config()
76
-
77
- # Load config using same mechanism as other commands
78
- # First check CLAUDE_EVOLVE_CONFIG env var
79
- config_env = os.environ.get('CLAUDE_EVOLVE_CONFIG')
80
- if config_env:
81
- self.config.load(config_env)
82
- else:
83
- # Load from working directory or current directory
84
- self.config.load(working_dir=working_dir)
85
-
101
+ def __init__(self, csv_path):
102
+ self.csv_path = csv_path
86
103
  self.display = TerminalDisplay()
87
104
  self.running = True
88
105
 
89
106
  def get_status_data(self):
90
107
  """Get current status data from CSV."""
91
- csv_path = self.config.resolve_path(self.config.data['csv_file'])
108
+ # Read CSV data directly
109
+ with open(self.csv_path, "r") as f:
110
+ reader = csv.DictReader(f)
111
+ rows = list(reader)
92
112
 
93
- with EvolutionCSV(csv_path) as csv:
94
- df = csv.df
95
-
96
- # Count by status
97
- status_counts = {
98
- 'pending': len(df[df['status'] == 'pending']),
99
- 'running': len(df[df['status'] == 'running']),
100
- 'complete': len(df[df['status'] == 'complete']),
101
- 'failed': len(df[df['status'] == 'failed']),
102
- 'total': len(df)
103
- }
104
-
105
- # Get performance stats for completed
106
- completed_df = df[df['status'] == 'complete']
107
- if not completed_df.empty and 'performance' in completed_df.columns:
108
- perf_values = completed_df['performance'].dropna()
109
- if not perf_values.empty:
110
- perf_stats = {
111
- 'min': perf_values.min(),
112
- 'max': perf_values.max(),
113
- 'mean': perf_values.mean(),
114
- 'count': len(perf_values)
115
- }
116
- else:
117
- perf_stats = None
118
- else:
119
- perf_stats = None
120
-
121
- # Get recent candidates (last N that fit on screen)
122
- max_candidates = max(1, self.display.rows - 15) # Reserve space for header/stats
123
- 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
124
128
 
125
- return {
126
- 'counts': status_counts,
127
- 'performance': perf_stats,
128
- 'recent': recent,
129
- 'csv_path': csv_path
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)
130
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
+ }
131
157
 
132
158
  def format_duration(self, seconds):
133
159
  """Format duration in human-readable form."""
@@ -163,12 +189,12 @@ class AutoStatus:
163
189
  # Timestamp
164
190
  self.display.move_cursor(row, 1)
165
191
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
166
- print(f"Updated: {timestamp} | Press 'q' to quit")
192
+ print(f"Updated: {timestamp} | Press '\''q'\'' to quit")
167
193
  row += 2
168
194
 
169
195
  # File info
170
196
  self.display.move_cursor(row, 1)
171
- print(f"CSV: {data['csv_path']}")
197
+ print(f"CSV: {data['\''csv_path'\'']}")
172
198
  row += 2
173
199
 
174
200
  # Status summary
@@ -176,38 +202,38 @@ class AutoStatus:
176
202
  print("\033[1mStatus Summary:\033[0m")
177
203
  row += 1
178
204
 
179
- counts = data['counts']
180
- status_line = (f" Total: {counts['total']} | "
181
- f"\033[33mPending: {counts['pending']}\033[0m | "
182
- f"\033[36mRunning: {counts['running']}\033[0m | "
183
- f"\033[32mComplete: {counts['complete']}\033[0m | "
184
- 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")
185
211
 
186
212
  self.display.move_cursor(row, 1)
187
213
  print(status_line)
188
214
  row += 2
189
215
 
190
216
  # Performance stats
191
- if data['performance']:
217
+ if data["performance"]:
192
218
  self.display.move_cursor(row, 1)
193
219
  print("\033[1mPerformance Stats:\033[0m")
194
220
  row += 1
195
221
 
196
- perf = data['performance']
222
+ perf = data["performance"]
197
223
  self.display.move_cursor(row, 1)
198
- print(f" Min: {perf['min']:.4f} | Max: {perf['max']:.4f} | "
199
- 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'\'']}")
200
226
  row += 2
201
227
 
202
228
  # Recent candidates
203
- if not data['recent'].empty:
229
+ if data["recent"]:
204
230
  self.display.move_cursor(row, 1)
205
231
  print("\033[1mRecent Candidates:\033[0m")
206
232
  row += 1
207
233
 
208
234
  # Table header
209
235
  self.display.move_cursor(row, 1)
210
- header_fmt = f"{'ID':>8} | {'Status':^10} | {'Performance':>11} | {'Description'}"
236
+ header_fmt = f"{"ID":>8} | {"Status":^10} | {"Performance":>11} | {"Description"}"
211
237
  print(header_fmt[:self.display.cols])
212
238
  row += 1
213
239
 
@@ -216,38 +242,41 @@ class AutoStatus:
216
242
  row += 1
217
243
 
218
244
  # Table rows
219
- for _, candidate in data['recent'].iterrows():
245
+ for candidate in data["recent"]:
220
246
  if row >= self.display.rows - 1: # Leave room for bottom
221
247
  break
222
248
 
223
249
  self.display.move_cursor(row, 1)
224
250
 
225
251
  # Color based on status
226
- status = candidate.get('status', 'unknown')
227
- if status == 'complete':
228
- color = '\033[32m' # Green
229
- elif status == 'running':
230
- color = '\033[36m' # Cyan
231
- elif status == 'failed':
232
- color = '\033[31m' # Red
233
- elif status == 'pending':
234
- color = '\033[33m' # Yellow
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
235
261
  else:
236
- color = '\033[0m' # Default
262
+ color = "\033[0m" # Default
237
263
 
238
264
  # Format performance
239
- if status == 'complete' and 'performance' in candidate:
240
- perf = f"{candidate['performance']:.4f}"
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 = "-"
241
270
  else:
242
271
  perf = "-"
243
272
 
244
273
  # Truncate description to fit
245
- desc = candidate.get('description', '')
274
+ desc = candidate.get("description", "")
246
275
  max_desc_len = self.display.cols - 35 # Account for other columns
247
276
  if len(desc) > max_desc_len:
248
277
  desc = desc[:max_desc_len-3] + "..."
249
278
 
250
- 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}"
251
280
  print(line[:self.display.cols])
252
281
  row += 1
253
282
 
@@ -259,7 +288,7 @@ class AutoStatus:
259
288
  """Check for keyboard input without blocking."""
260
289
  if select.select([sys.stdin], [], [], 0)[0]:
261
290
  char = sys.stdin.read(1)
262
- if char.lower() == 'q':
291
+ if char.lower() == "q":
263
292
  self.running = False
264
293
  return True
265
294
  return False
@@ -270,13 +299,21 @@ class AutoStatus:
270
299
  old_settings = termios.tcgetattr(sys.stdin)
271
300
 
272
301
  try:
273
- # Set terminal to raw mode for immediate input
274
- tty.setraw(sys.stdin.fileno())
302
+ # Set terminal to cbreak mode (allows Ctrl-C) instead of raw mode
303
+ tty.setcbreak(sys.stdin.fileno())
275
304
 
276
305
  self.display.hide_cursor()
277
306
 
278
307
  while self.running:
279
- self.render()
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
280
317
 
281
318
  # Check for input and wait
282
319
  for _ in range(10): # Check 10 times per second
@@ -285,7 +322,7 @@ class AutoStatus:
285
322
  time.sleep(0.1)
286
323
 
287
324
  except KeyboardInterrupt:
288
- pass
325
+ self.running = False
289
326
 
290
327
  finally:
291
328
  # Restore terminal settings
@@ -296,23 +333,8 @@ class AutoStatus:
296
333
  print("Exiting auto-status...")
297
334
 
298
335
 
299
- def main():
300
- """Main entry point."""
301
- parser = argparse.ArgumentParser(
302
- description="Auto-updating status display for claude-evolve that fits to terminal size.",
303
- epilog="Press 'q' to quit while running."
304
- )
305
- parser.add_argument(
306
- '--working-dir',
307
- help='Working directory containing claude-evolve.yaml config file'
308
- )
309
-
310
- args = parser.parse_args()
311
-
312
- # Run auto-status
313
- auto_status = AutoStatus(working_dir=args.working_dir)
314
- auto_status.run()
315
-
316
-
317
- if __name__ == '__main__':
318
- main()
336
+ # Main execution
337
+ csv_path = "'"$FULL_CSV_PATH"'"
338
+ auto_status = AutoStatus(csv_path)
339
+ auto_status.run()
340
+ '
package/lib/config.py ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python3
2
+ """Configuration loader for claude-evolve Python scripts."""
3
+
4
+ import os
5
+ import yaml
6
+ from pathlib import Path
7
+
8
+
9
+ class Config:
10
+ """Configuration manager that matches the bash config.sh functionality."""
11
+
12
+ # Default values matching config.sh
13
+ DEFAULTS = {
14
+ 'evolution_dir': 'evolution',
15
+ 'algorithm_file': 'algorithm.py',
16
+ 'evaluator_file': 'evaluator.py',
17
+ 'brief_file': 'BRIEF.md',
18
+ 'csv_file': 'evolution.csv',
19
+ 'output_dir': '',
20
+ 'parent_selection': 'best',
21
+ 'python_cmd': 'python3',
22
+ 'ideation': {
23
+ 'total_ideas': 15,
24
+ 'novel_exploration': 3,
25
+ 'hill_climbing': 5,
26
+ 'structural_mutation': 3,
27
+ 'crossover_hybrid': 4,
28
+ 'num_elites': 3,
29
+ 'num_revolution': 2
30
+ },
31
+ 'parallel': {
32
+ 'enabled': False,
33
+ 'max_workers': 4,
34
+ 'lock_timeout': 10
35
+ },
36
+ 'auto_ideate': True,
37
+ 'max_retries': 3
38
+ }
39
+
40
+ def __init__(self):
41
+ self.data = self.DEFAULTS.copy()
42
+ self.config_path = None
43
+ self.working_dir = None
44
+
45
+ def load(self, config_path=None, working_dir=None):
46
+ """Load configuration from YAML file."""
47
+ # Determine config file path
48
+ if config_path:
49
+ # Explicit config path provided
50
+ self.config_path = Path(config_path)
51
+ elif working_dir:
52
+ # Look for config.yaml in working directory
53
+ self.working_dir = Path(working_dir)
54
+ self.config_path = self.working_dir / 'config.yaml'
55
+ else:
56
+ # Default to evolution/config.yaml
57
+ self.config_path = Path('evolution/config.yaml')
58
+
59
+ # Load config if it exists
60
+ if self.config_path.exists():
61
+ with open(self.config_path, 'r') as f:
62
+ yaml_data = yaml.safe_load(f) or {}
63
+
64
+ # Merge with defaults
65
+ self.data.update(yaml_data)
66
+
67
+ # Handle nested structures
68
+ if 'ideation' in yaml_data:
69
+ self.data['ideation'] = {**self.DEFAULTS['ideation'], **yaml_data['ideation']}
70
+ if 'parallel' in yaml_data:
71
+ self.data['parallel'] = {**self.DEFAULTS['parallel'], **yaml_data['parallel']}
72
+
73
+ def resolve_path(self, relative_path):
74
+ """Resolve a path relative to the config directory."""
75
+ if not relative_path:
76
+ return None
77
+
78
+ # If config_path is set, use its parent directory
79
+ if self.config_path:
80
+ base_dir = self.config_path.parent
81
+ elif self.working_dir:
82
+ base_dir = self.working_dir
83
+ else:
84
+ base_dir = Path.cwd()
85
+
86
+ # Handle output_dir special case
87
+ if relative_path == self.data.get('output_dir', '') and not relative_path:
88
+ # Empty output_dir means use evolution_dir
89
+ relative_path = self.data.get('evolution_dir', 'evolution')
90
+
91
+ resolved = base_dir / relative_path
92
+ return str(resolved.resolve())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.4.10",
3
+ "version": "1.4.12",
4
4
  "bin": {
5
5
  "claude-evolve": "./bin/claude-evolve",
6
6
  "claude-evolve-main": "./bin/claude-evolve-main",