90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
"""Optional live runner for the installed technical-doc-flow Claude plugin."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from helpers import ROOT
|
|
|
|
|
|
CLAUDE_BIN = shutil.which("claude")
|
|
|
|
|
|
class LiveSkillError(RuntimeError):
|
|
"""The external skill run could not produce one verified run."""
|
|
|
|
|
|
def run_live_case(case_dir: Path, *, timeout: int = 300) -> tuple[Path, dict[str, Any]]:
|
|
if CLAUDE_BIN is None:
|
|
raise LiveSkillError("claude CLI를 찾을 수 없습니다.")
|
|
case_dir.mkdir(parents=True, exist_ok=True)
|
|
brief = case_dir / "brief.md"
|
|
draft = case_dir / "draft.md"
|
|
workspace = case_dir / "_workspace"
|
|
brief.write_text(
|
|
"이 초안을 HTTP 요청과 응답만 아는 주니어 백엔드 개발자가 따라갈 수 있게 수정하세요. "
|
|
"논리의 원인→요구→선택→검증→한계 흐름을 만들고, 전문용어는 쉬운 설명 뒤에 소개하세요. "
|
|
"light revise 경로를 사용하세요.\n",
|
|
encoding="utf-8",
|
|
)
|
|
draft.write_text(
|
|
"# 캐시 복구\n\n"
|
|
"42ms TTL 만료 뒤 cache stampede가 생기므로 `cacheKey`에 distributed mutex, "
|
|
"request coalescing, jitter를 적용한다. 자세한 배경은 https://example.test/cache 이다.\n\n"
|
|
"## 검증\n\nload test가 correctness와 resilience를 증명한다.\n",
|
|
encoding="utf-8",
|
|
)
|
|
prompt = (
|
|
"이 세션에 로드된 technical-doc-flow 스킬을 반드시 활성화해 전체 절차와 결정적 스크립트를 실행하세요. "
|
|
f"brief={brief}, draft={draft}, workspace={workspace}. "
|
|
"문서 종류는 explanation이고, 사용자가 light revise를 명시했습니다. "
|
|
"09_final_report.json verdict가 pass가 아니면 성공이라고 말하지 마세요."
|
|
)
|
|
command = [
|
|
CLAUDE_BIN,
|
|
"--print",
|
|
"--no-session-persistence",
|
|
"--plugin-dir",
|
|
str(ROOT),
|
|
"--permission-mode",
|
|
"acceptEdits",
|
|
"--allowedTools",
|
|
"Read,Write,Edit,Bash,Task",
|
|
prompt,
|
|
]
|
|
environment = os.environ.copy()
|
|
try:
|
|
completed = subprocess.run(
|
|
command,
|
|
cwd=case_dir,
|
|
stdin=subprocess.DEVNULL,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
env=environment,
|
|
check=False,
|
|
)
|
|
except subprocess.TimeoutExpired as exc:
|
|
raise LiveSkillError(f"live 스킬 실행이 {timeout}초를 넘었습니다.") from exc
|
|
if completed.returncode != 0:
|
|
diagnostic = (completed.stderr or completed.stdout)[-1000:]
|
|
raise LiveSkillError(f"claude CLI exit={completed.returncode}: {diagnostic}")
|
|
|
|
reports = sorted(workspace.glob("*/09_final_report.json"))
|
|
if len(reports) != 1:
|
|
raise LiveSkillError(
|
|
f"09_final_report.json이 정확히 하나여야 합니다: {[str(path) for path in reports]}"
|
|
)
|
|
try:
|
|
report = json.loads(reports[0].read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
raise LiveSkillError(f"final report를 읽을 수 없습니다: {exc}") from exc
|
|
if not isinstance(report, dict):
|
|
raise LiveSkillError("final report 최상위 값이 객체가 아닙니다.")
|
|
return reports[0].parent, report
|