"""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