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.
- package/bin/claude-evolve-autostatus +147 -125
- package/lib/config.py +92 -0
- 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,18 +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
|
-
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(
|
|
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(
|
|
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
|
|
78
|
+
print(f"\033[{row};{col}H", end="")
|
|
52
79
|
|
|
53
80
|
def clear_line(self):
|
|
54
81
|
"""Clear current line."""
|
|
55
|
-
print(
|
|
82
|
+
print("\033[2K", end="")
|
|
56
83
|
|
|
57
84
|
def hide_cursor(self):
|
|
58
85
|
"""Hide the cursor."""
|
|
59
|
-
print(
|
|
86
|
+
print("\033[?25l", end="")
|
|
60
87
|
|
|
61
88
|
def show_cursor(self):
|
|
62
89
|
"""Show the cursor."""
|
|
63
|
-
print(
|
|
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(
|
|
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,
|
|
75
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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[
|
|
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[
|
|
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[
|
|
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
|
|
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"{
|
|
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
|
|
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(
|
|
227
|
-
if status ==
|
|
228
|
-
color =
|
|
229
|
-
elif status ==
|
|
230
|
-
color =
|
|
231
|
-
elif status ==
|
|
232
|
-
color =
|
|
233
|
-
elif status ==
|
|
234
|
-
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
|
|
235
261
|
else:
|
|
236
|
-
color =
|
|
262
|
+
color = "\033[0m" # Default
|
|
237
263
|
|
|
238
264
|
# Format performance
|
|
239
|
-
if status ==
|
|
240
|
-
|
|
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(
|
|
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() ==
|
|
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
|
|
274
|
-
tty.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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())
|