538 lines
29 KiB
Python
538 lines
29 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate .claude/agents/*.md from org-os capability-families + role-profiles.
|
|
|
|
Agent 세트는 collaboration-default에 따라 갈린다(실제 subagent 분리):
|
|
- fan-out family(멤버 >= 2): 멤버 role마다 **개별 subagent**(fan-out 워커). 각자 격리 context에서
|
|
자기 관점만 작업하고 자기 .report.yaml을 쓴 뒤 경로를 반환한다. 분리 = context 오염 방지.
|
|
- collapse family: planner가 후보 중 concrete role 하나를 선택하고 그 role만 자기 method-skill로 실행.
|
|
- 단일 멤버 fan-out / ORCH: concrete role card 1개.
|
|
Family는 `.claude/agents` 카드가 아니라 generated family registry의 metadata다. `fam-*` 실행
|
|
identity/router/resolver card는 생성하지 않는다.
|
|
Orchestrator(메인 세션, Agent 도구 보유)가 fan-out을 구동한다. (현재 Claude Code는 nested subagent도
|
|
지원하지만, 이 하네스의 fan-out 종합 계약은 여전히 Orchestrator가 원본 보고서를 전부 읽고 종합하는 것을
|
|
기본으로 한다 — context 오염/이중종합 방지. 중첩 호출은 예외적으로만.)
|
|
|
|
각 에이전트 본문은 role-profiles.yaml의 실제 관점(관점)/시야(시야)/책임(책임)/근거(evidence-basis)를 담는다.
|
|
|
|
Usage: python3 .claude/hooks/gen_agents.py [--check]
|
|
(no args) -> 기존 .md 정리 후 (재)생성, 개수 출력
|
|
--check -> 메모리 검증만(개수·본문), 파일 안 씀
|
|
"""
|
|
import glob
|
|
import os
|
|
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")
|
|
FAMILIES = os.path.join(REG, "capability-families.yaml")
|
|
PROFILES = os.path.join(REG, "role-profiles.yaml")
|
|
WORKING_METHODS = os.path.join(REG, "role-working-methods.yaml") # v1 fallback(단일 파일)
|
|
RWM_DIR = os.path.join(REG, "role-working-methods") # P3: 파일분리 SoT
|
|
MATRIX = os.path.join(REG, "tool-permission-matrix.yaml") # #10: tools의 단일 정본(SoT)
|
|
REGISTRY = os.path.join(REG, "method-skill-registry.yaml") # P3: role→method-skill 배선 SoT
|
|
OUT_DIR = os.path.join(ROOT, ".claude", "agents")
|
|
|
|
# #10: agent 'tools' 프론트매터는 tool-permission-matrix.yaml의 agent-tools 섹션에서 DERIVE한다
|
|
# (하드코딩 금지 — 프론트매터·guard·매트릭스가 서로 다른 정본을 갖던 문제 제거).
|
|
# TOOLS/DEFAULT_TOOLS는 main()에서 load_tools_from_matrix()로 채운다.
|
|
TOOLS = {}
|
|
DEFAULT_TOOLS = "Read, Grep, Glob, Write, WebFetch, WebSearch" # 정본 로드 전 ADVISORY 폴백
|
|
MREG = {} # P3: method-skill-registry(main에서 load) — 카드 skills: frontmatter 파생
|
|
|
|
|
|
def load_tools_from_matrix(fams):
|
|
"""tool-permission-matrix.yaml(agent-tools 정본)에서 family-id -> tools 문자열 맵을 만든다.
|
|
|
|
반환: (family_id -> 'Tool, Tool, ...' dict, default_tools 문자열).
|
|
매트릭스에 agent-tools 섹션이 없거나 프로파일이 미정의면 **명확히 실패**(SoT 불일치 은폐 금지)."""
|
|
root = yaml.safe_load(open(MATRIX)) or {}
|
|
at = ((root.get("tool-permission-matrix") or {}).get("agent-tools"))
|
|
assert isinstance(at, dict), (
|
|
"tool-permission-matrix.yaml에 agent-tools 섹션이 없음 — tools의 단일 정본 필요(#10)")
|
|
profiles = at.get("profiles") or {}
|
|
|
|
def as_str(profile):
|
|
toks = profiles.get(profile)
|
|
assert isinstance(toks, list) and toks, \
|
|
f"agent-tools.profiles['{profile}'] 미정의/비어있음(tool-permission-matrix)"
|
|
return ", ".join(toks)
|
|
|
|
default_profile = at.get("default-profile") or "ADVISORY"
|
|
default_tools = as_str(default_profile)
|
|
fam_profiles = at.get("family-profiles") or {}
|
|
# family-profiles의 키가 실제 family인지 검증(오타로 정본이 조용히 무시되는 것 방지)
|
|
fam_ids = {f["family-id"] for f in fams}
|
|
for fid in fam_profiles:
|
|
assert fid in fam_ids, f"agent-tools.family-profiles의 미지 family-id: {fid}"
|
|
tmap = {}
|
|
for f in fams:
|
|
fid = f["family-id"]
|
|
tmap[fid] = as_str(fam_profiles.get(fid, default_profile))
|
|
return tmap, default_tools
|
|
|
|
|
|
def dedup(seq):
|
|
seen, out = set(), []
|
|
for x in seq:
|
|
if x not in seen:
|
|
seen.add(x)
|
|
out.append(x)
|
|
return out
|
|
|
|
|
|
def load_working_methods():
|
|
"""P3: 파일분리(role-working-methods/) 우선, 없으면 단일 파일(v1 fallback)."""
|
|
idx = os.path.join(RWM_DIR, "index.yaml")
|
|
if os.path.exists(idx):
|
|
merged = {}
|
|
includes = (yaml.safe_load(open(idx)) or {}).get("role-method-contracts", {}).get("includes", [])
|
|
for inc in includes:
|
|
d = yaml.safe_load(open(os.path.join(RWM_DIR, inc))) or {}
|
|
merged.update(d.get("role-working-methods") or {})
|
|
if merged:
|
|
return merged
|
|
if not os.path.exists(WORKING_METHODS):
|
|
return {}
|
|
d = yaml.safe_load(open(WORKING_METHODS)) or {}
|
|
return (d.get("role-working-methods") or {}) if isinstance(d, dict) else {}
|
|
|
|
|
|
def load_method_registry(fams):
|
|
"""method-skill-registry(role→method-skill 배선) 로드 + 키 실존·완전성 검증."""
|
|
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):
|
|
"""P3: 카드에 남는 얇은 절차 잔여(역할-파생). 공통 불변식 재나열 안 함(spec §7).
|
|
essence 1줄 + 프레임워크 이름 최대 3 + method-skill pointer/load-guard. 전체 절차는 skill."""
|
|
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()
|
|
|
|
|
|
# 디자인·비주얼 직무: craft 표준(SKILL)을 두 방식으로 준다(finding #17):
|
|
# (1) frontmatter `skills:` 로 **전체 skill을 preload**(현재 Claude Code는 subagent도 skill 로드 가능).
|
|
# (2) 본문에 핵심 제약층 체크리스트를 짧게 embed(in-context 리마인더 — skill 요지를 눈앞에 둔다).
|
|
# 예전엔 "subagent는 skill을 auto-load 못 한다"는 가정으로 (2)만 했는데, 이제 (1)이 정본 전체를 싣는다.
|
|
CRAFT = {
|
|
"DES-PROD": "design-craft",
|
|
"DES-PLATFORM": "design-craft",
|
|
"DES-INTERNAL": "design-craft",
|
|
"DOC-VISUAL": "diagram-craft",
|
|
}
|
|
# P3: capability-skill(design-craft/build-loop) 배선은 method-skill-registry로 이관(하드코딩 흡수).
|
|
# skills_fm_line 이 registry(roles.capability-skills / families.capability-skills)에서 파생한다.
|
|
|
|
|
|
def skills_fm_line(kind, *, rid=None):
|
|
"""P3: concrete role/lead card의 registry 파생 skills frontmatter."""
|
|
if kind not in ("role", "lead") or not rid:
|
|
raise ValueError(f"concrete role skill binding required: kind={kind!r}, role={rid!r}")
|
|
roles = MREG["roles"]
|
|
sk = [roles[rid]["method-skill"]]
|
|
sk += roles[rid].get("capability-skills") or []
|
|
sk = dedup(sk)
|
|
return f"skills: [{', '.join(sk)}]\n" if sk else ""
|
|
|
|
|
|
def craft_block(rid):
|
|
skill = CRAFT.get(rid)
|
|
if not skill:
|
|
return ""
|
|
if skill == "diagram-craft":
|
|
return """## 디자인 craft 표준 (필수 — `.claude/skills/diagram-craft` + `design-craft`)
|
|
전문가급 산출의 핵심은 프레임워크 지식이 아니라 **제약층**이다. 빈 추론층은 모델이 generic으로 채운다(제약>묘사).
|
|
- **abstraction-first**: 도구보다 C4 레벨·독자·전달 메시지를 먼저 정한다. one diagram, one message.
|
|
- **엔진 우선순위: D2(아키텍처·의존성·중첩, 1급) → Excalidraw(설명·손그림) → Mermaid(폴백만)**. Mermaid로 보여주는 건 실무급 시각자료가 아니다 — 자제한다.
|
|
- D2 관용구: 중첩 컨테이너로 계층/경계, 큰 그래프는 layout=elk, direction 고정, 테마로 색 통일. render_consult가 `{type: d2, code}`를 d2 CLI로 실물 SVG 렌더.
|
|
- notation 규율: 스코프 한 줄 제목·범례·일관된 방향·예약색(색은 의미 전용).
|
|
- self-check: Mermaid로 도망치지 않았나? 아키텍처·의존성이면 D2여야 한다."""
|
|
return """## 디자인 craft 표준 (필수 — `.claude/skills/design-craft`)
|
|
전문가급 산출의 핵심은 프레임워크 지식이 아니라 **제약층**이다(제약>묘사). 빈 추론층은 모델이 generic으로 채운다.
|
|
- **design-brief를 먼저**(design-brief-spec): brief(무엇/누구/달성) → references → tokens → decisions → donts.
|
|
- **레퍼런스는 형용사가 아니라 구체 신호**: "modern/clean/minimal" 금지. 구체 제품 3–6개 + 나르는 신호(밀도·간격·색 규율)를 명명한다.
|
|
- **토큰은 값+의도+경계**(경계 없는 토큰 금지). 컴포넌트는 **판단로직**(언제 A vs B). **명시적 Don'ts 5개+**.
|
|
- anti-generic self-check: 내 산출을 "modern/clean"으로 설명할 수 있으면 generic이다 — 명명된 레퍼런스로 다시 앵커한다."""
|
|
|
|
|
|
# #9: 실물 산출물(RFC/ADR·data-model·threat-model·api-contract·code)을 report 한 줄로 축소하지 않고
|
|
# 실제 파일로 써서 primary-artifacts[]에 등재하게 하는 공통 계약 라인(모든 에이전트 본문에 embed).
|
|
# 보고서는 실물의 envelope(경로+검증+리스크)다. design/spec/build/completion 유형은 validator가 실존을 강제.
|
|
PRIMARY_ARTIFACTS_CONTRACT = (
|
|
"- **실물 산출물은 보고서로 대체 금지 — primary-artifacts 분리(#9)**: RFC/ADR·데이터모델·"
|
|
"threat-model·api-contract·실제 코드 같은 실물 deliverable은 **실제 파일로 써서**(Write) "
|
|
"`primary-artifacts: [{path, kind, sha?, verification}]`에 등재한다. 보고서(.report.yaml)는 그 실물의 "
|
|
"**경로+검증+리스크를 담는 envelope**이며, 보고서 안 몇 줄 요약으로 실물을 대체하지 않는다. "
|
|
"design/spec/build/completion 유형 산출은 validate_report가 primary-artifacts 실존(과 receipt)을 강제한다."
|
|
)
|
|
|
|
def build_concrete_role_agent(rid, profile, fam, wm_map=None, *, direct=False):
|
|
"""One executable concrete role selected by the role planner.
|
|
|
|
Collapse families expose isolated candidates; single-member families expose one direct
|
|
role. Family metadata stays in the registry and never becomes an agent card.
|
|
"""
|
|
fid = fam["family-id"]
|
|
name = rid.lower()
|
|
tools = TOOLS.get(fid, DEFAULT_TOOLS)
|
|
resp = "\n".join(f" - {value}" for value in profile["responsibilities"])
|
|
evidence = "\n".join(f"- {value}" for value in dedup(profile["evidence-basis"]))
|
|
method = method_spine([rid], wm_map or {})
|
|
craft = craft_block(rid)
|
|
execution = ""
|
|
if fid in {"FAM-ENG-FRONTEND", "FAM-ENG-BACKEND", "FAM-ENG-SPECIAL", "FAM-PLATFORM-INFRA"}:
|
|
execution = """## 구현 루프 (build-loop)
|
|
1. 호출부와 계약을 inspect한다.
|
|
2. smallest safe change를 구현한다.
|
|
3. 변경 diff를 inspect한다.
|
|
4. targeted verify를 실행한다.
|
|
5. broader verify를 실행한다.
|
|
6. 실패 시 수정-검증 루프를 반복한다.
|
|
7. 실행한 것과 실행하지 않은 것을 정직하게 completion record에 남긴다.
|
|
"""
|
|
worker_kind = "direct-role" if direct else "collapse-primary-candidate"
|
|
worker_desc = "single-member direct worker" if direct else "collapse concrete worker"
|
|
selection_text = "resolved-worker" if direct else "primary-worker"
|
|
triggers = fam.get("invocation-triggers", "")
|
|
exclusions = fam.get("exclusions", "")
|
|
fm = (
|
|
"---\n"
|
|
f"name: {name}\n"
|
|
f"description: \"{profile['role-name']} ({rid}) — {fid} {worker_desc}. "
|
|
f"Use when {triggers}. Do NOT use for {exclusions}. "
|
|
f"role_selector가 이 역할을 {selection_text}로 선택했을 때만 실행.\"\n"
|
|
f"tools: {tools}\n"
|
|
"model: inherit\n"
|
|
f"{skills_fm_line('role', rid=rid)}"
|
|
f"family: {fid}\n"
|
|
f"role-id: {rid}\n"
|
|
f"collaboration-role: {worker_kind}\n"
|
|
"---\n"
|
|
)
|
|
body = f"""
|
|
당신은 **{profile['role-name']} ({rid})** 입니다. `{fid}`가 아니라 이 concrete 역할로 실행합니다.
|
|
family 멤버 전체의 방법론을 합치지 않으며, resolver가 `{selection_text}: {rid}`를 반환했을 때만 작업합니다.
|
|
|
|
## 나의 관점·시야·책임
|
|
- 관점: {profile['perspective']}
|
|
- 시야: {profile['scope']}
|
|
- 책임:
|
|
{resp}
|
|
|
|
## 근거 기준 (evidence-basis)
|
|
{evidence}
|
|
|
|
{method}
|
|
|
|
{craft}
|
|
|
|
{execution}
|
|
## Output contract
|
|
- context-package의 target-role-agent는 `{name}`이어야 하며 family id는 금지됩니다.
|
|
- report-header/evidence와 immutable `.report.yaml`을 남깁니다.
|
|
{PRIMARY_ARTIFACTS_CONTRACT}
|
|
- external side-effect는 tool-permission-matrix에 따릅니다.
|
|
"""
|
|
return name, fm + body
|
|
|
|
|
|
def first_sentence(text, n=140):
|
|
s = str(text).strip().split(". ")[0]
|
|
return (s[:n] + "…") if len(s) > n else s
|
|
|
|
|
|
def build_role_agent(rid, p, fam, wm_map=None):
|
|
"""fan-out family의 한 멤버 role = 개별 subagent(격리 워커)."""
|
|
fid = fam["family-id"]
|
|
name = rid.lower()
|
|
lenses = ", ".join(fam.get("carries-lenses") or []) or "(no lens)"
|
|
tools = TOOLS.get(fid, DEFAULT_TOOLS)
|
|
resp = "\n".join(f" - {r}" for r in p["responsibilities"])
|
|
ev_lines = "\n".join(f"- {e}" for e in dedup(p["evidence-basis"]))
|
|
wm_text = method_spine([rid], wm_map or {})
|
|
wm_section = (wm_text + "\n\n") if wm_text else ""
|
|
craft_text = craft_block(rid)
|
|
craft_section = (craft_text + "\n\n") if craft_text else ""
|
|
|
|
desc = (
|
|
f"{p['role-name']} ({rid}) — {fid} fan-out 워커. {first_sentence(p['perspective'])} "
|
|
f"Use when {fam.get('invocation-triggers', '')}. Orchestrator가 role planner 선택 후 격리 subagent로 호출한다. "
|
|
f"Do NOT use for {fam.get('exclusions', '')}. "
|
|
f"Do NOT use for 종합·최종결정(-> Orchestrator/lead) 또는 다른 역할 관점."
|
|
)
|
|
fm = (
|
|
"---\n"
|
|
f"name: {name}\n"
|
|
f"description: \"{desc.replace(chr(34), chr(39))}\"\n"
|
|
f"tools: {tools}\n"
|
|
"model: inherit\n"
|
|
f"{skills_fm_line('role', rid=rid)}"
|
|
f"family: {fid}\n"
|
|
f"role-id: {rid}\n"
|
|
"collaboration-role: fan-out-worker\n"
|
|
"---\n"
|
|
)
|
|
body = f"""
|
|
당신은 **{p['role-name']} ({rid})** 입니다 — {fid}의 fan-out 워커 (lens: {lenses}).
|
|
이 family는 여러 역할을 하나로 합치지 않습니다. 당신은 **자신의 관점만** 독립적으로 담당합니다(context 오염 방지).
|
|
|
|
## 나의 관점·시야·책임
|
|
- 관점: {p['perspective']}
|
|
- 시야: {p['scope']}
|
|
- 책임:
|
|
{resp}
|
|
|
|
## 근거 기준 (evidence-basis)
|
|
{ev_lines}
|
|
|
|
{wm_section}{craft_section}## Fan-out 워커 계약
|
|
- 나는 **이 한 역할의 관점만** 낸다. 다른 역할의 결론을 대변·종합하지 않는다.
|
|
- **종합·최종결정은 내가 하지 않는다** — Orchestrator/상위 직무자가 내 보고서(와 동료 역할 보고서들)를 **전부 읽고** 수행한다.
|
|
- 산출물은 내 `.report.yaml` 하나(report-header BLUF). 최종 메시지로 **그 경로 + 1줄 bottom-line만 반환**한다(요약 본문 금지).
|
|
|
|
## When invoked
|
|
1. context-package(assigned-lens/objective/must-read/task-boundaries)를 확인한다. 없으면 시작하지 않는다.
|
|
2. must-read만 읽고 forbidden-context(secrets/PII/raw-log)는 배제한다. 내 관점·근거로만 판단한다.
|
|
3. `completion-records/<id>.report.yaml`에 report-header(BLUF)로 시작하는 보고서를 쓴다.
|
|
4. 최종 메시지 = 그 경로 + 1줄 bottom-line.
|
|
|
|
## Output contract (hook이 강제)
|
|
- report-header 없이 종료 금지. evidence 없는 confidence:High 금지. E4/E5는 실행/실존 아티팩트 필요.
|
|
{PRIMARY_ARTIFACTS_CONTRACT}
|
|
- 대표용 MD는 `render_report.py`가 생성한다 — MD를 손으로 쓰지 않는다.
|
|
- external side-effect(slack/PR/deploy/secret/db-write) 기본 금지(tool-permission-matrix).
|
|
- 판단/설계는 공식 문서·표준·1차 자료를 근거로(WebFetch/WebSearch/context7 → evidence에 출처 첨부).
|
|
"""
|
|
return name, fm + body
|
|
|
|
|
|
def build_lead_agent(rid, p, fam, wm_map=None):
|
|
"""fan-out family의 lead-role-id = 프레임+종합을 담당하는 synthesis-lead 에이전트.
|
|
워커와 달리 이 역할은 하위 보고서를 전부 읽고 종합한다(synthesized-by)."""
|
|
fid = fam["family-id"]
|
|
name = rid.lower()
|
|
lenses = ", ".join(fam.get("carries-lenses") or []) or "(no lens)"
|
|
tools = TOOLS.get(fid, DEFAULT_TOOLS)
|
|
resp = "\n".join(f" - {r}" for r in p["responsibilities"])
|
|
ev_lines = "\n".join(f"- {e}" for e in dedup(p["evidence-basis"]))
|
|
wm_text = method_spine([rid], wm_map or {})
|
|
wm_section = (wm_text + "\n\n") if wm_text else ""
|
|
workers = [m for m in fam["member-role-ids"] if m != rid]
|
|
worker_names = ", ".join(workers)
|
|
|
|
desc = (
|
|
f"{p['role-name']} ({rid}) — {fid} synthesis-lead. {first_sentence(p['perspective'])} "
|
|
f"Use when {fam.get('invocation-triggers', '')}. Do NOT use for {fam.get('exclusions', '')}. "
|
|
f"분과 워커({worker_names})를 프레임하고 그 보고서를 전부 읽어 Pyramid Principle로 종합한다. "
|
|
f"Do NOT use for 개별 분과 관점 생산(-> 해당 워커) 또는 최종 방향 결정(-> FAM-CEO/사람)."
|
|
)
|
|
fm = (
|
|
"---\n"
|
|
f"name: {name}\n"
|
|
f"description: \"{desc.replace(chr(34), chr(39))}\"\n"
|
|
f"tools: {tools}\n"
|
|
"model: inherit\n"
|
|
f"{skills_fm_line('lead', rid=rid)}"
|
|
f"family: {fid}\n"
|
|
f"role-id: {rid}\n"
|
|
"collaboration-role: synthesis-lead\n"
|
|
"---\n"
|
|
)
|
|
body = f"""
|
|
당신은 **{p['role-name']} ({rid})** 입니다 — {fid}의 **synthesis-lead** (lens: {lenses}).
|
|
당신은 엔게이지먼트를 시작(프레임)하고 끝(종합)냅니다. 분과 워커({worker_names})는 각자 관점만 냅니다 — 종합은 당신이 합니다.
|
|
|
|
## 나의 관점·시야·책임
|
|
- 관점: {p['perspective']}
|
|
- 시야: {p['scope']}
|
|
- 책임:
|
|
{resp}
|
|
|
|
## 근거 기준 (evidence-basis)
|
|
{ev_lines}
|
|
|
|
{wm_section}## Synthesis-lead 계약 (2단계로 일한다)
|
|
### ① FRAME (분과 투입 전)
|
|
- 문제를 SCQA로 프레이밍하고 **이슈트리(MECE)**로 분해한다. **Day-1 가설**을 세운다.
|
|
- 각 분과 워커가 무엇을 파고들지 workstream 경계를 정해 context-package로 넘긴다(shared-constraints 포함).
|
|
### ② SYNTHESIZE (분과 보고 후)
|
|
- 분과 워커 `.report.yaml`을 **▶전부 읽는다◀**(synthesis-rehydration — 요약본이 아니라 원본). dissent를 죽이지 않는다.
|
|
- **Pyramid Principle**로 지배 메시지(governing thought) 아래 논리적으로 종합한다.
|
|
- 종합 보고서는 `synthesized-by`·`linked-reports`(워커 전부)·`conflicts`를 반드시 포함한다(hook 강제). 이견 없으면 conflicts: [].
|
|
- 대표용 **문서+덱** 생성을 위해 `storyline:` 블록을 만든다: 각 슬라이드 = 액션타이틀(완결문장·정량주장) + exhibit + evidence. one-message-per-slide.
|
|
- exhibit 타입 2계열: **정량·개념 차트**는 손제작 SVG 아키타입(waterfall/matrix2x2/harvey/valuechain/benchmark/issuetree/process). **소프트웨어 구조·흐름·의존성 그래프**는 `{{type: d2, code: "...", layout: elk}}`로 실제 diagram-as-code 산출(render_consult가 d2 CLI로 실물 SVG — 1급). Mermaid(`{{type: mermaid}}`)는 최후 폴백만 — 실무급 시각자료가 아니다. 주제에 맞게: 소프트웨어 구조/흐름=D2, 정량 비교=아키타입.
|
|
|
|
## When invoked
|
|
1. context-package(mode/tier/assigned-lens/objective/must-read)를 확인한다. 없으면 시작하지 않는다.
|
|
2. FRAME이면 이슈트리·Day-1·workstream 경계를 산출한다. SYNTHESIZE이면 워커 보고서를 전부 읽고 종합+storyline을 산출한다.
|
|
3. `completion-records/<id>.report.yaml`에 report-header(BLUF)로 시작하는 보고서를 쓴다.
|
|
4. 최종 메시지 = 그 경로 + 1줄 bottom-line.
|
|
|
|
## Output contract (hook이 강제)
|
|
- report-header 없이 종료 금지. evidence 없는 confidence:High 금지. E4/E5는 실행/실존 아티팩트 필요.
|
|
{PRIMARY_ARTIFACTS_CONTRACT}
|
|
- 종합 보고서는 `synthesized-by` + `linked-reports`(비어있지 않음) + `conflicts` 필수.
|
|
- 대표용 MD/덱은 `render_consult.py`가 storyline에서 생성한다 — MD를 손으로 쓰지 않는다.
|
|
- external side-effect(slack/PR/deploy/secret/db-write) 기본 금지(tool-permission-matrix).
|
|
- 판단/종합은 공식 문서·표준·1차 자료를 근거로(WebFetch/WebSearch → evidence에 출처 첨부).
|
|
"""
|
|
return name, fm + body
|
|
|
|
def is_fanout_split(fam):
|
|
return fam.get("collaboration-default") == "fan-out" and len(fam["member-role-ids"]) >= 2
|
|
|
|
|
|
def main():
|
|
global TOOLS, DEFAULT_TOOLS, MREG
|
|
check = "--check" in sys.argv
|
|
fams = yaml.safe_load(open(FAMILIES))["capability-families"]["families"]
|
|
profiles = {p["role-id"]: p for p in yaml.safe_load(open(PROFILES))["role-profiles"]["profiles"]}
|
|
wm_map = load_working_methods()
|
|
MREG = load_method_registry(fams) # P3: 카드 skills: frontmatter · spine pointer 파생
|
|
|
|
# #10: tools를 tool-permission-matrix.yaml(정본)에서 파생한다.
|
|
TOOLS, DEFAULT_TOOLS = load_tools_from_matrix(fams)
|
|
# 불변식: audit-capable family는 자기 불변 보고서·설계 산출을 위해 반드시 Write를 갖는다.
|
|
for f in fams:
|
|
if f.get("audit-capable"):
|
|
assert "Write" in TOOLS[f["family-id"]], (
|
|
f"{f['family-id']}: audit-capable family인데 Write 미부여 "
|
|
"— 감사자가 Bash redirection으로 불변 guard를 우회하게 된다(#10). "
|
|
"tool-permission-matrix agent-tools에서 Write 포함 프로파일로 매핑하라.")
|
|
|
|
agents = [] # (name, content, kind)
|
|
for fam in fams:
|
|
lead = fam.get("lead-role-id")
|
|
if fam.get("collaboration-default") == "collapse":
|
|
for rid in fam["member-role-ids"]:
|
|
name, content = build_concrete_role_agent(rid, profiles[rid], fam, wm_map)
|
|
agents.append((name, content, "collapse-role"))
|
|
elif lead:
|
|
# fan-out family with a designated synthesis-lead: concrete lead + worker cards only.
|
|
lp = profiles.get(lead)
|
|
assert lp, f"{fam['family-id']}: role-profile 없음 for lead {lead}"
|
|
name, content = build_lead_agent(lead, lp, fam, wm_map)
|
|
agents.append((name, content, "lead"))
|
|
for rid in fam["member-role-ids"]:
|
|
if rid == lead:
|
|
continue
|
|
p = profiles.get(rid)
|
|
assert p, f"{fam['family-id']}: role-profile 없음 for {rid}"
|
|
name, content = build_role_agent(rid, p, fam, wm_map)
|
|
agents.append((name, content, "role"))
|
|
elif is_fanout_split(fam):
|
|
# Family metadata is consumed by role_selector; only concrete workers are discoverable.
|
|
for rid in fam["member-role-ids"]:
|
|
p = profiles.get(rid)
|
|
assert p, f"{fam['family-id']}: role-profile 없음 for {rid}"
|
|
name, content = build_role_agent(rid, p, fam, wm_map)
|
|
agents.append((name, content, "role"))
|
|
else:
|
|
# Single-member fan-out and FAM-ORCH emit their concrete role only.
|
|
assert len(fam["member-role-ids"]) == 1, (
|
|
f"{fam['family-id']}: non-collapse/non-split family must have one member")
|
|
rid = fam["member-role-ids"][0]
|
|
name, content = build_concrete_role_agent(
|
|
rid, profiles[rid], fam, wm_map, direct=True)
|
|
agents.append((name, content, "direct-role"))
|
|
|
|
# 검증
|
|
names = [a[0] for a in agents]
|
|
assert len(names) == len(set(names)), "중복 agent name"
|
|
role_n = sum(1 for a in agents if a[2] == "role")
|
|
lead_n = sum(1 for a in agents if a[2] == "lead")
|
|
for name, content, kind in agents:
|
|
meta = yaml.safe_load(content.split("---\n")[1])
|
|
assert meta["name"] == name
|
|
if kind == "collapse-role":
|
|
assert meta.get("collaboration-role") == "collapse-primary-candidate", f"{name} not collapse candidate"
|
|
assert meta.get("role-id") and meta.get("skills"), f"{name} missing concrete role/method skill"
|
|
assert "family 멤버 전체의 방법론을 합치지" in content, f"{name} collapse isolation missing"
|
|
assert PRIMARY_ARTIFACTS_CONTRACT in content, f"{name} missing primary artifact contract"
|
|
elif kind == "direct-role":
|
|
assert meta.get("collaboration-role") == "direct-role", f"{name} not direct role"
|
|
assert meta.get("role-id") and meta.get("skills"), f"{name} missing concrete role/method skill"
|
|
assert "family 멤버 전체의 방법론을 합치지" in content, f"{name} direct isolation missing"
|
|
assert PRIMARY_ARTIFACTS_CONTRACT in content, f"{name} missing primary artifact contract"
|
|
elif kind == "role":
|
|
assert meta.get("collaboration-role") == "fan-out-worker", f"{name} not worker"
|
|
assert "## 나의 관점·시야·책임" in content and "## Fan-out 워커 계약" in content, f"{name} thin"
|
|
assert "관점:" in content and "evidence-basis" in content
|
|
if name in ("des-prod", "des-platform", "des-internal", "doc-visual"):
|
|
assert "디자인 craft 표준" in content, f"{name} missing craft block"
|
|
if name == "doc-visual": # 다이어그램 직무는 D2 우선·Mermaid 폴백이어야
|
|
assert "D2" in content and "폴백" in content, f"{name} craft not D2-first"
|
|
elif kind == "lead":
|
|
assert meta.get("collaboration-role") == "synthesis-lead", f"{name} not lead"
|
|
assert "## Synthesis-lead 계약" in content and "storyline" in content, f"{name} thin lead"
|
|
assert "synthesized-by" in content, f"{name} lead missing synthesis contract"
|
|
assert "type: d2" in content, f"{name} lead storyline not D2-first"
|
|
else:
|
|
assert "Use PROACTIVELY when" in meta["description"]
|
|
assert "Do NOT use for" in meta["description"]
|
|
assert "## 대표 역할별 관점·시야·책임" in content and "관점:" in content, f"{name} not rich"
|
|
assert "## 협업 실행" in content, f"{name} missing collaboration block"
|
|
if wm_map: # P3: working-method는 method-skill로 분리 — 카드엔 spine/pointer + skills 참조, full embed 없음
|
|
assert "## 일하는 방식" not in content, f"{name} still has full method embed"
|
|
meta2 = yaml.safe_load(content.split("---\n")[1])
|
|
assert "## 핵심 작업 방법" in content, f"{name} missing method spine"
|
|
assert meta2.get("skills"), f"{name} missing skills frontmatter"
|
|
|
|
# 개수 계약: 75 reference roles == 75 executable concrete cards.
|
|
assert role_n == 43, f"expected 43 role agents, got {role_n}"
|
|
assert lead_n == 3, f"expected 3 lead agents, got {lead_n}"
|
|
collapse_role_n = sum(1 for a in agents if a[2] == "collapse-role")
|
|
direct_role_n = sum(1 for a in agents if a[2] == "direct-role")
|
|
assert collapse_role_n == 19, f"expected 19 collapse concrete roles, got {collapse_role_n}"
|
|
assert direct_role_n == 10, f"expected 10 single-member direct roles, got {direct_role_n}"
|
|
assert len(agents) == 75, f"expected 75 concrete agents, got {len(agents)}"
|
|
|
|
if not check:
|
|
for old in glob.glob(os.path.join(OUT_DIR, "*.md")):
|
|
os.remove(old)
|
|
for name, content, _ in agents:
|
|
with open(os.path.join(OUT_DIR, f"{name}.md"), "w") as out:
|
|
out.write(content)
|
|
print(f"OK gen_agents: {len(agents)} concrete agents ({role_n} fan-out workers + {collapse_role_n} collapse workers + "
|
|
f"{direct_role_n} direct workers + {lead_n} lead; family metadata cards=0) "
|
|
f"{'validated' if check else 'written'} (profiles={len(profiles)})")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|