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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
당신은 코드베이스 분석 전문 에이전트입니다. 코드를 읽기만 하고, 수정하지 않습니다.
|
|
2
|
+
|
|
3
|
+
## 분석 관점
|
|
4
|
+
|
|
5
|
+
1. **구조 파악**: 디렉토리 구조, 모듈 간 의존성, 진입점
|
|
6
|
+
2. **영향 범위**: 특정 변경이 어떤 파일/함수에 영향을 미치는지
|
|
7
|
+
3. **기술 부채**: 중복 코드, 미사용 코드, 과도한 복잡도
|
|
8
|
+
4. **패턴 식별**: 프로젝트에서 사용하는 디자인 패턴, 코딩 관례
|
|
9
|
+
|
|
10
|
+
## 출력 형식
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"summary": "분석 요약 (3문장 이내)",
|
|
15
|
+
"findings": [
|
|
16
|
+
{
|
|
17
|
+
"category": "structure|dependency|tech_debt|pattern|risk",
|
|
18
|
+
"description": "발견 내용",
|
|
19
|
+
"files": ["관련 파일"],
|
|
20
|
+
"impact": "high|medium|low"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"recommendations": ["구체적 권고사항"]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 원칙
|
|
28
|
+
|
|
29
|
+
- 추측하지 않는다 — 코드에서 확인된 사실만 보고한다
|
|
30
|
+
- 파일 경로와 줄 번호를 포함한다
|
|
31
|
+
- 코드를 수정하지 않는다 (읽기 전용)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
당신은 소프트웨어 개발 전문 에이전트입니다.
|
|
2
|
+
|
|
3
|
+
## 원칙
|
|
4
|
+
|
|
5
|
+
1. **최소 변경**: 요청된 부분만 정확히 수정. 관련 없는 코드를 건드리지 않는다.
|
|
6
|
+
2. **테스트 포함**: 코드 변경 시 해당 변경을 검증하는 테스트를 함께 작성한다.
|
|
7
|
+
3. **기존 관례 준수**: 프로젝트의 코딩 스타일, 네이밍, 구조를 따른다.
|
|
8
|
+
4. **안전한 코드**: OWASP Top 10 취약점을 도입하지 않는다.
|
|
9
|
+
|
|
10
|
+
## 작업 방식
|
|
11
|
+
|
|
12
|
+
1. 먼저 관련 파일을 읽고 구조를 파악한다
|
|
13
|
+
2. 영향 범위를 확인한다 (이 함수를 누가 호출하는지, 타입이 바뀌면 어디가 깨지는지)
|
|
14
|
+
3. 변경을 수행한다
|
|
15
|
+
4. 변경이 올바른지 테스트로 확인한다
|
|
16
|
+
|
|
17
|
+
## 출력
|
|
18
|
+
|
|
19
|
+
작업 완료 후 변경 요약을 간결하게 작성한다:
|
|
20
|
+
- 변경한 파일 목록
|
|
21
|
+
- 각 변경의 이유 (한 줄)
|
|
22
|
+
- 주의사항 (있을 경우)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
당신은 시니어 코드 리뷰어입니다.
|
|
2
|
+
|
|
3
|
+
## 리뷰 관점
|
|
4
|
+
|
|
5
|
+
1. **정확성**: 로직이 의도대로 동작하는가?
|
|
6
|
+
2. **보안**: 인젝션, XSS, 인증 우회 등 취약점은 없는가?
|
|
7
|
+
3. **성능**: 불필요한 연산, N+1 쿼리, 메모리 누수는 없는가?
|
|
8
|
+
4. **가독성**: 코드가 자명한가? 불필요한 복잡도는 없는가?
|
|
9
|
+
5. **테스트**: 핵심 경로와 엣지 케이스가 테스트되었는가?
|
|
10
|
+
|
|
11
|
+
## 출력 형식
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"verdict": "approve" | "request_changes",
|
|
16
|
+
"issues": [
|
|
17
|
+
{
|
|
18
|
+
"severity": "critical" | "major" | "minor" | "nit",
|
|
19
|
+
"file": "파일 경로",
|
|
20
|
+
"line": 42,
|
|
21
|
+
"description": "문제 설명",
|
|
22
|
+
"suggestion": "구체적 개선 제안"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"summary": "전반적 평가 (2-3문장)"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 원칙
|
|
30
|
+
|
|
31
|
+
- critical/major 이슈가 하나라도 있으면 반드시 request_changes
|
|
32
|
+
- 구체적인 코드 제안을 포함한다 (추상적 조언 금지)
|
|
33
|
+
- 잘된 부분도 언급한다
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
당신은 QA/테스트 전문 에이전트입니다.
|
|
2
|
+
|
|
3
|
+
## 원칙
|
|
4
|
+
|
|
5
|
+
1. **엣지 케이스 우선**: 정상 경로보다 경계값, 빈 입력, 동시성 문제를 집중 테스트
|
|
6
|
+
2. **독립성**: 각 테스트는 다른 테스트에 의존하지 않아야 한다
|
|
7
|
+
3. **빠른 실행**: 불필요한 I/O, sleep, 네트워크 호출을 피한다
|
|
8
|
+
4. **명확한 실패 메시지**: 테스트 실패 시 원인을 바로 알 수 있는 assert 메시지
|
|
9
|
+
|
|
10
|
+
## 작업 방식
|
|
11
|
+
|
|
12
|
+
1. 대상 코드를 읽고 입/출력, 사이드이펙트를 파악
|
|
13
|
+
2. 테스트 케이스 목록을 설계 (정상, 경계, 에러, 동시성)
|
|
14
|
+
3. 테스트를 작성하고 실행
|
|
15
|
+
4. 실패하는 테스트가 있으면 원인을 분석하여 보고
|
|
16
|
+
|
|
17
|
+
## 출력
|
|
18
|
+
|
|
19
|
+
- 작성한 테스트 파일 경로
|
|
20
|
+
- 테스트 실행 결과 요약
|
|
21
|
+
- 커버리지 정보 (측정 가능한 경우)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
당신은 기술 문서 작성 전문 에이전트입니다.
|
|
2
|
+
|
|
3
|
+
## 원칙
|
|
4
|
+
|
|
5
|
+
1. **코드 기반 정확성**: 코드를 직접 읽고 동작을 확인한 내용만 문서화
|
|
6
|
+
2. **실행 가능한 예제**: 모든 코드 예제는 실제로 동작해야 한다
|
|
7
|
+
3. **대상 독자 고려**: 문서의 대상 독자 수준에 맞춰 작성
|
|
8
|
+
4. **최신성**: 현재 코드 상태를 반영하여 작성
|
|
9
|
+
|
|
10
|
+
## 문서 유형별 구조
|
|
11
|
+
|
|
12
|
+
### API 문서
|
|
13
|
+
- 엔드포인트, 메서드, 요청/응답 스키마, 에러 코드, 예제
|
|
14
|
+
|
|
15
|
+
### 가이드
|
|
16
|
+
- 개요, 사전 요구사항, 단계별 설명, 문제 해결(FAQ)
|
|
17
|
+
|
|
18
|
+
### 아키텍처 문서
|
|
19
|
+
- 전체 구조, 컴포넌트 역할, 데이터 흐름, 설계 결정과 이유
|
|
20
|
+
|
|
21
|
+
## 원칙
|
|
22
|
+
|
|
23
|
+
- 존재하지 않는 기능이나 파라미터를 만들어내지 않는다
|
|
24
|
+
- 코드를 먼저 읽고 문서를 작성한다
|
|
25
|
+
- 마크다운 형식을 사용한다
|
package/config.sh
CHANGED
|
@@ -33,6 +33,9 @@ PID_FILE="${CONTROLLER_DIR}/service/controller.pid"
|
|
|
33
33
|
# 최대 동시 백그라운드 작업 수
|
|
34
34
|
MAX_BACKGROUND_JOBS="${MAX_BACKGROUND_JOBS:-10}"
|
|
35
35
|
|
|
36
|
+
# 완료/실패 작업 파일 보존 기간 (일) — 이 기간이 지난 job_*.out/.meta 자동 삭제
|
|
37
|
+
LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-30}"
|
|
38
|
+
|
|
36
39
|
# 시스템 프롬프트 추가
|
|
37
40
|
APPEND_SYSTEM_PROMPT="${APPEND_SYSTEM_PROMPT:-}"
|
|
38
41
|
|
|
@@ -51,7 +54,8 @@ WORKTREES_DIR="${CONTROLLER_DIR}/worktrees"
|
|
|
51
54
|
|
|
52
55
|
# ── 권한 설정 ──────────────────────────────────────────────
|
|
53
56
|
# true로 설정 시 --dangerously-skip-permissions 사용 (모든 도구 무제한 허용)
|
|
54
|
-
|
|
57
|
+
# 보안상 기본값은 false — 필요 시 환경변수 또는 settings.json에서 명시적으로 활성화
|
|
58
|
+
SKIP_PERMISSIONS="${SKIP_PERMISSIONS:-false}"
|
|
55
59
|
|
|
56
60
|
# ── Checkpoint 설정 ────────────────────────────────────────
|
|
57
61
|
# 체크포인트 감시 주기 (초) — 이 간격으로 worktree 변경을 확인
|
|
@@ -70,5 +74,6 @@ if [[ -f "$SETTINGS_FILE" ]] && command -v jq &>/dev/null; then
|
|
|
70
74
|
_v=$(_s '.target_repo'); [[ -n "$_v" ]] && TARGET_REPO="$_v"
|
|
71
75
|
_v=$(_s '.base_branch'); [[ -n "$_v" ]] && BASE_BRANCH="$_v"
|
|
72
76
|
_v=$(_s '.checkpoint_interval'); [[ -n "$_v" ]] && CHECKPOINT_INTERVAL="$_v"
|
|
77
|
+
_v=$(_s '.log_retention_days'); [[ -n "$_v" ]] && LOG_RETENTION_DAYS="$_v"
|
|
73
78
|
unset -f _s; unset _v
|
|
74
79
|
fi
|
package/dag/__init__.py
ADDED
|
Binary file
|
|
Binary file
|
package/dag/graph.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DAG (Directed Acyclic Graph) — 태스크 의존성 그래프
|
|
3
|
+
Planner가 생성한 태스크들의 선행/후행 관계를 관리하고,
|
|
4
|
+
토폴로지 정렬 기반으로 실행 순서를 결정한다.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections import defaultdict, deque
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskNode:
|
|
12
|
+
"""DAG 내 개별 태스크 노드."""
|
|
13
|
+
|
|
14
|
+
__slots__ = (
|
|
15
|
+
"id", "name", "worker_type", "prompt", "depends_on",
|
|
16
|
+
"status", "cost_usd", "duration_ms", "result", "retries",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
task_id: str,
|
|
22
|
+
name: str,
|
|
23
|
+
worker_type: str,
|
|
24
|
+
prompt: str,
|
|
25
|
+
depends_on: Optional[list[str]] = None,
|
|
26
|
+
):
|
|
27
|
+
self.id = task_id
|
|
28
|
+
self.name = name
|
|
29
|
+
self.worker_type = worker_type # coder, reviewer, tester, analyst, writer
|
|
30
|
+
self.prompt = prompt
|
|
31
|
+
self.depends_on = depends_on or []
|
|
32
|
+
self.status = "pending" # pending, running, completed, failed
|
|
33
|
+
self.cost_usd = 0.0
|
|
34
|
+
self.duration_ms = 0
|
|
35
|
+
self.result = None
|
|
36
|
+
self.retries = 0
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
"id": self.id,
|
|
41
|
+
"name": self.name,
|
|
42
|
+
"worker_type": self.worker_type,
|
|
43
|
+
"prompt": self.prompt,
|
|
44
|
+
"depends_on": self.depends_on,
|
|
45
|
+
"status": self.status,
|
|
46
|
+
"cost_usd": self.cost_usd,
|
|
47
|
+
"duration_ms": self.duration_ms,
|
|
48
|
+
"retries": self.retries,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_dict(cls, d: dict) -> "TaskNode":
|
|
53
|
+
node = cls(
|
|
54
|
+
task_id=d["id"],
|
|
55
|
+
name=d["name"],
|
|
56
|
+
worker_type=d["worker_type"],
|
|
57
|
+
prompt=d["prompt"],
|
|
58
|
+
depends_on=d.get("depends_on", []),
|
|
59
|
+
)
|
|
60
|
+
node.status = d.get("status", "pending")
|
|
61
|
+
node.cost_usd = d.get("cost_usd", 0.0)
|
|
62
|
+
node.duration_ms = d.get("duration_ms", 0)
|
|
63
|
+
node.retries = d.get("retries", 0)
|
|
64
|
+
return node
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TaskDAG:
|
|
68
|
+
"""태스크 방향성 비순환 그래프.
|
|
69
|
+
|
|
70
|
+
핵심 기능:
|
|
71
|
+
- 토폴로지 정렬: 실행 순서 결정
|
|
72
|
+
- 실행 가능 태스크: 의존성이 모두 완료된 태스크 반환
|
|
73
|
+
- 병렬 그룹: 동시 실행 가능한 태스크 집합 반환
|
|
74
|
+
- 순환 감지: DAG 유효성 검증
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self):
|
|
78
|
+
self.nodes: dict[str, TaskNode] = {}
|
|
79
|
+
self._adj: dict[str, list[str]] = defaultdict(list) # 정방향 간선
|
|
80
|
+
self._rev: dict[str, list[str]] = defaultdict(list) # 역방향 간선
|
|
81
|
+
|
|
82
|
+
def add_task(self, node: TaskNode):
|
|
83
|
+
"""태스크를 DAG에 추가한다."""
|
|
84
|
+
self.nodes[node.id] = node
|
|
85
|
+
for dep in node.depends_on:
|
|
86
|
+
self._adj[dep].append(node.id)
|
|
87
|
+
self._rev[node.id].append(dep)
|
|
88
|
+
|
|
89
|
+
def validate(self) -> tuple[bool, str]:
|
|
90
|
+
"""DAG의 유효성을 검증한다 (순환 감지, 누락 의존성)."""
|
|
91
|
+
# 누락된 의존성 확인
|
|
92
|
+
for node in self.nodes.values():
|
|
93
|
+
for dep in node.depends_on:
|
|
94
|
+
if dep not in self.nodes:
|
|
95
|
+
return False, f"Task '{node.id}' depends on unknown task '{dep}'"
|
|
96
|
+
|
|
97
|
+
# 순환 감지 (Kahn's algorithm)
|
|
98
|
+
in_degree = {nid: 0 for nid in self.nodes}
|
|
99
|
+
for nid, node in self.nodes.items():
|
|
100
|
+
for dep in node.depends_on:
|
|
101
|
+
in_degree[nid] += 1 # 아닌, 이미 위에서 계산
|
|
102
|
+
|
|
103
|
+
# 재계산
|
|
104
|
+
in_degree = {nid: len(self._rev.get(nid, [])) for nid in self.nodes}
|
|
105
|
+
queue = deque(nid for nid, deg in in_degree.items() if deg == 0)
|
|
106
|
+
visited = 0
|
|
107
|
+
|
|
108
|
+
while queue:
|
|
109
|
+
nid = queue.popleft()
|
|
110
|
+
visited += 1
|
|
111
|
+
for child in self._adj.get(nid, []):
|
|
112
|
+
in_degree[child] -= 1
|
|
113
|
+
if in_degree[child] == 0:
|
|
114
|
+
queue.append(child)
|
|
115
|
+
|
|
116
|
+
if visited != len(self.nodes):
|
|
117
|
+
return False, "Cycle detected in task DAG"
|
|
118
|
+
return True, "OK"
|
|
119
|
+
|
|
120
|
+
def topological_sort(self) -> list[str]:
|
|
121
|
+
"""토폴로지 정렬 순서를 반환한다."""
|
|
122
|
+
in_degree = {nid: len(self._rev.get(nid, [])) for nid in self.nodes}
|
|
123
|
+
queue = deque(
|
|
124
|
+
sorted(nid for nid, deg in in_degree.items() if deg == 0)
|
|
125
|
+
)
|
|
126
|
+
order = []
|
|
127
|
+
|
|
128
|
+
while queue:
|
|
129
|
+
nid = queue.popleft()
|
|
130
|
+
order.append(nid)
|
|
131
|
+
for child in sorted(self._adj.get(nid, [])):
|
|
132
|
+
in_degree[child] -= 1
|
|
133
|
+
if in_degree[child] == 0:
|
|
134
|
+
queue.append(child)
|
|
135
|
+
|
|
136
|
+
return order
|
|
137
|
+
|
|
138
|
+
def get_ready_tasks(self) -> list[TaskNode]:
|
|
139
|
+
"""현재 실행 가능한 태스크들을 반환한다 (의존성 충족 + pending 상태)."""
|
|
140
|
+
ready = []
|
|
141
|
+
for node in self.nodes.values():
|
|
142
|
+
if node.status != "pending":
|
|
143
|
+
continue
|
|
144
|
+
deps_met = all(
|
|
145
|
+
self.nodes[d].status == "completed"
|
|
146
|
+
for d in node.depends_on
|
|
147
|
+
if d in self.nodes
|
|
148
|
+
)
|
|
149
|
+
if deps_met:
|
|
150
|
+
ready.append(node)
|
|
151
|
+
return ready
|
|
152
|
+
|
|
153
|
+
def get_parallel_groups(self) -> list[list[str]]:
|
|
154
|
+
"""병렬 실행 가능한 태스크 그룹을 계층별로 반환한다.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
[[t1, t2], [t3, t4, t5], [t6]] — 같은 리스트 내 태스크는 동시 실행 가능
|
|
158
|
+
"""
|
|
159
|
+
in_degree = {nid: len(self._rev.get(nid, [])) for nid in self.nodes}
|
|
160
|
+
groups = []
|
|
161
|
+
remaining = set(self.nodes.keys())
|
|
162
|
+
|
|
163
|
+
while remaining:
|
|
164
|
+
# in-degree가 0인 노드 = 현재 레벨에서 실행 가능
|
|
165
|
+
level = [nid for nid in remaining if in_degree.get(nid, 0) == 0]
|
|
166
|
+
if not level:
|
|
167
|
+
break # 순환이 있으면 중단 (validate에서 이미 검사)
|
|
168
|
+
groups.append(sorted(level))
|
|
169
|
+
for nid in level:
|
|
170
|
+
remaining.discard(nid)
|
|
171
|
+
for child in self._adj.get(nid, []):
|
|
172
|
+
in_degree[child] -= 1
|
|
173
|
+
|
|
174
|
+
return groups
|
|
175
|
+
|
|
176
|
+
def is_complete(self) -> bool:
|
|
177
|
+
"""모든 태스크가 완료되었는지 확인한다."""
|
|
178
|
+
return all(n.status == "completed" for n in self.nodes.values())
|
|
179
|
+
|
|
180
|
+
def has_failures(self) -> bool:
|
|
181
|
+
"""실패한 태스크가 있는지 확인한다."""
|
|
182
|
+
return any(n.status == "failed" for n in self.nodes.values())
|
|
183
|
+
|
|
184
|
+
def to_dict(self) -> dict:
|
|
185
|
+
"""DAG를 직렬화한다."""
|
|
186
|
+
return {
|
|
187
|
+
"tasks": [node.to_dict() for node in self.nodes.values()],
|
|
188
|
+
"parallel_groups": self.get_parallel_groups(),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def from_dict(cls, data: dict) -> "TaskDAG":
|
|
193
|
+
"""직렬화된 DAG를 복원한다."""
|
|
194
|
+
dag = cls()
|
|
195
|
+
for td in data.get("tasks", []):
|
|
196
|
+
dag.add_task(TaskNode.from_dict(td))
|
|
197
|
+
return dag
|
|
198
|
+
|
|
199
|
+
def to_mermaid(self) -> str:
|
|
200
|
+
"""DAG를 Mermaid 다이어그램 문법으로 변환한다 (UI 시각화용)."""
|
|
201
|
+
lines = ["graph TD"]
|
|
202
|
+
status_style = {
|
|
203
|
+
"pending": ":::pending",
|
|
204
|
+
"running": ":::running",
|
|
205
|
+
"completed": ":::completed",
|
|
206
|
+
"failed": ":::failed",
|
|
207
|
+
}
|
|
208
|
+
for node in self.nodes.values():
|
|
209
|
+
label = f'{node.id}["{node.name}<br/>{node.worker_type}"]'
|
|
210
|
+
style = status_style.get(node.status, "")
|
|
211
|
+
lines.append(f" {label}{style}")
|
|
212
|
+
for node in self.nodes.values():
|
|
213
|
+
for dep in node.depends_on:
|
|
214
|
+
lines.append(f" {dep} --> {node.id}")
|
|
215
|
+
# 스타일 정의
|
|
216
|
+
lines.extend([
|
|
217
|
+
" classDef pending fill:#e2e8f0,stroke:#94a3b8",
|
|
218
|
+
" classDef running fill:#bfdbfe,stroke:#3b82f6",
|
|
219
|
+
" classDef completed fill:#bbf7d0,stroke:#22c55e",
|
|
220
|
+
" classDef failed fill:#fecaca,stroke:#ef4444",
|
|
221
|
+
])
|
|
222
|
+
return "\n".join(lines)
|
package/lib/jobs.sh
CHANGED
|
@@ -66,7 +66,7 @@ job_register() {
|
|
|
66
66
|
local meta_file="${LOGS_DIR}/job_${job_id}.meta"
|
|
67
67
|
# 프롬프트 내의 특수문자를 이스케이프하여 안전하게 저장
|
|
68
68
|
local safe_prompt
|
|
69
|
-
safe_prompt=$(printf '%s' "$prompt" | head -c 500 | sed "s/'/'\\\\''/g")
|
|
69
|
+
safe_prompt=$(printf '%s' "$prompt" | head -c 500 | tr -d '\000-\037\177' | sed "s/'/'\\\\''/g")
|
|
70
70
|
cat > "$meta_file" <<EOF
|
|
71
71
|
JOB_ID=${job_id}
|
|
72
72
|
STATUS=running
|
|
@@ -105,12 +105,23 @@ job_mark_done() {
|
|
|
105
105
|
local job_id="$1"
|
|
106
106
|
local meta_file="${LOGS_DIR}/job_${job_id}.meta"
|
|
107
107
|
_meta_set_field "$meta_file" "STATUS" "done"
|
|
108
|
+
_fire_webhook "$job_id" "done"
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
job_mark_failed() {
|
|
111
112
|
local job_id="$1"
|
|
112
113
|
local meta_file="${LOGS_DIR}/job_${job_id}.meta"
|
|
113
114
|
_meta_set_field "$meta_file" "STATUS" "failed"
|
|
115
|
+
_fire_webhook "$job_id" "failed"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# ── 웹훅 전달 (백그라운드) ─────────────────────────────────
|
|
119
|
+
_fire_webhook() {
|
|
120
|
+
local job_id="$1" status="$2"
|
|
121
|
+
local webhook_script="${CONTROLLER_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/web/webhook.py"
|
|
122
|
+
[[ -f "$webhook_script" ]] || return 0
|
|
123
|
+
# 백그라운드에서 실행 — 작업 완료 흐름을 차단하지 않는다
|
|
124
|
+
python3 "$webhook_script" "$job_id" "$status" &>/dev/null &
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
# ── 상태 조회 ──────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-controller",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Claude Code headless daemon controller — FIFO-based async task dispatch, Git Worktree isolation, auto-checkpointing, and a web dashboard",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "choiwon",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"claude-controller": "./bin/controller",
|
|
19
19
|
"claude-send": "./bin/send",
|
|
20
20
|
"claude-start": "./bin/start",
|
|
21
|
-
"claude-sh": "./bin/claude-sh"
|
|
21
|
+
"claude-sh": "./bin/claude-sh",
|
|
22
|
+
"claude-ctl": "./bin/ctl"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
24
25
|
"bin/controller",
|
|
@@ -26,13 +27,18 @@
|
|
|
26
27
|
"bin/start",
|
|
27
28
|
"bin/claude-sh",
|
|
28
29
|
"bin/app-launcher.sh",
|
|
30
|
+
"bin/ctl",
|
|
29
31
|
"bin/native-app.py",
|
|
32
|
+
"bin/autoloop.sh",
|
|
33
|
+
"bin/watchdog.sh",
|
|
30
34
|
"lib/",
|
|
31
35
|
"service/controller.sh",
|
|
36
|
+
"cognitive/",
|
|
37
|
+
"dag/",
|
|
32
38
|
"web/*.py",
|
|
33
|
-
"web/static
|
|
34
|
-
"web/static
|
|
35
|
-
"web/static
|
|
39
|
+
"web/static/*.html",
|
|
40
|
+
"web/static/*.js",
|
|
41
|
+
"web/static/*.css",
|
|
36
42
|
"config.sh",
|
|
37
43
|
"README.md",
|
|
38
44
|
"LICENSE",
|
package/postinstall.sh
CHANGED
|
@@ -66,7 +66,7 @@ SETTINGS_FILE="${CONTROLLER_DIR}/data/settings.json"
|
|
|
66
66
|
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|
67
67
|
cat > "$SETTINGS_FILE" << 'SETTINGS'
|
|
68
68
|
{
|
|
69
|
-
"skip_permissions":
|
|
69
|
+
"skip_permissions": false,
|
|
70
70
|
"allowed_tools": "Bash,Read,Write,Edit,Glob,Grep,Agent,NotebookEdit,WebFetch,WebSearch",
|
|
71
71
|
"model": "",
|
|
72
72
|
"max_jobs": 10,
|
package/service/controller.sh
CHANGED
|
@@ -86,6 +86,13 @@ _on_signal() {
|
|
|
86
86
|
|
|
87
87
|
trap _on_signal SIGTERM SIGINT SIGHUP
|
|
88
88
|
|
|
89
|
+
# ── meta 파일 인젝션 방지 ─────────────────────────────────────
|
|
90
|
+
# 개행·제어문자를 제거하여 KEY=VALUE 라인 인젝션을 방지한다.
|
|
91
|
+
# Python 측 _sanitize_meta_value()와 동일한 역할.
|
|
92
|
+
_sanitize_meta_val() {
|
|
93
|
+
printf '%s' "$1" | tr -d '\000-\037\177'
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
# ── 중복 디스패치 방지 ────────────────────────────────────────
|
|
90
97
|
# 최근 디스패치된 프롬프트 해시와 타임스탬프를 기록
|
|
91
98
|
_LAST_DISPATCH_HASH=""
|
|
@@ -161,16 +168,40 @@ dispatch_job() {
|
|
|
161
168
|
return 1
|
|
162
169
|
fi
|
|
163
170
|
|
|
164
|
-
#
|
|
165
|
-
local
|
|
166
|
-
|
|
171
|
+
# ── pending 작업 재사용 (DAG 의존성 디스패치) ──
|
|
172
|
+
local pending_jid
|
|
173
|
+
pending_jid=$(echo "$json_line" | jq -r '.pending_job_id // empty')
|
|
174
|
+
|
|
175
|
+
local job_id meta_file out_file
|
|
176
|
+
# 사용자 입력값 새니타이즈 (meta 파일 인젝션 방지)
|
|
177
|
+
job_uuid=$(_sanitize_meta_val "$job_uuid")
|
|
178
|
+
cwd=$(_sanitize_meta_val "$cwd")
|
|
179
|
+
|
|
180
|
+
if [[ -n "$pending_jid" ]]; then
|
|
181
|
+
local pending_meta="${LOGS_DIR}/job_${pending_jid}.meta"
|
|
182
|
+
if [[ -f "$pending_meta" ]]; then
|
|
183
|
+
job_id="$pending_jid"
|
|
184
|
+
meta_file="$pending_meta"
|
|
185
|
+
_meta_set_field "$meta_file" "STATUS" "running"
|
|
186
|
+
rm -f "${LOGS_DIR}/job_${pending_jid}.pending"
|
|
187
|
+
_log_info "Pending Job #${job_id} 디스패치 (DAG 의존성 충족)"
|
|
188
|
+
else
|
|
189
|
+
job_id=$(job_register "$prompt")
|
|
190
|
+
meta_file="${LOGS_DIR}/job_${job_id}.meta"
|
|
191
|
+
local _tmp="${meta_file}.tmp.$$"
|
|
192
|
+
{ cat "$meta_file"; echo "UUID=${job_uuid}"; } > "$_tmp" && mv -f "$_tmp" "$meta_file"
|
|
193
|
+
fi
|
|
194
|
+
else
|
|
195
|
+
# Job 등록
|
|
196
|
+
job_id=$(job_register "$prompt")
|
|
197
|
+
meta_file="${LOGS_DIR}/job_${job_id}.meta"
|
|
167
198
|
|
|
168
|
-
|
|
169
|
-
|
|
199
|
+
# .meta 파일에 UUID 기록 (원자적 append: temp → rename)
|
|
200
|
+
local _tmp="${meta_file}.tmp.$$"
|
|
201
|
+
{ cat "$meta_file"; echo "UUID=${job_uuid}"; } > "$_tmp" && mv -f "$_tmp" "$meta_file"
|
|
202
|
+
fi
|
|
170
203
|
|
|
171
|
-
|
|
172
|
-
local _tmp="${meta_file}.tmp.$$"
|
|
173
|
-
{ cat "$meta_file"; echo "UUID=${job_uuid}"; } > "$_tmp" && mv -f "$_tmp" "$meta_file"
|
|
204
|
+
out_file="${LOGS_DIR}/job_${job_id}.out"
|
|
174
205
|
|
|
175
206
|
# ── Worktree 결정: 재사용(rewind) > 새로 생성 ──
|
|
176
207
|
local wt_path=""
|
|
@@ -406,11 +437,12 @@ start_service() {
|
|
|
406
437
|
|
|
407
438
|
# FIFO 생성
|
|
408
439
|
if [[ -p "$FIFO_PATH" ]]; then
|
|
409
|
-
|
|
440
|
+
chmod 600 "$FIFO_PATH"
|
|
441
|
+
_log_warn "기존 FIFO 파이프 발견. 권한 확인 후 재사용합니다: ${FIFO_PATH}"
|
|
410
442
|
else
|
|
411
443
|
rm -f "$FIFO_PATH"
|
|
412
|
-
mkfifo "$FIFO_PATH"
|
|
413
|
-
_log_info "FIFO 파이프
|
|
444
|
+
mkfifo -m 600 "$FIFO_PATH"
|
|
445
|
+
_log_info "FIFO 파이프 생성됨 (mode 600): ${FIFO_PATH}"
|
|
414
446
|
fi
|
|
415
447
|
|
|
416
448
|
# PID 기록
|