claude-controller 0.1.2 → 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.
- package/README.md +2 -2
- package/bin/autoloop.sh +382 -0
- package/bin/ctl +1189 -0
- package/bin/native-app.py +6 -3
- package/bin/watchdog.sh +357 -0
- package/cognitive/__init__.py +14 -0
- package/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/dispatcher.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/evaluator.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/goal_engine.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/learning.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/orchestrator.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/planner.cpython-314.pyc +0 -0
- package/cognitive/dispatcher.py +192 -0
- package/cognitive/evaluator.py +289 -0
- package/cognitive/goal_engine.py +232 -0
- package/cognitive/learning.py +189 -0
- package/cognitive/orchestrator.py +303 -0
- package/cognitive/planner.py +207 -0
- package/cognitive/prompts/analyst.md +31 -0
- package/cognitive/prompts/coder.md +22 -0
- package/cognitive/prompts/reviewer.md +33 -0
- package/cognitive/prompts/tester.md +21 -0
- package/cognitive/prompts/writer.md +25 -0
- package/config.sh +6 -1
- package/dag/__init__.py +5 -0
- package/dag/__pycache__/__init__.cpython-314.pyc +0 -0
- package/dag/__pycache__/graph.cpython-314.pyc +0 -0
- package/dag/graph.py +222 -0
- package/lib/jobs.sh +12 -1
- package/package.json +11 -5
- package/postinstall.sh +1 -1
- package/service/controller.sh +43 -11
- package/web/audit.py +122 -0
- package/web/checkpoint.py +80 -0
- package/web/config.py +2 -5
- package/web/handler.py +634 -473
- package/web/handler_fs.py +153 -0
- package/web/handler_goals.py +203 -0
- package/web/handler_jobs.py +372 -0
- package/web/handler_memory.py +203 -0
- package/web/handler_sessions.py +132 -0
- package/web/jobs.py +585 -13
- package/web/personas.py +419 -0
- package/web/pipeline.py +981 -0
- package/web/presets.py +506 -0
- package/web/projects.py +246 -0
- package/web/static/api.js +141 -0
- package/web/static/app.js +25 -1937
- package/web/static/attachments.js +144 -0
- package/web/static/base.css +497 -0
- package/web/static/context.js +204 -0
- package/web/static/dirs.js +246 -0
- package/web/static/form.css +763 -0
- package/web/static/goals.css +363 -0
- package/web/static/goals.js +300 -0
- package/web/static/i18n.js +625 -0
- package/web/static/index.html +215 -13
- package/web/static/{styles.css → jobs.css} +746 -1141
- package/web/static/jobs.js +1270 -0
- package/web/static/memoryview.js +117 -0
- package/web/static/personas.js +228 -0
- package/web/static/pipeline.css +338 -0
- package/web/static/pipelines.js +487 -0
- package/web/static/presets.js +244 -0
- package/web/static/send.js +135 -0
- package/web/static/settings-style.css +291 -0
- package/web/static/settings.js +81 -0
- package/web/static/stream.js +534 -0
- package/web/static/utils.js +131 -0
- package/web/webhook.py +210 -0
package/bin/native-app.py
CHANGED
|
@@ -61,7 +61,7 @@ def main():
|
|
|
61
61
|
use_ssl = os.path.isfile(SSL_CERT) and os.path.isfile(SSL_KEY)
|
|
62
62
|
scheme = "https" if use_ssl else "http"
|
|
63
63
|
|
|
64
|
-
server = http.server.
|
|
64
|
+
server = http.server.ThreadingHTTPServer(("127.0.0.1", PORT), ControllerHandler)
|
|
65
65
|
|
|
66
66
|
if use_ssl:
|
|
67
67
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
@@ -73,7 +73,7 @@ def main():
|
|
|
73
73
|
┌──────────────────────────────────────────────┐
|
|
74
74
|
│ Claude Controller │
|
|
75
75
|
├──────────────────────────────────────────────┤
|
|
76
|
-
│ API : {scheme}://localhost:{PORT:<
|
|
76
|
+
│ API : {scheme}://localhost:{PORT:<24}│
|
|
77
77
|
│ App : {PUBLIC_URL:<35s}│
|
|
78
78
|
│ SSL : {'ON' if use_ssl else 'OFF (HTTP 모드)':<35s}│
|
|
79
79
|
├──────────────────────────────────────────────┤
|
|
@@ -89,7 +89,10 @@ def main():
|
|
|
89
89
|
print(f" mkcert -install && mkcert -cert-file certs/localhost+1.pem \\")
|
|
90
90
|
print(f" -key-file certs/localhost+1-key.pem localhost 127.0.0.1\n")
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
try:
|
|
93
|
+
webbrowser.open(PUBLIC_URL)
|
|
94
|
+
except Exception:
|
|
95
|
+
pass # Docker 등 headless 환경에서는 브라우저 없음
|
|
93
96
|
|
|
94
97
|
try:
|
|
95
98
|
server.serve_forever()
|
package/bin/watchdog.sh
ADDED
|
@@ -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
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|