Files
company-haness/.claude/hooks/commit_company_context.py

105 lines
5.4 KiB
Python

#!/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 <path> [--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:]))