#!/usr/bin/env python3 """Standalone enforcement tests: run the hooks against good/bad fixtures and assert exit codes. No pytest needed. Exit 0 = all pass. """ import json import os import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) HOOKS = os.path.join(ROOT, ".claude", "hooks") FIX = os.path.join(ROOT, ".claude", "tests", "fixtures") PY = sys.executable os.makedirs(FIX, exist_ok=True) passed, failed = 0, 0 def check(name, ok): global passed, failed if ok: passed += 1 print(f" PASS {name}") else: failed += 1 print(f" FAIL {name}") def run(script, args=None, stdin=None, env=None): e = dict(os.environ) e["CLAUDE_PROJECT_DIR"] = ROOT if env: e.update(env) return subprocess.run( [PY, os.path.join(HOOKS, script)] + (args or []), input=stdin, capture_output=True, text=True, env=e, ) def _inject_identity(text, path): """P0-3/P0-5 migration: report fixtures now need report-type + identity. Inject sane defaults into any fixture that looks like a report and lacks them, so evidence/synthesis tests keep exercising their intended violation without each fixture repeating boilerplate. Non-report files (evidence logs) and fixtures already carrying the fields are untouched. Use write_raw() to bypass injection when a test deliberately omits report-type.""" if "report-header:" not in text and "synthesized-by:" not in text: return text pre = [] if "report-type:" not in text: pre.append("report-type: work") if "work-summary:" not in text: pre.append('work-summary: "fixture"') if "report-id:" not in text: base = os.path.basename(path) rid = base[:-len(".report.yaml")] if base.endswith(".report.yaml") else "fix" pre.append(f"report-id: {rid}") if "workflow-id:" not in text: pre.append("workflow-id: wf-test") if "role-id:" not in text: pre.append("role-id: TST-GENERIC-FIXTURE") # P3-B: 테스트 전용 역할(계약 강제 면제, 활성화 무관) return ("\n".join(pre) + "\n" + text.lstrip("\n")) if pre else text def write(path, text): text = _inject_identity(text, path) with open(path, "w") as f: f.write(text) return path def write_raw(path, text): """Write a fixture WITHOUT identity injection (for tests that deliberately omit report-type).""" with open(path, "w") as f: f.write(text) return path # real evidence artifact (exists) EV = write(os.path.join(FIX, "evidence.log"), "test run: 12 passed\n") EV_REL = ".claude/tests/fixtures/evidence.log" GOOD = write(os.path.join(FIX, "good.report.yaml"), f""" report-header: bottom-line: "가격 A안을 권고한다." decision-needed: {{ needed: false, approver: EXEC-CEO }} confidence: {{ value: Med, derived-from: evidence }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) BAD_BLUF = write(os.path.join(FIX, "bad_bluf.report.yaml"), f""" report-header: bottom-line: "" decision-needed: {{ needed: false }} confidence: {{ value: Med }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) BAD_EVID = write(os.path.join(FIX, "bad_evid.report.yaml"), """ report-header: bottom-line: "배포하자." decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: - source-uri: does/not/exist.log grade: E4 """) BAD_CONF = write(os.path.join(FIX, "bad_conf.report.yaml"), f""" report-header: bottom-line: "확실히 된다." decision-needed: {{ needed: false }} confidence: {{ value: High }} risks: [] evidence: - source-uri: {EV_REL} grade: E1 """) GOOD_E5 = write(os.path.join(FIX, "good_e5.report.yaml"), """ report-header: bottom-line: "테스트 통과, 배포 가능." decision-needed: { needed: false } confidence: { value: High } risks: [] evidence: - command: "pytest -q" exit-code: 0 grade: E5 """) BAD_INFLATE = write(os.path.join(FIX, "bad_inflate.report.yaml"), """ report-header: bottom-line: "됐다고 본다." decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: - command: "flaky-check" exit-code: 1 grade: E5 """) SYNTH_GOOD = write(os.path.join(FIX, "synth_good.report.yaml"), f""" synthesized-by: Orchestrator linked-reports: [{os.path.relpath(GOOD, ROOT)}, {os.path.relpath(BAD_CONF, ROOT)}] conflicts: - "A는 X, B는 Y로 갈림(보존)" report-header: bottom-line: "종합 결론" decision-needed: {{ needed: false }} confidence: {{ value: Med }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) SYNTH_BAD = write(os.path.join(FIX, "synth_bad.report.yaml"), f""" synthesized-by: Orchestrator linked-reports: [{os.path.relpath(GOOD, ROOT)}] report-header: bottom-line: "종합 결론(이견 삭제됨)" decision-needed: {{ needed: false }} confidence: {{ value: Med }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) URL_E2 = write(os.path.join(FIX, "url_e2.report.yaml"), """ report-header: bottom-line: "외부 시장자료 기반 결론" decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: - source-uri: https://example.com/market-report grade: E2 """) URL_E4 = write(os.path.join(FIX, "url_e4.report.yaml"), """ report-header: bottom-line: "URL을 E4로 인플레" decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: - source-uri: https://example.com/market-report grade: E4 """) print("== validate_report ==") missing_report = run( "validate_report.py", [os.path.join(ROOT, "does-not-exist.report.yaml")]) check("explicit nonexistent report path -> 2 (not silent no-op)", missing_report.returncode == 2 and "존재하지" in missing_report.stderr) check("good report -> 0", run("validate_report.py", [GOOD]).returncode == 0) check("URL evidence @E2 -> 0 (외부 근거 인정)", run("validate_report.py", [URL_E2]).returncode == 0) check("URL evidence @E4 -> 2 (URL은 강한근거 불가)", run("validate_report.py", [URL_E4]).returncode == 2) check("missing BLUF -> 2", run("validate_report.py", [BAD_BLUF]).returncode == 2) check("fake evidence -> 2", run("validate_report.py", [BAD_EVID]).returncode == 2) check("overconfident -> 2", run("validate_report.py", [BAD_CONF]).returncode == 2) # --- #6 receipt 기반 evidence (C5/C6): 자기신고 E5는 ledger receipt로만 검증된다 --- import shutil as _shx EVWS = os.path.join(FIX, "evws") _shx.rmtree(EVWS, ignore_errors=True) def _mk_ws(name): ws = os.path.join(EVWS, name) os.makedirs(os.path.join(ws, "evidence"), exist_ok=True) return ws def _seed_report(ws, wf, fname, text): d = os.path.join(ws, "completion-records", wf) os.makedirs(d, exist_ok=True) return write(os.path.join(d, fname), text) def _seed_ledger(ws, receipts): with open(os.path.join(ws, "evidence", "ledger.jsonl"), "w") as f: for r in receipts: f.write(json.dumps(r) + "\n") E5_CMD_BODY = """report-header: bottom-line: "테스트 통과, 배포 가능." decision-needed: { needed: false } confidence: { value: High } risks: [] evidence: - command: "pytest -q" exit-code: 0 grade: E5 """ # (A) 실행 receipt 없는 자기신고 E5 -> 이제 차단 (예전 line~90 fixture는 통과했음) _WS_NO = _mk_ws("e5_noreceipt") _E5_NO = _seed_report(_WS_NO, "wfE", "role-x-20260101T000000Z.report.yaml", E5_CMD_BODY) check("self-reported E5 (command+exit0, NO ledger receipt) -> 2 (semantic block)", run("validate_report.py", [_E5_NO]).returncode == 2) # (B) 일치하는 receipt를 /ledger.jsonl 에 시드하면 통과 _WS_OK = _mk_ws("e5_receipt") _E5_OK = _seed_report(_WS_OK, "wfE", "role-x-20260101T000000Z.report.yaml", E5_CMD_BODY) _seed_ledger(_WS_OK, [{"tool_use_id": "t1", "tool_name": "Bash", "ts": "2026-07-10T00:00:00Z", "cwd": _WS_OK, "command": "pytest -q", "exit_code": 0, "stdout_sha256": "deadbeef", "workflow_id": "wf-test", "session_id": "fixture-session", "agent_id": "fixture-agent", "receipt_type": "test-run", "assertion_status": "passed"}]) check("receipt-backed E5 (matching ledger.jsonl) -> 0 (verified)", run("validate_report.py", [_E5_OK]).returncode == 0) # (B') ledger에 있어도 exit_code!=0 receipt는 뒷받침이 아니다 -> 차단 _WS_FAIL = _mk_ws("e5_failreceipt") _E5_FAIL = _seed_report(_WS_FAIL, "wfE", "role-x-20260101T000000Z.report.yaml", E5_CMD_BODY) _seed_ledger(_WS_FAIL, [{"tool_use_id": "t2", "tool_name": "Bash", "ts": "2026-07-10T00:00:00Z", "cwd": _WS_FAIL, "command": "pytest -q", "exit_code": 1, "workflow_id": "wf-test", "session_id": "fixture-session", "agent_id": "fixture-agent", "receipt_type": "test-run", "assertion_status": "failed"}]) check("E5 with only failing receipt (exit!=0) -> 2", run("validate_report.py", [_E5_FAIL]).returncode == 2) # 예전 flat GOOD_E5 fixture(line~90)도 이제 차단 — 빈 워크스페이스로 결정적 확인 EMPTY_WS = os.path.join(FIX, "empty_ws") _shx.rmtree(EMPTY_WS, ignore_errors=True) os.makedirs(os.path.join(EMPTY_WS, "evidence"), exist_ok=True) check("flat GOOD_E5 self-report (no receipt) -> 2 (was PASS, now BLOCK)", run("validate_report.py", [GOOD_E5], env={"ORGOS_WORKSPACE": EMPTY_WS}).returncode == 2) # 기존 파일(CLAUDE.md) 단순 참조만으로 E5 불가(존재 ≠ 산출 receipt) CLAUDEMD_E5 = write(os.path.join(FIX, "claudemd_e5.report.yaml"), """ report-header: bottom-line: "CLAUDE.md 있으니 근거 충분(주장)" decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: - source-uri: CLAUDE.md grade: E5 """) check("plain pre-existing CLAUDE.md as E5 -> 2 (존재≠산출 receipt)", run("validate_report.py", [CLAUDEMD_E5], env={"ORGOS_WORKSPACE": EMPTY_WS}).returncode == 2) # --- #5 회사 문맥 상한: company-context 미채움 상태에서 회사 네임스페이스를 E3+로 인용 불가 --- print("== #5 company-context confidence cap ==") _CCE3 = write(os.path.join(FIX, "company_e3.report.yaml"), """ report-header: bottom-line: "회사 전략상 이 방향이 맞다(빈 회사문맥 인용)" decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: - source-uri: org-os/01-company/company-context.yaml grade: E3 """) check("#5 empty company-context cited as E3 -> 2 (일반론은 E1/E2 상한)", run("validate_report.py", [_CCE3], env={"ORGOS_WORKSPACE": EMPTY_WS}).returncode == 2) # 같은 회사 경로를 E2로 낮추면 통과(참고 근거로는 허용) _CCE2 = write(os.path.join(FIX, "company_e2.report.yaml"), """ report-header: bottom-line: "회사 문맥 참고(E2)" decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: - source-uri: org-os/01-company/company-context.yaml grade: E2 """) check("#5 same company path at E2 -> 0 (참고 근거 허용)", run("validate_report.py", [_CCE2], env={"ORGOS_WORKSPACE": EMPTY_WS}).returncode == 0) # 회사 네임스페이스 밖(README) E3 는 영향 없음(회사문맥 규칙은 타깃된 범위만) _NONCC = write(os.path.join(FIX, "noncompany_e3.report.yaml"), """ report-header: bottom-line: "일반 파일 E3 근거" decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: - source-uri: README.md grade: E3 """) check("#5 non-company E3 (README) unaffected -> 0", run("validate_report.py", [_NONCC], env={"ORGOS_WORKSPACE": EMPTY_WS}).returncode == 0) # --- #5 status 어휘: _company_context_populated() 는 operating(+구 populated 읽기호환)만 True --- # 실제 SoT 파일을 건드리지 않도록 모듈 상수 VR._COMPANY_CTX 를 임시파일로 monkeypatch. import tempfile # noqa: E402 import yaml as _yaml_vocab # noqa: E402 sys.path.insert(0, os.path.join(ROOT, ".claude", "hooks")) import validate_report as VR # noqa: E402 _orig_ctx = VR._COMPANY_CTX def _set_ctx(status): fd, p = tempfile.mkstemp(suffix=".yaml") os.close(fd) with open(p, "w", encoding="utf-8") as fh: _yaml_vocab.safe_dump({"schema-version": 2, "status": status, "company": {"facts": [], "strategic-decisions": [], "hypotheses": [], "validation-state": {"stage": "pre-traction", "validated": [], "open": [], "refuted": []}}, "projects": []}, fh, allow_unicode=True) VR._COMPANY_CTX = p def _restore_ctx(): VR._COMPANY_CTX = _orig_ctx _set_ctx("operating") check("operating -> populated True", VR._company_context_populated() is True) _set_ctx("provisional") check("provisional -> populated False(회사인용 상한 유지)", VR._company_context_populated() is False) _set_ctx("template") check("template -> populated False", VR._company_context_populated() is False) _restore_ctx() # --- Task 14: _is_hypothesis_company_ref — source-uri anchor 문자열만 파싱(파일 접근 없음) --- check("HYP anchor -> hypothesis ref True", VR._is_hypothesis_company_ref("org-os/01-company/company-context.yaml#HYP-001") is True) check("FACT anchor -> hypothesis ref False", VR._is_hypothesis_company_ref("org-os/01-company/company-context.yaml#FACT-001") is False) check("no anchor -> False", VR._is_hypothesis_company_ref("org-os/01-company/company-context.yaml") is False) # --- Task 14 review gap: E2E 회귀(design test #10) — company-context 를 populated(operating)로 # monkeypatch 한 뒤, #HYP anchor @E3 는 상한(cap)에 걸리고 #FACT anchor 는 걸리지 않는지 # validate() 를 in-process 로 직접 호출해 diff 로 검증한다(다른 공통 에러는 양쪽에 동일하게 나오므로 차감됨). _set_ctx("operating") def _rep_anchor(anchor): return { "report-type": "work", "report-header": { "bottom-line": "x", "decision-needed": {"needed": False}, "confidence": {"value": "Med"}, "risks": [], "evidence": [{"source-uri": f"org-os/01-company/company-context.yaml#{anchor}", "grade": "E3"}], }, } _errs_hyp = VR.validate(_rep_anchor("HYP-001")) _errs_fact = VR.validate(_rep_anchor("FACT-001")) _extra = [e for e in _errs_hyp if e not in _errs_fact] check("E2E: #HYP @E3 under operating -> capped (extra error vs #FACT)", len(_extra) >= 1) check("E2E: the extra error is the company-context hypothesis cap", any("hypothesis 기반" in e for e in _extra)) _restore_ctx() check("grade inflation (E5, exit!=0) -> 2", run("validate_report.py", [BAD_INFLATE]).returncode == 2) check("synthesis WITH conflicts -> 0", run("validate_report.py", [SYNTH_GOOD]).returncode == 0) check("synthesis WITHOUT conflicts -> 2 (dissent 보존 강제)", run("validate_report.py", [SYNTH_BAD]).returncode == 2) # 워커가 출처로 linked-reports를 달아도 (synthesized-by 없으면) 종합으로 오인 금지 WORKER_LINKED = write(os.path.join(FIX, "worker_linked.report.yaml"), f""" role-id: TST-GENERIC-FIXTURE linked-reports: [a.report.yaml, b.report.yaml] report-header: bottom-line: "워커 판정(출처 인용)" decision-needed: {{ needed: false }} confidence: {{ value: Med }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) check("worker citing linked-reports (no synthesized-by) -> 0", run("validate_report.py", [WORKER_LINKED]).returncode == 0) # --- 종합 dissent/linked 강화: 존재하지 않는 링크·conflicts:null 차단 --- SYNTH_BADLINK = write(os.path.join(FIX, "synth_badlink.report.yaml"), f""" synthesized-by: Orchestrator linked-reports: [does/not/exist-a.report.yaml, does/not/exist-b.report.yaml] conflicts: - "이견 보존됨" report-header: bottom-line: "종합인데 링크가 허위" decision-needed: {{ needed: false }} confidence: {{ value: Med }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) check("synthesis with nonexistent linked-report -> 2 (허위 종합 차단)", run("validate_report.py", [SYNTH_BADLINK]).returncode == 2) SYNTH_NULLCONF = write(os.path.join(FIX, "synth_nullconf.report.yaml"), f""" synthesized-by: Orchestrator linked-reports: [{os.path.relpath(GOOD, ROOT)}] conflicts: null report-header: bottom-line: "이견을 null로 뭉갬" decision-needed: {{ needed: false }} confidence: {{ value: Med }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) check("synthesis conflicts: null -> 2 (dissent 보존 미증명; null != [])", run("validate_report.py", [SYNTH_NULLCONF]).returncode == 2) # --- role-id 정합: 미등록/소문자 role은 lens 판별 불가 -> 차단 --- LOWROLE = write(os.path.join(FIX, "lowrole.report.yaml"), f""" role-id: eng-backend report-header: bottom-line: "소문자/미등록 role" decision-needed: {{ needed: false }} confidence: {{ value: Med }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) check("unknown/lowercase role-id (lens=0, 판별 불가) -> 2", run("validate_report.py", [LOWROLE]).returncode == 2) # (등록 role 통과는 아래 LOWREG=arch-solution 로 확인. 일반 fixture 는 TST-GENERIC-FIXTURE 로 계약 강제 격리.) # --- P2: 등록 role의 소문자(agent-card 이름) 표기도 accept — 대소문자 무관 등록 매칭 --- # 근거: context_package.target-role-agent 는 소문자 카드명(arch-solution.md)을, validate_report 는 # 등록 role-id 를 요구한다. 두 축이 같은 역할의 다른 표기라 case 만 달라도 거부되면 매 fan-out spawn 이 # 오탐 거부된다(실측: /design 라운드). registered set 대조를 대소문자 무관으로 하되 '미등록'은 그대로 차단. LOWREG = write(os.path.join(FIX, "lowreg_registered.report.yaml"), f""" report-type: work role-id: arch-solution tier: light work-summary: "등록된 ARCH-SOLUTION 의 소문자(카드명) 표기" report-header: bottom-line: "등록된 role 의 소문자 표기(카드명)는 유효해야 한다" decision-needed: {{ needed: false }} confidence: {{ value: Med, derived-from: evidence }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) check("lowercase form of a REGISTERED role (arch-solution -> ARCH-SOLUTION) -> 0 (대소문자 무관)", run("validate_report.py", [LOWREG]).returncode == 0) # --- report-type 판별자 + JSON Schema 유형별 필수필드 --- BLOCKED_OK = write(os.path.join(FIX, "blocked_ok.report.yaml"), f""" report-type: blocked blocker: "외부 API 키 부재 — 사람 승인 필요" resume-condition: "유효한 API 키 receipt 확보" report-header: bottom-line: "블로커로 중단" decision-needed: {{ needed: true, approver: HUMAN-001 }} confidence: {{ value: Low }} risks: [] evidence: - source-uri: {EV_REL} grade: E2 """) check("report-type=blocked WITH blocker+resume-condition -> 0 (schema/gate aligned)", run("validate_report.py", [BLOCKED_OK]).returncode == 0) BLOCKED_BAD = write(os.path.join(FIX, "blocked_bad.report.yaml"), f""" report-type: blocked report-header: bottom-line: "blocker 필드 누락" decision-needed: {{ needed: true, approver: HUMAN-001 }} confidence: {{ value: Low }} risks: [] evidence: - source-uri: {EV_REL} grade: E2 """) check("report-type=blocked WITHOUT blocker -> 2 (type schema 필수필드)", run("validate_report.py", [BLOCKED_BAD]).returncode == 2) UNKNOWN_TYPE = write(os.path.join(FIX, "unknown_type.report.yaml"), f""" report-type: banana report-header: bottom-line: "미지 유형은 이제 거부된다(P0-5 위장/오타 차단)" decision-needed: {{ needed: false }} confidence: {{ value: Med }} risks: [] evidence: - source-uri: {EV_REL} grade: E3 """) # finding P0-5: 미지/오타 report-type 은 이제 거부한다(typed-schema 우회·위장 차단). check("unknown report-type -> BLOCK (P0-5)", run("validate_report.py", [UNKNOWN_TYPE]).returncode == 2) # --- render_report 게이트: 검증 실패 보고서는 대표용 MD로 렌더 거부(nonzero) --- RG_BAD = write(os.path.join(FIX, "render_gate_bad.report.yaml"), """ report-header: bottom-line: "" decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: [] """) _RG_OUT = os.path.join(FIX, "rg_bad.md") if os.path.exists(_RG_OUT): os.remove(_RG_OUT) _rg = run("render_report.py", [RG_BAD, "--type", "work", "--out", _RG_OUT]) check("render_report REFUSES invalid report (nonzero)", _rg.returncode != 0) check("render_report gate produced no MD for invalid report", not os.path.exists(_RG_OUT)) print("== guard_tools ==") def gt(tool, ti): return run("guard_tools.py", stdin=json.dumps({"tool_name": tool, "tool_input": ti})).returncode check("Read -> 0", gt("Read", {"file_path": "x"}) == 0) check("Bash pytest -> 0", gt("Bash", {"command": "pytest -q"}) == 0) check("gh pr create -> 2", gt("Bash", {"command": "gh pr create --title x"}) == 2) check("git push -> 2", gt("Bash", {"command": "git push origin main"}) == 2) check("cat .env -> 2", gt("Bash", {"command": "cat .env"}) == 2) check("rm -rf -> 2", gt("Bash", {"command": "rm -rf build/"}) == 2) check("kubectl -> 2", gt("Bash", {"command": "kubectl apply -f d.yaml"}) == 2) check("write .env -> 2", gt("Write", {"file_path": "config/.env"}) == 2) check("write normal -> 0", gt("Write", {"file_path": "org-os/x.yaml"}) == 0) print("== stop_validate (via CLAUDE_REPORT_PATH) ==") check("good -> 0", run("stop_validate.py", stdin="{}", env={"CLAUDE_REPORT_PATH": GOOD}).returncode == 0) check("bad -> 2", run("stop_validate.py", stdin="{}", env={"CLAUDE_REPORT_PATH": BAD_BLUF}).returncode == 2) check("no report -> 0", run("stop_validate.py", stdin="{}", env={"CLAUDE_REPORT_PATH": "/nonexistent"}).returncode == 0) print("== gen_agents ==") r = run("gen_agents.py", ["--check"]) check("gen_agents --check -> 0 (75 concrete agents)", r.returncode == 0 and "75 concrete agents" in r.stdout) check("gen_agents emits concrete roles and no family cards", "19 collapse workers" in r.stdout and "family metadata cards=0" in r.stdout) print("== notify_slack (redact + format + outbox) ==") import glob as _glob NTMP = os.path.join(FIX, "notifytest") os.makedirs(NTMP, exist_ok=True) SEC = write(os.path.join(FIX, "sec.report.yaml"), """ report-header: bottom-line: "가격 승인 요청" decision-needed: { needed: true, approver: HUMAN-001 } confidence: { value: Med } risks: ["담당자 theorose49@gmail.com, 토큰 xoxb-1-secretval 노출 주의"] evidence: [] """) _env = dict(os.environ); _env["CLAUDE_PROJECT_DIR"] = NTMP; _env["ORGOS_WORKSPACE"] = "test-labs-documents" _r = subprocess.run([PY, os.path.join(HOOKS, "notify_slack.py"), "human-review", SEC, "--title", "가격 승인"], capture_output=True, text=True, env=_env) _out = _r.stdout check("notify emits :raising_hand:", ":raising_hand:" in _out) check("notify has BLUF section", "핵심(BLUF)" in _out) check("notify redacts email", "@gmail" not in _out) check("notify redacts token", "xoxb-1-secretval" not in _out) check("notify enqueues to outbox", len(_glob.glob(os.path.join(NTMP, "test-labs-documents/slack-outbox/*.json"))) >= 1) # agent-report 템플릿 (템플릿 3): 직무정체성·BLUF·결정필요·동료 cc REP = write(os.path.join(FIX, "agentrep.report.yaml"), f""" role-id: TST-GENERIC-FIXTURE role-name: Pricing Strategist AI lens: LENS-FINANCE tags: [doc-mgmt-app, monetization] report-header: bottom-line: "가격 3-tier 개편 권고" decision-needed: {{ needed: true, approver: FAM-CFO }} confidence: {{ value: Med }} risks: ["엔터프라이즈 연간계약군 반발 가능"] evidence: - source-uri: {EV_REL} grade: E3 findings: ["가격탄력성 회귀 -0.6", "몬테카를로 5k회"] """) _rr = subprocess.run([PY, os.path.join(HOOKS, "notify_slack.py"), "report", REP, "--title", "가격 개편"], capture_output=True, text=True, env=_env) _ro = _rr.stdout check("report emits :round_pushpin:", ":round_pushpin:" in _ro) check("report shows role+lens identity", "role: Pricing Strategist AI" in _ro and "lens: LENS-FINANCE" in _ro) check("report has BLUF + evidence grade", "*BLUF*" in _ro and "E3" in _ro) check("report shows approver (DACI)", "승인자: *FAM-CFO*" in _ro) check("report cc's peer tags", "cc" in _ro and "#monetization" in _ro) print("== collaboration-default classification ==") import yaml as _yaml _fams = _yaml.safe_load(open(os.path.join(ROOT, "org-os/00-role-registry/capability-families.yaml")))["capability-families"]["families"] _cd = [f.get("collaboration-default") for f in _fams] _byid = {f["family-id"]: f for f in _fams} check("28 families each have valid collaboration-default", len(_cd) == 28 and all(c in ("fan-out", "collapse", "n/a") for c in _cd)) check("fan-out=21", _cd.count("fan-out") == 21) check("collapse=6", _cd.count("collapse") == 6) check("n/a=1 (ORCH)", _cd.count("n/a") == 1 and _byid["FAM-ORCH"]["collaboration-default"] == "n/a") check("FAM-ENG-BACKEND collapse", _byid["FAM-ENG-BACKEND"]["collaboration-default"] == "collapse") check("FAM-UX-RESEARCH fan-out", _byid["FAM-UX-RESEARCH"]["collaboration-default"] == "fan-out") check("FAM-GTM-SALES fan-out", _byid["FAM-GTM-SALES"]["collaboration-default"] == "fan-out") print("== collaboration-map ==") _cm = _yaml.safe_load(open(os.path.join(ROOT, "org-os/06-agent-work/collaboration-map.yaml")))["collaboration-map"] _famids = set(_byid.keys()) def _refs(node, acc): if isinstance(node, str): if node.startswith("FAM-") and all(c not in node for c in "*/ "): acc.add(node) elif isinstance(node, list): for x in node: _refs(x, acc) elif isinstance(node, dict): for x in node.values(): _refs(x, acc) return acc _missing = _refs(_cm, set()) - _famids check("collaboration-map family-ids all exist", not _missing) _edges = _cm["cross-group-edges"]["edges"] check("6 cross-group edges", len(_edges) == 6) check("edges bidirectional (a,b,a-to-b,b-to-a)", all(e.get("a") and e.get("b") and e.get("a-to-b") and e.get("b-to-a") for e in _edges)) check("cascade has DECIDE/DESIGN/BUILD", {p["phase"] for p in _cm["cascade-phases"]} >= {"DECIDE", "DESIGN", "BUILD"}) print("== render_report (YAML -> MD) ==") RMD = os.path.join(FIX, "render_out.md") _r = run("render_report.py", [GOOD, "--type", "decision", "--title", "T", "--out", RMD]) check("render single -> 0", _r.returncode == 0) _md = open(RMD).read() if os.path.exists(RMD) else "" check("md has BLUF 결론", "결론" in _md and "가격 A안" in _md) check("md has 근거 table + grade", "## 📎 근거" in _md and "E3" in _md) # #13: render_report 는 배지를 코드에 하드코딩하지 않고 report-templates.yaml(SSOT)에서 읽는다. import importlib as _il # noqa: E402 import yaml as _yaml # noqa: E402 sys.path.insert(0, os.path.join(ROOT, ".claude", "hooks")) _RR = _il.import_module("render_report") _rbraw = _yaml.safe_load(open(os.path.join(ROOT, "org-os/06-agent-work/report-templates.yaml"))) _rb = _rbraw["report-templates"]["human-md-rendering"]["render-badges"] check("#13 render_report consumes report-templates.yaml render-badges (not hardcoded)", all(tuple(map(str, v)) == _RR.TYPE_BADGE.get(k) for k, v in _rb.items()) and "spec" in _RR.TYPE_BADGE) MA = write(os.path.join(FIX, "mem_a.report.yaml"), """ role-name: CFO lens: LENS-FINANCE report-header: bottom-line: "재무 관점 결론 A" decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: [] findings: - "재무발견_ALPHA 단위마진 하한 필요" next-actions: - "COGS 연동 단가 시뮬레이션" """) MB = write(os.path.join(FIX, "mem_b.report.yaml"), """ role-name: CPO report-header: bottom-line: "제품 관점 결론 B" decision-needed: { needed: false } confidence: { value: High } risks: [] evidence: [] findings: - "제품발견_BETA activation 문턱" """) RMD2 = os.path.join(FIX, "render_fanout.md") run("render_report.py", [GOOD, "--type", "decision", "--members", MA, MB, "--out", RMD2]) _md2 = open(RMD2).read() if os.path.exists(RMD2) else "" check("md has 역할별 요약 table", "## 👥 역할별 핵심 결론" in _md2) check("role table has both members", "| CFO |" in _md2 and "| CPO |" in _md2) check("member bottom-lines preserved (no summary loss)", "재무 관점 결론 A" in _md2 and "제품 관점 결론 B" in _md2) check("per-role 상세 section present", "## 📋 역할별 상세" in _md2) check("member findings embedded (not just linked)", "재무발견_ALPHA" in _md2 and "제품발견_BETA" in _md2) check("member body sections rendered", "🔎 핵심 발견" in _md2 and "➡️ 다음 액션" in _md2) # 단일 보고서도 자체 본문(findings)을 embed SGL = write(os.path.join(FIX, "single_body.report.yaml"), """ report-header: bottom-line: "단일 결론" decision-needed: { needed: false } confidence: { value: Med } risks: [] evidence: - source-uri: .claude/tests/fixtures/evidence.log grade: E3 findings: - "단일발견_GAMMA" """) RMD3 = os.path.join(FIX, "render_single_body.md") run("render_report.py", [SGL, "--type", "completion", "--out", RMD3]) _md3 = open(RMD3).read() if os.path.exists(RMD3) else "" check("single report embeds its own body", "단일발견_GAMMA" in _md3 and "🔎 핵심 발견" in _md3) print("== agent split: per-role fan-out workers vs collapse family ==") _AG = os.path.join(ROOT, ".claude/agents") check("75 concrete agent files", len(_glob.glob(os.path.join(_AG, "*.md"))) == 75) check("family agent cards are absent", _glob.glob(os.path.join(_AG, "fam-*.md")) == []) # collapse family = planner metadata pool + selected concrete worker, never an integrated identity. check("eng-be is executable collapse concrete role", "collaboration-role: collapse-primary-candidate" in open(os.path.join(_AG, "eng-be.md")).read()) # fan-out family도 concrete role worker만 노출한다. check("ux-researcher.md exists (role worker)", os.path.exists(os.path.join(_AG, "ux-researcher.md"))) check("data-analyst.md exists (role worker)", os.path.exists(os.path.join(_AG, "data-analyst.md"))) _uxr = open(os.path.join(_AG, "ux-researcher.md")).read() check("ux-researcher carries fan-out-worker contract", "collaboration-role: fan-out-worker" in _uxr and "## Fan-out 워커 계약" in _uxr) check("ux-researcher has own role perspective", "## 나의 관점·시야·책임" in _uxr and "관점:" in _uxr) check("ux-researcher does not self-synthesize", "종합·최종결정은 내가 하지 않는다" in _uxr) # P3: working-method(방법론·근거·출처)는 method-skill로 분리. 카드=spine+skills 참조, 전체=skill 파일. check("ux-researcher card has method spine + skill ref (not full embed)", "## 핵심 작업 방법" in _uxr and "ux-researcher-method" in _uxr and "## 일하는 방식" not in _uxr) _uxr_skill = open(os.path.join(ROOT, ".claude/skills/ux-researcher-method/SKILL.md")).read() # P3-B: v1 render("주요 프레임워크"/"판단 근거 자료") 또는 v2 provenance 꼬리("프레임워크 계보"/"근거 종류") # 둘 중 하나로 프레임워크·근거 provenance 를 보존한다(역할이 v2 계약으로 승격돼도 계보 유지). check("method-skill cites frameworks + evidence", ("주요 프레임워크" in _uxr_skill or "프레임워크 계보" in _uxr_skill) and ("판단 근거 자료" in _uxr_skill or "근거 종류" in _uxr_skill)) check("method-skill cites web sources (http)", "참고 출처" in _uxr_skill and "http" in _uxr_skill) _rwm_idx = _yaml.safe_load(open(os.path.join(ROOT, "org-os/00-role-registry/role-working-methods/index.yaml")))["role-method-contracts"] _rwm_merged = {} for _inc in _rwm_idx["includes"]: _rwm_merged.update((_yaml.safe_load(open(os.path.join( ROOT, "org-os/00-role-registry/role-working-methods", _inc))) or {}).get("role-working-methods") or {}) check("role-working-methods covers 75 roles (파일분리 병합)", len(_rwm_merged) == 75) _be2 = open(os.path.join(_AG, "eng-be.md")).read() check("collapse concrete worker loads only its own method-skill", "eng-be-method" in _be2 and "eng-begen-method" not in _be2 and "## 일하는 방식" not in _be2) print("== immutable reports (guard + new_report + index) ==") import shutil as _sh IMWS = os.path.join(FIX, "imws") # 격리 워크스페이스(실제 프로젝트 오염 방지) _imenv = {"ORGOS_WORKSPACE": IMWS} IMDIR = os.path.join(IMWS, "completion-records", "wf-test-immutable") os.makedirs(IMDIR, exist_ok=True) IMREP = os.path.join(IMDIR, "role-x-20260101T000000Z.report.yaml") write(IMREP, "report-id: x\ncreated-at: 2026-01-01T00:00:00Z\nreport-header: { bottom-line: t, decision-needed: { needed: false }, confidence: { value: Med }, risks: [], evidence: [] }\n") check("guard blocks overwrite of existing report", gt("Write", {"file_path": IMREP}) == 2) check("guard blocks Edit of existing report", gt("Edit", {"file_path": IMREP}) == 2) check("guard allows NEW report path", gt("Write", {"file_path": os.path.join(IMDIR, "role-y-20990101T000000Z.report.yaml")}) == 0) _r1 = run("new_report.py", ["--workflow", "wf-test-immutable", "--role", "role-mint", "--stub"], env=_imenv).stdout.strip() _r2 = run("new_report.py", ["--workflow", "wf-test-immutable", "--role", "role-mint", "--stub"], env=_imenv).stdout.strip() check("new_report mints under completion-records//", "completion-records/wf-test-immutable/role-mint" in _r1) check("new_report never collides (2 calls -> 2 files)", _r1 != _r2) run("render_report.py", ["--index"], env=_imenv) _idx = open(os.path.join(IMWS, "reports", "INDEX.md")).read() check("INDEX is append-only + workflow-grouped", "append-only" in _idx and "created-at" in _idx and "## wf-test-immutable" in _idx) _sh.rmtree(IMWS, ignore_errors=True) print("== token budget (#1) + lens cap (#2) ==") TOKPROJ = os.path.join(FIX, "tokproj") os.makedirs(TOKPROJ, exist_ok=True) _tlenv = {"CLAUDE_PROJECT_DIR": TOKPROJ, "ORGOS_WORKSPACE": "test-labs-documents"} # 실제 원장과 격리(기본 예산 fallback: standard=500k) run("token_ledger.py", ["log", "--workflow", "wfx", "--role", "r1", "--tokens", "100000", "--tier", "standard"], env=_tlenv) check("token check under budget -> 0", run("token_ledger.py", ["check", "--workflow", "wfx", "--tier", "standard"], env=_tlenv).returncode == 0) check("token check over budget (+450k) -> 2", run("token_ledger.py", ["check", "--workflow", "wfx", "--tier", "standard", "--add", "450000"], env=_tlenv).returncode == 2) run("token_ledger.py", ["dashboard"], env=_tlenv) check("dashboard renders TOKENS.md", os.path.exists(os.path.join(TOKPROJ, "test-labs-documents/reports/TOKENS.md"))) # #19: 예산은 per-wave. 여러 정상 wave가 쌓여도(합 > 예산) 각 wave check는 통과해야 한다. run("token_ledger.py", ["log", "--workflow", "wfw", "--role", "r1", "--tokens", "300000", "--tier", "standard", "--wave", "1"], env=_tlenv) run("token_ledger.py", ["log", "--workflow", "wfw", "--role", "r2", "--tokens", "300000", "--tier", "standard", "--wave", "2"], env=_tlenv) check("#19 per-wave: wave1 under budget -> 0", run("token_ledger.py", ["check", "--workflow", "wfw", "--tier", "standard", "--wave", "1"], env=_tlenv).returncode == 0) check("#19 per-wave: wave2 under budget -> 0 (no cross-wave overage)", run("token_ledger.py", ["check", "--workflow", "wfw", "--tier", "standard", "--wave", "2"], env=_tlenv).returncode == 0) check("#19 per-wave: single wave over budget still -> 2", run("token_ledger.py", ["check", "--workflow", "wfw", "--tier", "standard", "--wave", "1", "--add", "300000"], env=_tlenv).returncode == 2) check("#19 per-wave: no --wave sums only '-' bucket (no bleed from wave1/2) -> 0", run("token_ledger.py", ["check", "--workflow", "wfw", "--tier", "standard"], env=_tlenv).returncode == 0) _sh.rmtree(TOKPROJ, ignore_errors=True) check("lens_cap standard distinct-subspecialty same-lens -> 0", run("lens_cap.py", ["--tier", "standard", "--roles", "SEC-ENGINEER,SEC-APPSEC"]).returncode == 0) # #12: 같은 LENS-SECURITY라도 security-eng vs appsec는 다른 전문분야(삭제 금지) check("lens_cap standard true-duplicate (same role x2) -> 2", run("lens_cap.py", ["--tier", "standard", "--roles", "SEC-ENGINEER,SEC-ENGINEER"]).returncode == 2) # 진짜 중복(동일 sub-specialty)은 여전히 차단 check("lens_cap heavy dup-lens -> 0", run("lens_cap.py", ["--tier", "heavy", "--roles", "SEC-ENGINEER,SEC-APPSEC"]).returncode == 0) check("lens_cap standard distinct-lens -> 0", run("lens_cap.py", ["--tier", "standard", "--roles", "PROD-PM,UX-RESEARCHER"]).returncode == 0) print("== report_tags (peer discovery) ==") TAGPROJ = os.path.join(FIX, "tagproj") os.makedirs(os.path.join(TAGPROJ, "test-labs-documents/completion-records/wfT"), exist_ok=True) write(os.path.join(TAGPROJ, "test-labs-documents/completion-records/wfT/r.report.yaml"), "role-id: PROD-PM\ntags: [topicX, product]\nreport-header: { bottom-line: hi, decision-needed: { needed: false }, confidence: { value: Med }, risks: [], evidence: [] }\n") _tg = run("report_tags.py", ["--tag", "topicX"], env={"CLAUDE_PROJECT_DIR": TAGPROJ, "ORGOS_WORKSPACE": "test-labs-documents"}) check("report_tags finds tagged peer report", "topicX" in _tg.stdout and "PROD-PM" in _tg.stdout) _sh.rmtree(TAGPROJ, ignore_errors=True) print("== slack_inbox (pre-work read) ==") IBPROJ = os.path.join(FIX, "ibproj") os.makedirs(IBPROJ, exist_ok=True) _ib = run("slack_inbox.py", ["--workflow", "wfI"], stdin='{"messages":[{"user":"CEO","text":"좁은 ICP로","ts":"1783440000"}]}', env={"CLAUDE_PROJECT_DIR": IBPROJ, "ORGOS_WORKSPACE": "test-labs-documents"}) check("slack_inbox writes inbox md", os.path.exists(os.path.join(IBPROJ, "test-labs-documents/slack-inbox/wfI.md"))) _sh.rmtree(IBPROJ, ignore_errors=True) print("== consulting layer (FAM-CONSULTING / LENS-ADVISORY / render) ==") _ga = run("gen_agents.py", ["--check"]) check("gen_agents --check ok (75 concrete agents)", _ga.returncode == 0 and "75 concrete agents" in _ga.stdout) check("consult-em generated as synthesis-lead", os.path.exists(os.path.join(_AG, "consult-em.md")) and "synthesis-lead" in open(os.path.join(_AG, "consult-em.md")).read()) check("doc-lead generated as synthesis-lead (FAM-DOC-CONSULT)", os.path.exists(os.path.join(_AG, "doc-lead.md")) and "synthesis-lead" in open(os.path.join(_AG, "doc-lead.md")).read()) check("doc-visual generated as fan-out-worker", os.path.exists(os.path.join(_AG, "doc-visual.md")) and "fan-out-worker" in open(os.path.join(_AG, "doc-visual.md")).read()) check("consult-strat generated as fan-out-worker", os.path.exists(os.path.join(_AG, "consult-strat.md")) and "fan-out-worker" in open(os.path.join(_AG, "consult-strat.md")).read()) _lens = _yaml.safe_load(open(os.path.join(ROOT, "org-os/00-role-registry/lens-registry.yaml")))["lens-registry"]["lenses"] _lids = [l["lens-id"] for l in _lens] check("lens-registry has 12 lenses", len(_lids) == 12) check("LENS-ADVISORY present", "LENS-ADVISORY" in _lids) _fams = _yaml.safe_load(open(os.path.join(ROOT, "org-os/00-role-registry/capability-families.yaml")))["capability-families"]["families"] _fc = [f for f in _fams if f["family-id"] == "FAM-CONSULTING"] check("FAM-CONSULTING lead=CONSULT-EM + 6 members", len(_fc) == 1 and _fc[0].get("lead-role-id") == "CONSULT-EM" and len(_fc[0]["member-role-ids"]) == 6) _ex = run("consult_exhibits.py") check("consult_exhibits 7 signature types", _ex.returncode == 0 and "7 types" in _ex.stdout) print("== render_consult (storyline -> 문서 + 덱) ==") CDIR = os.path.join(FIX, "consult") os.makedirs(CDIR, exist_ok=True) _crep = write(os.path.join(CDIR, "syn.report.yaml"), "synthesized-by: CONSULT-EM\ntitle: T\n" "report-header:\n bottom-line: 결론 한 줄.\n decision-needed: { needed: true, approver: HUMAN-001 }\n" " confidence: { value: Med, derived-from: evidence }\n risks: [r1]\n" " evidence:\n - { source-uri: org-os/00-role-registry/capability-families.yaml, grade: E3 }\n" "storyline:\n title: 렌더테스트\n client: ca-tmpl\n" " scqa: { situation: s, complication: c, question: q, answer: a }\n" " slides:\n" " - action-title: 준수도는 4개 레버로 상승한다\n" " exhibit: { type: waterfall, unit: '%', start: ['현재',35], deltas: [['역전',18]], end: ['목표',72] }\n" " body: [b1]\n evidence: [E3]\n" "conflicts: [ '[속도 vs 완결성] 이견 보존' ]\n" "linked-reports: [ org-os/00-role-registry/capability-families.yaml ]\n") _rc = run("render_consult.py", [_crep, "--outdir", os.path.join(CDIR, "out")]) _docmd = os.path.join(CDIR, "out", "렌더테스트-report.md") _deckhtml = os.path.join(CDIR, "out", "렌더테스트-deck.html") check("render_consult exit 0", _rc.returncode == 0) check("document.md carries action-title (horizontal logic)", os.path.exists(_docmd) and "준수도는 4개 레버로 상승한다" in open(_docmd).read()) check("offline HTML deck embeds inline ", os.path.exists(_deckhtml) and " blocked(2)", run("validate_report.py", [_bad]).returncode == 2) # mermaid exhibit(실제 diagram-as-code) 경로 — mmdc 없이도 폴백 SVG로 파이프라인 유지 _mrep = write(os.path.join(CDIR, "merm.report.yaml"), "synthesized-by: DOC-LEAD\ntitle: M\n" "report-header:\n bottom-line: mermaid.\n decision-needed: { needed: false }\n" " confidence: { value: Med, derived-from: evidence }\n risks: []\n" " evidence:\n - { source-uri: org-os/00-role-registry/lens-registry.yaml, grade: E3 }\n" "storyline:\n title: MermaidRender\n slides:\n" " - action-title: 흐름을 mermaid로 실제 렌더한다\n" " exhibit: { type: mermaid, code: \"flowchart TB\\n A --> B\\n B --> C\" }\n" " body: [b]\n") _rm = run("render_consult.py", [_mrep, "--outdir", os.path.join(CDIR, "mout")], env={"RENDER_CONSULT_NO_MMDC": "1"}) _mhtml = os.path.join(CDIR, "mout", "mermaidrender-deck.html") _mimg = os.path.join(CDIR, "mout", "img") check("mermaid exhibit routes + produces svg (fallback)", _rm.returncode == 0 and os.path.isdir(_mimg) and any(f.endswith(".svg") for f in os.listdir(_mimg)) and os.path.exists(_mhtml) and " b: dep\", layout: dagre }\n" " body: [b]\n") _rd = run("render_consult.py", [_drep, "--outdir", os.path.join(CDIR, "dout")], env={"RENDER_CONSULT_NO_D2": "1"}) _dhtml = os.path.join(CDIR, "dout", "d2render-deck.html") _dimg = os.path.join(CDIR, "dout", "img") check("d2 exhibit routes + produces svg (fallback)", _rd.returncode == 0 and os.path.isdir(_dimg) and any(f.endswith(".svg") for f in os.listdir(_dimg)) and os.path.exists(_dhtml) and " excalidraw > mermaid", [list(x)[0] for x in _dbs["diagram-extension"]["engine-priority"]] == ["d2", "excalidraw", "mermaid"]) _cps = open(os.path.join(ROOT, "org-os/06-agent-work/context-package-spec.yaml")).read() check("context-package wires design-brief", "design-brief" in _cps and "design-brief-spec.yaml" in _cps) for _sk in ("design-craft", "diagram-craft"): _skp = os.path.join(ROOT, ".claude/skills", _sk, "SKILL.md") _skt = open(_skp).read() if os.path.exists(_skp) else "" check(f"skill {_sk} exists with frontmatter", _skt.startswith("---") and f"name: {_sk}" in _skt) check("diagram-craft skill demotes Mermaid to fallback", "폴백" in open(os.path.join(ROOT, ".claude/skills/diagram-craft/SKILL.md")).read()) _wmr = _yaml.safe_load(open(os.path.join(ROOT, "org-os/00-role-registry/role-working-methods.yaml")))["role-working-methods"] check("DES-PROD working-method is reference-driven constraint (design-brief, ban 'modern')", any("design-brief" in s for s in _wmr["DES-PROD"]["working-method"]) and any("modern" in s for s in _wmr["DES-PROD"]["working-method"])) _dvwm = " ".join(_wmr["DOC-VISUAL"]["working-method"]) check("DOC-VISUAL working-method D2-first, Mermaid demoted", "D2" in _dvwm and "폴백" in _dvwm and _dvwm.index("D2") < _dvwm.index("Mermaid")) for _a in ("des-prod", "des-platform", "des-internal", "doc-visual"): check(f"agent {_a} carries craft standard", "디자인 craft 표준" in open(os.path.join(ROOT, ".claude/agents", _a + ".md")).read()) _dva = open(os.path.join(ROOT, ".claude/agents/doc-visual.md")).read() check("doc-visual agent D2-first (Mermaid fallback)", "D2" in _dva and "폴백" in _dva) for _l in ("consult-em", "doc-lead"): check(f"lead {_l} storyline recommends D2 exhibit", "type: d2" in open(os.path.join(ROOT, ".claude/agents", _l + ".md")).read()) # P3: skills = method-skill + capability-skill(design-craft/build-loop) 조합(정확일치 아님, 포함 검사) _dp_fm = open(os.path.join(ROOT, ".claude/agents/des-prod.md")).read().split("---\n")[1] check("des-prod skills include method + design-craft", "des-prod-method" in _dp_fm and "design-craft" in _dp_fm) _dv_fm = open(os.path.join(ROOT, ".claude/agents/doc-visual.md")).read().split("---\n")[1] check("doc-visual skills include method + design-craft + diagram-craft", all(s in _dv_fm for s in ("doc-visual-method", "design-craft", "diagram-craft"))) check("every agent now has method-skill (arch-app has arch-app-method)", "arch-app-method" in open(os.path.join(ROOT, ".claude/agents/arch-app.md")).read().split("---\n")[1]) # #17: tier -> model/effort (강한 tier = 강한 추론), SoT = governance-tiers.model-effort-by-tier sys.path.insert(0, os.path.join(ROOT, ".claude", "hooks")) _CP = _il.import_module("context_package") check("#17 light -> sonnet/low", _CP.model_effort_for_tier("light") == {"model": "sonnet", "effort": "low"}) check("#17 heavy -> opus/high (strongest reasoning)", _CP.model_effort_for_tier("heavy") == {"model": "opus", "effort": "high"}) check("#17 heavy != light (tier changes reasoning, not just agent count)", _CP.model_effort_for_tier("heavy") != _CP.model_effort_for_tier("light")) check("#17 synthesis-lead bumps effort (consult-em standard -> high)", _CP.model_effort_for_tier("standard", "consult-em")["effort"] == "high") _gt = _yaml.safe_load(open(os.path.join(ROOT, "org-os/06-agent-work/governance-tiers.yaml")))["governance-tiers"] check("#17 governance-tiers.yaml is the SoT (model-effort-by-tier present)", isinstance(_gt.get("model-effort-by-tier"), dict) and "heavy" in _gt["model-effort-by-tier"]) print("== design-system pipeline (preview_ui + /design-system 커맨드) ==") _pv = run("preview_ui.py", ["--help"]) check("preview_ui.py runs (--help)", _pv.returncode == 0 and "preview_ui" in (_pv.stdout + _pv.stderr)) check("/design-system command exists", os.path.exists(os.path.join(ROOT, ".claude/commands/design-system.md"))) # 참고: 예제 design-system 슬라이스(구 ca-tmpl/design-system, 테스트 전용 폴더)는 제거됨. # 파이프라인 로직·품질게이트(discovery·토큰순수성·대비·포커스·preview 게이트)는 # test_p1_design.py·test_p2_cascade_design.py 가 폴더 없이 커버한다. print(f"\n{passed} passed, {failed} failed") sys.exit(1 if failed else 0)