#!/usr/bin/env python3 """commit_company_context.py — candidate → 공식 company-context.yaml 원자적 교체(trusted CLI, §9.3). 절차: schema/lint(Hard Fail 0) → candidate 로드(목표 status 확인) → **목표 status 가 provisional/operating 이면 venture-decision human acceptance receipt(HUMAN-001, report-sha256 바인딩) 검증이 필수**(--require-human 와 무관하게 강제 — status=template 만 receipt 없이 commit 가능) → candidate-status 제거 → 임시파일 write → os.replace(원자) → state_engine의 제한된 company-context event writer. 실패 시 공식 파일 **무변경**(receipt 검증은 어떤 write 보다도 먼저 수행돼 신뢰경계가 write 경로 밖으로 새지 않는다). OPS-ORCH 가 실행(에이전트는 guard_tools 로 공식 파일 직접쓰기 차단 — 다만 guard 는 committer '호출' 자체는 허용하므로, 내부 human-gate 는 이 스크립트가 candidate 의 목표 status 로만 판단해 자체 강제한다). CLI: commit_company_context.py --workflow WF --candidate [--require-human] --require-human 는 여전히 유효하지만 provisional/operating 대상에는 이미 항상 강제되므로 redundant. """ import os, sys, argparse, tempfile ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) OFFICIAL = os.path.join(ROOT, "org-os", "01-company", "company-context.yaml") sys.path.insert(0, os.path.join(ROOT, ".claude", "hooks")) def _fail(msg): sys.stderr.write(f"[commit_company_context] FAIL: {msg}\n"); return 1 def main(argv): ap = argparse.ArgumentParser() ap.add_argument("--workflow", required=True) ap.add_argument("--candidate", required=True) ap.add_argument("--require-human", action="store_true") ns = ap.parse_args(argv) import yaml if not os.path.exists(ns.candidate): return _fail(f"candidate 없음: {ns.candidate}") import lint_company_context as L hard, warn = L.lint_file(ns.candidate, is_candidate=True) for w in warn: sys.stderr.write(f"[commit_company_context] WARN: {w}\n") if hard: return _fail("candidate lint Hard Fail: " + "; ".join(hard)) # candidate 로드(목표 status 확인용 — 아직 공식 파일엔 아무것도 쓰지 않는다) with open(ns.candidate, encoding="utf-8") as fh: doc = yaml.safe_load(fh) or {} target_status = str(doc.get("status", "")).strip().lower() # human-gate: --require-human 플래그가 아니라 candidate 의 목표 status 로 강제 여부를 판단한다. # provisional/operating 을 공식화하는 건 template 이 아닌 실질적 회사 사실/결정을 SoT 로 반영하는 # 것이므로, receipt 없이 이 경로를 타는 걸 막는다(신뢰경계 구멍 봉인 — --require-human 미지정으로 # 우회 불가). template 대상만 receipt 없이 commit 가능(초기 스캐폴딩). need_human = bool(ns.require_human) or target_status in ("provisional", "operating") if need_human: try: import state_engine as SE decisions = ((doc.get("company") or {}).get("strategic-decisions") or []) source_decision_ids = { str(item.get("source-decision-id")) for item in decisions if isinstance(item, dict) and str(item.get("accepted-by", "")).upper() == "HUMAN-001" and item.get("source-decision-id") } if not source_decision_ids: return _fail("HUMAN-001 strategic-decision의 source-decision-id 없음") if not SE._venture_decision_receipt_ok(ns.workflow, source_decision_ids): return _fail( f"candidate source-decision-id와 일치하는 HUMAN-001 venture-decision " f"acceptance receipt(report-sha256 바인딩) 없음 — " f"target status='{target_status}' 는 human 게이트 필수(§9.4, provisional/operating)") except Exception as e: return _fail(f"human acceptance 검증 오류: {e}") # candidate → 공식: candidate-status 제거 doc.pop("candidate-status", None) # 최종 공식 형태 재-lint(안전) tmp_fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(OFFICIAL), suffix=".tmp") try: with os.fdopen(tmp_fd, "w", encoding="utf-8") as fh: yaml.safe_dump(doc, fh, allow_unicode=True, sort_keys=False) hard2, _ = L.lint_file(tmp_path, is_candidate=False) if hard2: os.unlink(tmp_path) return _fail("최종 공식형 lint Hard Fail: " + "; ".join(hard2)) os.replace(tmp_path, OFFICIAL) # 원자적 교체 except Exception as e: if os.path.exists(tmp_path): os.unlink(tmp_path) return _fail(f"원자 교체 실패(공식 파일 무변경): {e}") # 아티팩트 등록(company-context-artifact-recorded predicate 근거) try: import state_engine as SE ok, err = SE._record_internal_artifact(ns.workflow, "company-context", OFFICIAL, actor="OPS-ORCH") if not ok: raise RuntimeError(err) except Exception as e: sys.stderr.write(f"[commit_company_context] WARN: record_artifact 실패: {e}\n") print(f"[commit_company_context] OK — {OFFICIAL} (status={doc.get('status')})") return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))