init: company-haness 설계
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
#!/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:]))
|
||||
Reference in New Issue
Block a user