Files
company-haness/docs/superpowers/plans/2026-07-13-p3-prompt-skill-separation.md
T

1189 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P3-A 프롬프트/skill 분리 (구조 인프라) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 에이전트 카드에 인라인 중복 삽입되던 실무 절차(working-method)를 역할별 method-skill로 분리해, 카드=정체성·경계 / skill=절차 / context-package=문맥 3층으로 나눈다. **범위=구조 이동(P3-A)** — 절차 내용을 재작성·심화하지 않는다. 절차를 실행 계약으로 강화하는 것은 **P3-B**(spec §17, 별도 사이클).
**Architecture:** `role-working-methods.yaml`(유일 편집 SoT) → `gen_method_skills.py`가 역할별 `.claude/skills/generated/<role>-method/SKILL.md` 생성 → `method-skill-registry.yaml`(role→skill 배선 SoT) → `gen_agents.py`가 카드에 얇은 spine + `skills:` frontmatter 방출. 무결성은 `doctor.py`·`lint_refs.py` 게이트가 강제(참조 실존·고아0·drift0). 품질 중립(절차 내용 위치만 이동).
**Tech Stack:** Python 3(stdlib + PyYAML), 기존 하네스 hook 패턴. 테스트=standalone `check()` 컨벤션(pytest 아님), `run_all.py``test_*.py` 자동 발견.
**Spec:** `docs/superpowers/specs/2026-07-13-p3-prompt-skill-separation-design.md`
## Global Constraints
- **품질 중립(내용 불변)**: working-method 문장을 재작성·개선하지 않는다. 카드 embed → skill로 **위치만 이동**. 생성 skill 본문 = YAML의 working-method/key-frameworks/evidence-they-use/sources를 그대로 옮긴 것.
- **공통 하네스 불변식 재복제 금지**: 고유관점/비종합/근거+반증/실물≠요약은 카드의 기존 전용 섹션(`## When invoked`·`## Fan-out 워커 계약`·`## Output contract`)에만 유지. spine·생성 skill self-check에 재나열 금지.
- **spine = 역할-파생 얇은 3줄**: `working-method[0]`(essence) + `key-frameworks[:3]` **이름만**(정의 문장 없음) + method-skill pointer/load-guard. 그 외 절차 문장 금지.
- **생성물 편집 금지**: `.claude/skills/generated/*/SKILL.md`·`.claude/agents/*.md`는 생성물. 헤더 경고 + `--check` drift 게이트. 수기 편집 금지.
- **협업역할 3정책**: 워커(43)=자기 method-skill / lead(3)=자기 method-skill만 / router(10)=멤버 method 제거·pointer table·family capability만 / family agent(16, 실제 수행자)=멤버 method-skill union + family capability.
- **불변 개수**: 에이전트 72(43 워커 + 3 lead + 16 family + 10 router), 역할 75, method-skill 75(=agent-bound 역할). 75==75==75.
- **registry 위치**: `org-os/00-role-registry/method-skill-registry.yaml`(다른 registry와 동거). generated-dir: `.claude/skills/generated`(Phase 0 확정).
- **실행 환경**: 테스트는 `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox`. 커밋 trailer: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`. 커밋 전 churn된 render fixture는 `git checkout -- .claude/tests/fixtures/`로 복구.
- **생성 순서**: gen_method_skills(skill 먼저 실존) → gen_agents(카드가 skill 참조). doctor가 순서 위반(참조 있는데 skill 없음)을 FAIL.
## File Structure
**신설:**
- `org-os/00-role-registry/method-skill-registry.yaml` — role→method-skill(+capability-skills), family→policy 배선 SoT.
- `.claude/hooks/gen_method_skills.py` — role-working-methods.yaml → 생성 method-skill(+`--check` drift).
- `.claude/hooks/skill_refs.py` — 공유 헬퍼(`.claude/skills/**` 스캔 → skill 이름 집합, `skills:` frontmatter 파싱). doctor·lint_refs가 소비.
- `.claude/skills/generated/<role>-method/SKILL.md` × 75 — 생성물.
- `.claude/tests/test_p3_prompt_skill_separation.py` — 16 checks.
**개정:**
- `org-os/00-role-registry/role-working-methods.yaml` — optional `self-check` 필드 additive + DES-PROD exemplar.
- `.claude/hooks/gen_agents.py``wm_block``method_spine`, `skills_fm_line` registry 파생, router pointer table, craft_block 축소, assert 갱신.
- `.claude/hooks/doctor.py``check_method_skill_wiring` 신설 + main 등록.
- `.claude/hooks/lint_refs.py` — 에이전트 `skills:` 참조 해소 추가.
- `.claude/tests/test_enforcement.py` — craft/skills assert 갱신(마이그레이션).
- `CLAUDE.md` — 구조·gen 흐름·검증 섹션 갱신.
---
## Task 0: Phase 0 — skill 발견·auto-load 실측 (게이트, controller 수행)
**Files:** (임시 probe — 커밋 안 함)
이 태스크는 런타임 동작(subagent가 `skills:` frontmatter를 auto-load하는가) 검증이라 pytest가 아니라 **controller가 live subagent로 실측**한다. 결과가 이후 모든 태스크의 `generated-dir` 레이아웃을 확정한다.
- [ ] **Step 1: probe skill 생성**
```bash
mkdir -p .claude/skills/generated/probe-method
cat > .claude/skills/generated/probe-method/SKILL.md <<'EOF'
---
name: probe-method
description: Use when the user says the exact token PROBE-P3 — respond only with the sentinel.
---
# Probe
When invoked, reply with exactly: PROBE-LOADED-OK-7F3A
EOF
```
- [ ] **Step 2: probe agent 생성**
```bash
cat > .claude/agents/probe-p3.md <<'EOF'
---
name: probe-p3
description: P3 phase-0 probe. Use only for the PROBE-P3 discovery test.
tools: Read
model: inherit
skills: [probe-method]
---
당신은 probe입니다. probe-method skill의 지시를 따르세요.
EOF
```
- [ ] **Step 3: controller가 probe 서브에이전트를 dispatch**
controller는 `probe-p3` 에이전트를 "PROBE-P3" 입력으로 띄운다. sentinel `PROBE-LOADED-OK-7F3A`를 반환하면 **중첩 `.claude/skills/generated/` auto-load 확인**.
- [ ] **Step 4: 레이아웃 확정 + probe 제거**
- sentinel 반환 → `generated-dir: .claude/skills/generated`(spec 기본) 확정.
- 반환 안 됨 → **flat 폴백**: `generated-dir: .claude/skills` + skill 이름 규약 `<role>-method`(디자인 3종과 flat 동거, 이름으로 그룹). registry `generated-dir` 한 줄만 다르다.
- 결과를 `.superpowers/sdd/progress.md`에 기록(이후 태스크가 참조). probe 파일 제거:
```bash
rm -rf .claude/skills/generated/probe-method .claude/agents/probe-p3.md
```
> 이후 태스크 코드는 `reg["generated-dir"]`을 읽어 레이아웃 무관하게 동작한다. Phase 0은 그 한 값을 확정할 뿐이다.
---
## Task 1: method-skill-registry.yaml
**Files:**
- Create: `org-os/00-role-registry/method-skill-registry.yaml`
- Test: `.claude/tests/test_p3_prompt_skill_separation.py` (Task 7에서 full; 여기선 아래 스텝의 인라인 검증만)
**Interfaces:**
- Produces: `method-skill-registry.yaml``{version, generated-dir, method-skill-suffix, roles: {ROLE: {method-skill, capability-skills}}, families: {FAM: {policy, lead?, capability-skills}}}`. gen_method_skills·gen_agents·doctor가 소비.
- [ ] **Step 1: 완전성 확인 테스트 작성 (임시 스크립트)**
`/tmp/claude-*/scratchpad/t1.py`:
```python
import yaml
reg = yaml.safe_load(open("org-os/00-role-registry/method-skill-registry.yaml"))["method-skill-registry"]
fams = yaml.safe_load(open("org-os/00-role-registry/capability-families.yaml"))["capability-families"]["families"]
bound = set()
for f in fams: bound |= set(f["member-role-ids"])
roles = reg["roles"]
assert bound == set(roles), f"registry roles != agent-bound: {bound ^ set(roles)}"
assert all(r["method-skill"] == rid.lower()+"-method" for rid, r in roles.items()), "method-skill 이름 규약 위반"
famset = {f["family-id"] for f in fams}
assert set(reg["families"]) == famset, "families 키 불일치"
print("OK task1 registry", len(roles), "roles", len(reg["families"]), "families")
```
- [ ] **Step 2: 실패 확인**
Run: `python3 /tmp/claude-*/scratchpad/t1.py`
Expected: FAIL (파일 없음).
- [ ] **Step 3: registry 작성**
`org-os/00-role-registry/method-skill-registry.yaml` (아래 전체 — 75 roles + 28 families):
```yaml
# method-skill-registry — role→skill 배선의 단일 정본(SoT). (P3)
# role-working-methods.yaml 이 절차 SoT, gen_method_skills 가 method-skill 을 생성,
# gen_agents 가 이 파일로 카드의 skills: frontmatter 를 방출한다.
# 기존 gen_agents 하드코딩(CRAFT/SKILLS_FM/IMPL_FAMILIES)을 이 registry 로 흡수·단일화.
# capability-skills = 수제 공용 skill(design-craft/build-loop/diagram-craft).
# 멤버 method-skill 은 member-role-ids 에서 파생하므로 families 에 재나열하지 않는다.
method-skill-registry:
version: 1
generated-dir: .claude/skills/generated # Phase 0(Task 0)에서 확정. flat 폴백이면 .claude/skills
method-skill-suffix: -method
roles:
EXEC-CEO: { method-skill: exec-ceo-method, capability-skills: [] }
OPS-ORCH: { method-skill: ops-orch-method, capability-skills: [] }
EXEC-CTO: { method-skill: exec-cto-method, capability-skills: [] }
EXEC-CPO: { method-skill: exec-cpo-method, capability-skills: [] }
EXEC-CFO: { method-skill: exec-cfo-method, capability-skills: [] }
EXEC-COO: { method-skill: exec-coo-method, capability-skills: [] }
EXEC-CPTO: { method-skill: exec-cpto-method, capability-skills: [] }
EXEC-VPENG: { method-skill: exec-vpeng-method, capability-skills: [] }
PROD-PM: { method-skill: prod-pm-method, capability-skills: [] }
PROD-PO: { method-skill: prod-po-method, capability-skills: [] }
PROD-TPO: { method-skill: prod-tpo-method, capability-skills: [] }
PROD-PPO: { method-skill: prod-ppo-method, capability-skills: [] }
UX-RESEARCHER: { method-skill: ux-researcher-method, capability-skills: [] }
DATA-ANALYST: { method-skill: data-analyst-method, capability-skills: [] }
DES-DIRECTOR: { method-skill: des-director-method, capability-skills: [] }
DES-PROD: { method-skill: des-prod-method, capability-skills: [design-craft] }
DES-PLATFORM: { method-skill: des-platform-method, capability-skills: [design-craft] }
DES-INTERNAL: { method-skill: des-internal-method, capability-skills: [design-craft] }
DES-VISUAL: { method-skill: des-visual-method, capability-skills: [design-craft] }
STR-ANALYST: { method-skill: str-analyst-method, capability-skills: [] }
ENG-FE: { method-skill: eng-fe-method, capability-skills: [] }
ENG-FEPLAT: { method-skill: eng-feplat-method, capability-skills: [] }
ENG-FEUX: { method-skill: eng-feux-method, capability-skills: [] }
ENG-BE: { method-skill: eng-be-method, capability-skills: [] }
ENG-BEGEN: { method-skill: eng-begen-method, capability-skills: [] }
ENG-PRODSERVER: { method-skill: eng-prodserver-method, capability-skills: [] }
ENG-PLATSERVER: { method-skill: eng-platserver-method, capability-skills: [] }
ENG-PRODUCTMINDED: { method-skill: eng-productminded-method, capability-skills: [] }
ENG-SW: { method-skill: eng-sw-method, capability-skills: [] }
ENG-DESKTOP: { method-skill: eng-desktop-method, capability-skills: [] }
ENG-PRODCHAPTER: { method-skill: eng-prodchapter-method, capability-skills: [] }
INFRA-DEV: { method-skill: infra-dev-method, capability-skills: [] }
INFRA-PLATFORM: { method-skill: infra-platform-method, capability-skills: [] }
INFRA-DEVOPS: { method-skill: infra-devops-method, capability-skills: [] }
SRE: { method-skill: sre-method, capability-skills: [] }
SEC-DEVSECOPS: { method-skill: sec-devsecops-method, capability-skills: [] }
ARCH-EA: { method-skill: arch-ea-method, capability-skills: [] }
ARCH-SOLUTION: { method-skill: arch-solution-method, capability-skills: [] }
ARCH-APP: { method-skill: arch-app-method, capability-skills: [] }
ARCH-TECH: { method-skill: arch-tech-method, capability-skills: [] }
ARCH-IT: { method-skill: arch-it-method, capability-skills: [] }
ARCH-SYSANALYST: { method-skill: arch-sysanalyst-method, capability-skills: [] }
ARCH-SWAT: { method-skill: arch-swat-method, capability-skills: [] }
ARCH-BA: { method-skill: arch-ba-method, capability-skills: [] }
ARCH-BIZANALYST: { method-skill: arch-bizanalyst-method, capability-skills: [] }
ARCH-DATA: { method-skill: arch-data-method, capability-skills: [] }
DATA-ENGINEER: { method-skill: data-engineer-method, capability-skills: [] }
DATA-BIGDATA: { method-skill: data-bigdata-method, capability-skills: [] }
QA: { method-skill: qa-method, capability-skills: [] }
SEC-ENGINEER: { method-skill: sec-engineer-method, capability-skills: [] }
SEC-APPSEC: { method-skill: sec-appsec-method, capability-skills: [] }
SEC-CHAMPION: { method-skill: sec-champion-method, capability-skills: [] }
OPS-CH: { method-skill: ops-ch-method, capability-skills: [] }
OPS-CREW: { method-skill: ops-crew-method, capability-skills: [] }
GTM-GROWTHPM: { method-skill: gtm-growthpm-method, capability-skills: [] }
GTM-DEMANDGEN: { method-skill: gtm-demandgen-method, capability-skills: [] }
GTM-PMM: { method-skill: gtm-pmm-method, capability-skills: [] }
GTM-CI: { method-skill: gtm-ci-method, capability-skills: [] }
GTM-SALES: { method-skill: gtm-sales-method, capability-skills: [] }
GTM-CS: { method-skill: gtm-cs-method, capability-skills: [] }
GTM-PARTNER: { method-skill: gtm-partner-method, capability-skills: [] }
GTM-REVOPS: { method-skill: gtm-revops-method, capability-skills: [] }
GTM-PRICING: { method-skill: gtm-pricing-method, capability-skills: [] }
GTM-LEGAL: { method-skill: gtm-legal-method, capability-skills: [] }
CONSULT-EM: { method-skill: consult-em-method, capability-skills: [] }
CONSULT-STRAT: { method-skill: consult-strat-method, capability-skills: [] }
CONSULT-OPS: { method-skill: consult-ops-method, capability-skills: [] }
CONSULT-ORG: { method-skill: consult-org-method, capability-skills: [] }
CONSULT-DIGITAL: { method-skill: consult-digital-method, capability-skills: [] }
CONSULT-FIN: { method-skill: consult-fin-method, capability-skills: [] }
DOC-LEAD: { method-skill: doc-lead-method, capability-skills: [] }
DOC-WRITER: { method-skill: doc-writer-method, capability-skills: [] }
DOC-IA: { method-skill: doc-ia-method, capability-skills: [] }
DOC-VISUAL: { method-skill: doc-visual-method, capability-skills: [design-craft, diagram-craft] }
DOC-EDU: { method-skill: doc-edu-method, capability-skills: [] }
families:
FAM-CEO: { policy: fan-out, capability-skills: [] }
FAM-ORCH: { policy: n/a, capability-skills: [] }
FAM-CTO: { policy: fan-out, capability-skills: [] }
FAM-CPO: { policy: fan-out, capability-skills: [] }
FAM-CFO: { policy: fan-out, capability-skills: [] }
FAM-COO: { policy: fan-out, capability-skills: [] }
FAM-CPTO: { policy: fan-out, capability-skills: [] }
FAM-VPENG: { policy: fan-out, capability-skills: [] }
FAM-PRODUCT-MGMT: { policy: fan-out, capability-skills: [] }
FAM-UX-RESEARCH: { policy: fan-out, capability-skills: [] }
FAM-DESIGN: { policy: fan-out, lead: DES-DIRECTOR, capability-skills: [] }
FAM-STRATEGY: { policy: fan-out, capability-skills: [] }
FAM-ENG-FRONTEND: { policy: collapse, capability-skills: [build-loop] }
FAM-ENG-BACKEND: { policy: collapse, capability-skills: [build-loop] }
FAM-ENG-SPECIAL: { policy: collapse, capability-skills: [build-loop] }
FAM-PLATFORM-INFRA: { policy: collapse, capability-skills: [build-loop] }
FAM-ARCHITECTURE-TECH: { policy: fan-out, capability-skills: [] }
FAM-ARCHITECTURE-BIZ: { policy: fan-out, capability-skills: [] }
FAM-DATA: { policy: fan-out, capability-skills: [] }
FAM-QA: { policy: collapse, capability-skills: [] }
FAM-SECURITY: { policy: fan-out, capability-skills: [] }
FAM-OPS-DELIVERY: { policy: collapse, capability-skills: [] }
FAM-GTM-GROWTH: { policy: fan-out, capability-skills: [] }
FAM-GTM-SALES: { policy: fan-out, capability-skills: [] }
FAM-REVOPS: { policy: fan-out, capability-skills: [] }
FAM-LEGAL: { policy: fan-out, capability-skills: [] }
FAM-CONSULTING: { policy: fan-out, lead: CONSULT-EM, capability-skills: [] }
FAM-DOC-CONSULT: { policy: fan-out, lead: DOC-LEAD, capability-skills: [] }
```
> 참고: 이 내용은 `capability-families.yaml`의 member-role-ids로부터 기계적으로 산출된 것(계획 저자가 스크립트로 생성). 이후 새 역할이 생기면 이 파일을 직접 편집하고 doctor가 sync를 강제한다.
- [ ] **Step 4: 통과 확인**
Run: `python3 /tmp/claude-*/scratchpad/t1.py`
Expected: `OK task1 registry 75 roles 28 families`
- [ ] **Step 5: Commit**
```bash
git add org-os/00-role-registry/method-skill-registry.yaml
git commit -m "P3 Task1: method-skill-registry.yaml (75 roles + 28 families, CRAFT/IMPL 흡수)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 2: gen_method_skills.py + optional self-check 필드 + 75 skill 생성
**Files:**
- Create: `.claude/hooks/gen_method_skills.py`
- Modify: `org-os/00-role-registry/role-working-methods.yaml` (DES-PROD에 optional `self-check` 추가 + field-schema 주석)
- Create: `.claude/skills/generated/<role>-method/SKILL.md` × 75 (생성물)
**Interfaces:**
- Produces: `gen_method_skills.build_all() -> {skill_name: content_str}`; CLI `--check`(drift, exit 1 on mismatch) / 무인자(생성). doctor가 `--check`를 subprocess로 호출.
- [ ] **Step 1: self-check 필드 추가 (role-working-methods.yaml)**
`field-schema` 블록에 한 줄 추가(파일 상단):
```yaml
self-check: (선택) 역할 고유 자기검증 문항 — 공통 하네스 불변식은 넣지 않는다(카드 계약 섹션이 담당)
```
DES-PROD 엔트리 끝(`sources:` 리스트 다음)에 exemplar 추가:
```yaml
self-check:
- 사용자 행동 근거 없이 시각 취향으로 결정하지 않았나?
- 핵심 흐름과 예외 상태를 모두 설계했나?
- generic한 "modern/clean" 표현으로 방향을 대체하지 않았나?
```
- [ ] **Step 2: 실패 테스트 작성 (임시)**
`/tmp/claude-*/scratchpad/t2.py`:
```python
import importlib.util, os
spec = importlib.util.spec_from_file_location("g", ".claude/hooks/gen_method_skills.py")
g = importlib.util.module_from_spec(spec); spec.loader.exec_module(g)
out = g.build_all()
assert len(out) == 75, len(out)
c = out["des-prod-method"]
assert c.startswith("---\nname: des-prod-method\n"), "frontmatter"
assert "generated-from: role-working-methods.yaml#DES-PROD" in c
assert "Double Diamond" in c, "working-method 내용 보존"
assert "## 자기검증" in c and "시각 취향" in c, "self-check role-specific"
# 공통 불변식은 self-check 에 없어야
assert "다른 역할" not in c.split("## 자기검증")[1], "공통 불변식 재복제 금지"
# self-check 없는 역할은 섹션 생략
assert "## 자기검증" not in out["arch-app-method"], "self-check optional"
print("OK task2", len(out))
```
- [ ] **Step 3: 실패 확인**
Run: `python3 /tmp/claude-*/scratchpad/t2.py`
Expected: FAIL (모듈 없음).
- [ ] **Step 4: gen_method_skills.py 작성**
```python
#!/usr/bin/env python3
"""Generate .claude/skills/generated/<role>-method/SKILL.md from role-working-methods.yaml. (P3)
절차(How I work) 층. 카드에 인라인 embed 하던 working-method 를 역할별 method-skill 로
분리한다. role-working-methods.yaml = 유일 편집 SoT, 이 스크립트 = 생성물(수기편집 금지).
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")
WORKING_METHODS = os.path.join(REG, "role-working-methods.yaml")
PROFILES = os.path.join(REG, "role-profiles.yaml")
REGISTRY = os.path.join(REG, "method-skill-registry.yaml")
GEN_HEADER = ("<!-- GENERATED from role-working-methods.yaml — do not edit. "
"Rerun: python3 .claude/hooks/gen_method_skills.py -->")
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 _first_sentence(text, n=140):
s = str(text or "").strip().split(". ")[0]
return (s[:n] + "…") if len(s) > n else s
def method_skill_md(rid, wm, prof, skill_name):
role_name = prof.get("role-name", rid)
desc = (f"Use when working AS the {role_name} ({rid}) role — the step-by-step working "
f"method, frameworks, and evidence types for this role. "
f"Auto-loaded via the {rid.lower()} agent's skills: frontmatter.")
lines = [
"---",
f"name: {skill_name}",
f"description: \"{desc.replace(chr(34), chr(39))}\"",
f"generated-from: role-working-methods.yaml#{rid}",
"---",
GEN_HEADER,
"",
f"# {role_name} ({rid}) 실무 절차 (일하는 방식)",
"",
"## 절차 (working-method)",
]
for s in (wm.get("working-method") or []):
lines.append(f"- {s}")
if wm.get("key-frameworks"):
lines += ["", "## 주요 프레임워크"]
lines += [f"- {f}" for f in wm["key-frameworks"]]
if wm.get("evidence-they-use"):
lines += ["", "## 판단 근거 자료 (evidence)"]
lines += [f"- {e}" for e in wm["evidence-they-use"]]
if wm.get("sources"):
lines += ["", "## 참고 출처"]
lines += [f"- {u}" for u in wm["sources"]]
if wm.get("self-check"): # optional, role-specific only (§5). 공통 불변식 금지.
lines += ["", "## 자기검증 (self-check) — 역할 고유 검증만"]
lines += [f"- {c}" for c in wm["self-check"]]
return "\n".join(lines).rstrip() + "\n"
def build_all():
"""{skill_name: content_str} for every registry role (=agent-bound). SoT=YAML."""
wm_root = _load(WORKING_METHODS).get("role-working-methods") or {}
profiles = {p["role-id"]: p for p in _load(PROFILES)["role-profiles"]["profiles"]}
roles = _registry()["roles"]
out = {}
for rid, r in roles.items():
wm = wm_root.get(rid)
assert wm, f"role-working-methods.yaml 에 {rid} 없음(registry가 참조)"
out[r["method-skill"]] = method_skill_md(rid, wm, 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))
# flat 폴백 시 수제 skill(design-craft 등)은 제외 — method-skill 만 대상
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())
```
- [ ] **Step 5: 통과 확인 (build_all)**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 /tmp/claude-*/scratchpad/t2.py`
Expected: `OK task2 75`
- [ ] **Step 6: 75 skill 생성 + drift 자기확인**
```bash
CLAUDE_PROJECT_DIR="$PWD" python3 .claude/hooks/gen_method_skills.py
CLAUDE_PROJECT_DIR="$PWD" python3 .claude/hooks/gen_method_skills.py --check
ls .claude/skills/generated | wc -l # 75
```
Expected: 생성 75 → `--check` OK → 75.
- [ ] **Step 7: Commit**
```bash
git add .claude/hooks/gen_method_skills.py org-os/00-role-registry/role-working-methods.yaml .claude/skills/generated
git commit -m "P3 Task2: gen_method_skills.py + optional self-check + 75 생성 method-skill
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 3: skill_refs.py 공유 헬퍼
**Files:**
- Create: `.claude/hooks/skill_refs.py`
- Test: 임시 인라인.
**Interfaces:**
- Produces: `known_skill_names(root) -> set[str]`(`.claude/skills/**/SKILL.md` 스캔, frontmatter name + 디렉터리명), `parse_skills(fm_value) -> list[str]`. doctor·lint_refs가 소비.
- [ ] **Step 1: 실패 테스트 (임시)**
`/tmp/claude-*/scratchpad/t3.py`:
```python
import importlib.util, os
spec = importlib.util.spec_from_file_location("sr", ".claude/hooks/skill_refs.py")
sr = importlib.util.module_from_spec(spec); spec.loader.exec_module(sr)
names = sr.known_skill_names(os.getcwd())
assert "des-prod-method" in names and "design-craft" in names, sorted(names)[:5]
assert sr.parse_skills(["a", "b"]) == ["a", "b"]
assert sr.parse_skills(None) == []
print("OK task3", len(names))
```
- [ ] **Step 2: 실패 확인** — Run: `python3 /tmp/claude-*/scratchpad/t3.py` → FAIL(모듈 없음).
- [ ] **Step 3: skill_refs.py 작성**
```python
#!/usr/bin/env python3
"""skill_refs — .claude/skills 참조 무결성 공유 헬퍼 (P3). doctor·lint_refs 소비."""
import glob
import os
import yaml
def known_skill_names(root):
""".claude/skills/**/SKILL.md 를 스캔해 알려진 skill 이름 집합(frontmatter name + 디렉터리명)."""
names = set()
base = os.path.join(root, ".claude", "skills")
for p in glob.glob(os.path.join(base, "**", "SKILL.md"), recursive=True):
names.add(os.path.basename(os.path.dirname(p)))
try:
fm = yaml.safe_load(p and open(p).read().split("---\n")[1]) or {}
if isinstance(fm, dict) and fm.get("name"):
names.add(str(fm["name"]))
except Exception: # noqa: BLE001 — 깨진 frontmatter는 디렉터리명으로만 등록
pass
return names
def parse_skills(val):
"""agent frontmatter 의 skills: 값 -> 이름 리스트. list/문자열('[a, b]') 모두 허용."""
if not val:
return []
if isinstance(val, list):
return [str(x).strip() for x in val if str(x).strip()]
return [s.strip() for s in str(val).strip("[]").split(",") if s.strip()]
```
- [ ] **Step 4: 통과 확인** — Run: `python3 /tmp/claude-*/scratchpad/t3.py``OK task3 <N>`.
- [ ] **Step 5: Commit**
```bash
git add .claude/hooks/skill_refs.py
git commit -m "P3 Task3: skill_refs.py 공유 헬퍼(skill 이름 스캔·skills frontmatter 파싱)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 4: gen_agents.py 개정 (spine + registry skills + router pointer) + 72 재생성
**Files:**
- Modify: `.claude/hooks/gen_agents.py`
- Regenerate: `.claude/agents/*.md` (72)
**Interfaces:**
- Consumes: `method-skill-registry.yaml`(MREG), `skill_refs`(불요), `role-working-methods.yaml`(wm_map).
- Produces: 72 카드 — 워커/lead에 `skills:[method,..]` + `## 핵심 작업 방법`; router에 `skills:[family capability]` + `## fan-out 멤버 → method-skill`; family에 `skills:[member methods union,..]` + spine. `## 일하는 방식` full embed 제거.
- [ ] **Step 1: 실패 테스트 (임시)**
`/tmp/claude-*/scratchpad/t4.py`:
```python
import subprocess, os
r = subprocess.run(["python3", ".claude/hooks/gen_agents.py"], env={**os.environ, "CLAUDE_PROJECT_DIR": os.getcwd()}, capture_output=True, text=True)
print(r.stdout, r.stderr)
assert r.returncode == 0, "gen_agents failed"
dp = open(".claude/agents/des-prod.md").read()
assert "## 핵심 작업 방법" in dp and "## 일하는 방식" not in dp, "spine not swapped"
assert "des-prod-method" in dp and "design-craft" in dp, "skills"
# 공통 불변식 spine 재나열 금지
spine = dp.split("## 핵심 작업 방법")[1].split("##")[0]
assert "다른 역할" not in spine and "반증" not in spine, "공통 불변식 spine 재복제"
fd = open(".claude/agents/fam-design.md").read() # router
assert "fan-out 멤버 → method-skill" in fd, "router pointer"
assert "## 일하는 방식" not in fd, "router still has member method embed"
assert "des-prod-method" in fd, "router pointer names skill"
fb = open(".claude/agents/fam-eng-backend.md").read() # collapse family
assert "eng-be-method" in fb, "collapse family loads member method-skill"
print("OK task4")
```
- [ ] **Step 2: 실패 확인** — Run: `python3 /tmp/claude-*/scratchpad/t4.py` → FAIL(현재 카드엔 `## 일하는 방식`).
- [ ] **Step 3: gen_agents.py 개정**
**(a) 모듈 상단** — registry 로드용 전역 추가 + frontmatter 하드코딩 dict 제거. **`SKILLS_FM`·`IMPL_FAMILIES` 삭제**(registry가 대체). **`CRAFT` dict은 유지**(`craft_block`이 body 리마인더 선택에 계속 사용 — frontmatter가 아님). 아래 추가(파일 상단 `TOOLS = {}` 근처):
```python
MREG = {} # method-skill-registry (main 에서 로드; TOOLS 패턴과 동일)
REGISTRY = os.path.join(REG, "method-skill-registry.yaml")
def load_method_registry(fams):
root = yaml.safe_load(open(REGISTRY))["method-skill-registry"]
roles, families = root["roles"], root["families"]
fam_ids = {f["family-id"] for f in fams}
for fid in families:
assert fid in fam_ids, f"method-skill-registry.families 미지 family-id: {fid}"
bound = set()
for f in fams:
bound |= set(f["member-role-ids"])
missing = bound - set(roles)
assert not missing, f"method-skill-registry.roles 미등록 agent-bound 역할: {sorted(missing)}"
return root
def _fw_name(fw):
"""key-frameworks 항목에서 이름만 추출('OKR (전략...)' -> 'OKR', 'design-brief (...): ...' -> 'design-brief')."""
return str(fw).split(" (")[0].split(":")[0].strip()
def method_spine(rids, wm_map, names=None):
"""카드에 남는 얇은 절차 잔여(역할-파생). 공통 불변식 재나열 안 함(§7)."""
entries = [(rid, wm_map.get(rid)) for rid in rids if wm_map.get(rid)]
if not entries:
return ""
multi = len(entries) > 1
lines = ["## 핵심 작업 방법 (전체 절차는 skill)"]
for rid, wm in entries:
if multi:
lines.append(f"### {(names or {}).get(rid, rid)}")
wmlist = wm.get("working-method") or []
if wmlist:
lines.append(f"- 핵심 접근: {wmlist[0]}")
fws = wm.get("key-frameworks") or []
if fws:
lines.append("- 주요 프레임워크: " + ", ".join(_fw_name(f) for f in fws[:3]))
skill = MREG["roles"][rid]["method-skill"]
lines.append(f"- 전체 실무 절차·체크리스트·자기검증·handoff는 `{skill}` skill을 따른다. "
"skill 미적재 시 작업 시작 금지.")
if multi:
lines.append("")
return "\n".join(lines).rstrip()
```
**(b) `wm_block` 함수 삭제** (lines 87-106). `method_spine`이 대체.
**(c) `skills_fm_line` 함수 교체** (기존 133-140 전체 교체):
```python
def skills_fm_line(kind, *, rid=None, fid=None, members=None):
"""registry 파생 skills: frontmatter 줄. kind: role|lead|router|family."""
roles, families = MREG["roles"], MREG["families"]
sk = []
if kind in ("role", "lead"):
sk.append(roles[rid]["method-skill"])
sk += roles[rid].get("capability-skills") or []
elif kind == "router":
sk += (families.get(fid) or {}).get("capability-skills") or []
elif kind == "family":
for m in members:
sk.append(roles[m]["method-skill"])
sk += roles[m].get("capability-skills") or []
sk += (families.get(fid) or {}).get("capability-skills") or []
sk = dedup(sk)
return f"skills: [{', '.join(sk)}]\n" if sk else ""
```
**(d) `craft_block` 축소** (155-160 반환 문자열 교체 — design-craft가 skills:에 이미 있으므로 1줄 pointer + anti-generic self-check만):
```python
return ("## 디자인 craft 표준 (skill `design-craft` 참조)\n"
"- 제약>묘사: design-brief(무엇/누구/달성)→references(구체 신호, 'modern/clean' 금지)"
"→tokens(값+의도+경계)→decisions→donts. 전체 절차는 design-craft skill.\n"
"- anti-generic self-check: 내 산출을 'modern/clean'으로 설명할 수 있으면 generic이다.")
```
그리고 diagram-craft 분기(148-154)도 축소:
```python
if skill == "diagram-craft":
return ("## 다이어그램 craft 표준 (skill `diagram-craft`+`design-craft` 참조)\n"
"- abstraction-first(C4 레벨·독자·메시지 먼저), one diagram one message.\n"
"- 엔진: **D2 우선**(아키텍처·의존성·중첩) → Excalidraw → Mermaid(폴백만). "
"Mermaid로 도망치지 않는다. 전체 절차는 diagram-craft skill.")
```
**(e) `build_role_agent`** (280-341): `wm_text = wm_block([rid], wm_map or {})``wm_text = method_spine([rid], wm_map or {})`. frontmatter `{skills_fm_line(rid, fid)}``{skills_fm_line('role', rid=rid)}`.
**(f) `build_lead_agent`** (344-412): `wm_text = wm_block([rid], wm_map or {})``method_spine([rid], wm_map or {})`. lead frontmatter에 skills 줄이 없으므로 **추가**: fm 문자열의 `f"role-id: {rid}\n"` 다음에 `f"{skills_fm_line('lead', rid=rid)}"` 삽입.
**(g) `build_router_agent`** (415-504): 멤버 method embed 제거 → pointer table.
- `wm_text = wm_block(members, wm_map or {}, names)` / `wm_section = ...` 블록(468-469)을 아래로 교체:
```python
pointer_lines = ["## fan-out 멤버 → method-skill (라우팅 대상)"]
for rid in members:
pointer_lines.append(
f"- {profiles[rid]['role-name']} ({rid}) — agent: {rid.lower()}, "
f"skill: {MREG['roles'][rid]['method-skill']}")
wm_section = "\n".join(pointer_lines) + "\n\n"
```
- router frontmatter에 skills 추가: fm 문자열 `f"family: {fid}\n"` 다음에 `f"{skills_fm_line('router', fid=fid)}"` 삽입.
- 단독 경로 문구(484): "위 역할별 관점·시야·책임을 각 역할 섹션으로 구조화해" 뒤에 "(각 멤버의 method-skill을 이름으로 로드)" 추가.
**(h) `build_agent`** (175-272, family agent): `wm_text = wm_block(members, wm_map or {}, names)``method_spine(members, wm_map or {}, names)`. frontmatter `{skills_fm_line(fid, fid)}``{skills_fm_line('family', fid=fid, members=members)}`.
**(i) `main`** — registry 로드 + 전역 세팅. `wm_map = load_working_methods()` 다음에:
```python
global MREG
MREG = load_method_registry(fams)
```
**(j) assert 갱신** (605-606):
```python
if wm_map: # P3: working-method 는 이제 method-skill 로 분리 — 카드엔 spine + skills 참조
assert "## 핵심 작업 방법" in content, f"{name} missing method spine"
meta2 = yaml.safe_load(content.split("---\n")[1])
if kind in ("role", "lead", "family"):
assert meta2.get("skills"), f"{name} missing skills frontmatter"
assert "## 일하는 방식" not in content, f"{name} still has full method embed"
```
craft assert(591)의 문구가 바뀌었으면 갱신: `"디자인 craft 표준" in content` 유지(문구 보존). doc-visual `"D2" in content and "폴백" in content` 유지.
- [ ] **Step 4: 통과 확인 (재생성 + 테스트)**
```bash
CLAUDE_PROJECT_DIR="$PWD" python3 .claude/hooks/gen_agents.py # OK ... 72 agents
python3 /tmp/claude-*/scratchpad/t4.py # OK task4
```
Expected: gen_agents 72 written, t4 OK.
- [ ] **Step 5: Commit**
```bash
git add .claude/hooks/gen_agents.py .claude/agents
git commit -m "P3 Task4: gen_agents spine+registry skills(router pointer/collapse union) + 72 재생성
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 5: doctor.py check_method_skill_wiring
**Files:**
- Modify: `.claude/hooks/doctor.py`
- Test: 임시(Task 7에 통합).
**Interfaces:**
- Consumes: registry, `skill_refs.known_skill_names`, gen_method_skills `--check`(subprocess).
- Produces: `check_method_skill_wiring(report)` — 완전성·실존·참조해소·고아0·drift0·키정합. main 등록 + section "10.".
- [ ] **Step 1: 실패 테스트 (임시)**
`/tmp/claude-*/scratchpad/t5.py`:
```python
import subprocess, os
r = subprocess.run(["python3", ".claude/hooks/doctor.py"], env={**os.environ, "CLAUDE_PROJECT_DIR": os.getcwd()}, capture_output=True, text=True)
assert "method-skill 배선(P3)" in r.stdout, "section missing"
assert "10. method-skill 배선(P3)" in r.stdout
assert "[FAIL]" not in r.stdout.split("method-skill 배선(P3)")[1][:200] or "OK" in r.stdout, r.stdout[-400:]
print("OK task5 doctor has P3 section")
```
- [ ] **Step 2: 실패 확인** — Run → FAIL(섹션 없음).
- [ ] **Step 3: doctor.py 개정**
상단 import 근처(HOOKS_DIR 정의 이후)에 없음 — `check_method_skill_wiring``check_design_direction_wiring` 다음에 추가:
```python
def check_method_skill_wiring(report):
"""10. method-skill 배선(P3): registry 완전성·실존·참조해소·고아0·drift0·키정합."""
section = "10. method-skill 배선(P3)"
if HOOKS_DIR not in sys.path:
sys.path.insert(0, HOOKS_DIR)
try:
import glob as _glob
import subprocess as _sp
import yaml
from skill_refs import known_skill_names, parse_skills
reg_path = os.path.join(REPO, "org-os/00-role-registry/method-skill-registry.yaml")
if not os.path.exists(reg_path):
report.fail(section, "method-skill-registry.yaml 없음"); return
reg = yaml.safe_load(open(reg_path))["method-skill-registry"]
roles = reg["roles"]
gen_dir = os.path.join(REPO, reg["generated-dir"])
fams = yaml.safe_load(open(os.path.join(REPO, "org-os/00-role-registry/capability-families.yaml")))["capability-families"]["families"]
bound = set()
for f in fams:
bound |= set(f["member-role-ids"])
# 1. 완전성
miss = sorted(bound - set(roles))
if miss:
report.fail(section, "registry 미등록 역할: " + ", ".join(miss)); return
# 6. 키 정합
roles_txt = open(os.path.join(REPO, "org-os/00-role-registry/roles.yaml")).read()
badk = [r for r in roles if r not in roles_txt]
if badk:
report.fail(section, "registry 미지 role-id: " + ", ".join(badk)); return
# 2/4. 실존 + 고아
want = {r["method-skill"] for r in roles.values()}
miss_sk = sorted(s for s in want if not os.path.exists(os.path.join(gen_dir, s, "SKILL.md")))
if miss_sk:
report.fail(section, "생성 skill 파일 없음: " + ", ".join(miss_sk)); return
on_disk = {os.path.basename(os.path.dirname(p))
for p in _glob.glob(os.path.join(gen_dir, "*", "SKILL.md"))
if os.path.basename(os.path.dirname(p)).endswith("-method")}
orphan = sorted(on_disk - want)
if orphan:
report.fail(section, "고아 생성 skill: " + ", ".join(orphan)); return
# 3. 카드 skills: 참조 해소
known = known_skill_names(REPO)
unresolved = []
for a in _glob.glob(os.path.join(REPO, ".claude/agents/*.md")):
try:
fm = yaml.safe_load(open(a).read().split("---\n")[1]) or {}
except Exception: # noqa: BLE001
continue
for s in parse_skills(fm.get("skills")):
if s not in known:
unresolved.append(f"{os.path.basename(a)}:{s}")
if unresolved:
report.fail(section, "미해결 skills 참조: " + ", ".join(unresolved)); return
# 5. drift
rc = _sp.run([sys.executable, os.path.join(HOOKS_DIR, "gen_method_skills.py"), "--check"],
capture_output=True, text=True, env={**os.environ, "CLAUDE_PROJECT_DIR": REPO})
if rc.returncode != 0:
report.fail(section, "method-skill drift: " + (rc.stdout or rc.stderr).strip()); return
report.ok(section, f"method-skill 배선 OK ({len(roles)} roles · {len(on_disk)} skills · 참조·drift 정상)")
except Exception as e: # noqa: BLE001
report.fail(section, f"method-skill 배선 점검 오류: {e}")
```
`main()``sections` 리스트에 추가: `"10. method-skill 배선(P3)",`. 그리고 for 루프 tuple에 `check_method_skill_wiring` 추가:
```python
for fn in (check_python_deps, check_workspace, check_lint_refs, check_ssot_consumption,
check_company_context_lint, check_venture_bootstrap_wiring, check_design_direction_wiring,
check_method_skill_wiring):
```
- [ ] **Step 4: 통과 확인** — Run: `python3 /tmp/claude-*/scratchpad/t5.py` + `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/hooks/doctor.py | grep -A1 "method-skill 배선"``[ OK ]`.
- [ ] **Step 5: Commit**
```bash
git add .claude/hooks/doctor.py
git commit -m "P3 Task5: doctor check_method_skill_wiring(완전성·실존·참조·고아·drift·키정합)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 6: lint_refs.py 에이전트 skills 참조 해소
**Files:**
- Modify: `.claude/hooks/lint_refs.py`
**Interfaces:**
- Consumes: `skill_refs`. Produces: `check_skill_refs(base) -> list[str]`; main이 커맨드 참조 문제와 병합.
- [ ] **Step 1: 실패 테스트 (임시)**
`/tmp/claude-*/scratchpad/t6.py`:
```python
import importlib.util, os, tempfile, shutil
spec = importlib.util.spec_from_file_location("lr", ".claude/hooks/lint_refs.py")
lr = importlib.util.module_from_spec(spec); spec.loader.exec_module(lr)
assert lr.check_skill_refs(os.getcwd()) == [], "clean tree should pass"
# 깨진 참조 주입
p = ".claude/agents/_p3probe.md"
open(p, "w").write("---\nname: _p3probe\nskills: [no-such-skill-xyz]\n---\nx")
try:
probs = lr.check_skill_refs(os.getcwd())
assert any("no-such-skill-xyz" in x for x in probs), probs
finally:
os.remove(p)
print("OK task6")
```
- [ ] **Step 2: 실패 확인** — Run → FAIL(`check_skill_refs` 없음).
- [ ] **Step 3: lint_refs.py 개정**
import에 추가(상단): 없음 — 함수 내부에서 지연 import. `check_refs` 다음에 추가:
```python
def check_skill_refs(root=None):
"""에이전트 카드 skills: frontmatter → 실존 SKILL.md 해소(빈 리스트=통과)."""
base = root or ROOT
if HOOKS_DIR not in sys.path:
sys.path.insert(0, HOOKS_DIR)
try:
import yaml
from skill_refs import known_skill_names, parse_skills
except Exception as e: # noqa: BLE001
return [f"skill_refs 로드 실패: {e}"]
known = known_skill_names(base)
problems = []
for a in sorted(glob.glob(os.path.join(base, ".claude", "agents", "*.md"))):
rel = os.path.relpath(a, base)
try:
fm = yaml.safe_load(open(a, encoding="utf-8").read().split("---\n")[1]) or {}
except Exception as e: # noqa: BLE001
problems.append(f"{rel}: frontmatter 파싱 실패 ({e})")
continue
for s in parse_skills(fm.get("skills")):
if s not in known:
problems.append(f"{rel}: skill `{s}` 미존재(.claude/skills/**/SKILL.md 없음)")
return problems
```
`main()`을 병합형으로 교체:
```python
def main():
problems = check_refs() + check_skill_refs()
if problems:
print("REF-LINT FAIL: 미해결 참조 %d건" % len(problems))
for p in problems:
print(f" - {p}")
return 1
n = len(glob.glob(COMMANDS_GLOB))
a = len(glob.glob(os.path.join(AGENTS_DIR, "*.md")))
print(f"OK lint_refs: {n} command 참조 + {a} agent skills 참조 모두 해결됨")
return 0
```
(상단에 `import sys`는 이미 존재. `HOOKS_DIR` 이미 정의됨.)
- [ ] **Step 4: 통과 확인** — Run: `python3 /tmp/claude-*/scratchpad/t6.py` + `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/hooks/lint_refs.py` → OK.
- [ ] **Step 5: Commit**
```bash
git add .claude/hooks/lint_refs.py
git commit -m "P3 Task6: lint_refs 에이전트 skills: 참조 해소(깨진 참조 검출)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 7: test_p3_prompt_skill_separation.py (16 checks)
**Files:**
- Create: `.claude/tests/test_p3_prompt_skill_separation.py`
**Interfaces:**
- Consumes: 전 산출물. `run_all.py`가 자동 발견.
- [ ] **Step 1: 테스트 작성**
```python
#!/usr/bin/env python3
"""P3 프롬프트/skill 분리 강제기 — standalone check(pytest 아님). exit 0=통과."""
import glob
import importlib.util
import os
import subprocess
import sys
import yaml
ROOT = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
HOOKS = os.path.join(ROOT, ".claude", "hooks")
REG = os.path.join(ROOT, "org-os", "00-role-registry")
def _load(mod, path):
spec = importlib.util.spec_from_file_location(mod, os.path.join(HOOKS, path))
m = importlib.util.module_from_spec(spec)
sys.path.insert(0, HOOKS)
spec.loader.exec_module(m)
return m
G = _load("gen_method_skills", "gen_method_skills.py")
SR = _load("skill_refs", "skill_refs.py")
LR = _load("lint_refs", "lint_refs.py")
passed = failed = 0
def check(name, ok):
global passed, failed
if ok:
passed += 1; print(f" ✅ {name}")
else:
failed += 1; print(f" ❌ {name}")
def read(*p):
return open(os.path.join(ROOT, *p), encoding="utf-8").read()
reg = yaml.safe_load(read("org-os/00-role-registry/method-skill-registry.yaml"))["method-skill-registry"]
roles = reg["roles"]
gen_dir = os.path.join(ROOT, reg["generated-dir"])
skills_out = G.build_all()
# 1. 생성 완전성
check("75 method-skills generated", len(skills_out) == 75)
disk = [os.path.basename(os.path.dirname(p)) for p in glob.glob(os.path.join(gen_dir, "*", "SKILL.md"))
if os.path.basename(os.path.dirname(p)).endswith("-method")]
check("75 method-skills on disk", len(disk) == 75)
_c = skills_out["des-prod-method"]
check("skill frontmatter valid", _c.startswith("---\nname: des-prod-method\n") and "generated-from: role-working-methods.yaml#DES-PROD" in _c)
# 2. 내용 불변(품질 중립)
_wm = yaml.safe_load(read("org-os/00-role-registry/role-working-methods.yaml"))["role-working-methods"]["DES-PROD"]
check("skill preserves working-method content", all(s in _c for s in _wm["working-method"]))
# 3. --check drift
_r = subprocess.run([sys.executable, os.path.join(HOOKS, "gen_method_skills.py"), "--check"],
capture_output=True, text=True, env={**os.environ, "CLAUDE_PROJECT_DIR": ROOT})
check("gen_method_skills --check clean", _r.returncode == 0)
# 4. registry 완전성·키 정합
fams = yaml.safe_load(read("org-os/00-role-registry/capability-families.yaml"))["capability-families"]["families"]
bound = set()
for f in fams:
bound |= set(f["member-role-ids"])
check("registry covers all agent-bound roles", bound == set(roles))
_rtxt = read("org-os/00-role-registry/roles.yaml")
check("registry keys ⊆ roles.yaml", all(r in _rtxt for r in roles))
# 5. gen_agents 방출: 워커 spine + skills, no full embed
_dp = read(".claude/agents/des-prod.md")
check("worker card has spine + skills, no full embed",
"## 핵심 작업 방법" in _dp and "des-prod-method" in _dp and "## 일하는 방식" not in _dp)
# 6. spine 얇음·공통불변식 재나열 금지·프레임워크 이름만
_spine = _dp.split("## 핵심 작업 방법")[1].split("\n## ")[0]
check("spine has no common-invariant restatement",
"다른 역할" not in _spine and "반증" not in _spine and "실물" not in _spine)
check("spine frameworks names-only (no definition sentence)", "제약>묘사" not in _spine)
# 7. skill self-check 정책: 역할 고유만, 없으면 생략
check("skill self-check role-specific, no common invariant",
"## 자기검증" in _c and "시각 취향" in _c and "다른 역할" not in _c.split("## 자기검증")[1])
check("self-check optional (arch-app omits)", "## 자기검증" not in skills_out["arch-app-method"])
# 8. router 정책
_fd = read(".claude/agents/fam-design.md")
check("router: member pointer table, no member method embed, no member method-skill in skills",
"fan-out 멤버 → method-skill" in _fd and "## 일하는 방식" not in _fd
and "\nskills: [des-prod-method" not in _fd)
# 9. lead 정책
_ld = read(".claude/agents/des-director.md")
check("lead: own method-skill only, no member method-skill in skills",
"des-director-method" in _ld and "des-prod-method" not in _ld.split("---\n")[1])
# 10. collapse/family 정책(수행능력 보존)
_fb = read(".claude/agents/fam-eng-backend.md")
check("collapse family loads member method-skill union", "eng-be-method" in _fb)
# 11. capability-skill 흡수
check("des-prod skills include design-craft", "design-craft" in _dp.split("---\n")[1])
check("IMPL family includes build-loop", "build-loop" in _fb.split("---\n")[1])
# 12. 하네스 불변식 잔류
check("harness invariants remain in card sections",
"## Output contract" in _dp and "primary-artifacts" in _dp and "## When invoked" in _dp)
# 13. doctor 게이트 OK
_dr = subprocess.run([sys.executable, os.path.join(HOOKS, "doctor.py")],
capture_output=True, text=True, env={**os.environ, "CLAUDE_PROJECT_DIR": ROOT})
check("doctor method-skill wiring OK", "method-skill 배선 OK" in _dr.stdout and _dr.returncode == 0)
# 14. lint_refs skills 참조 clean
check("lint_refs skill refs clean", LR.check_skill_refs(ROOT) == [])
# 15. 개수 계약
check("72 agents · 75 method-skills",
len(glob.glob(os.path.join(ROOT, ".claude/agents/*.md"))) == 72 and len(skills_out) == 75)
# 16. 전 카드 spine + no full embed
_bad = [os.path.basename(a) for a in glob.glob(os.path.join(ROOT, ".claude/agents/*.md"))
if "## 일하는 방식" in read(".claude/agents", os.path.basename(a))]
check("no agent retains full method embed", _bad == [])
print(f"\n{passed} passed · {failed} failed")
sys.exit(1 if failed else 0)
```
- [ ] **Step 2: 실행**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_p3_prompt_skill_separation.py`
Expected: `16 passed · 0 failed`. (실패 항목이 있으면 해당 산출 태스크로 회귀.)
- [ ] **Step 3: Commit**
```bash
git add .claude/tests/test_p3_prompt_skill_separation.py
git commit -m "P3 Task7: test_p3 통합 강제기 16종
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 8: 마이그레이션(기존 테스트 갱신) + 문서 + 최종 green
**Files:**
- Modify: `.claude/tests/test_enforcement.py` (craft/skills assert)
- Modify: `CLAUDE.md` (구조·gen 흐름)
**Interfaces:** 없음(정리·검증 태스크).
- [ ] **Step 1: test_enforcement.py 갱신**
기존 `skills: [design-craft]` 정확일치 assert(910-915)가 P3에서 깨진다(이제 `[des-prod-method, design-craft]`). 아래로 교체:
```python
# P3: skills 는 method-skill + capability-skill 조합. capability-skill 포함 여부로 검증(정확일치 아님).
_dpm = open(os.path.join(ROOT, ".claude/agents/des-prod.md")).read()
check("des-prod skills include method + design-craft",
"des-prod-method" in _dpm and "design-craft" in _dpm.split("---\n")[1])
_dvm = open(os.path.join(ROOT, ".claude/agents/doc-visual.md")).read()
check("doc-visual skills include method + design-craft + diagram-craft",
all(s in _dvm.split("---\n")[1] for s in ("doc-visual-method", "design-craft", "diagram-craft")))
check("every agent now has skills: frontmatter (arch-app has method)",
"arch-app-method" in open(os.path.join(ROOT, ".claude/agents/arch-app.md")).read().split("---\n")[1])
```
craft 표준 문구 assert(905-908)는 `craft_block` 축소 후에도 `"디자인 craft 표준"`·doc-visual `"D2"`/`"폴백"`을 유지하므로 그대로 통과(Task4 craft_block이 문구 보존). 실행해 확인하고, 만약 문구가 바뀌었으면 새 문구로 갱신.
- [ ] **Step 2: 갱신 검증**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_enforcement.py`
Expected: 전 항목 통과(≈500 assert). 실패하면 해당 assert를 P3 구조에 맞게 갱신.
- [ ] **Step 3: CLAUDE.md 갱신**
`.claude/hooks/*.py` 줄에 `gen_method_skills`(method-skill 생성) 추가. 구조 블록에 `method-skill-registry.yaml`·`.claude/skills/generated/` 언급. "핵심 개념"의 gen 흐름에 "role-profiles/capability-families/role-working-methods 변경 후 `gen_agents`+`gen_method_skills` 재실행" 반영. 검증 섹션에 `gen_method_skills.py --check` 추가:
```bash
# 에이전트+method-skill 재생성(role-* 변경 후)
CLAUDE_PROJECT_DIR="$PWD" python3 .claude/hooks/gen_method_skills.py
CLAUDE_PROJECT_DIR="$PWD" python3 .claude/hooks/gen_agents.py
```
- [ ] **Step 4: render fixture 복구 + 전체 게이트**
```bash
git checkout -- .claude/tests/fixtures/ 2>/dev/null || true
CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py
```
Expected: 전 스위트 green(doctor OK 포함, `test_p3_*` 16 passed, `test_enforcement` 통과).
- [ ] **Step 5: Commit**
```bash
git add .claude/tests/test_enforcement.py CLAUDE.md
git commit -m "P3 Task8: 기존 테스트 마이그레이션(craft/skills assert) + CLAUDE.md gen 흐름
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Self-Review
**Spec coverage:** §3 파이프라인(Task1·2·4), §4 registry(Task1), §5 생성 skill(Task2), §6 3정책(Task4), §7 spine(Task4), §8 gen_agents(Task4), §9 무결성(Task5·6), §10 Phase0(Task0), §11 마이그레이션(Task8), §13 테스트 16종(Task7), §12 문서(Task8). 전 섹션 커버.
**Placeholder scan:** registry·gen_method_skills·skill_refs·doctor check·lint 확장·test 전부 완전 코드. gen_agents는 함수별 교체 지시 + 완전 새 함수 본문. 임시 테스트 스크립트는 `/tmp/.../scratchpad`(커밋 안 함), 영구 게이트는 test_p3 + doctor + lint.
**Type consistency:** `MREG["roles"][rid]["method-skill"]`(gen_agents·gen_method_skills·doctor 일관), `skills_fm_line(kind, *, rid/fid/members)`, `known_skill_names(root)`/`parse_skills(val)`(skill_refs, doctor·lint 동일 소비), `build_all()->{skill:content}`, `generated-dir` registry 필드로 레이아웃 무관. method-skill 이름 규약 `<role-lower>-method` 전 태스크 일치.
**Task 경계:** 각 태스크 독립 테스트 가능 — Task1(registry 검증) / Task2(생성+drift) / Task3(헬퍼) / Task4(카드 재생성) / Task5(doctor) / Task6(lint) / Task7(통합) / Task8(마이그레이션+green). Task0는 런타임 probe(게이트, 이후 레이아웃 확정).
## Execution Handoff
**Plan complete and saved to `docs/superpowers/plans/2026-07-13-p3-prompt-skill-separation.md`.**
**1. Subagent-Driven (recommended)** — 태스크별 fresh subagent + 2단 리뷰(spec 준수 + 품질), 빠른 반복.
**2. Inline Execution** — 이 세션에서 executing-plans로 배치 실행.
P1·P2와 동일하게 **Subagent-Driven** 권장.