claude-evolve 1.3.0 → 1.3.2

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.
@@ -29,6 +29,10 @@ while [[ $# -gt 0 ]]; do
29
29
  force_sequential="true"
30
30
  shift
31
31
  ;;
32
+ --keep-awake|--caffeinate)
33
+ use_caffeinate="true"
34
+ shift
35
+ ;;
32
36
  --help)
33
37
  cat <<EOF
34
38
  claude-evolve run - Execute evolution candidates
@@ -40,6 +44,7 @@ OPTIONS:
40
44
  --timeout <sec> Kill evaluator after specified seconds (default: no timeout)
41
45
  --parallel Force parallel execution mode
42
46
  --sequential Force sequential execution mode
47
+ --keep-awake Prevent system sleep during execution (macOS only)
43
48
  --help Show this help message
44
49
 
45
50
  DESCRIPTION:
@@ -59,6 +64,13 @@ EOF
59
64
  esac
60
65
  done
61
66
 
67
+ # Check if caffeinate should be used
68
+ if [[ "$use_caffeinate" == "true" ]] && command -v caffeinate >/dev/null 2>&1; then
69
+ echo "[INFO] Using caffeinate to prevent system sleep"
70
+ # Re-run this script with caffeinate
71
+ exec caffeinate -dims "$0" "$@"
72
+ fi
73
+
62
74
  # Determine execution mode
63
75
  use_parallel=false
64
76
  if [[ "$force_parallel" == "true" ]]; then
@@ -44,8 +44,8 @@ fi
44
44
  # Prepare logging
45
45
  mkdir -p logs
46
46
 
47
- # Track active workers
48
- declare -A worker_pids=()
47
+ # Track active workers (using regular array for compatibility)
48
+ worker_pids=()
49
49
  consecutive_failures=0
50
50
  MAX_FAILURES=10
51
51
  rate_limit_hit=false
@@ -57,7 +57,7 @@ count_pending() {
57
57
  return
58
58
  fi
59
59
 
60
- echo "$csv_content" | awk -F',' 'NR>1 && $5 == "pending" { count++ } END { print count+0 }'
60
+ echo "$csv_content" | awk -F',' 'NR>1 && ($5 == "pending" || $5 == "") { count++ } END { print count+0 }'
61
61
  }
62
62
 
63
63
  # Start a worker
@@ -68,8 +68,16 @@ start_worker() {
68
68
  echo "[DISPATCHER] Starting worker..."
69
69
  $worker_cmd &
70
70
  local pid=$!
71
- worker_pids[$pid]=1
72
- echo "[DISPATCHER] Worker $pid started"
71
+
72
+ # Verify worker started successfully
73
+ sleep 0.1
74
+ if kill -0 "$pid" 2>/dev/null; then
75
+ worker_pids+=($pid)
76
+ echo "[DISPATCHER] Worker $pid started"
77
+ else
78
+ echo "[ERROR] Worker failed to start" >&2
79
+ ((consecutive_failures++))
80
+ fi
73
81
  }
74
82
 
75
83
  # Check worker exit status and handle accordingly
@@ -100,13 +108,17 @@ handle_worker_exit() {
100
108
  esac
101
109
 
102
110
  # Remove from tracking
103
- unset worker_pids[$pid]
111
+ local new_pids=()
112
+ for p in "${worker_pids[@]}"; do
113
+ [[ $p != "$pid" ]] && new_pids+=($p)
114
+ done
115
+ worker_pids=("${new_pids[@]}")
104
116
  }
105
117
 
106
118
  # Signal handler for graceful shutdown
