1189 lines
55 KiB
Markdown
1189 lines
55 KiB
Markdown
# 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** 권장.
|