claude-evolve 1.7.23 → 1.7.26

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.
@@ -28,17 +28,32 @@ cleanup_on_exit() {
28
28
  echo "[WORKER-$$] Worker terminated while processing $CURRENT_CANDIDATE_ID" >&2
29
29
  # Grab current status; only reset if it is not already a failure or complete
30
30
  echo "[WORKER-$$] Determining status before resetting" >&2
31
- "$PYTHON_CMD" -c "
31
+
32
+ local reset_output
33
+ local reset_exit_code
34
+ reset_output=$("$PYTHON_CMD" -c "
32
35
  import sys
33
36
  sys.path.insert(0, '$SCRIPT_DIR/..')
34
37
  from lib.evolution_csv import EvolutionCSV
35
38
  csv = EvolutionCSV('$FULL_CSV_PATH')
36
39
  info = csv.get_candidate_info('$CURRENT_CANDIDATE_ID')
37
40
  status = info.get('status', '').lower() if info else ''
41
+ print(f'Current status: {status}', file=sys.stderr)
38
42
  # Do not reset if already failed, failed-ai-retry, or complete
39
- if status and status not in ('complete', 'failed', 'failed-ai-retry', 'failed-parent-missing'):
43
+ if status and status not in ('complete', 'failed', 'failed-ai-retry', 'failed-parent-missing'):
44
+ print(f'Resetting {status} -> pending', file=sys.stderr)
40
45
  csv.update_candidate_status('$CURRENT_CANDIDATE_ID', 'pending')
41
- " 2>/dev/null || echo "[WORKER-$$] Warning: failed to reset status" >&2
46
+ else:
47
+ print(f'Status {status} does not need reset', file=sys.stderr)
48
+ " 2>&1)
49
+ reset_exit_code=$?
50
+
51
+ if [[ $reset_exit_code -ne 0 ]]; then
52
+ echo "[WORKER-$$] Warning: Failed to reset status for $CURRENT_CANDIDATE_ID (exit code $reset_exit_code)" >&2
53
+ echo "[WORKER-$$] Error details: $reset_output" >&2
54
+ else
55
+ echo "[WORKER-$$] Status check result: $reset_output" >&2
56
+ fi
42
57
  fi
43
58
  }
44
59
 
@@ -596,19 +611,34 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
596
611
  csv.update_candidate_status('$candidate_id', 'failed-ai-retry')
597
612
  " 2>/dev/null || true
598
613
  elif [[ $process_exit_code -eq 78 ]]; then
599
- # Missing parent; mark as failed-parent-missing for clarity
600
- echo "[WORKER-$$] Parent missing for $candidate_id - marking failed-parent-missing"
614
+ # Missing parent; mark child as failed and auto-recover parent
615
+ echo "[WORKER-$$] Parent '$parent_id' missing for $candidate_id"
616
+ echo "[WORKER-$$] Marking $candidate_id as failed-parent-missing"
617
+ echo "[WORKER-$$] Auto-recovering: marking parent '$parent_id' as pending"
618
+
601
619
  "$PYTHON_CMD" -c "
602
620
  import sys
603
621
  sys.path.insert(0, '$SCRIPT_DIR/..')
604
622
  from lib.evolution_csv import EvolutionCSV
605
623
  with EvolutionCSV('$FULL_CSV_PATH') as csv:
624
+ # Mark child as failed
606
625
  csv.update_candidate_status('$candidate_id', 'failed-parent-missing')
607
- " 2>/dev/null || true
608
- # Clear current candidate to avoid cleanup resetting it
626
+
627
+ # Auto-recover: mark parent as pending so it gets processed
628
+ parent_info = csv.get_candidate_info('$parent_id')
629
+ if parent_info:
630
+ parent_status = parent_info.get('status', '').lower()
631
+ if parent_status in ('', 'skipped', 'failed-parent-missing'):
632
+ csv.update_candidate_status('$parent_id', 'pending')
633
+ print(f'Auto-recovered parent $parent_id: {parent_status} -> pending', file=sys.stderr)
634
+ else:
635
+ print(f'Parent $parent_id has status: {parent_status} (not auto-recovering)', file=sys.stderr)
636
+ else:
637
+ print(f'Warning: parent $parent_id not found in CSV', file=sys.stderr)
638
+ " 2>&1 | grep -E "Auto-recovered|Parent.*status|Warning" || true
639
+
640
+ # Clear current candidate and continue to next (don't break)
609
641
  CURRENT_CANDIDATE_ID=""
610
- # Stop looping so we don't keep retrying for missing dependency
611
- break
612
642
  else
613
643
  echo "[WORKER-$$] Failed to process $candidate_id"
614
644
  # Other failures (evaluation errors, etc) mark as failed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.7.23",
3
+ "version": "1.7.26",
4
4
  "bin": {
5
5
  "claude-evolve": "./bin/claude-evolve",
6
6
  "claude-evolve-main": "./bin/claude-evolve-main",
@@ -1,97 +0,0 @@
1
- #!/bin/bash
2
- # Python-based ideation wrapper for claude-evolve
3
- # This is a simple wrapper that calls the Python ideation helper
4
-
5
- set -e
6
-
7
- # Load configuration to ensure paths are set up
8
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
- # shellcheck source=../lib/config.sh
10
- source "$SCRIPT_DIR/../lib/config.sh"
11
-
12
- # Use CLAUDE_EVOLVE_CONFIG if set, otherwise default
13
- if [[ -n ${CLAUDE_EVOLVE_CONFIG:-} ]]; then
14
- load_config "$CLAUDE_EVOLVE_CONFIG"
15
- else
16
- # Check if config.yaml exists in current directory
17
- if [[ -f "config.yaml" ]]; then
18
- CONFIG_FILE="$(pwd)/config.yaml"
19
- load_config "$CONFIG_FILE"
20
- else
21
- load_config
22
- fi
23
- fi
24
-
25
- # Setup logging to file
26
- if [[ -n "${FULL_EVOLUTION_DIR:-}" ]]; then
27
- LOG_DIR="$FULL_EVOLUTION_DIR/logs"
28
- mkdir -p "$LOG_DIR"
29
- LOG_FILE="$LOG_DIR/ideate-py-$$-$(date +%Y%m%d-%H%M%S).log"
30
-
31
- # Log to both terminal and file with timestamps
32
- exec > >(while IFS= read -r line; do echo "$(date '+%Y-%m-%d %H:%M:%S'): $line"; done | tee -a "$LOG_FILE") 2>&1
33
- echo "[IDEATE-PY-$$] Logging to: $LOG_FILE"
34
- fi
35
-
36
- # Parse arguments
37
- use_strategies=true
38
-
39
- while [[ $# -gt 0 ]]; do
40
- case $1 in
41
- --help)
42
- cat <<EOF
43
- claude-evolve ideate (Python version) - Generate new algorithm ideas
44
-
45
- USAGE:
46
- claude-evolve ideate-py [--legacy N]
47
-
48
- OPTIONS:
49
- --legacy N Use legacy mode with N ideas (ignores strategy config)
50
- --help Show this help message
51
-
52
- DESCRIPTION:
53
- Python-based ideation that uses MD5 hashing to verify AI actually modified files.
54
- This solves the problem where AIs claim they edited files but didn't.
55
-
56
- Uses multi-strategy evolutionary approach:
57
- - Novel exploration: Pure creativity, global search
58
- - Hill climbing: Parameter tuning of top performers
59
- - Structural mutation: Algorithmic changes to top performers
60
- - Crossover hybrid: Combine successful approaches
61
-
62
- Strategy distribution is configured in evolution/config.yaml
63
- EOF
64
- exit 0
65
- ;;
66
- --legacy)
67
- use_strategies=false
68
- shift
69
- if [[ $1 =~ ^[0-9]+$ ]]; then
70
- export LEGACY_IDEA_COUNT=$1
71
- shift
72
- else
73
- echo "[ERROR] --legacy requires a number" >&2
74
- exit 1
75
- fi
76
- ;;
77
- *)
78
- echo "[ERROR] Unknown option: $1" >&2
79
- exit 1
80
- ;;
81
- esac
82
- done
83
-
84
- # Check workspace using config
85
- if [[ ! -d "$FULL_EVOLUTION_DIR" ]]; then
86
- echo "[ERROR] Evolution workspace not found: $FULL_EVOLUTION_DIR. Run 'claude-evolve setup' first." >&2
87
- exit 1
88
- fi
89
-
90
- # Call Python helper
91
- echo "[INFO] Using Python ideation helper (more reliable AI result verification)"
92
-
93
- if [[ $use_strategies == true ]]; then
94
- exec "$PYTHON_CMD" "$SCRIPT_DIR/../lib/ideation_helper.py" run
95
- else
96
- exec "$PYTHON_CMD" "$SCRIPT_DIR/../lib/ideation_helper.py" run --legacy "$LEGACY_IDEA_COUNT"
97
- fi
@@ -1,745 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Python ideation helper for claude-evolve.
4
- Replaces the complex shell script logic in claude-evolve-ideate.
5
- """
6
-
7
- import os
8
- import sys
9
- import csv
10
- import re
11
- import subprocess
12
- import tempfile
13
- import shutil
14
- import time
15
- from pathlib import Path
16
- from typing import List, Tuple, Optional, Dict
17
- from dataclasses import dataclass
18
-
19
- from evolution_csv import EvolutionCSV
20
- from config import Config
21
-
22
-
23
- @dataclass
24
- class IdeationStrategy:
25
- """Configuration for a single ideation strategy."""
26
- name: str
27
- count: int
28
- parent_required: bool = False
29
-
30
-
31
- class IdeationHelper:
32
- """Handles all ideation logic for claude-evolve."""
33
-
34
- def __init__(self, config: Config):
35
- """Initialize with configuration."""
36
- self.config = config
37
- self.working_dir = Path(config.working_dir)
38
- self.csv_path = self.working_dir / config.data['csv_file']
39
- self.brief_path = self.working_dir / config.data['brief_file']
40
- self.output_dir = self.working_dir / (config.data.get('output_dir') or '')
41
-
42
- # Ideation config
43
- ideation = config.data.get('ideation', {})
44
- self.total_ideas = ideation.get('total_ideas', 15)
45
- self.novel_exploration = ideation.get('novel_exploration', 3)
46
- self.hill_climbing = ideation.get('hill_climbing', 5)
47
- self.structural_mutation = ideation.get('structural_mutation', 3)
48
- self.crossover_hybrid = ideation.get('crossover_hybrid', 4)
49
- self.num_elites = ideation.get('num_elites', 3)
50
- self.num_revolution = ideation.get('num_revolution', 2)
51
-
52
- # Python command
53
- self.python_cmd = config.data.get('python_cmd', 'python3')
54
-
55
- # Paths to library scripts
56
- self.script_dir = Path(__file__).parent.parent / 'bin'
57
- self.lib_dir = Path(__file__).parent
58
-
59
- def get_next_generation(self) -> str:
60
- """
61
- Get the next generation number that doesn't have existing Python files.
62
- Returns formatted generation string like '01', '02', etc.
63
- """
64
- # Start with generation 1
65
- start_gen = 1
66
-
67
- if self.csv_path.exists():
68
- # Find max generation from CSV
69
- with EvolutionCSV(str(self.csv_path)) as csv_ops:
70
- rows = csv_ops._read_csv()
71
- max_gen = 0
72
-
73
- for row in rows[1:]: # Skip header
74
- if row and len(row) > 0:
75
- id_field = row[0].strip()
76
- if id_field.startswith('gen') and '-' in id_field:
77
- try:
78
- gen_part = id_field.split('-')[0] # e.g., 'gen01'
79
- gen_num = int(gen_part[3:]) # Extract number after 'gen'
80
- max_gen = max(max_gen, gen_num)
81
- except (ValueError, IndexError):
82
- pass
83
-
84
- start_gen = max_gen + 1
85
-
86
- # Keep incrementing until we find a generation with no Python files
87
- candidate_gen = start_gen
88
- while candidate_gen < 1000: # Safety limit
89
- gen_formatted = f"{candidate_gen:02d}"
90
-
91
- # Check if any Python files exist for this generation
92
- py_pattern = self.output_dir / f"evolution_gen{gen_formatted}-*.py"
93
- if not list(self.output_dir.glob(f"evolution_gen{gen_formatted}-*.py")):
94
- return gen_formatted
95
-
96
- print(f"[WARN] Generation {gen_formatted} already has Python files, skipping",
97
- file=sys.stderr)
98
- candidate_gen += 1
99
-
100
- raise RuntimeError("Could not find a safe generation number (checked up to 999)")
101
-
102
- def get_next_id(self, generation: str) -> str:
103
- """Get the next available ID for the given generation."""
104
- if not self.csv_path.exists():
105
- return f"gen{generation}-001"
106
-
107
- max_id = 0
108
- pattern = re.compile(rf'^gen{generation}-(\d+)$')
109
-
110
- with EvolutionCSV(str(self.csv_path)) as csv_ops:
111
- rows = csv_ops._read_csv()
112
- for row in rows[1:]: # Skip header
113
- if row and len(row) > 0:
114
- id_field = row[0].strip()
115
- match = pattern.match(id_field)
116
- if match:
117
- id_num = int(match.group(1))
118
- max_id = max(max_id, id_num)
119
-
120
- return f"gen{generation}-{max_id + 1:03d}"
121
-
122
- def get_next_ids(self, generation: str, count: int) -> List[str]:
123
- """Get the next N available IDs for the given generation."""
124
- if not self.csv_path.exists():
125
- start_id = 1
126
- else:
127
- max_id = 0
128
- pattern = re.compile(rf'^gen{generation}-(\d+)$')
129
-
130
- with EvolutionCSV(str(self.csv_path)) as csv_ops:
131
- rows = csv_ops._read_csv()
132
- for row in rows[1:]: # Skip header
133
- if row and len(row) > 0:
134
- id_field = row[0].strip()
135
- match = pattern.match(id_field)
136
- if match:
137
- id_num = int(match.group(1))
138
- max_id = max(max_id, id_num)
139
-
140
- start_id = max_id + 1
141
-
142
- return [f"gen{generation}-{start_id + i:03d}" for i in range(count)]
143
-
144
- def get_top_performers(self, num_requested: int) -> List[Tuple[str, str, float]]:
145
- """
146
- Get top performing algorithms.
147
- Returns list of (id, description, score) tuples.
148
- """
149
- if not self.csv_path.exists():
150
- return []
151
-
152
- with EvolutionCSV(str(self.csv_path)) as csv_ops:
153
- rows = csv_ops._read_csv()
154
-
155
- completed = []
156
- novel = []
157
-
158
- # Skip header
159
- for row in rows[1:]:
160
- if len(row) >= 5 and row[3] and row[4] == 'complete':
161
- try:
162
- candidate_id = row[0]
163
- parent_id = row[1] if len(row) > 1 else ''
164
- description = row[2] if len(row) > 2 else ''
165
- score = float(row[3])
166
-
167
- completed.append((candidate_id, description, score))
168
-
169
- # Track novel candidates separately
170
- if not parent_id:
171
- novel.append((candidate_id, description, score))
172
- except ValueError:
173
- pass
174
-
175
- # Sort by score (descending)
176
- completed.sort(key=lambda x: x[2], reverse=True)
177
- novel.sort(key=lambda x: x[2], reverse=True)
178
-
179
- # Collect top performers
180
- selected_ids = set()
181
- results = []
182
-
183
- # Add top absolute performers
184
- for candidate_id, description, score in completed[:num_requested]:
185
- results.append((candidate_id, description, score))
186
- selected_ids.add(candidate_id)
187
-
188
- # Add top novel candidates (if not already selected)
189
- for candidate_id, description, score in novel:
190
- if candidate_id not in selected_ids and len([r for r in results if r[0] not in selected_ids]) < self.num_revolution:
191
- results.append((candidate_id, description, score))
192
- selected_ids.add(candidate_id)
193
-
194
- return results
195
-
196
- def call_ai_model(self, model_name: str, prompt: str, timeout: int = 300) -> Tuple[str, int]:
197
- """
198
- Call an AI model using subprocess.
199
- Returns (output, exit_code).
200
- """
201
- # Map model names to commands
202
- model_commands = {
203
- 'opus': ['claude', '--dangerously-skip-permissions', '--mcp-config', '', '--model', 'opus', '-p', prompt],
204
- 'sonnet': ['claude', '--dangerously-skip-permissions', '--mcp-config', '', '--model', 'sonnet', '-p', prompt],
205
- 'haiku': ['claude', '--dangerously-skip-permissions', '--mcp-config', '', '--model', 'haiku', '-p', prompt],
206
- 'gemini-pro': ['gemini', '-y', '-m', 'gemini-2.5-pro', '-p', prompt],
207
- 'gemini-flash': ['gemini', '-y', '-m', 'gemini-2.5-flash', '-p', prompt],
208
- }
209
-
210
- if model_name not in model_commands:
211
- return f"Unknown model: {model_name}", 1
212
-
213
- try:
214
- result = subprocess.run(
215
- model_commands[model_name],
216
- capture_output=True,
217
- text=True,
218
- timeout=timeout
219
- )
220
- return result.stdout + result.stderr, result.returncode
221
- except subprocess.TimeoutExpired:
222
- return f"Model {model_name} timed out after {timeout}s", 124
223
- except Exception as e:
224
- return f"Error calling model {model_name}: {e}", 1
225
-
226
- def prepopulate_csv_stubs(self, temp_csv: Path, ids: List[str],
227
- placeholder_text: str, parent_id: str = "") -> None:
228
- """Pre-populate CSV with stub rows containing placeholders."""
229
- print(f"[INFO] Pre-populating CSV with stub rows: {' '.join(ids)}")
230
-
231
- with open(temp_csv, 'a') as f:
232
- for id_str in ids:
233
- f.write(f'{id_str},{parent_id},"{placeholder_text}",,pending\n')
234
-
235
- def validate_csv_modification(self, temp_csv: Path, expected_count: int,
236
- original_count: int, required_ids: List[str]) -> bool:
237
- """
238
- Validate that AI correctly modified the CSV.
239
- Returns True if valid, False otherwise.
240
- """
241
- if not temp_csv.exists():
242
- print("[ERROR] CSV file was not found after AI modification", file=sys.stderr)
243
- return False
244
-
245
- # Count data rows (skip header, skip empty lines)
246
- with open(temp_csv, 'r') as f:
247
- lines = [line for line in f if line.strip()]
248
- new_count = len(lines) - 1 # Subtract header
249
-
250
- if new_count < original_count:
251
- print(f"[ERROR] AI overwrote CSV instead of editing ({new_count} < {original_count})",
252
- file=sys.stderr)
253
- return False
254
-
255
- if new_count != original_count:
256
- print(f"[ERROR] Row count changed ({new_count} != {original_count})", file=sys.stderr)
257
- return False
258
-
259
- # Check that no placeholders remain
260
- with open(temp_csv, 'r') as f:
261
- content = f.read()
262
- if 'PLACEHOLDER' in content:
263
- placeholder_count = content.count('PLACEHOLDER')
264
- print(f"[ERROR] {placeholder_count} placeholders remain unfilled", file=sys.stderr)
265
- return False
266
-
267
- # Validate IDs are correct
268
- with open(temp_csv, 'r') as f:
269
- reader = csv.reader(f)
270
- next(reader) # Skip header
271
- rows = list(reader)
272
- actual_ids = [row[0].strip('"') for row in rows[-len(required_ids):]]
273
-
274
- if actual_ids != required_ids:
275
- print(f"[ERROR] AI used wrong IDs!", file=sys.stderr)
276
- print(f"[ERROR] Expected: {required_ids}", file=sys.stderr)
277
- print(f"[ERROR] Got: {actual_ids}", file=sys.stderr)
278
- return False
279
-
280
- print(f"[INFO] ✓ AI correctly used the specified IDs: {' '.join(actual_ids)}")
281
- return True
282
-
283
- def apply_csv_changes(self, temp_csv: Path, added_count: int,
284
- ai_model: str) -> bool:
285
- """
286
- Apply changes from temp CSV to main CSV with proper locking.
287
- """
288
- print("[INFO] Acquiring CSV lock to apply changes...")
289
-
290
- try:
291
- with EvolutionCSV(str(self.csv_path)) as csv_ops:
292
- # Get the original line count
293
- with open(self.csv_path, 'r') as f:
294
- original_lines = len(f.readlines())
295
-
296
- # Append only the new lines from temp CSV
297
- with open(temp_csv, 'r') as f_temp:
298
- temp_lines = f_temp.readlines()
299
- new_lines = temp_lines[original_lines:]
300
-
301
- with open(self.csv_path, 'a') as f_main:
302
- f_main.writelines(new_lines)
303
-
304
- # Update idea-LLM field for newly added rows
305
- if ai_model:
306
- print(f"[INFO] Recording that {ai_model} generated the ideas")
307
- # Get IDs of newly added rows
308
- reader = csv.reader(new_lines)
309
- for row in reader:
310
- if row and row[0] and row[0] != 'id':
311
- candidate_id = row[0].strip('"')
312
- csv_ops.update_candidate_field(candidate_id, 'idea-LLM', ai_model)
313
-
314
- print(f"[INFO] Successfully added {added_count} ideas to CSV")
315
- return True
316
-
317
- except Exception as e:
318
- print(f"[ERROR] Failed to apply CSV changes: {e}", file=sys.stderr)
319
- return False
320
-
321
- def call_ai_for_ideation(self, prompt: str, generation: str, expected_count: int,
322
- temp_csv_path: Path) -> Optional[str]:
323
- """
324
- Call AI models in round-robin fashion until one successfully fills placeholders.
325
-
326
- This is the critical function that addresses the main problem:
327
- - Takes MD5 hash of CSV before calling AI
328
- - Takes MD5 hash after AI completes
329
- - Only accepts result if:
330
- 1. File actually changed (hash is different)
331
- 2. Row count stayed the same (no adds/deletes)
332
- 3. All PLACEHOLDERs are gone (AI filled them in)
333
-
334
- Returns the model name that succeeded, or None if all failed.
335
- """
336
- import hashlib
337
-
338
- # Get models for ideation (hardcoded for now, could be from config)
339
- models = ['sonnet', 'gemini-pro', 'opus']
340
-
341
- # Calculate starting index for round-robin
342
- gen_num = int(generation.lstrip('0')) if generation.lstrip('0') else 1
343
- start_index = gen_num % len(models)
344
-
345
- # Create ordered list based on round-robin
346
- ordered_models = [models[(start_index + i) % len(models)] for i in range(len(models))]
347
-
348
- print(f"[AI] Model order for ideate (round-robin): {' '.join(ordered_models)}",
349
- file=sys.stderr)
350
-
351
- # Get original CSV count and hash BEFORE calling AI
352
- with open(temp_csv_path, 'rb') as f:
353
- original_content = f.read()
354
- original_hash = hashlib.md5(original_content).hexdigest()
355
- original_lines = [line for line in original_content.decode('utf-8').split('\n') if line.strip()]
356
- original_count = len(original_lines) - 1 # Subtract header
357
-
358
- print(f"[DEBUG] Pre-AI state: {original_count} rows, hash={original_hash[:8]}...", file=sys.stderr)
359
-
360
- # Backup temp CSV
361
- temp_csv_backup = Path(str(temp_csv_path) + '.backup')
362
- shutil.copy(temp_csv_path, temp_csv_backup)
363
-
364
- # Try each model
365
- for model in ordered_models:
366
- print(f"[AI] Attempting ideate with {model}", file=sys.stderr)
367
-
368
- # Restore temp CSV from backup before each attempt
369
- shutil.copy(temp_csv_backup, temp_csv_path)
370
-
371
- # Re-hash after restore to ensure we start from known state
372
- with open(temp_csv_path, 'rb') as f:
373
- pre_call_hash = hashlib.md5(f.read()).hexdigest()
374
-
375
- print(f"[DEBUG] Before {model}: hash={pre_call_hash[:8]}...", file=sys.stderr)
376
-
377
- # Call the model
378
- output, exit_code = self.call_ai_model(model, prompt)
379
-
380
- # CRITICAL: Hash the file AFTER the AI call to detect if it actually changed
381
- with open(temp_csv_path, 'rb') as f:
382
- post_call_content = f.read()
383
- post_call_hash = hashlib.md5(post_call_content).hexdigest()
384
- post_call_lines = [line for line in post_call_content.decode('utf-8').split('\n') if line.strip()]
385
- new_count = len(post_call_lines) - 1 # Subtract header
386
-
387
- print(f"[DEBUG] After {model}: {new_count} rows, hash={post_call_hash[:8]}...", file=sys.stderr)
388
-
389
- # Check 1: Did the file actually change?
390
- if post_call_hash == pre_call_hash:
391
- print(f"[WARN] {model} CLAIMED to edit but file hash is IDENTICAL - file was NOT modified!",
392
- file=sys.stderr)
393
- print(f"[DEBUG] AI output (last 500 chars): ...{output[-500:]}", file=sys.stderr)
394
- continue
395
-
396
- print(f"[INFO] ✓ {model} actually modified the file (hash changed)", file=sys.stderr)
397
-
398
- # Check 2: Row count should stay the same (we're editing, not adding)
399
- if new_count != original_count:
400
- print(f"[WARN] {model} changed row count ({original_count} -> {new_count}) - should only edit, not add/delete",
401
- file=sys.stderr)
402
- continue
403
-
404
- print(f"[INFO] ✓ Row count unchanged ({new_count} rows)", file=sys.stderr)
405
-
406
- # Check 3: All placeholders should be gone
407
- content_str = post_call_content.decode('utf-8')
408
- placeholder_count = content_str.count('PLACEHOLDER')
409
-
410
- if placeholder_count > 0:
411
- print(f"[WARN] {model} left {placeholder_count} PLACEHOLDERs unfilled - trying next model",
412
- file=sys.stderr)
413
- # Show which rows still have placeholders for debugging
414
- for i, line in enumerate(content_str.split('\n')):
415
- if 'PLACEHOLDER' in line:
416
- print(f"[DEBUG] Row {i} still has PLACEHOLDER: {line[:100]}", file=sys.stderr)
417
- continue
418
-
419
- print(f"[INFO] ✓ All {expected_count} placeholders filled", file=sys.stderr)
420
-
421
- # All checks passed! This is a successful edit
422
- print(f"[INFO] CSV successfully modified by {model}: filled {expected_count} placeholder rows ✓",
423
- file=sys.stderr)
424
-
425
- # Run CSV fixer to ensure proper formatting
426
- fixed_csv = Path(str(temp_csv_path) + '.fixed')
427
- result = subprocess.run(
428
- [self.python_cmd, str(self.lib_dir / 'csv_fixer.py'),
429
- str(temp_csv_path), str(fixed_csv)],
430
- capture_output=True
431
- )
432
-
433
- if result.returncode == 0:
434
- shutil.move(fixed_csv, temp_csv_path)
435
- print("[INFO] CSV format validated and fixed if needed", file=sys.stderr)
436
- else:
437
- print(f"[WARN] CSV fixer failed, using original: {result.stderr.decode()}", file=sys.stderr)
438
-
439
- # Cleanup backup
440
- temp_csv_backup.unlink()
441
- return model
442
-
443
- # All models failed
444
- temp_csv_backup.unlink()
445
- print("[ERROR] All AI models failed to generate ideas", file=sys.stderr)
446
- print("[ERROR] Either they claimed to edit but didn't, or they added/removed rows instead of editing",
447
- file=sys.stderr)
448
- return None
449
-
450
- def generate_strategy_ideas(self, strategy_name: str, count: int, generation: str,
451
- top_performers: List[Tuple[str, str, float]]) -> bool:
452
- """
453
- Generate ideas for a specific strategy.
454
- Returns True if successful, False otherwise.
455
- """
456
- print(f"[INFO] Generating {count} {strategy_name} ideas")
457
-
458
- # Get next IDs
459
- next_ids = self.get_next_ids(generation, count)
460
- print(f"[INFO] Using IDs: {' '.join(next_ids)}")
461
-
462
- # Create temp CSV
463
- temp_csv = self.working_dir / f"temp-csv-{os.getpid()}.csv"
464
- shutil.copy(self.csv_path, temp_csv)
465
-
466
- # Pre-populate with stubs
467
- placeholder_map = {
468
- 'novel': '[PLACEHOLDER: Replace this with your algorithmic idea]',
469
- 'hill-climbing': '[PLACEHOLDER: Replace with parameter tuning idea]',
470
- 'structural': '[PLACEHOLDER: Replace with structural modification idea]',
471
- 'crossover': '[PLACEHOLDER: Replace with crossover hybrid idea]'
472
- }
473
- placeholder_text = placeholder_map.get(strategy_name, '[PLACEHOLDER]')
474
-
475
- # Get parent ID for non-novel strategies
476
- parent_id = ""
477
- if strategy_name != 'novel' and top_performers:
478
- parent_id = top_performers[0][0]
479
-
480
- self.prepopulate_csv_stubs(temp_csv, next_ids, placeholder_text, parent_id)
481
-
482
- # Count lines for offset calculation
483
- with open(temp_csv, 'r') as f:
484
- total_lines = len(f.readlines())
485
- read_offset = max(1, total_lines - 25)
486
-
487
- # Build prompt based on strategy
488
- prompt = self._build_strategy_prompt(
489
- strategy_name, count, generation, next_ids,
490
- top_performers, temp_csv.name, total_lines, read_offset
491
- )
492
-
493
- # Change to working directory
494
- original_cwd = os.getcwd()
495
- os.chdir(self.working_dir)
496
-
497
- try:
498
- # Call AI
499
- successful_model = self.call_ai_for_ideation(
500
- prompt, generation, count, temp_csv
501
- )
502
-
503
- if not successful_model:
504
- return False
505
-
506
- # Validate
507
- with open(self.csv_path, 'r') as f:
508
- original_count = len([line for line in f if line.strip()]) - 1
509
-
510
- if not self.validate_csv_modification(temp_csv, count, original_count + count, next_ids):
511
- return False
512
-
513
- # Apply changes
514
- return self.apply_csv_changes(temp_csv, count, successful_model)
515
-
516
- finally:
517
- os.chdir(original_cwd)
518
- if temp_csv.exists():
519
- temp_csv.unlink()
520
-
521
- def _build_strategy_prompt(self, strategy_name: str, count: int, generation: str,
522
- next_ids: List[str], top_performers: List[Tuple[str, str, float]],
523
- temp_csv_name: str, total_lines: int, read_offset: int) -> str:
524
- """Build the AI prompt for a specific strategy."""
525
-
526
- # Common header
527
- prompt = f"""I need you to use your file editing capabilities to fill in PLACEHOLDER descriptions in the CSV file: {temp_csv_name}
528
-
529
- THE FILE HAS {total_lines} TOTAL LINES. Read from line {read_offset} to see the placeholder rows at the end.
530
-
531
- ⚠️ CRITICAL FILE READING INSTRUCTIONS ⚠️
532
- THE CSV FILE IS VERY LARGE (OVER 100,000 TOKENS). YOU WILL RUN OUT OF CONTEXT IF YOU READ IT ALL!
533
- - DO NOT read the entire file or you will exceed context limits and CRASH
534
- - Use: Read(file_path='{temp_csv_name}', offset={read_offset}, limit=25)
535
- - This will read ONLY the last 25 lines where the placeholders are
536
- - DO NOT READ FROM OFFSET 0 - that will load the entire huge file and fail!
537
-
538
- CRITICAL TASK:
539
- The CSV file already contains {count} stub rows with these IDs: {' '.join(next_ids)}
540
- Each stub row has a PLACEHOLDER description.
541
- Your job is to REPLACE each PLACEHOLDER with a real algorithmic idea description.
542
-
543
- CRITICAL INSTRUCTIONS:
544
- 1. Read ONLY the last 25 lines using the offset specified above
545
- 2. DO NOT ADD OR DELETE ANY ROWS - only EDIT the placeholder descriptions
546
- 3. DO NOT CHANGE THE IDs - they are already correct
547
- 4. Use the Edit tool to replace EACH PLACEHOLDER text with a real idea
548
- 5. When editing, preserve the CSV structure
549
- 6. ALWAYS wrap descriptions in double quotes
550
- 7. DO NOT use any git commands
551
- """
552
-
553
- # Strategy-specific instructions
554
- if strategy_name == 'novel':
555
- prompt += f"""
556
- 8. Focus on creative, ambitious ideas that haven't been tried yet
557
- 9. Consider machine learning, new indicators, regime detection, risk management, etc.
558
- 10. Each description should be one clear sentence describing a novel algorithmic approach
559
-
560
- Current context:
561
- - Generation: {generation}
562
- - Brief: {self._get_brief_preview()}
563
- """
564
-
565
- elif strategy_name == 'hill-climbing':
566
- valid_parent_ids = ','.join([p[0] for p in top_performers])
567
- performers_str = '\n'.join([f"{p[0]},{p[1]},{p[2]}" for p in top_performers])
568
-
569
- prompt += f"""
570
- 8. You MUST use one of these exact parent IDs: {valid_parent_ids}
571
- 9. Generate parameter tuning ideas based on these top performers:
572
- {performers_str}
573
-
574
- 10. Focus on adjusting specific parameters - include current and new values
575
- 11. Example: "Lower rsi_entry from 21 to 18" or "Increase MA period from 20 to 50"
576
- 12. You may change the parent_id field to reference a different top performer
577
- """
578
-
579
- elif strategy_name == 'structural':
580
- valid_parent_ids = ','.join([p[0] for p in top_performers])
581
- performers_str = '\n'.join([f"{p[0]},{p[1]},{p[2]}" for p in top_performers])
582
-
583
- prompt += f"""
584
- 8. You MUST use one of these exact parent IDs: {valid_parent_ids}
585
- 9. Generate structural modification ideas based on these top performers:
586
- {performers_str}
587
-
588
- 10. Focus on architectural/structural changes
589
- 11. DO NOT read evolution_*.py files - generate ideas based on descriptions only
590
- 12. You may change the parent_id field to reference a different top performer
591
- """
592
-
593
- elif strategy_name == 'crossover':
594
- valid_parent_ids = ','.join([p[0] for p in top_performers])
595
- performers_str = '\n'.join([f"{p[0]},{p[1]},{p[2]}" for p in top_performers])
596
-
597
- prompt += f"""
598
- 8. You MUST use ONLY these exact parent IDs: {valid_parent_ids}
599
- 9. Combine 2+ algorithms from these top performers:
600
- {performers_str}
601
-
602
- 10. Each description should combine actual elements from 2+ top performers
603
- 11. Reference at least 2 algorithms in each idea
604
- 12. You may change the parent_id field (choose the primary parent)
605
- """
606
-
607
- return prompt
608
-
609
- def _get_brief_preview(self) -> str:
610
- """Get a preview of the brief file."""
611
- if not self.brief_path.exists():
612
- return "No brief file found"
613
-
614
- with open(self.brief_path, 'r') as f:
615
- lines = f.readlines()[:5]
616
- text = ''.join(lines)
617
- return text[:500] if len(text) > 500 else text
618
-
619
- def run_ideation(self, generation: str, use_strategies: bool = True) -> bool:
620
- """
621
- Run the full ideation process.
622
- Returns True if successful, False otherwise.
623
- """
624
- if not self.brief_path.exists():
625
- print(f"[ERROR] {self.config.data['brief_file']} not found", file=sys.stderr)
626
- return False
627
-
628
- # Ensure CSV exists
629
- if not self.csv_path.exists():
630
- with open(self.csv_path, 'w') as f:
631
- f.write("id,basedOnId,description,performance,status,idea-LLM,run-LLM\n")
632
-
633
- if use_strategies:
634
- # Validate strategy configuration
635
- total_check = (self.novel_exploration + self.hill_climbing +
636
- self.structural_mutation + self.crossover_hybrid)
637
- if total_check != self.total_ideas:
638
- print(f"[ERROR] Strategy counts don't sum to total_ideas ({total_check} != {self.total_ideas})",
639
- file=sys.stderr)
640
- return False
641
-
642
- print(f"[INFO] Generating {self.total_ideas} ideas using multi-strategy approach:")
643
- print(f" Novel exploration: {self.novel_exploration}")
644
- print(f" Hill climbing: {self.hill_climbing}")
645
- print(f" Structural mutation: {self.structural_mutation}")
646
- print(f" Crossover hybrid: {self.crossover_hybrid}")
647
-
648
- # Get top performers
649
- top_performers = self.get_top_performers(self.num_elites)
650
- if not top_performers:
651
- print("[INFO] No completed algorithms found, will use baseline for hill climbing")
652
- top_performers = [("000", "Baseline Algorithm (algorithm.py)", 0.0)]
653
-
654
- # Run each strategy
655
- strategies_attempted = 0
656
- strategies_succeeded = 0
657
-
658
- strategies = [
659
- ('novel', self.novel_exploration, False),
660
- ('hill-climbing', self.hill_climbing, True),
661
- ('structural', self.structural_mutation, True),
662
- ('crossover', self.crossover_hybrid, True),
663
- ]
664
-
665
- for strategy_name, count, needs_parents in strategies:
666
- if count > 0:
667
- strategies_attempted += 1
668
- parents = top_performers if needs_parents else []
669
-
670
- if self.generate_strategy_ideas(strategy_name, count, generation, parents):
671
- strategies_succeeded += 1
672
- else:
673
- print(f"[WARN] {strategy_name} strategy failed", file=sys.stderr)
674
-
675
- print(f"[INFO] Strategy results: {strategies_succeeded}/{strategies_attempted} succeeded")
676
-
677
- # REQUIRE ALL strategies to succeed
678
- if strategies_succeeded == strategies_attempted:
679
- return True
680
- else:
681
- print(f"[ERROR] Not all strategies succeeded ({strategies_succeeded}/{strategies_attempted})",
682
- file=sys.stderr)
683
- print("[ERROR] Rejecting partial results - will retry with exponential backoff",
684
- file=sys.stderr)
685
- return False
686
- else:
687
- # Legacy mode - not implemented yet
688
- print("[ERROR] Legacy mode not implemented in Python helper yet", file=sys.stderr)
689
- return False
690
-
691
-
692
- def main():
693
- """CLI interface for testing."""
694
- if len(sys.argv) < 2:
695
- print("Usage: ideation_helper.py <command> [args...]")
696
- print("Commands:")
697
- print(" run [--legacy N] - Run ideation")
698
- sys.exit(1)
699
-
700
- command = sys.argv[1]
701
-
702
- # Load config
703
- config = Config()
704
- config_path = os.environ.get('CLAUDE_EVOLVE_CONFIG')
705
- if config_path:
706
- config.load(config_path)
707
- elif Path('config.yaml').exists():
708
- config.load('config.yaml')
709
- else:
710
- config.load()
711
-
712
- helper = IdeationHelper(config)
713
-
714
- if command == 'run':
715
- use_strategies = True
716
- if len(sys.argv) > 2 and sys.argv[2] == '--legacy':
717
- use_strategies = False
718
-
719
- generation = helper.get_next_generation()
720
- print(f"[INFO] Starting ideation for generation {generation}")
721
-
722
- # Run with retry logic
723
- retry_count = 0
724
- wait_seconds = 300 # 5 minutes
725
- max_wait_seconds = 1800 # 30 minutes
726
-
727
- while True:
728
- if helper.run_ideation(generation, use_strategies):
729
- print(f"[INFO] Ideation complete! Check {helper.csv_path} for new ideas.")
730
- sys.exit(0)
731
-
732
- retry_count += 1
733
- print(f"[WARN] All ideation attempts failed (retry #{retry_count})", file=sys.stderr)
734
- print(f"[INFO] Waiting {wait_seconds} seconds before retrying...", file=sys.stderr)
735
-
736
- time.sleep(wait_seconds)
737
-
738
- wait_seconds = min(wait_seconds * 2, max_wait_seconds)
739
- else:
740
- print(f"Unknown command: {command}")
741
- sys.exit(1)
742
-
743
-
744
- if __name__ == '__main__':
745
- main()