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/bin/ctl
CHANGED
|
@@ -36,6 +36,11 @@ Usage:
|
|
|
36
36
|
ctl pipeline tick 모든 활성 파이프라인 진행
|
|
37
37
|
ctl pipeline reset --id <id> 상태 초기화
|
|
38
38
|
ctl pipeline delete --id <id> 파이프라인 삭제
|
|
39
|
+
ctl summary 전체 상태 요약 (AI 컨텍스트용)
|
|
40
|
+
ctl stats [--limit N] 작업 통계 집계
|
|
41
|
+
ctl health 헬스체크 + 디스크 + stuck 작업
|
|
42
|
+
ctl watchdog [start|stop|status|install|uninstall] 프로세스 워치독 관리
|
|
43
|
+
ctl job <id> text 최종 텍스트 결과만 추출
|
|
39
44
|
|
|
40
45
|
Output: 기본 JSON. --pretty 플래그로 포맷팅된 출력.
|
|
41
46
|
"""
|
|
@@ -55,7 +60,7 @@ sys.path.insert(0, str(CONTROLLER_DIR / "web"))
|
|
|
55
60
|
from config import (
|
|
56
61
|
LOGS_DIR, UPLOADS_DIR, DATA_DIR,
|
|
57
62
|
RECENT_DIRS_FILE, SETTINGS_FILE, SESSIONS_DIR,
|
|
58
|
-
CLAUDE_PROJECTS_DIR,
|
|
63
|
+
CLAUDE_PROJECTS_DIR, FIFO_PATH,
|
|
59
64
|
)
|
|
60
65
|
from jobs import get_all_jobs, get_job_result, send_to_fifo, start_controller_service, stop_controller_service
|
|
61
66
|
from checkpoint import get_job_checkpoints, rewind_job
|
|
@@ -65,6 +70,7 @@ from pipeline import (
|
|
|
65
70
|
list_pipelines, get_pipeline_status, create_pipeline, delete_pipeline,
|
|
66
71
|
run_next, force_run, reset_phase, tick, tick_all,
|
|
67
72
|
)
|
|
73
|
+
import shutil
|
|
68
74
|
|
|
69
75
|
|
|
70
76
|
# ══════════════════════════════════════════════════════════════
|
|
@@ -245,6 +251,11 @@ def cmd_job(args):
|
|
|
245
251
|
err(error)
|
|
246
252
|
out(result, args.pretty)
|
|
247
253
|
|
|
254
|
+
# ── text (최종 텍스트만) ──
|
|
255
|
+
elif args.action == "text":
|
|
256
|
+
cmd_job_text(job_id, args.pretty, raw=getattr(args, "raw", False))
|
|
257
|
+
return
|
|
258
|
+
|
|
248
259
|
# ── 기본: 결과 조회 ──
|
|
249
260
|
else:
|
|
250
261
|
result, error = get_job_result(job_id)
|
|
@@ -349,7 +360,7 @@ def cmd_sessions(args):
|
|
|
349
360
|
def cmd_config(args):
|
|
350
361
|
"""설정 조회 또는 변경."""
|
|
351
362
|
defaults = {
|
|
352
|
-
"skip_permissions":
|
|
363
|
+
"skip_permissions": False,
|
|
353
364
|
"allowed_tools": "Bash,Read,Write,Edit,Glob,Grep,Agent,NotebookEdit,WebFetch,WebSearch",
|
|
354
365
|
"model": "",
|
|
355
366
|
"max_jobs": 10,
|
|
@@ -734,6 +745,296 @@ def cmd_pipeline(args):
|
|
|
734
745
|
err(error)
|
|
735
746
|
out({"deleted": True, "pipeline": result}, args.pretty)
|
|
736
747
|
|
|
748
|
+
elif action == "evolution":
|
|
749
|
+
from pipeline import get_evolution_summary
|
|
750
|
+
summary = get_evolution_summary()
|
|
751
|
+
if args.pretty:
|
|
752
|
+
print(f"파이프라인 진화 요약")
|
|
753
|
+
print(f" 활성: {summary['active_count']}/{summary['total_pipelines']}")
|
|
754
|
+
print(f" 총 실행: {summary['total_runs']}회 | 총 비용: ${summary['total_cost_usd']}")
|
|
755
|
+
print(f" 효율성: {summary['efficiency_pct']}% (변경 있는 실행 비율)")
|
|
756
|
+
cls = summary["classifications"]
|
|
757
|
+
print(f" 분류: 변경있음={cls.get('has_change',0)} / 변경없음={cls.get('no_change',0)} / 불명={cls.get('unknown',0)}")
|
|
758
|
+
if summary["interval_adaptations"]:
|
|
759
|
+
print(f" 인터벌 적응:")
|
|
760
|
+
for ia in summary["interval_adaptations"]:
|
|
761
|
+
print(f" {ia['name']}: {ia['base_sec']}s → {ia['effective_sec']}s ({ia['change_pct']:+d}%)")
|
|
762
|
+
else:
|
|
763
|
+
out(summary, False)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
# ══════════════════════════════════════════════════════════════
|
|
767
|
+
# AI 컨텍스트 명령어 — summary, stats, health, job text
|
|
768
|
+
# ══════════════════════════════════════════════════════════════
|
|
769
|
+
|
|
770
|
+
def cmd_summary(args):
|
|
771
|
+
"""서비스 + 최근 작업 + 파이프라인을 한 번에 요약."""
|
|
772
|
+
running, pid = is_service_running()
|
|
773
|
+
|
|
774
|
+
jobs = get_all_jobs()
|
|
775
|
+
total = len(jobs)
|
|
776
|
+
by_status = {}
|
|
777
|
+
recent_failures = []
|
|
778
|
+
total_cost = 0.0
|
|
779
|
+
running_jobs = []
|
|
780
|
+
|
|
781
|
+
for j in jobs:
|
|
782
|
+
st = j.get("status", "unknown")
|
|
783
|
+
by_status[st] = by_status.get(st, 0) + 1
|
|
784
|
+
if j.get("cost_usd"):
|
|
785
|
+
total_cost += j["cost_usd"]
|
|
786
|
+
if st == "running":
|
|
787
|
+
running_jobs.append({
|
|
788
|
+
"job_id": j["job_id"],
|
|
789
|
+
"prompt": (j.get("prompt") or "")[:80],
|
|
790
|
+
"cwd": j.get("cwd"),
|
|
791
|
+
})
|
|
792
|
+
if st == "failed":
|
|
793
|
+
recent_failures.append({
|
|
794
|
+
"job_id": j["job_id"],
|
|
795
|
+
"prompt": (j.get("prompt") or "")[:80],
|
|
796
|
+
"result": (j.get("result") or "")[:200],
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
done = by_status.get("done", 0)
|
|
800
|
+
failed = by_status.get("failed", 0)
|
|
801
|
+
success_rate = round(done / (done + failed), 3) if (done + failed) > 0 else None
|
|
802
|
+
|
|
803
|
+
# 파이프라인 요약
|
|
804
|
+
pipes = list_pipelines()
|
|
805
|
+
active_pipes = [
|
|
806
|
+
{"id": p["id"], "name": p.get("name", ""), "status": p.get("status", ""), "command": (p.get("command") or "")[:60]}
|
|
807
|
+
for p in pipes if p.get("status") in ("running", "idle")
|
|
808
|
+
]
|
|
809
|
+
|
|
810
|
+
result = {
|
|
811
|
+
"service": {"running": running, "pid": pid},
|
|
812
|
+
"jobs": {
|
|
813
|
+
"total": total,
|
|
814
|
+
"by_status": by_status,
|
|
815
|
+
"success_rate": success_rate,
|
|
816
|
+
"total_cost_usd": round(total_cost, 4),
|
|
817
|
+
"running": running_jobs,
|
|
818
|
+
"recent_failures": recent_failures[:5],
|
|
819
|
+
},
|
|
820
|
+
"pipelines": {"total": len(pipes), "active": active_pipes},
|
|
821
|
+
}
|
|
822
|
+
out(result, args.pretty)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def cmd_stats(args):
|
|
826
|
+
"""작업 통계 집계 — 성공률, 비용, 소요시간, 에러 패턴."""
|
|
827
|
+
jobs = get_all_jobs()
|
|
828
|
+
|
|
829
|
+
# 기간 필터
|
|
830
|
+
limit = args.limit or len(jobs)
|
|
831
|
+
jobs = jobs[:limit]
|
|
832
|
+
|
|
833
|
+
total = len(jobs)
|
|
834
|
+
done_count = 0
|
|
835
|
+
failed_count = 0
|
|
836
|
+
costs = []
|
|
837
|
+
durations = []
|
|
838
|
+
error_patterns = {}
|
|
839
|
+
|
|
840
|
+
for j in jobs:
|
|
841
|
+
st = j.get("status", "unknown")
|
|
842
|
+
if st == "done":
|
|
843
|
+
done_count += 1
|
|
844
|
+
elif st == "failed":
|
|
845
|
+
failed_count += 1
|
|
846
|
+
# 에러 패턴 수집
|
|
847
|
+
result_text = (j.get("result") or "")[:300]
|
|
848
|
+
if result_text:
|
|
849
|
+
# 첫 줄을 패턴 키로 사용
|
|
850
|
+
key = result_text.split("\n")[0][:80]
|
|
851
|
+
error_patterns[key] = error_patterns.get(key, 0) + 1
|
|
852
|
+
|
|
853
|
+
if j.get("cost_usd"):
|
|
854
|
+
costs.append(j["cost_usd"])
|
|
855
|
+
if j.get("duration_ms"):
|
|
856
|
+
durations.append(j["duration_ms"])
|
|
857
|
+
|
|
858
|
+
completed = done_count + failed_count
|
|
859
|
+
result = {
|
|
860
|
+
"total_jobs": total,
|
|
861
|
+
"completed": completed,
|
|
862
|
+
"done": done_count,
|
|
863
|
+
"failed": failed_count,
|
|
864
|
+
"success_rate": round(done_count / completed, 3) if completed > 0 else None,
|
|
865
|
+
"cost": {
|
|
866
|
+
"total_usd": round(sum(costs), 4) if costs else 0,
|
|
867
|
+
"avg_usd": round(sum(costs) / len(costs), 4) if costs else None,
|
|
868
|
+
"max_usd": round(max(costs), 4) if costs else None,
|
|
869
|
+
},
|
|
870
|
+
"duration": {
|
|
871
|
+
"avg_ms": round(sum(durations) / len(durations)) if durations else None,
|
|
872
|
+
"median_ms": round(sorted(durations)[len(durations) // 2]) if durations else None,
|
|
873
|
+
"max_ms": max(durations) if durations else None,
|
|
874
|
+
},
|
|
875
|
+
"top_errors": dict(sorted(error_patterns.items(), key=lambda x: -x[1])[:5]),
|
|
876
|
+
}
|
|
877
|
+
out(result, args.pretty)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def cmd_health(args):
|
|
881
|
+
"""서비스 상태 + 디스크 사용량 + stuck 작업 + 고아 프로세스."""
|
|
882
|
+
running, pid = is_service_running()
|
|
883
|
+
|
|
884
|
+
# 디스크 사용량
|
|
885
|
+
def dir_size_mb(path):
|
|
886
|
+
total = 0
|
|
887
|
+
p = Path(path)
|
|
888
|
+
if not p.exists():
|
|
889
|
+
return 0
|
|
890
|
+
for f in p.rglob("*"):
|
|
891
|
+
if f.is_file():
|
|
892
|
+
total += f.stat().st_size
|
|
893
|
+
return round(total / (1024 * 1024), 2)
|
|
894
|
+
|
|
895
|
+
disk = {
|
|
896
|
+
"logs_mb": dir_size_mb(LOGS_DIR),
|
|
897
|
+
"uploads_mb": dir_size_mb(UPLOADS_DIR),
|
|
898
|
+
"worktrees_mb": dir_size_mb(CONTROLLER_DIR / "worktrees"),
|
|
899
|
+
}
|
|
900
|
+
total_disk = CONTROLLER_DIR.resolve()
|
|
901
|
+
usage = shutil.disk_usage(str(total_disk))
|
|
902
|
+
disk["system_free_gb"] = round(usage.free / (1024 ** 3), 2)
|
|
903
|
+
|
|
904
|
+
# Stuck 작업 (30분 이상 running)
|
|
905
|
+
jobs = get_all_jobs()
|
|
906
|
+
stuck = []
|
|
907
|
+
import datetime
|
|
908
|
+
now = datetime.datetime.now()
|
|
909
|
+
for j in jobs:
|
|
910
|
+
if j.get("status") != "running":
|
|
911
|
+
continue
|
|
912
|
+
created = j.get("created_at", "")
|
|
913
|
+
if not created:
|
|
914
|
+
continue
|
|
915
|
+
try:
|
|
916
|
+
started = datetime.datetime.strptime(created, "%Y-%m-%d %H:%M:%S")
|
|
917
|
+
elapsed_min = (now - started).total_seconds() / 60
|
|
918
|
+
if elapsed_min > 30:
|
|
919
|
+
stuck.append({
|
|
920
|
+
"job_id": j["job_id"],
|
|
921
|
+
"prompt": (j.get("prompt") or "")[:60],
|
|
922
|
+
"elapsed_min": round(elapsed_min, 1),
|
|
923
|
+
})
|
|
924
|
+
except ValueError:
|
|
925
|
+
continue
|
|
926
|
+
|
|
927
|
+
# FIFO 상태
|
|
928
|
+
fifo_ok = FIFO_PATH.exists() and Path(FIFO_PATH).is_fifo() if hasattr(Path, 'is_fifo') else FIFO_PATH.exists()
|
|
929
|
+
|
|
930
|
+
result = {
|
|
931
|
+
"service": {"running": running, "pid": pid, "fifo_ok": fifo_ok},
|
|
932
|
+
"disk": disk,
|
|
933
|
+
"stuck_jobs": stuck,
|
|
934
|
+
"job_counts": {
|
|
935
|
+
"total": len(jobs),
|
|
936
|
+
"running": sum(1 for j in jobs if j.get("status") == "running"),
|
|
937
|
+
},
|
|
938
|
+
}
|
|
939
|
+
out(result, args.pretty)
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def cmd_watchdog(args):
|
|
943
|
+
"""워치독 관리 — start/stop/status/install/uninstall."""
|
|
944
|
+
action = args.action or "status"
|
|
945
|
+
watchdog_sh = CONTROLLER_DIR / "bin" / "watchdog.sh"
|
|
946
|
+
state_file = CONTROLLER_DIR / "data" / "watchdog_state.json"
|
|
947
|
+
pid_file = CONTROLLER_DIR / "service" / "watchdog.pid"
|
|
948
|
+
|
|
949
|
+
if action in ("start", "stop", "install", "uninstall"):
|
|
950
|
+
import subprocess
|
|
951
|
+
result = subprocess.run(
|
|
952
|
+
["bash", str(watchdog_sh), action],
|
|
953
|
+
capture_output=True, text=True
|
|
954
|
+
)
|
|
955
|
+
if result.stdout.strip():
|
|
956
|
+
if args.pretty:
|
|
957
|
+
out({"action": action, "message": result.stdout.strip()}, True)
|
|
958
|
+
else:
|
|
959
|
+
print(result.stdout.strip())
|
|
960
|
+
if result.returncode != 0 and result.stderr.strip():
|
|
961
|
+
print(result.stderr.strip(), file=sys.stderr)
|
|
962
|
+
sys.exit(result.returncode)
|
|
963
|
+
else:
|
|
964
|
+
# status — JSON 출력
|
|
965
|
+
running = False
|
|
966
|
+
pid = None
|
|
967
|
+
state = {}
|
|
968
|
+
|
|
969
|
+
if pid_file.exists():
|
|
970
|
+
try:
|
|
971
|
+
pid = int(pid_file.read_text().strip())
|
|
972
|
+
import signal
|
|
973
|
+
os.kill(pid, 0)
|
|
974
|
+
running = True
|
|
975
|
+
except (ValueError, OSError):
|
|
976
|
+
pid = None
|
|
977
|
+
|
|
978
|
+
if state_file.exists():
|
|
979
|
+
try:
|
|
980
|
+
state = json.loads(state_file.read_text())
|
|
981
|
+
except (json.JSONDecodeError, OSError):
|
|
982
|
+
pass
|
|
983
|
+
|
|
984
|
+
out({
|
|
985
|
+
"running": running,
|
|
986
|
+
"pid": pid,
|
|
987
|
+
"status": state.get("status", "unknown"),
|
|
988
|
+
"restart_count": state.get("restart_count", 0),
|
|
989
|
+
"consecutive_fails": state.get("consecutive_fails", 0),
|
|
990
|
+
"last_restart": state.get("last_restart", ""),
|
|
991
|
+
"last_check": state.get("last_check", ""),
|
|
992
|
+
"message": state.get("message", ""),
|
|
993
|
+
}, args.pretty)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def cmd_job_text(job_id, pretty=False, raw=False):
|
|
997
|
+
"""작업의 최종 텍스트 결과만 추출 (이벤트 스트림 없이)."""
|
|
998
|
+
result, error = get_job_result(job_id)
|
|
999
|
+
if error:
|
|
1000
|
+
err(error)
|
|
1001
|
+
|
|
1002
|
+
text = result.get("result") if result else None
|
|
1003
|
+
if text is None:
|
|
1004
|
+
# result 없으면 .out 에서 text 이벤트를 합산
|
|
1005
|
+
out_file = LOGS_DIR / f"job_{job_id}.out"
|
|
1006
|
+
if out_file.exists():
|
|
1007
|
+
parts = []
|
|
1008
|
+
try:
|
|
1009
|
+
with open(out_file, "r") as f:
|
|
1010
|
+
for raw_line in f:
|
|
1011
|
+
if '"type":"assistant"' not in raw_line:
|
|
1012
|
+
continue
|
|
1013
|
+
try:
|
|
1014
|
+
evt = json.loads(raw_line)
|
|
1015
|
+
if evt.get("type") == "assistant":
|
|
1016
|
+
content = evt.get("message", {}).get("content", [])
|
|
1017
|
+
for c in content:
|
|
1018
|
+
if c.get("type") == "text" and c.get("text"):
|
|
1019
|
+
parts.append(c["text"])
|
|
1020
|
+
except json.JSONDecodeError:
|
|
1021
|
+
continue
|
|
1022
|
+
except OSError:
|
|
1023
|
+
pass
|
|
1024
|
+
text = "\n".join(parts) if parts else None
|
|
1025
|
+
|
|
1026
|
+
if raw:
|
|
1027
|
+
# --raw: JSON 래핑 없이 텍스트만 stdout에 출력
|
|
1028
|
+
print(text or "")
|
|
1029
|
+
return
|
|
1030
|
+
|
|
1031
|
+
out({
|
|
1032
|
+
"job_id": job_id,
|
|
1033
|
+
"status": result.get("status") if result else "unknown",
|
|
1034
|
+
"text": text,
|
|
1035
|
+
"cost_usd": result.get("cost_usd") if result else None,
|
|
1036
|
+
"session_id": result.get("session_id") if result else None,
|
|
1037
|
+
}, pretty)
|
|
737
1038
|
|
|
738
1039
|
|
|
739
1040
|
# ══════════════════════════════════════════════════════════════
|
|
@@ -771,11 +1072,12 @@ def build_parser():
|
|
|
771
1072
|
p_job = sub.add_parser("job", parents=[common], help="단일 작업 조작")
|
|
772
1073
|
p_job.add_argument("job_id", help="작업 ID")
|
|
773
1074
|
p_job.add_argument("action", nargs="?",
|
|
774
|
-
choices=["stream", "checkpoints", "rewind", "delete"],
|
|
775
|
-
help="수행할 작업 (생략 시 결과
|
|
1075
|
+
choices=["stream", "checkpoints", "rewind", "delete", "text"],
|
|
1076
|
+
help="수행할 작업 (생략 시 결과 조회, text: 최종 텍스트만)")
|
|
776
1077
|
p_job.add_argument("--offset", type=int, default=0, help="스트림 오프셋 (stream용)")
|
|
777
1078
|
p_job.add_argument("--checkpoint", help="체크포인트 해시 (rewind용)")
|
|
778
1079
|
p_job.add_argument("--prompt", dest="prompt", help="리와인드 프롬프트 (rewind용)")
|
|
1080
|
+
p_job.add_argument("--raw", action="store_true", help="텍스트만 출력 (text 액션용, JSON 래핑 없이)")
|
|
779
1081
|
|
|
780
1082
|
# ── sessions ──
|
|
781
1083
|
p_sessions = sub.add_parser("sessions", parents=[common], help="세션 목록 조회")
|
|
@@ -815,10 +1117,26 @@ def build_parser():
|
|
|
815
1117
|
p_proj.add_argument("--desc", help="프로젝트 설명")
|
|
816
1118
|
p_proj.add_argument("--no-git", action="store_true", help="git init 건너뛰기 (create용)")
|
|
817
1119
|
|
|
1120
|
+
# ── summary (AI 컨텍스트) ──
|
|
1121
|
+
sub.add_parser("summary", parents=[common], help="서비스 + 작업 + 파이프라인 전체 요약 (AI용)")
|
|
1122
|
+
|
|
1123
|
+
# ── stats ──
|
|
1124
|
+
p_stats = sub.add_parser("stats", parents=[common], help="작업 통계 집계")
|
|
1125
|
+
p_stats.add_argument("--limit", "-n", type=int, help="최근 N개 작업만 집계")
|
|
1126
|
+
|
|
1127
|
+
# ── health ──
|
|
1128
|
+
sub.add_parser("health", parents=[common], help="서비스 헬스체크 + 디스크 + stuck 작업")
|
|
1129
|
+
|
|
1130
|
+
# ── watchdog ──
|
|
1131
|
+
p_wd = sub.add_parser("watchdog", parents=[common], help="프로세스 워치독 관리")
|
|
1132
|
+
p_wd.add_argument("action", nargs="?",
|
|
1133
|
+
choices=["start", "stop", "status", "install", "uninstall"],
|
|
1134
|
+
help="수행할 작업 (생략 시 status)")
|
|
1135
|
+
|
|
818
1136
|
# ── pipeline ──
|
|
819
1137
|
p_pipe = sub.add_parser("pipeline", parents=[common], help="파이프라인 관리/실행")
|
|
820
1138
|
p_pipe.add_argument("action", nargs="?",
|
|
821
|
-
choices=["list", "create", "status", "run", "tick", "tick-all", "reset", "delete"],
|
|
1139
|
+
choices=["list", "create", "status", "run", "tick", "tick-all", "reset", "delete", "evolution"],
|
|
822
1140
|
help="수행할 작업 (생략 시 목록)")
|
|
823
1141
|
p_pipe.add_argument("path", nargs="?", help="프로젝트 경로 (하위 호환)")
|
|
824
1142
|
p_pipe.add_argument("--project", "-p", help="프로젝트 이름 또는 ID (create용)")
|
|
@@ -849,6 +1167,10 @@ DISPATCH = {
|
|
|
849
1167
|
"upload": cmd_upload,
|
|
850
1168
|
"project": cmd_project,
|
|
851
1169
|
"pipeline": cmd_pipeline,
|
|
1170
|
+
"summary": cmd_summary,
|
|
1171
|
+
"stats": cmd_stats,
|
|
1172
|
+
"health": cmd_health,
|
|
1173
|
+
"watchdog": cmd_watchdog,
|
|
852
1174
|
}
|
|
853
1175
|
|
|
854
1176
|
|
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)
|
|
@@ -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()
|