claude-controller 0.2.0 → 0.3.0

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.
Files changed (68) hide show
  1. package/README.md +2 -2
  2. package/bin/autoloop.sh +382 -0
  3. package/bin/ctl +327 -5
  4. package/bin/native-app.py +5 -2
  5. package/bin/watchdog.sh +357 -0
  6. package/cognitive/__init__.py +14 -0
  7. package/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/cognitive/__pycache__/dispatcher.cpython-314.pyc +0 -0
  9. package/cognitive/__pycache__/evaluator.cpython-314.pyc +0 -0
  10. package/cognitive/__pycache__/goal_engine.cpython-314.pyc +0 -0
  11. package/cognitive/__pycache__/learning.cpython-314.pyc +0 -0
  12. package/cognitive/__pycache__/orchestrator.cpython-314.pyc +0 -0
  13. package/cognitive/__pycache__/planner.cpython-314.pyc +0 -0
  14. package/cognitive/dispatcher.py +192 -0
  15. package/cognitive/evaluator.py +289 -0
  16. package/cognitive/goal_engine.py +232 -0
  17. package/cognitive/learning.py +189 -0
  18. package/cognitive/orchestrator.py +303 -0
  19. package/cognitive/planner.py +207 -0
  20. package/cognitive/prompts/analyst.md +31 -0
  21. package/cognitive/prompts/coder.md +22 -0
  22. package/cognitive/prompts/reviewer.md +33 -0
  23. package/cognitive/prompts/tester.md +21 -0
  24. package/cognitive/prompts/writer.md +25 -0
  25. package/config.sh +6 -1
  26. package/dag/__init__.py +5 -0
  27. package/dag/__pycache__/__init__.cpython-314.pyc +0 -0
  28. package/dag/__pycache__/graph.cpython-314.pyc +0 -0
  29. package/dag/graph.py +222 -0
  30. package/lib/jobs.sh +12 -1
  31. package/package.json +5 -1
  32. package/postinstall.sh +1 -1
  33. package/service/controller.sh +43 -11
  34. package/web/audit.py +122 -0
  35. package/web/checkpoint.py +80 -0
  36. package/web/config.py +2 -5
  37. package/web/handler.py +464 -26
  38. package/web/handler_fs.py +15 -14
  39. package/web/handler_goals.py +203 -0
  40. package/web/handler_jobs.py +165 -42
  41. package/web/handler_memory.py +203 -0
  42. package/web/jobs.py +576 -12
  43. package/web/personas.py +419 -0
  44. package/web/pipeline.py +682 -50
  45. package/web/presets.py +506 -0
  46. package/web/projects.py +58 -4
  47. package/web/static/api.js +90 -3
  48. package/web/static/app.js +8 -0
  49. package/web/static/base.css +51 -12
  50. package/web/static/context.js +14 -4
  51. package/web/static/form.css +3 -2
  52. package/web/static/goals.css +363 -0
  53. package/web/static/goals.js +300 -0
  54. package/web/static/i18n.js +288 -0
  55. package/web/static/index.html +142 -6
  56. package/web/static/jobs.css +951 -4
  57. package/web/static/jobs.js +890 -54
  58. package/web/static/memoryview.js +117 -0
  59. package/web/static/personas.js +228 -0
  60. package/web/static/pipeline.css +308 -1
  61. package/web/static/pipelines.js +249 -14
  62. package/web/static/presets.js +244 -0
  63. package/web/static/send.js +26 -4
  64. package/web/static/settings-style.css +34 -3
  65. package/web/static/settings.js +37 -1
  66. package/web/static/stream.js +242 -19
  67. package/web/static/utils.js +54 -2
  68. package/web/webhook.py +210 -0
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # watchdog.sh — Controller 프로세스 워치독
4
+ #
5
+ # 역할:
6
+ # - Controller 서비스를 10초 주기로 감시
7
+ # - 크래시 감지 시 자동 재시작
8
+ # - 연속 실패 시 지수 백오프 (10s → 20s → 40s → 최대 120s)
9
+ # - 정상 가동 60초 이상이면 백오프 리셋
10
+ # - macOS 알림으로 재시작/실패 통보
11
+ #
12
+ # 사용법:
13
+ # watchdog.sh start — 워치독 데몬 시작
14
+ # watchdog.sh stop — 워치독 중지
15
+ # watchdog.sh status — 워치독 상태 확인
16
+ # watchdog.sh install — macOS launchd plist 설치
17
+ # watchdog.sh uninstall — launchd plist 제거
18
+ # ============================================================
19
+ set -uo pipefail
20
+
21
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ CONTROLLER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
23
+
24
+ # ── 경로 ──
25
+ PID_FILE="${CONTROLLER_DIR}/service/watchdog.pid"
26
+ CONTROLLER_PID_FILE="${CONTROLLER_DIR}/service/controller.pid"
27
+ STATE_FILE="${CONTROLLER_DIR}/data/watchdog_state.json"
28
+ LOG_FILE="${CONTROLLER_DIR}/logs/watchdog.log"
29
+ CONTROLLER_BIN="${CONTROLLER_DIR}/bin/controller"
30
+
31
+ # ── 설정 ──
32
+ CHECK_INTERVAL=10 # 기본 감시 간격 (초)
33
+ MAX_BACKOFF=120 # 최대 백오프 간격 (초)
34
+ STABLE_THRESHOLD=60 # 정상 가동 판정 시간 (초)
35
+ MAX_CONSECUTIVE_FAILS=10 # 연속 실패 시 대기 모드 진입 횟수
36
+
37
+ # ── launchd ──
38
+ PLIST_LABEL="com.orchestration.controller.watchdog"
39
+ PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
40
+
41
+ # 디렉토리 보장
42
+ mkdir -p "${CONTROLLER_DIR}/logs" "${CONTROLLER_DIR}/data" "${CONTROLLER_DIR}/service"
43
+
44
+ # ── 로깅 ──
45
+ _log() {
46
+ local ts
47
+ ts=$(date '+%Y-%m-%d %H:%M:%S')
48
+ echo "[${ts}] $*" >> "$LOG_FILE"
49
+ }
50
+
51
+ # ── 상태 파일 갱신 ──
52
+ _write_state() {
53
+ local status="$1"
54
+ local restart_count="$2"
55
+ local last_restart="${3:-}"
56
+ local message="${4:-}"
57
+ local now
58
+ now=$(date '+%Y-%m-%dT%H:%M:%S')
59
+
60
+ cat > "$STATE_FILE" <<EOF
61
+ {
62
+ "status": "${status}",
63
+ "pid": $$,
64
+ "restart_count": ${restart_count},
65
+ "consecutive_fails": ${_CONSECUTIVE_FAILS:-0},
66
+ "last_restart": "${last_restart}",
67
+ "last_check": "${now}",
68
+ "uptime_since": "${_STARTED_AT:-${now}}",
69
+ "message": "${message}"
70
+ }
71
+ EOF
72
+ }
73
+
74
+ # ── 컨트롤러 상태 확인 ──
75
+ _is_controller_alive() {
76
+ if [[ ! -f "$CONTROLLER_PID_FILE" ]]; then
77
+ return 1
78
+ fi
79
+ local pid
80
+ pid=$(cat "$CONTROLLER_PID_FILE" 2>/dev/null)
81
+ if [[ -z "$pid" ]]; then
82
+ return 1
83
+ fi
84
+ kill -0 "$pid" 2>/dev/null
85
+ }
86
+
87
+ # ── 컨트롤러 재시작 ──
88
+ _restart_controller() {
89
+ _log "Controller 재시작 시도..."
90
+
91
+ # 기존 좀비 정리
92
+ if [[ -f "$CONTROLLER_PID_FILE" ]]; then
93
+ local old_pid
94
+ old_pid=$(cat "$CONTROLLER_PID_FILE" 2>/dev/null)
95
+ if [[ -n "$old_pid" ]]; then
96
+ kill "$old_pid" 2>/dev/null || true
97
+ sleep 1
98
+ kill -9 "$old_pid" 2>/dev/null || true
99
+ fi
100
+ rm -f "$CONTROLLER_PID_FILE"
101
+ fi
102
+
103
+ # controller start (백그라운드)
104
+ nohup "$CONTROLLER_BIN" start >> "$LOG_FILE" 2>&1 &
105
+ sleep 3
106
+
107
+ if _is_controller_alive; then
108
+ _log "Controller 재시작 성공 (PID: $(cat "$CONTROLLER_PID_FILE" 2>/dev/null))"
109
+ return 0
110
+ else
111
+ _log "Controller 재시작 실패"
112
+ return 1
113
+ fi
114
+ }
115
+
116
+ # ── macOS 알림 ──
117
+ _notify() {
118
+ local title="$1"
119
+ local message="$2"
120
+ local sound="${3:-default}"
121
+ osascript -e "display notification \"${message}\" with title \"${title}\" sound name \"${sound}\"" 2>/dev/null || true
122
+ }
123
+
124
+ # ── 메인 감시 루프 ──
125
+ _watchdog_loop() {
126
+ _STARTED_AT=$(date '+%Y-%m-%dT%H:%M:%S')
127
+ local restart_count=0
128
+ _CONSECUTIVE_FAILS=0
129
+ local current_interval=$CHECK_INTERVAL
130
+ local last_restart_time=""
131
+ local controller_up_since=0
132
+
133
+ _log "워치독 시작 (PID: $$, 간격: ${CHECK_INTERVAL}초)"
134
+ _write_state "running" "$restart_count" "" "감시 시작"
135
+
136
+ while true; do
137
+ sleep "$current_interval"
138
+
139
+ if _is_controller_alive; then
140
+ # 정상 — 백오프 리셋 조건 체크
141
+ local now_epoch
142
+ now_epoch=$(date +%s)
143
+ if [[ $controller_up_since -eq 0 ]]; then
144
+ controller_up_since=$now_epoch
145
+ fi
146
+
147
+ local uptime=$(( now_epoch - controller_up_since ))
148
+ if [[ $uptime -ge $STABLE_THRESHOLD && $_CONSECUTIVE_FAILS -gt 0 ]]; then
149
+ _log "정상 가동 ${uptime}초 — 백오프 리셋"
150
+ _CONSECUTIVE_FAILS=0
151
+ current_interval=$CHECK_INTERVAL
152
+ fi
153
+
154
+ _write_state "running" "$restart_count" "$last_restart_time" "정상 감시 중"
155
+ else
156
+ # 다운 감지
157
+ controller_up_since=0
158
+ (( _CONSECUTIVE_FAILS++ )) || true
159
+ _log "Controller 다운 감지 (연속 실패: ${_CONSECUTIVE_FAILS})"
160
+
161
+ if [[ $_CONSECUTIVE_FAILS -ge $MAX_CONSECUTIVE_FAILS ]]; then
162
+ _log "연속 실패 ${MAX_CONSECUTIVE_FAILS}회 도달 — 대기 모드"
163
+ _write_state "cooldown" "$restart_count" "$last_restart_time" "연속 실패 ${_CONSECUTIVE_FAILS}회 — 수동 확인 필요"
164
+ _notify "Watchdog" "Controller 복구 실패 (${_CONSECUTIVE_FAILS}회). 수동 확인 필요." "Basso"
165
+
166
+ # 5분 대기 후 다시 시도
167
+ sleep 300
168
+ _CONSECUTIVE_FAILS=0
169
+ current_interval=$CHECK_INTERVAL
170
+ _log "대기 모드 종료, 감시 재개"
171
+ _write_state "running" "$restart_count" "$last_restart_time" "감시 재개"
172
+ continue
173
+ fi
174
+
175
+ # 재시작 시도
176
+ if _restart_controller; then
177
+ (( restart_count++ )) || true
178
+ last_restart_time=$(date '+%Y-%m-%dT%H:%M:%S')
179
+ _CONSECUTIVE_FAILS=0
180
+ current_interval=$CHECK_INTERVAL
181
+ controller_up_since=$(date +%s)
182
+
183
+ _write_state "running" "$restart_count" "$last_restart_time" "재시작 성공"
184
+ _notify "Watchdog" "Controller 자동 재시작 완료 (#${restart_count})" "Glass"
185
+ else
186
+ # 지수 백오프
187
+ current_interval=$(( CHECK_INTERVAL * (2 ** (_CONSECUTIVE_FAILS - 1)) ))
188
+ if [[ $current_interval -gt $MAX_BACKOFF ]]; then
189
+ current_interval=$MAX_BACKOFF
190
+ fi
191
+
192
+ _write_state "retrying" "$restart_count" "$last_restart_time" "재시작 실패, ${current_interval}초 후 재시도"
193
+ _log "재시작 실패 — 다음 체크 ${current_interval}초 후"
194
+ _notify "Watchdog" "Controller 재시작 실패. ${current_interval}초 후 재시도." "Basso"
195
+ fi
196
+ fi
197
+ done
198
+ }
199
+
200
+ # ── cleanup ──
201
+ _cleanup() {
202
+ _log "워치독 종료 (PID: $$)"
203
+ _write_state "stopped" "0" "" "정상 종료"
204
+ rm -f "$PID_FILE"
205
+ }
206
+ trap _cleanup EXIT SIGTERM SIGINT SIGHUP
207
+
208
+ # ── start ──
209
+ cmd_start() {
210
+ # 이미 실행 중인지 확인
211
+ if [[ -f "$PID_FILE" ]]; then
212
+ local existing_pid
213
+ existing_pid=$(cat "$PID_FILE" 2>/dev/null)
214
+ if [[ -n "$existing_pid" ]] && kill -0 "$existing_pid" 2>/dev/null; then
215
+ echo "워치독이 이미 실행 중입니다 (PID: ${existing_pid})"
216
+ exit 1
217
+ fi
218
+ rm -f "$PID_FILE"
219
+ fi
220
+
221
+ # 데몬화 (백그라운드)
222
+ if [[ "${_WATCHDOG_FOREGROUND:-}" != "true" ]]; then
223
+ _WATCHDOG_FOREGROUND=true nohup "$0" start >> "$LOG_FILE" 2>&1 &
224
+ local bg_pid=$!
225
+ echo "$bg_pid" > "$PID_FILE"
226
+ echo "워치독 시작됨 (PID: ${bg_pid})"
227
+ echo "로그: ${LOG_FILE}"
228
+ exit 0
229
+ fi
230
+
231
+ # 포그라운드 실행 (데몬 모드)
232
+ echo $$ > "$PID_FILE"
233
+ _watchdog_loop
234
+ }
235
+
236
+ # ── stop ──
237
+ cmd_stop() {
238
+ if [[ ! -f "$PID_FILE" ]]; then
239
+ echo "실행 중인 워치독이 없습니다."
240
+ return 1
241
+ fi
242
+
243
+ local pid
244
+ pid=$(cat "$PID_FILE" 2>/dev/null)
245
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
246
+ kill "$pid" 2>/dev/null
247
+ local waited=0
248
+ while kill -0 "$pid" 2>/dev/null && [[ $waited -lt 5 ]]; do
249
+ sleep 1
250
+ (( waited++ )) || true
251
+ done
252
+ if kill -0 "$pid" 2>/dev/null; then
253
+ kill -9 "$pid" 2>/dev/null || true
254
+ fi
255
+ rm -f "$PID_FILE"
256
+ echo "워치독 종료됨 (PID: ${pid})"
257
+ else
258
+ rm -f "$PID_FILE"
259
+ echo "워치독이 이미 종료되어 있습니다. PID 파일을 정리했습니다."
260
+ fi
261
+ }
262
+
263
+ # ── status ──
264
+ cmd_status() {
265
+ if [[ -f "$PID_FILE" ]]; then
266
+ local pid
267
+ pid=$(cat "$PID_FILE" 2>/dev/null)
268
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
269
+ echo "워치독 실행 중 (PID: ${pid})"
270
+ if [[ -f "$STATE_FILE" ]]; then
271
+ cat "$STATE_FILE"
272
+ fi
273
+ return 0
274
+ else
275
+ echo "워치독 중지됨 (오래된 PID: ${pid})"
276
+ rm -f "$PID_FILE"
277
+ return 1
278
+ fi
279
+ else
280
+ echo "워치독이 실행 중이지 않습니다."
281
+ return 1
282
+ fi
283
+ }
284
+
285
+ # ── install (macOS launchd) ──
286
+ cmd_install() {
287
+ local watchdog_path
288
+ watchdog_path="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
289
+
290
+ mkdir -p "$HOME/Library/LaunchAgents"
291
+
292
+ cat > "$PLIST_PATH" <<PLIST
293
+ <?xml version="1.0" encoding="UTF-8"?>
294
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
295
+ <plist version="1.0">
296
+ <dict>
297
+ <key>Label</key>
298
+ <string>${PLIST_LABEL}</string>
299
+ <key>ProgramArguments</key>
300
+ <array>
301
+ <string>/bin/bash</string>
302
+ <string>${watchdog_path}</string>
303
+ <string>start</string>
304
+ </array>
305
+ <key>RunAtLoad</key>
306
+ <true/>
307
+ <key>KeepAlive</key>
308
+ <true/>
309
+ <key>StandardOutPath</key>
310
+ <string>${LOG_FILE}</string>
311
+ <key>StandardErrorPath</key>
312
+ <string>${LOG_FILE}</string>
313
+ <key>EnvironmentVariables</key>
314
+ <dict>
315
+ <key>_WATCHDOG_FOREGROUND</key>
316
+ <string>true</string>
317
+ <key>PATH</key>
318
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
319
+ </dict>
320
+ </dict>
321
+ </plist>
322
+ PLIST
323
+
324
+ launchctl load "$PLIST_PATH" 2>/dev/null || true
325
+ echo "launchd plist 설치 완료: ${PLIST_PATH}"
326
+ echo "워치독이 부팅 시 자동으로 시작됩니다."
327
+ echo ""
328
+ echo "수동 제어:"
329
+ echo " launchctl start ${PLIST_LABEL} # 즉시 시작"
330
+ echo " launchctl stop ${PLIST_LABEL} # 즉시 중지"
331
+ }
332
+
333
+ # ── uninstall ──
334
+ cmd_uninstall() {
335
+ if [[ -f "$PLIST_PATH" ]]; then
336
+ launchctl unload "$PLIST_PATH" 2>/dev/null || true
337
+ rm -f "$PLIST_PATH"
338
+ echo "launchd plist 제거 완료."
339
+ else
340
+ echo "설치된 plist가 없습니다."
341
+ fi
342
+ # 실행 중이면 중지
343
+ cmd_stop 2>/dev/null || true
344
+ }
345
+
346
+ # ── 메인 진입점 ──
347
+ case "${1:-status}" in
348
+ start) cmd_start ;;
349
+ stop) cmd_stop ;;
350
+ status) cmd_status ;;
351
+ install) cmd_install ;;
352
+ uninstall) cmd_uninstall ;;
353
+ *)
354
+ echo "사용법: watchdog.sh {start|stop|status|install|uninstall}"
355
+ exit 1
356
+ ;;
357
+ esac
@@ -0,0 +1,14 @@
1
+ """Cognitive Layer — 자율 개발 에이전트의 인지 아키텍처."""
2
+
3
+ from cognitive.goal_engine import GoalEngine, GoalStatus, ExecutionMode
4
+ from cognitive.orchestrator import Orchestrator
5
+ from cognitive.planner import Planner
6
+ from cognitive.dispatcher import Dispatcher
7
+ from cognitive.evaluator import Evaluator
8
+ from cognitive.learning import LearningModule
9
+
10
+ __all__ = [
11
+ "GoalEngine", "GoalStatus", "ExecutionMode",
12
+ "Orchestrator", "Planner", "Dispatcher",
13
+ "Evaluator", "LearningModule",
14
+ ]
@@ -0,0 +1,192 @@
1
+ """
2
+ Dispatcher — DAG 기반 작업 배분기
3
+ DAG의 실행 순서에 따라 Worker를 배정하고, claude -p 프로세스를 관리한다.
4
+
5
+ 핵심 정책:
6
+ - 의존성 충족된 태스크만 실행
7
+ - Worker 유형별 전문화된 시스템 프롬프트 주입
8
+ - 동시성 제한 준수
9
+ - 실패 태스크 자동 재시도 (최대 2회, 프롬프트 변형)
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import subprocess
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Optional, Callable
18
+
19
+ from dag.graph import TaskDAG, TaskNode
20
+
21
+
22
+ class WorkerProcess:
23
+ """실행 중인 Worker 프로세스를 추적한다."""
24
+
25
+ __slots__ = ("task_id", "process", "started_at", "output_path")
26
+
27
+ def __init__(self, task_id: str, process: subprocess.Popen, output_path: str):
28
+ self.task_id = task_id
29
+ self.process = process
30
+ self.started_at = time.time()
31
+ self.output_path = output_path
32
+
33
+
34
+ class Dispatcher:
35
+ """DAG 순서에 따라 태스크를 Worker에게 디스패치한다."""
36
+
37
+ MAX_RETRIES = 2
38
+
39
+ def __init__(
40
+ self,
41
+ claude_bin: str,
42
+ logs_dir: str,
43
+ prompts_dir: str,
44
+ max_concurrent: int = 5,
45
+ on_task_complete: Optional[Callable] = None,
46
+ on_task_fail: Optional[Callable] = None,
47
+ ):
48
+ self.claude_bin = claude_bin
49
+ self.logs_dir = Path(logs_dir)
50
+ self.prompts_dir = Path(prompts_dir)
51
+ self.max_concurrent = max_concurrent
52
+ self.on_task_complete = on_task_complete
53
+ self.on_task_fail = on_task_fail
54
+
55
+ self._active: dict[str, WorkerProcess] = {} # task_id → WorkerProcess
56
+ self.logs_dir.mkdir(parents=True, exist_ok=True)
57
+
58
+ def run_dag(self, dag: TaskDAG, cwd: str, goal_id: str) -> TaskDAG:
59
+ """DAG 전체를 실행한다. 모든 태스크가 완료/실패할 때까지 루프.
60
+
61
+ Args:
62
+ dag: 실행할 태스크 DAG
63
+ cwd: 작업 디렉토리
64
+ goal_id: 목표 ID (로그 구분용)
65
+
66
+ Returns:
67
+ 실행 완료된 DAG (각 태스크에 상태/결과 포함)
68
+ """
69
+ while not dag.is_complete() and not self._all_blocked(dag):
70
+ # 1. 완료된 프로세스 수확
71
+ self._harvest_completed(dag)
72
+
73
+ # 2. 실행 가능한 태스크 디스패치
74
+ ready = dag.get_ready_tasks()
75
+ slots = self.max_concurrent - len(self._active)
76
+
77
+ for task in ready[:slots]:
78
+ self._dispatch_task(task, cwd, goal_id)
79
+
80
+ # 3. 짧은 대기 (폴링 주기)
81
+ if self._active:
82
+ time.sleep(2)
83
+
84
+ # 마지막 수확
85
+ self._harvest_completed(dag)
86
+ return dag
87
+
88
+ def _dispatch_task(self, task: TaskNode, cwd: str, goal_id: str):
89
+ """개별 태스크를 claude -p 프로세스로 실행한다."""
90
+ system_prompt = self._load_worker_prompt(task.worker_type)
91
+ output_path = str(self.logs_dir / f"{goal_id}_{task.id}.out")
92
+
93
+ cmd = [
94
+ self.claude_bin,
95
+ "-p", task.prompt,
96
+ "--output-format", "json",
97
+ "--allowedTools", self._tools_for_worker(task.worker_type),
98
+ ]
99
+
100
+ if system_prompt:
101
+ cmd.extend(["--append-system-prompt", system_prompt])
102
+
103
+ out_file = open(output_path, "w")
104
+ process = subprocess.Popen(
105
+ cmd,
106
+ cwd=cwd,
107
+ stdout=out_file,
108
+ stderr=subprocess.STDOUT,
109
+ env=os.environ.copy(),
110
+ )
111
+
112
+ task.status = "running"
113
+ self._active[task.id] = WorkerProcess(task.id, process, output_path)
114
+
115
+ def _harvest_completed(self, dag: TaskDAG):
116
+ """완료된 프로세스를 확인하고 태스크 상태를 갱신한다."""
117
+ done_ids = []
118
+ for task_id, wp in self._active.items():
119
+ ret = wp.process.poll()
120
+ if ret is None:
121
+ continue # 아직 실행 중
122
+
123
+ node = dag.nodes[task_id]
124
+ duration_ms = int((time.time() - wp.started_at) * 1000)
125
+ node.duration_ms = duration_ms
126
+
127
+ # 결과 파싱
128
+ cost = self._parse_cost(wp.output_path)
129
+ node.cost_usd = cost
130
+
131
+ if ret == 0:
132
+ node.status = "completed"
133
+ if self.on_task_complete:
134
+ self.on_task_complete(task_id, cost)
135
+ else:
136
+ node.retries += 1
137
+ if node.retries <= self.MAX_RETRIES:
138
+ # 재시도: 프롬프트 앞에 실패 맥락 추가
139
+ node.prompt = self._augment_retry_prompt(node)
140
+ node.status = "pending"
141
+ else:
142
+ node.status = "failed"
143
+ if self.on_task_fail:
144
+ self.on_task_fail(task_id, cost)
145
+
146
+ done_ids.append(task_id)
147
+
148
+ for tid in done_ids:
149
+ del self._active[tid]
150
+
151
+ def _all_blocked(self, dag: TaskDAG) -> bool:
152
+ """모든 남은 태스크가 실행 불가능한 상태인지 확인한다."""
153
+ if self._active:
154
+ return False # 아직 실행 중인 것이 있음
155
+ ready = dag.get_ready_tasks()
156
+ return len(ready) == 0
157
+
158
+ def _load_worker_prompt(self, worker_type: str) -> str:
159
+ """Worker 유형별 시스템 프롬프트를 로드한다."""
160
+ path = self.prompts_dir / f"{worker_type}.md"
161
+ if path.exists():
162
+ return path.read_text()
163
+ return ""
164
+
165
+ def _tools_for_worker(self, worker_type: str) -> str:
166
+ """Worker 유형에 따른 허용 도구를 반환한다."""
167
+ tool_sets = {
168
+ "analyst": "Read,Glob,Grep,Bash",
169
+ "coder": "Bash,Read,Write,Edit,Glob,Grep",
170
+ "tester": "Bash,Read,Write,Edit,Glob,Grep",
171
+ "reviewer": "Read,Glob,Grep,Bash",
172
+ "writer": "Read,Write,Edit,Glob,Grep",
173
+ }
174
+ return tool_sets.get(worker_type, "Bash,Read,Write,Edit,Glob,Grep")
175
+
176
+ def _augment_retry_prompt(self, node: TaskNode) -> str:
177
+ """재시도 시 프롬프트에 실패 맥락을 추가한다."""
178
+ return (
179
+ f"[재시도 {node.retries}/{self.MAX_RETRIES}] "
180
+ f"이전 시도가 실패했습니다. 다른 접근 방식을 시도하세요.\n\n"
181
+ f"원래 태스크:\n{node.prompt}"
182
+ )
183
+
184
+ def _parse_cost(self, output_path: str) -> float:
185
+ """출력 파일에서 비용 정보를 추출한다."""
186
+ try:
187
+ with open(output_path) as f:
188
+ data = json.load(f)
189
+ # claude --output-format json 응답에서 cost 추출
190
+ return float(data.get("cost_usd", 0) or 0)
191
+ except (json.JSONDecodeError, FileNotFoundError, KeyError):
192
+ return 0.0