105 lines
5.4 KiB
Python
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:]))
|