claude-evolve 1.3.39 → 1.3.40
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/README.md +40 -2
- package/bin/claude-evolve-analyze +34 -5
- package/bin/claude-evolve-cleanup +297 -0
- package/bin/claude-evolve-edit +293 -0
- package/bin/claude-evolve-ideate +6 -6
- package/bin/claude-evolve-main +35 -15
- package/bin/{claude-evolve-run-unified → claude-evolve-run} +135 -4
- package/bin/claude-evolve-status +220 -0
- package/bin/claude-evolve-worker +73 -11
- package/lib/config.sh +5 -3
- package/lib/csv-lock.sh +26 -4
- package/lib/csv_helper.py +1 -1
- package/package.json +1 -1
- package/templates/config.yaml +8 -7
- package/bin/claude-evolve-run-parallel.OLD +0 -389
- package/bin/claude-evolve-run.OLD +0 -662
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ claude-evolve setup # Initialize evolution workspace
|
|
|
55
55
|
claude-evolve ideate # Generate new algorithm ideas
|
|
56
56
|
claude-evolve run # Execute evolution candidates
|
|
57
57
|
claude-evolve analyze # Analyze evolution results
|
|
58
|
+
claude-evolve edit # Manage candidate statuses by generation
|
|
58
59
|
claude-evolve config # Manage configuration settings
|
|
59
60
|
```
|
|
60
61
|
|
|
@@ -90,6 +91,18 @@ Analyzes evolution progress and generates insights:
|
|
|
90
91
|
- Best-performing algorithm variants
|
|
91
92
|
- Suggestions for future evolution directions
|
|
92
93
|
|
|
94
|
+
#### claude-evolve-edit
|
|
95
|
+
Manages candidate statuses by generation for re-evaluation workflows:
|
|
96
|
+
- Mark generations as failed, complete, or pending
|
|
97
|
+
- Reset entire generations (delete files and clear scores)
|
|
98
|
+
- Essential for re-running evaluations when algorithms or evaluators change
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
claude-evolve edit gen03 failed # Mark all gen03 as failed
|
|
102
|
+
claude-evolve edit all pending # Mark everything as pending for re-run
|
|
103
|
+
claude-evolve edit gen02 reboot # Full reset of gen02 (delete files + clear data)
|
|
104
|
+
```
|
|
105
|
+
|
|
93
106
|
#### claude-evolve-config
|
|
94
107
|
Manages configuration settings:
|
|
95
108
|
- View current configuration
|
|
@@ -203,6 +216,30 @@ your-project/
|
|
|
203
216
|
└── (your main project files)
|
|
204
217
|
```
|
|
205
218
|
|
|
219
|
+
## Evolution CSV Format
|
|
220
|
+
|
|
221
|
+
The evolution.csv file tracks all candidates and their results. The core columns are:
|
|
222
|
+
|
|
223
|
+
**Required columns (positions 1-5):**
|
|
224
|
+
1. **id** - Unique identifier for each candidate (e.g., gen01-001, gen02-015)
|
|
225
|
+
2. **basedOnId** - Parent algorithm this was derived from (empty for novel ideas)
|
|
226
|
+
3. **description** - What changes this variant implements
|
|
227
|
+
4. **performance** - Score from evaluator (column 4) - this drives evolution selection
|
|
228
|
+
5. **status** - Current state: empty/"pending", "running", "complete", "failed", "timeout"
|
|
229
|
+
|
|
230
|
+
**Additional columns:**
|
|
231
|
+
- Any other metrics your evaluator outputs (fitness, sharpe, total_return, yearly_return, max_drawdown, volatility, etc.)
|
|
232
|
+
- Error messages for failed runs
|
|
233
|
+
- Execution time
|
|
234
|
+
|
|
235
|
+
**Key behaviors:**
|
|
236
|
+
- The system uses column 4 (performance) for evolution selection, regardless of its name
|
|
237
|
+
- Column 5 (status) determines what needs to be run
|
|
238
|
+
- Empty or "pending" status means the candidate is ready to run
|
|
239
|
+
- You can reset a candidate to pending by deleting all fields after the description (columns 4+)
|
|
240
|
+
- Additional columns are automatically added when your evaluator returns JSON with extra fields
|
|
241
|
+
- Rows with fewer than 5 fields are treated as pending candidates
|
|
242
|
+
|
|
206
243
|
## Evaluator Output Format
|
|
207
244
|
|
|
208
245
|
Your evaluator must output a performance score to stdout. The system looks for either `performance` or `score` fields. Four formats are supported:
|
|
@@ -275,8 +312,9 @@ print(score) # Simple number to stdout
|
|
|
275
312
|
Edit `evolution/config.yaml` to customize:
|
|
276
313
|
|
|
277
314
|
```yaml
|
|
278
|
-
#
|
|
279
|
-
|
|
315
|
+
# NOTE: The evolution directory is automatically inferred from this config file's location.
|
|
316
|
+
# For example, if this file is at /path/to/my-experiment/config.yaml,
|
|
317
|
+
# then the evolution directory will be /path/to/my-experiment/
|
|
280
318
|
|
|
281
319
|
# Algorithm and evaluator file paths
|
|
282
320
|
algorithm_file: "algorithm.py"
|
|
@@ -77,7 +77,30 @@ if [[ ! -f $csv_file ]]; then
|
|
|
77
77
|
exit 1
|
|
78
78
|
fi
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
# Determine what we're evolving based on paths
|
|
81
|
+
EVOLUTION_CONTEXT=""
|
|
82
|
+
if [[ -n "$EVOLUTION_DIR" ]]; then
|
|
83
|
+
# Get the evolution directory name (e.g., "evolution-atr" -> "ATR")
|
|
84
|
+
EVOLUTION_NAME=$(basename "$EVOLUTION_DIR")
|
|
85
|
+
EVOLUTION_CONTEXT="${EVOLUTION_NAME#evolution-}"
|
|
86
|
+
EVOLUTION_CONTEXT=$(echo "$EVOLUTION_CONTEXT" | tr '[:lower:]' '[:upper:]')
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# If we can't determine from evolution dir, try from algorithm path
|
|
90
|
+
if [[ -z "$EVOLUTION_CONTEXT" && -n "$ALGORITHM_FILE" ]]; then
|
|
91
|
+
# Get algorithm file name
|
|
92
|
+
if [[ -f "$FULL_ALGORITHM_PATH" ]]; then
|
|
93
|
+
ALGO_NAME=$(basename "$FULL_ALGORITHM_PATH" .py)
|
|
94
|
+
EVOLUTION_CONTEXT="$ALGO_NAME"
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Default if we still can't determine
|
|
99
|
+
if [[ -z "$EVOLUTION_CONTEXT" ]]; then
|
|
100
|
+
EVOLUTION_CONTEXT="Algorithm"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
echo "=== Evolution Analysis Summary - $EVOLUTION_CONTEXT ==="
|
|
81
104
|
echo
|
|
82
105
|
|
|
83
106
|
# Count totals (pure shell)
|
|
@@ -402,6 +425,7 @@ with open('$csv_file', 'r') as f:
|
|
|
402
425
|
|
|
403
426
|
max_perf = 0
|
|
404
427
|
max_id = ''
|
|
428
|
+
max_desc = ''
|
|
405
429
|
max_order = 0
|
|
406
430
|
completed_order = 0
|
|
407
431
|
|
|
@@ -415,12 +439,16 @@ with open('$csv_file', 'r') as f:
|
|
|
415
439
|
max_perf = perf_val
|
|
416
440
|
max_order = completed_order
|
|
417
441
|
max_id = row[0]
|
|
442
|
+
max_desc = row[2] if len(row) > 2 else ''
|
|
418
443
|
except ValueError:
|
|
419
444
|
pass
|
|
420
445
|
|
|
421
446
|
print(f'max_perf={max_perf}')
|
|
422
447
|
print(f'max_row={max_order}')
|
|
423
448
|
print(f'max_id=\"{max_id}\"')
|
|
449
|
+
# Escape special characters in description for shell
|
|
450
|
+
desc_escaped = max_desc.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"').replace('\$', '\\\\\$').replace('\`', '\\\\\`')
|
|
451
|
+
print(f'max_desc=\"{desc_escaped}\"')
|
|
424
452
|
")"
|
|
425
453
|
|
|
426
454
|
# Create generation averages file and track max generation
|
|
@@ -544,7 +572,7 @@ set multiplot layout 2,1 margins 0.08,0.82,0.15,0.95 spacing 0.1,0.15
|
|
|
544
572
|
|
|
545
573
|
#=================== TOP PLOT: Performance Over Time ===================
|
|
546
574
|
# AIDEV-NOTE: Removed x-axis to eliminate tick overlap and formatting issues
|
|
547
|
-
set title "Algorithm Evolution Performance Over Time" font ",14"
|
|
575
|
+
set title "$EVOLUTION_CONTEXT Algorithm Evolution Performance Over Time" font ",14"
|
|
548
576
|
unset xlabel
|
|
549
577
|
set ylabel "Performance Score"
|
|
550
578
|
set grid y # Only show horizontal grid lines
|
|
@@ -578,10 +606,11 @@ plot "$gen_avg_file" using 1:3 with boxes linecolor rgb "#4CAF50" notitle
|
|
|
578
606
|
|
|
579
607
|
unset multiplot
|
|
580
608
|
|
|
581
|
-
# Add winner label at bottom
|
|
582
|
-
set terminal png size 1200,
|
|
609
|
+
# Add winner label and description at bottom
|
|
610
|
+
set terminal png size 1200,850
|
|
583
611
|
set output "$output_file"
|
|
584
|
-
set label "Best Overall: $max_id (Score: $max_perf)" at screen 0.5, 0.
|
|
612
|
+
set label "Best Overall: $max_id (Score: $max_perf)" at screen 0.5, 0.07 center font ",12"
|
|
613
|
+
set label "$max_desc" at screen 0.5, 0.04 center font ",10" textcolor rgb "#666666"
|
|
585
614
|
replot
|
|
586
615
|
EOF
|
|
587
616
|
else
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
# Load configuration
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
# shellcheck source=../lib/config.sh
|
|
8
|
+
source "$SCRIPT_DIR/../lib/config.sh"
|
|
9
|
+
|
|
10
|
+
# Use CLAUDE_EVOLVE_CONFIG if set, otherwise default
|
|
11
|
+
if [[ -n ${CLAUDE_EVOLVE_CONFIG:-} ]]; then
|
|
12
|
+
load_config "$CLAUDE_EVOLVE_CONFIG"
|
|
13
|
+
else
|
|
14
|
+
load_config
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Function to show help
|
|
18
|
+
show_help() {
|
|
19
|
+
cat <<EOF
|
|
20
|
+
claude-evolve cleanup - Clean up unchanged algorithms and their descendants
|
|
21
|
+
|
|
22
|
+
USAGE:
|
|
23
|
+
claude-evolve cleanup [OPTIONS]
|
|
24
|
+
|
|
25
|
+
OPTIONS:
|
|
26
|
+
--dry-run Show what would be done without making changes
|
|
27
|
+
--force Actually perform the cleanup (required for real changes)
|
|
28
|
+
--help Show this help message
|
|
29
|
+
|
|
30
|
+
DESCRIPTION:
|
|
31
|
+
This tool finds algorithm files that are identical to their parent and:
|
|
32
|
+
1. Deletes the unchanged .py files
|
|
33
|
+
2. Resets those candidates to pending status in CSV
|
|
34
|
+
3. Finds and cleans up any descendants that inherited from the bad copies
|
|
35
|
+
|
|
36
|
+
Use --dry-run first to see what would be affected.
|
|
37
|
+
|
|
38
|
+
EXAMPLES:
|
|
39
|
+
claude-evolve cleanup --dry-run # Preview changes
|
|
40
|
+
claude-evolve cleanup --force # Actually clean up
|
|
41
|
+
EOF
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Parse arguments
|
|
45
|
+
DRY_RUN=true
|
|
46
|
+
FORCE=false
|
|
47
|
+
|
|
48
|
+
while [[ $# -gt 0 ]]; do
|
|
49
|
+
case $1 in
|
|
50
|
+
--dry-run)
|
|
51
|
+
DRY_RUN=true
|
|
52
|
+
shift
|
|
53
|
+
;;
|
|
54
|
+
--force)
|
|
55
|
+
FORCE=true
|
|
56
|
+
DRY_RUN=false
|
|
57
|
+
shift
|
|
58
|
+
;;
|
|
59
|
+
--help)
|
|
60
|
+
show_help
|
|
61
|
+
exit 0
|
|
62
|
+
;;
|
|
63
|
+
*)
|
|
64
|
+
echo "[ERROR] Unknown option: $1" >&2
|
|
65
|
+
exit 1
|
|
66
|
+
;;
|
|
67
|
+
esac
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
if [[ $FORCE == false ]]; then
|
|
71
|
+
DRY_RUN=true
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Validate configuration
|
|
75
|
+
if ! validate_config; then
|
|
76
|
+
echo "[ERROR] Configuration validation failed" >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Check if CSV exists
|
|
81
|
+
if [[ ! -f "$FULL_CSV_PATH" ]]; then
|
|
82
|
+
echo "[ERROR] Evolution CSV not found: $FULL_CSV_PATH" >&2
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
echo "🧹 Claude-Evolve Duplicate Cleanup Tool"
|
|
87
|
+
echo "========================================"
|
|
88
|
+
echo "Evolution directory: $FULL_EVOLUTION_DIR"
|
|
89
|
+
echo "CSV file: $FULL_CSV_PATH"
|
|
90
|
+
echo "Mode: $(if [[ $DRY_RUN == true ]]; then echo "DRY RUN (preview only)"; else echo "FORCE (will make changes)"; fi)"
|
|
91
|
+
echo ""
|
|
92
|
+
|
|
93
|
+
# Use Python to analyze and clean up duplicates
|
|
94
|
+
"$PYTHON_CMD" -c "
|
|
95
|
+
import csv
|
|
96
|
+
import os
|
|
97
|
+
import sys
|
|
98
|
+
import shutil
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
|
|
101
|
+
csv_file = '$FULL_CSV_PATH'
|
|
102
|
+
evolution_dir = '$FULL_EVOLUTION_DIR'
|
|
103
|
+
dry_run = '$DRY_RUN' == 'true'
|
|
104
|
+
algorithm_file = '$FULL_ALGORITHM_PATH'
|
|
105
|
+
|
|
106
|
+
def files_identical(file1, file2):
|
|
107
|
+
\"\"\"Check if two files have identical content.\"\"\"
|
|
108
|
+
if not os.path.exists(file1) or not os.path.exists(file2):
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
with open(file1, 'rb') as f1, open(file2, 'rb') as f2:
|
|
113
|
+
return f1.read() == f2.read()
|
|
114
|
+
except Exception:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def get_algorithm_file_path(candidate_id, base_algorithm):
|
|
118
|
+
\"\"\"Get the file path for a candidate's algorithm.\"\"\"
|
|
119
|
+
# Handle both old and new format IDs
|
|
120
|
+
if candidate_id.isdigit():
|
|
121
|
+
filename = f'evolution_id{candidate_id}.py'
|
|
122
|
+
else:
|
|
123
|
+
filename = f'evolution_{candidate_id}.py'
|
|
124
|
+
|
|
125
|
+
return os.path.join(evolution_dir, filename)
|
|
126
|
+
|
|
127
|
+
def get_parent_file_path(based_on_id, base_algorithm):
|
|
128
|
+
\"\"\"Get the file path for a parent algorithm.\"\"\"
|
|
129
|
+
if not based_on_id or based_on_id == '0' or based_on_id == '\"\"':
|
|
130
|
+
return base_algorithm
|
|
131
|
+
|
|
132
|
+
# Handle both old and new format IDs
|
|
133
|
+
if based_on_id.isdigit():
|
|
134
|
+
filename = f'evolution_id{based_on_id}.py'
|
|
135
|
+
else:
|
|
136
|
+
filename = f'evolution_{based_on_id}.py'
|
|
137
|
+
|
|
138
|
+
return os.path.join(evolution_dir, filename)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
# Read CSV
|
|
142
|
+
with open(csv_file, 'r') as f:
|
|
143
|
+
reader = csv.reader(f)
|
|
144
|
+
rows = list(reader)
|
|
145
|
+
|
|
146
|
+
if len(rows) <= 1:
|
|
147
|
+
print('No candidates found in CSV')
|
|
148
|
+
sys.exit(0)
|
|
149
|
+
|
|
150
|
+
header = rows[0]
|
|
151
|
+
candidates = {}
|
|
152
|
+
|
|
153
|
+
# Build candidate map
|
|
154
|
+
for i, row in enumerate(rows[1:], 1):
|
|
155
|
+
if len(row) >= 3:
|
|
156
|
+
candidate_id = row[0]
|
|
157
|
+
based_on_id = row[1] if len(row) > 1 else ''
|
|
158
|
+
description = row[2] if len(row) > 2 else ''
|
|
159
|
+
performance = row[3] if len(row) > 3 else ''
|
|
160
|
+
status = row[4] if len(row) > 4 else ''
|
|
161
|
+
|
|
162
|
+
candidates[candidate_id] = {
|
|
163
|
+
'row_index': i,
|
|
164
|
+
'based_on_id': based_on_id,
|
|
165
|
+
'description': description,
|
|
166
|
+
'performance': performance,
|
|
167
|
+
'status': status,
|
|
168
|
+
'file_path': get_algorithm_file_path(candidate_id, algorithm_file)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
print(f'Found {len(candidates)} candidates to analyze')
|
|
172
|
+
print('')
|
|
173
|
+
|
|
174
|
+
# Find unchanged candidates
|
|
175
|
+
unchanged_candidates = []
|
|
176
|
+
|
|
177
|
+
for candidate_id, info in candidates.items():
|
|
178
|
+
if not info['based_on_id'] or info['based_on_id'] == '0' or info['based_on_id'] == '\"\"':
|
|
179
|
+
# Skip root candidates (no parent)
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
parent_file = get_parent_file_path(info['based_on_id'], algorithm_file)
|
|
183
|
+
candidate_file = info['file_path']
|
|
184
|
+
|
|
185
|
+
if os.path.exists(candidate_file) and files_identical(candidate_file, parent_file):
|
|
186
|
+
unchanged_candidates.append(candidate_id)
|
|
187
|
+
print(f'📋 UNCHANGED: {candidate_id} is identical to parent {info[\"based_on_id\"]}')
|
|
188
|
+
print(f' File: {os.path.basename(candidate_file)}')
|
|
189
|
+
print(f' Description: {info[\"description\"]}')
|
|
190
|
+
print(f' Status: {info[\"status\"]}')
|
|
191
|
+
print('')
|
|
192
|
+
|
|
193
|
+
if not unchanged_candidates:
|
|
194
|
+
print('✅ No unchanged candidates found - all algorithms appear to be properly mutated!')
|
|
195
|
+
sys.exit(0)
|
|
196
|
+
|
|
197
|
+
print(f'Found {len(unchanged_candidates)} unchanged candidates')
|
|
198
|
+
print('')
|
|
199
|
+
|
|
200
|
+
# Find descendants of unchanged candidates
|
|
201
|
+
def find_descendants(bad_parent_id, all_candidates, found=None):
|
|
202
|
+
if found is None:
|
|
203
|
+
found = set()
|
|
204
|
+
|
|
205
|
+
for cand_id, info in all_candidates.items():
|
|
206
|
+
if info['based_on_id'] == bad_parent_id and cand_id not in found:
|
|
207
|
+
found.add(cand_id)
|
|
208
|
+
# Recursively find descendants of this candidate
|
|
209
|
+
find_descendants(cand_id, all_candidates, found)
|
|
210
|
+
|
|
211
|
+
return found
|
|
212
|
+
|
|
213
|
+
all_affected = set(unchanged_candidates)
|
|
214
|
+
|
|
215
|
+
# Find all descendants
|
|
216
|
+
for unchanged_id in unchanged_candidates:
|
|
217
|
+
descendants = find_descendants(unchanged_id, candidates)
|
|
218
|
+
all_affected.update(descendants)
|
|
219
|
+
|
|
220
|
+
if descendants:
|
|
221
|
+
print(f'🔗 DESCENDANTS of {unchanged_id}: {sorted(descendants)}')
|
|
222
|
+
|
|
223
|
+
print('')
|
|
224
|
+
print(f'📊 SUMMARY:')
|
|
225
|
+
print(f' • {len(unchanged_candidates)} unchanged candidates')
|
|
226
|
+
print(f' • {len(all_affected) - len(unchanged_candidates)} descendants affected')
|
|
227
|
+
print(f' • {len(all_affected)} total candidates to clean up')
|
|
228
|
+
print('')
|
|
229
|
+
|
|
230
|
+
if dry_run:
|
|
231
|
+
print('🔍 DRY RUN - Showing what would be done:')
|
|
232
|
+
print('')
|
|
233
|
+
|
|
234
|
+
for candidate_id in sorted(all_affected):
|
|
235
|
+
info = candidates[candidate_id]
|
|
236
|
+
action = 'DELETE FILE & RESET' if candidate_id in unchanged_candidates else 'RESET (descendant)'
|
|
237
|
+
print(f' {action}: {candidate_id}')
|
|
238
|
+
print(f' File: {os.path.basename(info[\"file_path\"])}')
|
|
239
|
+
print(f' Description: {info[\"description\"]}')
|
|
240
|
+
print('')
|
|
241
|
+
|
|
242
|
+
print('To actually perform cleanup, run with --force')
|
|
243
|
+
else:
|
|
244
|
+
print('🧹 PERFORMING CLEANUP:')
|
|
245
|
+
print('')
|
|
246
|
+
|
|
247
|
+
# Delete files and update CSV
|
|
248
|
+
files_deleted = 0
|
|
249
|
+
rows_updated = 0
|
|
250
|
+
|
|
251
|
+
for candidate_id in sorted(all_affected):
|
|
252
|
+
info = candidates[candidate_id]
|
|
253
|
+
|
|
254
|
+
# Delete file if it exists (for unchanged candidates)
|
|
255
|
+
if candidate_id in unchanged_candidates and os.path.exists(info['file_path']):
|
|
256
|
+
try:
|
|
257
|
+
os.remove(info['file_path'])
|
|
258
|
+
files_deleted += 1
|
|
259
|
+
print(f' ✅ DELETED: {os.path.basename(info[\"file_path\"])}')
|
|
260
|
+
except Exception as e:
|
|
261
|
+
print(f' ❌ FAILED to delete {os.path.basename(info[\"file_path\"])}: {e}')
|
|
262
|
+
|
|
263
|
+
# Reset CSV row (clear performance and status, keep description)
|
|
264
|
+
row_idx = info['row_index']
|
|
265
|
+
if len(rows[row_idx]) >= 5:
|
|
266
|
+
# Clear performance (column 3) and status (column 4), but keep first 3 columns
|
|
267
|
+
rows[row_idx] = rows[row_idx][:3] + ['', ''] + rows[row_idx][5:]
|
|
268
|
+
rows_updated += 1
|
|
269
|
+
print(f' ✅ RESET CSV: {candidate_id} -> pending')
|
|
270
|
+
|
|
271
|
+
# Write updated CSV
|
|
272
|
+
try:
|
|
273
|
+
with open(csv_file + '.tmp', 'w', newline='') as f:
|
|
274
|
+
writer = csv.writer(f)
|
|
275
|
+
writer.writerows(rows)
|
|
276
|
+
|
|
277
|
+
# Atomic replace
|
|
278
|
+
os.rename(csv_file + '.tmp', csv_file)
|
|
279
|
+
print('')
|
|
280
|
+
print(f'✅ CLEANUP COMPLETE:')
|
|
281
|
+
print(f' • {files_deleted} files deleted')
|
|
282
|
+
print(f' • {rows_updated} CSV rows reset to pending')
|
|
283
|
+
print(f' • CSV updated successfully')
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
print(f'❌ FAILED to update CSV: {e}')
|
|
287
|
+
sys.exit(1)
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
print(f'Error: {e}')
|
|
291
|
+
sys.exit(1)
|
|
292
|
+
"
|
|
293
|
+
|
|
294
|
+
echo ""
|
|
295
|
+
if [[ $DRY_RUN == true ]]; then
|
|
296
|
+
echo "💡 TIP: Run with --force to actually perform the cleanup"
|
|
297
|
+
fi
|