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.
- package/README.md +2 -2
- package/bin/autoloop.sh +382 -0
- package/bin/ctl +327 -5
- package/bin/native-app.py +5 -2
- 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 +5 -1
- 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 +464 -26
- package/web/handler_fs.py +15 -14
- package/web/handler_goals.py +203 -0
- package/web/handler_jobs.py +165 -42
- package/web/handler_memory.py +203 -0
- package/web/jobs.py +576 -12
- package/web/personas.py +419 -0
- package/web/pipeline.py +682 -50
- package/web/presets.py +506 -0
- package/web/projects.py +58 -4
- package/web/static/api.js +90 -3
- package/web/static/app.js +8 -0
- package/web/static/base.css +51 -12
- package/web/static/context.js +14 -4
- package/web/static/form.css +3 -2
- package/web/static/goals.css +363 -0
- package/web/static/goals.js +300 -0
- package/web/static/i18n.js +288 -0
- package/web/static/index.html +142 -6
- package/web/static/jobs.css +951 -4
- package/web/static/jobs.js +890 -54
- package/web/static/memoryview.js +117 -0
- package/web/static/personas.js +228 -0
- package/web/static/pipeline.css +308 -1
- package/web/static/pipelines.js +249 -14
- package/web/static/presets.js +244 -0
- package/web/static/send.js +26 -4
- package/web/static/settings-style.css +34 -3
- package/web/static/settings.js +37 -1
- package/web/static/stream.js +242 -19
- package/web/static/utils.js +54 -2
- package/web/webhook.py +210 -0
package/web/checkpoint.py
CHANGED
|
@@ -13,6 +13,14 @@ from config import LOGS_DIR
|
|
|
13
13
|
from utils import parse_meta_file
|
|
14
14
|
from jobs import send_to_fifo
|
|
15
15
|
|
|
16
|
+
_GIT_HASH_RE = re.compile(r'^[0-9a-fA-F]{4,40}$')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _validate_git_hash(value):
|
|
20
|
+
"""git commit hash 형식을 검증한다. 유효하지 않으면 ValueError를 발생시킨다."""
|
|
21
|
+
if not value or not _GIT_HASH_RE.match(value):
|
|
22
|
+
raise ValueError(f"유효하지 않은 git hash입니다: {value!r}")
|
|
23
|
+
|
|
16
24
|
|
|
17
25
|
def get_job_checkpoints(job_id):
|
|
18
26
|
"""worktree의 git log에서 해당 job의 checkpoint 커밋 목록을 반환한다."""
|
|
@@ -108,8 +116,80 @@ def extract_conversation_context(out_file, max_chars=4000):
|
|
|
108
116
|
return full[:max_chars]
|
|
109
117
|
|
|
110
118
|
|
|
119
|
+
def diff_checkpoints(job_id, from_hash, to_hash=None):
|
|
120
|
+
"""두 체크포인트 간 diff를 반환한다. to_hash 생략 시 from_hash의 부모와 비교."""
|
|
121
|
+
try:
|
|
122
|
+
_validate_git_hash(from_hash)
|
|
123
|
+
if to_hash:
|
|
124
|
+
_validate_git_hash(to_hash)
|
|
125
|
+
except ValueError as e:
|
|
126
|
+
return None, str(e)
|
|
127
|
+
|
|
128
|
+
meta_file = LOGS_DIR / f"job_{job_id}.meta"
|
|
129
|
+
if not meta_file.exists():
|
|
130
|
+
return None, "작업을 찾을 수 없습니다"
|
|
131
|
+
|
|
132
|
+
meta = parse_meta_file(meta_file)
|
|
133
|
+
wt_path = meta.get("WORKTREE", "")
|
|
134
|
+
if not wt_path or not os.path.isdir(wt_path):
|
|
135
|
+
return None, "워크트리를 찾을 수 없습니다"
|
|
136
|
+
|
|
137
|
+
# to_hash가 없으면 from_hash 단독 커밋의 변경사항 (부모 대비)
|
|
138
|
+
if not to_hash:
|
|
139
|
+
diff_spec = [f"{from_hash}^", from_hash]
|
|
140
|
+
else:
|
|
141
|
+
diff_spec = [from_hash, to_hash]
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
result = subprocess.run(
|
|
145
|
+
["git", "diff", "--no-color", "-U3"] + diff_spec,
|
|
146
|
+
cwd=wt_path, capture_output=True, text=True, timeout=15
|
|
147
|
+
)
|
|
148
|
+
raw_diff = result.stdout
|
|
149
|
+
|
|
150
|
+
# 파일별로 파싱
|
|
151
|
+
files = []
|
|
152
|
+
current = None
|
|
153
|
+
for line in raw_diff.split("\n"):
|
|
154
|
+
if line.startswith("diff --git"):
|
|
155
|
+
if current:
|
|
156
|
+
files.append(current)
|
|
157
|
+
# "diff --git a/path b/path" → path 추출
|
|
158
|
+
parts = line.split(" b/", 1)
|
|
159
|
+
fname = parts[1] if len(parts) > 1 else "unknown"
|
|
160
|
+
current = {"file": fname, "chunks": [], "additions": 0, "deletions": 0}
|
|
161
|
+
elif current is not None:
|
|
162
|
+
current["chunks"].append(line)
|
|
163
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
164
|
+
current["additions"] += 1
|
|
165
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
166
|
+
current["deletions"] += 1
|
|
167
|
+
|
|
168
|
+
if current:
|
|
169
|
+
files.append(current)
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"from": diff_spec[0],
|
|
173
|
+
"to": diff_spec[1],
|
|
174
|
+
"files": files,
|
|
175
|
+
"total_files": len(files),
|
|
176
|
+
"total_additions": sum(f["additions"] for f in files),
|
|
177
|
+
"total_deletions": sum(f["deletions"] for f in files),
|
|
178
|
+
}, None
|
|
179
|
+
|
|
180
|
+
except subprocess.TimeoutExpired:
|
|
181
|
+
return None, "diff 생성 시간 초과"
|
|
182
|
+
except OSError as e:
|
|
183
|
+
return None, f"diff 실패: {e}"
|
|
184
|
+
|
|
185
|
+
|
|
111
186
|
def rewind_job(job_id, checkpoint_hash, new_prompt):
|
|
112
187
|
"""job을 특정 checkpoint로 되돌리고 새 job을 디스패치한다."""
|
|
188
|
+
try:
|
|
189
|
+
_validate_git_hash(checkpoint_hash)
|
|
190
|
+
except ValueError as e:
|
|
191
|
+
return None, str(e)
|
|
192
|
+
|
|
113
193
|
meta_file = LOGS_DIR / f"job_{job_id}.meta"
|
|
114
194
|
out_file = LOGS_DIR / f"job_{job_id}.out"
|
|
115
195
|
|
package/web/config.py
CHANGED
|
@@ -21,7 +21,6 @@ RECENT_DIRS_FILE = DATA_DIR / "recent_dirs.json"
|
|
|
21
21
|
SETTINGS_FILE = DATA_DIR / "settings.json"
|
|
22
22
|
SERVICE_SCRIPT = CONTROLLER_DIR / "service" / "controller.sh"
|
|
23
23
|
SESSIONS_DIR = CONTROLLER_DIR / "sessions"
|
|
24
|
-
QUEUE_DIR = CONTROLLER_DIR / "queue"
|
|
25
24
|
CLAUDE_PROJECTS_DIR = Path.home() / ".claude" / "projects"
|
|
26
25
|
|
|
27
26
|
PORT = int(os.environ.get("PORT", 8420))
|
|
@@ -33,8 +32,6 @@ PORT = int(os.environ.get("PORT", 8420))
|
|
|
33
32
|
# 허용된 Origin 목록 (CORS)
|
|
34
33
|
# 환경변수 ALLOWED_ORIGINS로 오버라이드 가능 (쉼표 구분)
|
|
35
34
|
_DEFAULT_ORIGINS = [
|
|
36
|
-
"http://claude.won-space.com",
|
|
37
|
-
"https://claude.won-space.com",
|
|
38
35
|
"http://localhost:8420",
|
|
39
36
|
"https://localhost:8420",
|
|
40
37
|
]
|
|
@@ -54,11 +51,11 @@ AUTH_REQUIRED = os.environ.get("AUTH_REQUIRED", "false").lower() == "true"
|
|
|
54
51
|
|
|
55
52
|
# 인증 면제 경로 (AUTH_REQUIRED=true일 때만 적용)
|
|
56
53
|
AUTH_EXEMPT_PREFIXES = ("/static/", "/uploads/", "/api/auth/")
|
|
57
|
-
AUTH_EXEMPT_PATHS = {"/", "/index.html", "/styles.css", "/app.js"}
|
|
54
|
+
AUTH_EXEMPT_PATHS = {"/", "/index.html", "/styles.css", "/app.js", "/api/health"}
|
|
58
55
|
|
|
59
56
|
# 앱 실행 시 브라우저에서 열 공개 URL
|
|
60
57
|
# 환경변수 PUBLIC_URL로 오버라이드 가능
|
|
61
|
-
PUBLIC_URL = os.environ.get("PUBLIC_URL", "https://
|
|
58
|
+
PUBLIC_URL = os.environ.get("PUBLIC_URL", "https://localhost:8420")
|
|
62
59
|
|
|
63
60
|
# SSL 인증서 경로 (mkcert 생성 파일)
|
|
64
61
|
SSL_CERT = os.environ.get("SSL_CERT", str(CONTROLLER_DIR / "certs" / "localhost+1.pem"))
|