"""Phase 0 headless probe — 실제 claude -p 로 stage 를 헤드리스 실행할 수 있는지, process 를 넘겨도 원장+artifact 만으로 재개되는지 검증한다. slash 직접 실행이 안 되면 adapter prompt 로 전환. 실제 실행은 CLI 의 `probe --execute` 가 담당(예산·claude CLI 필요). 여기 함수는 순수 로직.""" import os import subprocess CLAUDE_CMD = os.environ.get("ORGOS_BENCH_CLAUDE", "claude") def build_stage_invocation(command_name, command_body, brief_path): """stage(command)를 headless 로 실행할 사양을 만든다. command_body 가 순수 slash 지시(첫 줄이 `# /`)면 direct-slash 로 `/` 프롬프트를, 아니면 command 본문을 펼친 adapter 프롬프트를 쓴다.""" first = (command_body.strip().splitlines() or [""])[0].strip() if first.startswith(f"# /{command_name}") or first == f"/{command_name}": mode = "direct-slash" prompt = f"/{command_name}\nbrief: {brief_path}" else: mode = "adapter" prompt = (f"다음 커맨드 절차를 이 brief 로 수행하라.\nbrief: {brief_path}\n\n" f"--- command: {command_name} ---\n{command_body}") argv = [CLAUDE_CMD, "-p", prompt, "--dangerously-skip-permissions"] return {"mode": mode, "prompt": prompt, "argv": argv} def resume_ok(ledger_before, ledger_after): """새 process 가 원장만으로 재개 가능한가 — stage 원장이 전진하고 accepted artifact 가 생겼는가.""" before = set((ledger_before or {}).get("stages", [])) after = set((ledger_after or {}).get("stages", [])) return bool(after - before) and bool((ledger_after or {}).get("accepted")) def run_probe(arm_commit, out_findings_path, execute=False): """실제 headless probe: worktree(arm_commit) → /ground 1회 headless → 종료 → 새 process 원장 재로드 → resume_ok → PROBE-FINDINGS.md 기록(헤드리스 가능성·adapter 여부·stage별 산출 파일 shape). execute=False 면 미실행(사양만, worktree 도 만들지 않는다) — 무거운 실행은 이 플래그 뒤에 숨긴다. 실행 절차(execute=True): 1. git worktree add → arm 커밋 부스트랩(runner.setup_worktree) 2. worktree 에서 runner.run_stage(ground) 로 headless 1회 실행(원장+산출물 기록) 3. process 종료(암묵적, exec_fn 이 subprocess 로 격리) 4. stage 결과를 원장 anchor 로 재구성(새 process 가 원장만 보고 재개 가능한지 시뮬레이션) 5. resume_ok 호출로 전진 검증 6. PROBE-FINDINGS.md 에 헤드리스 가능/adapter 여부/stage 산출물 shape 기록 """ plan = {"arm-commit": arm_commit, "executed": execute, "worktree": None, "workspace": None, "stage-result": None, "resume-ok": None} if not execute: _write_findings(out_findings_path, plan) return plan from . import paths, runner rid = paths.run_id(arm_commit) worktree = runner.setup_worktree(rid, "probe", arm_commit, paths.ROOT) workspace = paths.workspace_dir(rid, "probe") os.makedirs(workspace, exist_ok=True) env = runner.evidence_env(os.path.join(workspace, "evidence-pack")) def _exec_fn(argv, cwd, exec_env): r = subprocess.run(argv, cwd=cwd, env=exec_env, capture_output=True, text=True) return {"exit-code": r.returncode, "artifacts": [], "transcript": [r.stdout, r.stderr]} ledger_before = {"stages": [], "accepted": []} result = runner.run_stage(worktree, workspace, runner.STAGES[0], env, _exec_fn) # process 종료 후 "새 process" 가 보는 원장 상태 — 이 stage 의 반환값만이 그 process 의 유일한 # 산출 신호이므로, 성공한 stage 만 원장에 전진 기록된 것으로 재구성한다(원장 재로드 시뮬레이션). advanced = result["exit-code"] == 0 ledger_after = {"stages": [result["stage"]] if advanced else [], "accepted": [result["stage"]] if advanced else []} ok = resume_ok(ledger_before, ledger_after) plan.update({"worktree": worktree, "workspace": workspace, "stage-result": result, "resume-ok": ok}) _write_findings(out_findings_path, plan) return plan def _write_findings(path, plan): os.makedirs(os.path.dirname(path) or ".", exist_ok=True) lines = ["# PROBE-FINDINGS", "", f"executed: {plan.get('executed')}", f"arm-commit: {plan.get('arm-commit')}", f"resume-ok: {plan.get('resume-ok')}", f"stage-result: {plan.get('stage-result')}"] with open(path, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n")