#!/usr/bin/env python3 """Generate .claude/skills/-method/SKILL.md from role-working-methods/ (P3). 절차(How I work) 층. 카드에 인라인 embed 하던 working-method 를 역할별 method-skill 로 분리. role-working-methods/(파일분리 SoT) = 유일 편집 원천, 이 스크립트 = 생성물(수기편집 금지). 렌더 분기: - v1 flat(method-contract 없음): working-method/key-frameworks/evidence/sources/self-check. - v2 contract(method-contract.version==2): 역할경계 + method profile 별 실행 계약(P3-B §10). Phase 0 실측: 중첩 skill 미발견 → generated-dir=flat(.claude/skills), skill=-method. Usage: python3 .claude/hooks/gen_method_skills.py # (재)생성 python3 .claude/hooks/gen_method_skills.py --check # drift 검증만(파일 안 씀, exit 1 on mismatch) """ import glob import os import shutil import sys import yaml ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) REG = os.path.join(ROOT, "org-os", "00-role-registry") RWM_DIR = os.path.join(REG, "role-working-methods") PROFILES = os.path.join(REG, "role-profiles.yaml") REGISTRY = os.path.join(REG, "method-skill-registry.yaml") GEN_HEADER = ("") def _load(p): return yaml.safe_load(open(p)) or {} def _registry(): return _load(REGISTRY)["method-skill-registry"] def _gen_dir(): return os.path.join(ROOT, _registry()["generated-dir"]) def load_role_methods(): """role-working-methods/index.includes 병합 → {role-id: entry}. 중복/미include=에러.""" idx = _load(os.path.join(RWM_DIR, "index.yaml"))["role-method-contracts"] merged, srcs = {}, {} for inc in idx["includes"]: d = _load(os.path.join(RWM_DIR, inc)) for rid, entry in (d.get("role-working-methods") or {}).items(): assert rid not in merged, f"중복 role-id {rid} ({srcs.get(rid)} & {inc})" merged[rid] = entry srcs[rid] = inc return merged def _frontmatter(rid, role_name, skill_name): desc = (f"Use when working AS the {role_name} ({rid}) role — the step-by-step working " f"method/contract, frameworks, and evidence for this role. " f"Auto-loaded via the {rid.lower()} agent's skills: frontmatter.") return ("---\n" f"name: {skill_name}\n" f"description: \"{desc.replace(chr(34), chr(39))}\"\n" f"generated-from: role-working-methods/#{rid}\n" "---\n" f"{GEN_HEADER}\n") def _render_v1(rid, wm, role_name): L = [f"# {role_name} ({rid}) 실무 절차 (일하는 방식)", "", "## 절차 (working-method)"] L += [f"- {s}" for s in (wm.get("working-method") or [])] if wm.get("key-frameworks"): L += ["", "## 주요 프레임워크"] + [f"- {f}" for f in wm["key-frameworks"]] if wm.get("evidence-they-use"): L += ["", "## 판단 근거 자료 (evidence)"] + [f"- {e}" for e in wm["evidence-they-use"]] if wm.get("sources"): L += ["", "## 참고 출처"] + [f"- {u}" for u in wm["sources"]] if wm.get("self-check"): # optional, role-specific only. 공통 불변식 금지. L += ["", "## 자기검증 (self-check) — 역할 고유 검증만"] + [f"- {c}" for c in wm["self-check"]] return "\n".join(L) def _render_v2(rid, entry, role_name): """Contract v2: 역할경계 + method profile 별 실행 계약(P3-B §10).""" L = [f"# {role_name} ({rid}) 실무 계약 (Contract v2)"] rb = entry.get("role-boundary") or {} if rb: L += ["", "## 역할 경계", f"- owns: {', '.join(rb.get('owns', []))}", f"- not-owns: {', '.join(rb.get('not-owns', []))}"] for m in entry.get("methods", []): tt = ", ".join((m.get("applies-when") or {}).get("task-types", [])) L += ["", f"## Method: {m['method-id']} (task-types: {tt})"] if m.get("required-inputs"): L.append("### 필수 입력") L += [f"- {i.get('artifact-type')}{' (optional)' if i.get('optional') else ''}" for i in m["required-inputs"]] if m.get("workflow"): L.append("### 워크플로") for s in m["workflow"]: uc = s.get("uses-capability") or {} head = (f"- **{s['step-id']}**: {s.get('objective', '')}" + (f" · 기법 `{uc.get('skill-id')}#{uc.get('section-id')}`" if uc else "") + (f" · 산출 {s.get('required-output')}" if s.get("required-output") else "") + (" · skippable" if s.get("skippable") else "")) L.append(head) gates = s.get("completion-gates") or {} for g in gates.get("machine", []): L.append(f" - [machine:{g.get('enforcement', 'hard')}] {g.get('gate-id')}: " f"{g.get('check')} {g.get('artifact', '')}.{g.get('field', '')}") for g in gates.get("judgment", []): L.append(f" - [judgment] {g.get('gate-id')}: {g.get('criterion', '')} " f"(reviewer {g.get('reviewer-role', '')})") for key, title in [("decision-rules", "판단 규칙"), ("evidence-policy", "근거 정책"), ("alternatives-policy", "대안 정책"), ("output-artifacts", "산출물"), ("prohibited-shortcuts", "금지(shortcuts)"), ("escalation-conditions", "에스컬레이션"), ("self-check", "자기검증(역할 고유)")]: v = m.get(key) if not v: continue L.append(f"### {title}") if isinstance(v, list): L += [f"- {x}" for x in v] elif isinstance(v, dict): L += [f"- {k}: {vv}" for k, vv in v.items()] else: L.append(f"- {v}") if m.get("handoff-contract"): L.append("### Handoff (profile-to-profile)") for h in m["handoff-contract"]: to = h.get("to") or {} L.append(f"- {h.get('edge-id')}: -> {to.get('role-id')}/{to.get('method-id')}") # provenance 꼬리: v1 방법론 계보(프레임워크·근거·출처)를 보존한다 — 계약이 어디서 왔는지 # 추적선. 계약 본문(role-boundary/methods)이 절차를 규정하고, 이 절은 그 근거의 출처다. prov = [(k, t) for k, t in [("key-frameworks", "프레임워크 계보"), ("evidence-they-use", "근거 종류"), ("sources", "출처(웹조사 provenance)")] if entry.get(k)] if prov: L.append("") L.append("## 참고 출처 (provenance)") for key, title in prov: L.append(f"### {title}") L += [f"- {x}" for x in entry[key]] return "\n".join(L) def method_skill_md(rid, entry, prof, skill_name): role_name = prof.get("role-name", rid) is_v2 = (entry.get("method-contract") or {}).get("version") == 2 body = _render_v2(rid, entry, role_name) if is_v2 else _render_v1(rid, entry, role_name) return _frontmatter(rid, role_name, skill_name) + "\n" + body.rstrip() + "\n" def build_all(): """{skill_name: content_str} for every registry role. SoT=role-working-methods/.""" entries = load_role_methods() profiles = {p["role-id"]: p for p in _load(PROFILES)["role-profiles"]["profiles"]} roles = _registry()["roles"] out = {} for rid, r in roles.items(): entry = entries.get(rid) assert entry, f"role-working-methods/ 에 {rid} 없음(registry가 참조)" out[r["method-skill"]] = method_skill_md(rid, entry, profiles.get(rid, {}), r["method-skill"]) return out def main(): check = "--check" in sys.argv out = build_all() gen_dir = _gen_dir() if check: problems = [] for skill, content in out.items(): p = os.path.join(gen_dir, skill, "SKILL.md") if not os.path.exists(p): problems.append(f"missing: {skill}") elif open(p).read() != content: problems.append(f"drift: {skill}") for d in glob.glob(os.path.join(gen_dir, "*", "SKILL.md")): name = os.path.basename(os.path.dirname(d)) if name.endswith("-method") and name not in out: problems.append(f"orphan: {name}") if problems: print("GEN-METHOD-SKILLS CHECK FAIL: %d건" % len(problems)) for p in problems: print(f" - {p}") return 1 print(f"OK gen_method_skills --check: {len(out)} method-skills match SoT") return 0 # write: method-skill 디렉터리만 정리(수제 skill 보존) for d in glob.glob(os.path.join(gen_dir, "*")): if os.path.isdir(d) and os.path.basename(d).endswith("-method"): shutil.rmtree(d) for skill, content in out.items(): sd = os.path.join(gen_dir, skill) os.makedirs(sd, exist_ok=True) with open(os.path.join(sd, "SKILL.md"), "w") as f: f.write(content) print(f"OK gen_method_skills: {len(out)} method-skills written -> {gen_dir}") return 0 if __name__ == "__main__": sys.exit(main())