107
119
  shutdown_workers() {
108
120
  echo "[DISPATCHER] Shutting down workers..."
109
- for pid in "${!worker_pids[@]}"; do
121
+ for pid in "${worker_pids[@]}"; do
110
122
  if kill -0 "$pid" 2>/dev/null; then
111
123
  echo "[DISPATCHER] Stopping worker $pid"
112
124
  kill -TERM "$pid" 2>/dev/null || true
@@ -119,15 +131,17 @@ shutdown_workers() {
119
131
  sleep 1
120
132
  ((timeout--))
121
133
 
122
- for pid in "${!worker_pids[@]}"; do
123
- if ! kill -0 "$pid" 2>/dev/null; then
124
- unset worker_pids[$pid]
134
+ local new_pids=()
135
+ for pid in "${worker_pids[@]}"; do
136
+ if kill -0 "$pid" 2>/dev/null; then
137
+ new_pids+=($pid)
125
138
  fi
126
139
  done
140
+ worker_pids=("${new_pids[@]}")
127
141
  done
128
142
 
129
143
  # Force kill remaining workers
130
- for pid in "${!worker_pids[@]}"; do
144
+ for pid in "${worker_pids[@]}"; do
131
145
  if kill -0 "$pid" 2>/dev/null; then
132
146
  echo "[DISPATCHER] Force killing worker $pid"
133
147
  kill -KILL "$pid" 2>/dev/null || true
@@ -138,11 +152,75 @@ shutdown_workers() {
138
152
  exit 0
139
153
  }
140
154
 
141
- trap shutdown_workers INT TERM
155
+ # Better signal handling with logging
156
+ handle_signal() {
157
+ local signal="$1"
158
+ echo "[DISPATCHER] Received signal: $signal" >&2
159
+ echo "[DISPATCHER] Active workers: ${#worker_pids[@]}" >&2
160
+
161
+ # For expensive workers, give option to continue
162
+ if [[ ${#worker_pids[@]} -gt 0 ]]; then
163
+ echo "[DISPATCHER] Warning: ${#worker_pids[@]} expensive workers are still running!" >&2
164
+ echo "[DISPATCHER] Press Ctrl+C again within 5 seconds to force shutdown, or wait..." >&2
165
+
166
+ # Give 5 seconds to reconsider
167
+ local count=5
168
+ while [[ $count -gt 0 ]]; do
169
+ sleep 1
170
+ ((count--))
171
+ # Check if we got another signal
172
+ if [[ -f /tmp/evolve-force-shutdown-$$ ]]; then
173
+ echo "[DISPATCHER] Force shutdown requested" >&2
174
+ rm -f /tmp/evolve-force-shutdown-$$
175
+ shutdown_workers
176
+ exit 1
177
+ fi
178
+ done
179
+
180
+ echo "[DISPATCHER] Continuing with active workers..." >&2
181
+ return
182
+ fi
183
+
184
+ shutdown_workers
185
+ }
186
+
187
+ # Set up signal handlers
188
+ trap 'handle_signal INT' INT
189
+ trap 'handle_signal TERM' TERM
190
+ trap 'echo "[DISPATCHER] Exiting with code $?" >&2' EXIT
191
+
192
+ # Check for stuck "running" candidates from previous runs
193
+ check_stuck_candidates() {
194
+ if read_csv_with_lock csv_content; then
195
+ local stuck_count=$(echo "$csv_content" | awk -F',' 'NR>1 && $5 == "running" { count++ } END { print count+0 }')
196
+ if [[ $stuck_count -gt 0 ]]; then
197
+ echo "[DISPATCHER] Found $stuck_count candidates stuck in 'running' status"
198
+ echo "[DISPATCHER] Resetting them to 'pending' for retry..."
199
+
200
+ # Reset stuck candidates
201
+ if acquire_csv_lock; then
202
+ awk -F',' -v OFS=',' '
203
+ NR==1 { print }
204
+ NR>1 {
205
+ if ($5 == "running") $5 = "pending"
206
+ print
207
+ }
208
+ ' "$FULL_CSV_PATH" > "${FULL_CSV_PATH}.tmp" && mv -f "${FULL_CSV_PATH}.tmp" "$FULL_CSV_PATH"
209
+ release_csv_lock
210
+ fi
211
+ fi
212
+ fi
213
+ }
142
214
 
143
215
  # Main dispatcher loop
144
216
  echo "[DISPATCHER] Starting main dispatch loop"
145
217
 
218
+ # Check for stuck candidates from previous runs
219
+ check_stuck_candidates
220
+
221
+ # Set error handling
222
+ set +e # Don't exit on error in the main loop
223
+
146
224
  while true; do
147
225
  # Check if too many consecutive failures
148
226
  if [[ $consecutive_failures -ge $MAX_FAILURES ]]; then
@@ -158,29 +236,24 @@ while true; do
158
236
  fi
159
237
 
160
238
  # Count pending work
161
- pending_count=$(count_pending)
239
+ pending_count=$(count_pending || echo "0")
162
240
  active_workers=${#worker_pids[@]}
163
241
 
164
242
  echo "[DISPATCHER] Status: $pending_count pending, $active_workers active workers"
165
243
 
166
- # If no pending work and no active workers, try to generate more ideas
244
+ # Debug: Show CSV status if no pending
245
+ if [[ $pending_count -eq 0 ]]; then
246
+ total_rows=$(read_csv_with_lock csv_content && echo "$csv_content" | wc -l | xargs)
247
+ complete_count=$(read_csv_with_lock csv_content && echo "$csv_content" | awk -F',' 'NR>1 && $5 == "complete" { count++ } END { print count+0 }')
248
+ echo "[DISPATCHER] CSV has $((total_rows-1)) total candidates, $complete_count complete"
249
+ fi
250
+
251
+ # If no pending work and no active workers, we're done
167
252
  if [[ $pending_count -eq 0 && $active_workers -eq 0 ]]; then
168
- echo "[DISPATCHER] No work available. Generating new ideas..."
169
-
170
- ideate_script="$SCRIPT_DIR/claude-evolve-ideate"
171
- if [[ ! -f "$ideate_script" ]]; then
172
- echo "[DISPATCHER] No ideate script found. Evolution complete."
173
- break
174
- fi
175
-
176
- if ! "$ideate_script"; then
177
- echo "[ERROR] Failed to generate new ideas" >&2
178
- break
179
- fi
180
-
181
- # Recount after ideation
182
- pending_count=$(count_pending)
183
- echo "[DISPATCHER] Generated ideas. New pending count: $pending_count"
253
+ echo "[DISPATCHER] No pending candidates found. Evolution complete."
254
+ echo "[DISPATCHER] Run 'claude-evolve ideate' to generate more candidates."
255
+ echo "[DISPATCHER] Exiting main loop: no work remaining" >&2
256
+ break
184
257
  fi
185
258
 
186
259
  # Start workers if we have pending work and capacity
@@ -198,22 +271,43 @@ while true; do
198
271
 
199
272
  # Wait for any worker to finish
200
273
  if [[ $active_workers -gt 0 ]]; then
201
- # Wait for any child process to exit
202
- wait -n
274
+ # Poll for finished workers (macOS compatible)
275
+ sleep 5 # Check every 5 seconds
203
276
 
204
277
  # Check which workers have finished
205
- for pid in "${!worker_pids[@]}"; do
278
+ for pid in "${worker_pids[@]}"; do
206
279
  if ! kill -0 "$pid" 2>/dev/null; then
207
280
  # Get exit status
208
- wait "$pid"
281
+ wait "$pid" 2>/dev/null
209
282
  exit_code=$?
210
283
  handle_worker_exit "$pid" "$exit_code"
211
284
  fi
212
285
  done
286
+
287
+ # Safety check - if we have workers but the array is corrupted
288
+ if [[ ${#worker_pids[@]} -eq 0 ]] && jobs -r | grep -q .; then
289
+ echo "[DISPATCHER] Warning: Lost track of workers but jobs still running!" >&2
290
+ echo "[DISPATCHER] Attempting to recover..." >&2
291
+
292
+ # Try to recover PIDs from jobs
293
+ while read -r job_info; do
294
+ if [[ $job_info =~ \[([0-9]+)\][[:space:]]+([0-9]+) ]]; then
295
+ local recovered_pid="${BASH_REMATCH[2]}"
296
+ echo "[DISPATCHER] Recovered worker PID: $recovered_pid" >&2
297
+ worker_pids+=($recovered_pid)
298
+ fi
299
+ done < <(jobs -l)
300
+ fi
213
301
  else
214
302
  # No active workers but we might be waiting for ideation or have pending work
215
303
  sleep 1
216
304
  fi
217
305
  done
218
306
 
219
- echo "[DISPATCHER] Evolution run complete"
307
+ echo "[DISPATCHER] Evolution run complete"
308
+
309
+ # Final cleanup check
310
+ if [[ ${#worker_pids[@]} -gt 0 ]]; then
311
+ echo "[DISPATCHER] Warning: ${#worker_pids[@]} workers still active at exit"
312
+ shutdown_workers
313
+ fi
@@ -187,7 +187,8 @@ eval_exit_code=0
187
187
 
188
188
  if [[ -n $timeout_seconds ]]; then
189
189
  echo "[WORKER-$$] Evaluation timeout: ${timeout_seconds}s"
190
- if eval_output=$(timeout "$timeout_seconds" "$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$output_file" 2>&1); then
190
+ # For Modal compatibility, don't capture stderr
191
+ if eval_output=$(timeout "$timeout_seconds" "$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$output_file"); then
191
192
  eval_exit_code=0
192
193
  else
193
194
  eval_exit_code=$?
@@ -198,7 +199,8 @@ if [[ -n $timeout_seconds ]]; then
198
199
  fi
199
200
  fi
200
201
  else
201
- if eval_output=$("$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$output_file" 2>&1); then
202
+ # For Modal compatibility, don't capture stderr
203
+ if eval_output=$("$PYTHON_CMD" "$FULL_EVALUATOR_PATH" "$output_file"); then
202
204
  eval_exit_code=0
203
205
  else
204
206
  eval_exit_code=$?
package/lib/config.sh CHANGED
@@ -58,14 +58,29 @@ load_config() {
58
58
  # Simple YAML parsing for key: value pairs and nested structures
59
59
  local in_ideation_section=false
60
60
  local in_parallel_section=false
61
- while IFS=': ' read -r key value; do
61
+ while IFS='' read -r line; do
62
62
  # Skip comments and empty lines
63
- [[ $key =~ ^[[:space:]]*# ]] || [[ -z $key ]] && continue
63
+ [[ $line =~ ^[[:space:]]*# ]] || [[ -z $line ]] && continue
64
+
65
+ # Parse key:value from line
66
+ if [[ ! $line =~ ^([^:]+):(.*)$ ]]; then
67
+ continue
68
+ fi
69
+ key="${BASH_REMATCH[1]}"
70
+ value="${BASH_REMATCH[2]}"
71
+
72
+ # Check if key is indented (for nested sections)
73
+ local is_indented=false
74
+ [[ $key =~ ^[[:space:]]+ ]] && is_indented=true
75
+
64
76
 
65
77
  # Remove leading/trailing whitespace
66
78
  key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
67
79
  value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
68
80
 
81
+ # Remove inline comments from value
82
+ value=$(echo "$value" | sed 's/[[:space:]]*#.*$//')
83
+
69
84
  # Remove quotes from value
70
85
  value=$(echo "$value" | sed 's/^"//;s/"$//')
71
86
 
@@ -78,8 +93,8 @@ load_config() {
78
93
  in_parallel_section=true
79
94
  in_ideation_section=false
80
95
  continue
81
- elif [[ $key =~ ^[a-z_]+ ]] && [[ $in_ideation_section == true || $in_parallel_section == true ]]; then
82
- # Top-level key found, exit nested sections
96
+ elif [[ $is_indented == false ]] && [[ $in_ideation_section == true || $in_parallel_section == true ]]; then
97
+ # Non-indented key found while in a section, exit nested sections
83
98
  in_ideation_section=false
84
99
  in_parallel_section=false
85
100
  fi
package/lib/csv-lock.sh CHANGED
@@ -135,14 +135,18 @@ find_next_pending_with_lock() {
135
135
 
136
136
  # Find oldest pending candidate and update to running
137
137
  local candidate=$(awk -F',' '
138
- NR>1 && $5 == "pending" { print $1; exit }
138
+ NR>1 && ($5 == "pending" || $5 == "") { print $1; exit }
139
139
  ' "$csv_file")
140
140
 
141
141
  if [ -n "$candidate" ]; then
142
142
  # Update status to running while we have the lock
143
143
  awk -F',' -v OFS=',' -v id="$candidate" '
144
144
  NR==1 || $1 != id { print }
145
- $1 == id { $5 = "running"; print }
145
+ $1 == id {
146
+ # Preserve existing fields but set status to running
147
+ if ($5 == "" || $5 == "pending") $5 = "running"
148
+ print
149
+ }
146
150
  ' "$csv_file" > "${csv_file}.tmp" && mv -f "${csv_file}.tmp" "$csv_file"
147
151
  fi
148
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "bin": {
5
5
  "claude-evolve": "./bin/claude-evolve",
6
6
  "claude-evolve-main": "./bin/claude-evolve-main",