Files
company-haness/docs/superpowers/plans/2026-07-13-p2-design-direction.md

78 KiB
Raw Permalink Blame History

P2 — Design Direction 스테이지 Implementation Plan (rev2)

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: 디자인이 탐색 없이 design-system으로 조기고착되는 결함을 끊는다 — 제품 cascade에 종속된 별도 design-direction child plan이 독립 발산(3안)·단일 수렴·coded prototype·비평 재작업 루프를 state machine으로 강제하고, 그 승인 결과가 /design-system과 부모 design→spec 전이의 선행조건이 된다.

Architecture: Phase 0(P1 의존성·baseline) + A(7 아티팩트 계약·lint 정본) + B(역할 2종·gen_agents router+lead+workers·count) + C(child plan·plan별 초기stage·finalize stage·cycle model) + D(trusted binding·dedup·approval 등록 CLI) + E(approval validator 8점·live staleness) + F(커맨드·부모 cascade gate) + G(doctor) + H(격리 통합테스트·green). 모든 전이 OPS-ORCH 집행. 승인 아티팩트는 불변 report(별도 mutable canonical 없음)이며 부모 workflow·제품 결정·direction-input-brief live hash에 정확히 바인딩. 반복(critique loop)은 cycle-id로 active pointer만 판정.

Tech Stack: Python 3(표준 라이브러리 + pyyaml), 기존 하네스 hook 규약. 테스트는 pytest가 아니라 저장소 표준 standalone check(name, ok) 하니스 + 격리 워크스페이스 + uuid workflow-id(재실행 오염 방지). 프로토타입 렌더는 기존 preview_ui.py.

Spec: docs/superpowers/specs/2026-07-13-p2-design-direction-design.md. 본 rev2는 리뷰 12 Blocker를 반영해 §5(7 아티팩트)·§4(finalize stage)·§7(exact 바인딩)을 정정한다.

Global Constraints

  • 워크스페이스 필수 + 격리: 모든 hook/test 실행은 CLAUDE_PROJECT_DIR="$PWD" + ORGOS_WORKSPACE=<ws>. 테스트는 매 케이스 격리 워크스페이스 + dd-{uuid} workflow-id(고정 ID 금지 — 이전 원장 오염). churn 시 커밋 전 git checkout -- .claude/tests/fixtures/.
  • CI 게이트: 매 task 종료 시 관련 test green, 매 phase 종료 시 run_all.py green.
  • 테스트 규약: pytest 금지. check(name, ok)/카운터/sys.exit(1 if failed else 0). 통합 테스트는 subprocess로 state_engine.py CLI를 실제 호출(helper 단위테스트로 끝내지 않는다 — 핵심 위험은 workflow/hash/receipt 통합 바인딩).
  • brief 분리·불변(S1/Blocker3): direction-input-brief부모 /design이 완성·불변화하고 child init에서 hash 바인딩. child discovery는 brief를 수정하지 않고 direction-discovery 아티팩트(분석)를 낸다. input-brief 필수 9필드 + 금지 6필드(lint 강제).
  • 승인 지연(S2/Blocker1): critique pass는 design-direction-finalize로 가고, finalize→approved에서만 approved-direction 불변 report가 생성·검증된다. 8단계·9전이.
  • 정본 계약(Blocker8): 아티팩트 계약의 정본은 YAML(design-direction-spec.yaml) + lint_design_direction.py(hand-coded, lint_company_context.py 형제). 별도 JSON Schema 파일은 만들지 않는다(unused-artifact 모순 제거).
  • 신뢰경계: 에이전트는 state_engine.py transition·원장·부모 approval-link를 직접 못 쓴다(guard_tools). design-direction 전이 전부 allowed-by: [OPS-ORCH]. 승인 판정 facts 전부 _PROTECTED_FACTS. 부모 approval 등록은 **trusted CLI register-direction-approval**로만.
  • 불변식: .claude/agents/*.md는 생성물(수기편집 금지). 보고서 불변. 카운트: roles 73→75, agents 70→72(workers 42→43·leads 2→3·routers 10·family 16), role-working-methods 73→75.
  • tier 어휘: light|standard|heavy. 게이트는 _is_ui_bearing(led) && tier∈{standard,heavy}.
  • 커밋 트레일러: Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>.

File Structure

신규: org-os/06-agent-work/design-direction-spec.yaml(7 아티팩트) · .claude/hooks/lint_design_direction.py(정본 린터) · .claude/commands/design-direction.md·design-review.md · .claude/tests/test_design_direction.py(격리 통합). 수정: design-brief-spec.yaml(brief-phase) · roles.yaml·role-profiles.yaml·role-working-methods.yaml·capability-families.yaml(2역할+router-entrypoint) · gen_agents.py(router+lead+workers) · execution-plans.yaml(plan) · state-transition-rules.yaml(9전이) · state_engine.py(초기stage·facts·cycle·binding·validator·CLI) · collaboration-map.yaml(direction-gate) · doctor.py · design-system.md·design.md·ceo-intake.md · test_enforcement.py(count).


Phase 0 — Baseline / Dependency Preflight

Task 0: 의존성·baseline 확인 (P2는 P1의 report-sha256 acceptance binding을 전제)

Files: (검증 전용, 커밋 없음 — 스냅샷만 기록)

  • Step 1: git clean + baseline green

Run: git status --short && CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py 2>&1 | tail -3 && python3 .claude/hooks/doctor.py 2>&1 | tail -2 Expected: 워킹트리 clean(또는 fixture만), run_all green, doctor OK.

  • Step 2: P1 의존성 확인 — acceptance_log report-sha256

Run: grep -n "report_sha256\|report-sha256" .claude/hooks/acceptance_log.py | head -3 Expected: report_sha256 파라미터 + ev["report-sha256"] + --report-sha256 CLI 존재. 없으면 중단 — P1을 먼저 완료해야 P2가 성립(P2는 이 hash 바인딩 위에 선다).

  • Step 3: trusted CLI 정책 + gen_agents/fam-design 스냅샷

Run: grep -n "lead-role-id\|router\|def build_router_agent\|def build_lead_agent\|== 70\|len(agents)" .claude/hooks/gen_agents.py | head -12 && ls .claude/agents/fam-design.md Expected: gen_agents가 if lead: (lead) else: (router) 분기(lead 있으면 router 미생성)임을 확인 + fam-design.md(router) 실존. 이 스냅샷이 Task 6(gen_agents 수정)의 기준.

  • Step 4: _has_human_signoff/원장 로더/워크스페이스 헬퍼 실제 함수명 확인 (Task 12/15/17에서 사용)

Run: grep -n "def _load_ledger\|def _state_dir\|def _workspace\|def _load_ledger_safe\|def _plan_stages\|INITIAL_STAGE\|def _accepted_design_types\|def _al_has_accepted\|def read_events\|def _resolve_report_path" .claude/hooks/state_engine.py .claude/hooks/acceptance_log.py Expected: 실제 함수명 목록 확보(후속 task 코드의 헬퍼 호출을 이 이름에 맞춘다).


Phase A — 아티팩트 계약 (7 artifacts, lint 정본)

Task 1: design-direction-spec.yaml (7 아티팩트)

Files: Create org-os/06-agent-work/design-direction-spec.yaml

Interfaces: Produces 7 아티팩트 계약 SoT. Task 45 lint, Task 9 predicate가 필드명 참조.

  • Step 1: 파일 생성 (YAML 문자열 인용 주의 — Blocker 8)
# design-direction-spec — design-direction child plan 의 7 아티팩트 계약(정본).
# 정본 = 이 YAML + lint_design_direction.py(hand-coded). 별도 JSON Schema 파일 없음.
# spec: docs/superpowers/specs/2026-07-13-p2-design-direction-design.md (rev2: 7 아티팩트·finalize·exact 바인딩)
design-direction-spec:
  version: 2
  purpose: "제품 cascade 종속 child plan 의 아티팩트 계약. brief 를 발산 이전/승인 이후로 분리(S1)."

  artifacts:
    # ① 발산 이전 입력 — 부모 /design 이 완성·불변화, child init 에서 hash 바인딩. child 는 수정 금지.
    direction-input-brief:
      required: [product-goal, core-users, core-tasks, information-density,
                 required-accessibility, brand-constraints, avoid-cliches,
                 representative-screen-requirement, tech-platform-constraints]
      prohibited: [reference-cluster, color-palette, typography, layout-grammar, tokens, visual-metaphor]
      note: "prohibited 항목은 승인 이후 design-system-brief 가 approved-direction 으로부터 확정."

    # ② discovery 산출 — 불변 brief 를 분석(수정 아님).
    direction-discovery:
      required: [direction-input-brief-sha256, findings, constraints-restated, opportunity-notes]

    # ③ divergence 산출 — 3안 독립 생산 + 실제 비교 렌더.
    direction-set:
      required: [direction-cycle-id, representative-screen, directions, comparison-preview]
      representative-screen: {required: [id, kind, description], kind-enum: [first-entry, core-task, signature-moment]}
      comparison-preview:
        required: [receipt-ref, receipt-sha256, gallery-path, representative-screen-id]
      direction-required: [id, producer-role-id, producer-run-id, context-package-id,
                           concept-artifact, reference-cluster, visual-thesis, layout-grammar,
                           interaction-grammar, typography-token-direction, coded-slice, coded-slice-sha256]
      reference-cluster-rules:
        - "3~6개로 집중한다(20개 산만보다 6개 집중)"
        - "각 항목은 name, signal, why-relevant 를 가진다"
        - "형용사(modern/clean/minimal/sleek)만으로 설명하지 않는다"

    # ④ decision 산출 — 1안 선택(승인 아님). 평균금지(S5). direction-set 에 바인딩.
    selected-direction:
      required: [direction-cycle-id, direction-set-ref, direction-set-sha256, selected-direction-id,
                 rejected-directions, locked-invariants, parent-workflow-id, product-decision-id,
                 direction-input-brief-sha256, selection-acceptance-receipt]
      optional: [flexible-elements, adopted-elements]
      rules:
        - "selected-direction-id 정확히 1개, direction-set 에 실존"
        - "rejected-directions 가 모든 비선택 방향을 정확히 덮는다(유령/누락 금지), 각 reason 필수"
        - "locked-invariants >= 3"
        - "secondary-influence-id 필드 금지(평균의 뒷문)"
        - "adopted-elements <= 1, from-direction-id 실존, element-id+rationale 필수, 포괄표현 금지, locked-invariant 침범 금지"

    # ⑤ prototype 산출 — 승자 핵심흐름 + 실제 preview receipt. revision 추적.
    winner-prototype:
      required: [direction-cycle-id, selected-direction-ref, selected-direction-sha256,
                 prototype-path, prototype-sha256, preview-receipt-ref, preview-receipt-sha256, revision]

    # ⑥ critique 산출 — 다관점 패널(S3). producer != reviewer.
    design-review-panel:
      required: [direction-cycle-id, target-prototype, preview-receipt, reviews, synthesis]
      review-required: [reviewer-role-id, reviewer-run-id, lens, verdict, report-ref, report-sha256]
      required-lenses: [product-fit, usability, distinctiveness, systematizability, market-memorability, implementability]
      synthesis: {required: [role-id, verdict, unresolved-dissent], verdict-enum: [pass, minor-revision, concept-flaw]}

    # ⑦ terminal 불변 report — 전부를 hash 로 묶음(S2). completion-records 경로.
    approved-direction:
      required: [child-workflow-id, direction-cycle-id, selected-direction-ref, selected-direction-sha256,
                 winner-prototype-ref, winner-prototype-sha256, critique-report-refs, critique-pass-receipt,
                 preview-receipt-ref, locked-invariants, approved-at,
                 parent-workflow-id, product-decision-id, direction-input-brief-sha256]
      path: "<ws>/completion-records/<child-workflow-id>/approved-direction-<ts>.report.yaml"
      parent-ledger-ref: {key: design-direction-approval, fields: [report-ref, report-sha256, child-workflow-id]}

  cross-references:
    - "design-brief-spec.yaml (design-system-brief = brief-phase: system-ready)"
    - "state-transition-rules.yaml / execution-plans.yaml (design-direction plan)"
    - "collaboration-map.yaml (direction-gate)"
  • Step 2: 파싱 확인 + 커밋

Run: python3 -c "import yaml; d=yaml.safe_load(open('org-os/06-agent-work/design-direction-spec.yaml'))['design-direction-spec']; assert set(d['artifacts'])=={'direction-input-brief','direction-discovery','direction-set','selected-direction','winner-prototype','design-review-panel','approved-direction'}; print('OK', d['version'])" Expected: OK 2

git add org-os/06-agent-work/design-direction-spec.yaml
git commit -m "P2(A): design-direction-spec — 7 아티팩트 계약(discovery·winner-prototype·cycle·comparison-preview 추가)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 2: design-brief-spec.yaml — brief-phase 분리

Files: Modify org-os/06-agent-work/design-brief-spec.yaml Interfaces: Produces brief-phase + approved-direction-ref/sha256. Task 15 /design-system이 system-ready brief 요구.

  • Step 1: schema: 최상단에 필드 추가
    brief-phase: pre-direction | system-ready   # P2 S1 — direction 승인 전/후
    approved-direction-ref:      # system-ready 필수 — 승인 방향 불변 report 경로
    approved-direction-sha256:   # system-ready 필수 — staleness 대조
  • Step 2: rules:에 추가
    - 'design-system-brief 는 brief-phase=system-ready 이며 approved-direction-ref/sha256 를 인용한다(P2). reference-cluster·색·typography·token 은 승인 방향에서 확정하고 발명하지 않는다. pre-direction(direction-input-brief)에는 확정 시각 항목 금지(발산 전 고착 방지).'
  • Step 3: 확인 + 커밋

Run: python3 -c "import yaml; assert 'brief-phase' in yaml.safe_load(open('org-os/06-agent-work/design-brief-spec.yaml'))['design-brief-spec']['schema']; print('OK')" Expected: OK

git add org-os/06-agent-work/design-brief-spec.yaml
git commit -m "P2(A): design-brief-spec brief-phase(pre-direction/system-ready) + approved-direction 인용

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 3: lint_design_direction.py — input-brief + direction-set (정본 린터)

Files: Create .claude/hooks/lint_design_direction.py + .claude/tests/test_design_direction.py(스켈레톤) Interfaces: Produces lint_file(path, kind) (input-brief/direction-set/direction-discovery/winner-prototype) + lint_selected_direction(selected_path, direction_set_path)(bundle). 파일 실존·hash 검사 포함.

  • Step 1: 실패 테스트 (스켈레톤 + input-brief 필수/금지)

.claude/tests/test_design_direction.py:

#!/usr/bin/env python3
"""P2 design-direction 강제기 — 격리 워크스페이스 + uuid, standalone check(pytest 아님)."""
import os, sys, tempfile, importlib.util, yaml, uuid, hashlib, subprocess, contextlib, shutil

ROOT = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
HOOKS = os.path.join(ROOT, ".claude", "hooks")
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
L = _load("lint_design_direction", "lint_design_direction.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 _tmp(doc):
    fd, p = tempfile.mkstemp(suffix=".yaml"); os.close(fd); yaml.safe_dump(doc, open(p, "w")); return p
def _sha(p): return hashlib.sha256(open(p, "rb").read()).hexdigest()

@contextlib.contextmanager
def isolated_ws():
    d = tempfile.mkdtemp(prefix="dd-ws-")
    old = os.environ.get("ORGOS_WORKSPACE")
    os.environ["ORGOS_WORKSPACE"] = d
    try: yield d
    finally:
        os.environ.pop("ORGOS_WORKSPACE", None)
        if old: os.environ["ORGOS_WORKSPACE"] = old
        shutil.rmtree(d, ignore_errors=True)

_IB_REQ = {"product-goal": "x", "core-users": "y", "core-tasks": "z", "information-density": "high",
           "required-accessibility": "AA", "brand-constraints": "n/a", "avoid-cliches": "none",
           "representative-screen-requirement": "core-task", "tech-platform-constraints": "web"}
check("input-brief missing required -> Hard Fail",
      any("필수" in h or "required" in h for h in L.lint_file(_tmp({"product-goal": "x"}), "direction-input-brief")[0]))
check("input-brief with reference-cluster -> Hard Fail",
      any("reference-cluster" in h for h in L.lint_file(_tmp({**_IB_REQ, "reference-cluster": [1]}), "direction-input-brief")[0]))
check("clean full input-brief -> ok", not L.lint_file(_tmp(_IB_REQ), "direction-input-brief")[0])

print(f"\n{passed} passed, {failed} failed"); sys.exit(1 if failed else 0)
  • Step 2: 실패 확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: FAIL(모듈 없음).

  • Step 3: 린터 구현 (input-brief 필수+금지 + direction-set 파일/hash)
#!/usr/bin/env python3
"""design-direction 아티팩트 린터(정본) — lint_company_context.py 형제. 파일 실존·hash·평균금지 강제."""
import os, sys, yaml, hashlib

ROOT = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
_IB_REQUIRED = ["product-goal", "core-users", "core-tasks", "information-density",
                "required-accessibility", "brand-constraints", "avoid-cliches",
                "representative-screen-requirement", "tech-platform-constraints"]
_IB_PROHIBITED = ["reference-cluster", "color-palette", "typography", "layout-grammar", "tokens", "visual-metaphor"]
_DIR_REQUIRED = ["id", "producer-role-id", "producer-run-id", "context-package-id", "concept-artifact",
                 "reference-cluster", "visual-thesis", "layout-grammar", "interaction-grammar",
                 "typography-token-direction", "coded-slice", "coded-slice-sha256"]
_GENERIC = ["modern", "clean", "minimal", "sleek"]
_VAGUE = ["분위기", "감성", "스타일", "느낌", "mood", "vibe", "aesthetic"]

def _load(path):
    try: return yaml.safe_load(open(path)) or {}
    except Exception as e: return {"__err__": str(e)}
def _abs(p): return p if os.path.isabs(p) else os.path.join(ROOT, p)
def _file_sha(p):
    ap = _abs(p)
    return hashlib.sha256(open(ap, "rb").read()).hexdigest() if os.path.exists(ap) else None

def lint_file(path, kind):
    doc = _load(path)
    if "__err__" in doc: return ([f"{kind}: YAML 파싱 실패 — {doc['__err__']}"], [])
    if not isinstance(doc, dict): return ([f"{kind}: 최상위가 매핑 아님"], [])
    return {"direction-input-brief": _lint_input_brief, "direction-set": _lint_direction_set,
            "direction-discovery": _lint_discovery, "winner-prototype": _lint_winner}.get(
        kind, lambda d: ([f"unknown kind: {kind}"], []))(doc)

def _lint_input_brief(doc):
    hard = [f"direction-input-brief: 필수 '{k}' 없음" for k in _IB_REQUIRED if not doc.get(k)]
    hard += [f"direction-input-brief: '{k}' 포함 금지(발산 전 고착 S1)" for k in _IB_PROHIBITED if doc.get(k)]
    return (hard, [])

def _lint_direction_set(doc):
    hard, warn = [], []
    dirs = doc.get("directions") or []
    if len(dirs) < 3: hard.append(f"direction-set: 방향 >= 3 필요(현재 {len(dirs)})")
    if not doc.get("direction-cycle-id"): hard.append("direction-set: direction-cycle-id 필요")
    rs = doc.get("representative-screen") or {}
    if not (rs.get("id") and rs.get("kind")): hard.append("direction-set: representative-screen(id/kind) 필요")
    cp = doc.get("comparison-preview") or {}
    for k in ["receipt-ref", "receipt-sha256", "gallery-path", "representative-screen-id"]:
        if not cp.get(k): hard.append(f"direction-set: comparison-preview.{k} 필요(실제 비교 렌더 증거)")
    if cp.get("representative-screen-id") and rs.get("id") and cp["representative-screen-id"] != rs["id"]:
        hard.append("direction-set: comparison-preview 가 대표화면과 불일치")
    for d in dirs:
        if not isinstance(d, dict): hard.append("direction-set: direction 이 매핑 아님"); continue
        miss = [k for k in _DIR_REQUIRED if not d.get(k)]
        if miss: hard.append(f"direction-set: {d.get('id','?')} 필수 누락 {miss}")
        cs, csha = d.get("coded-slice"), d.get("coded-slice-sha256")
        if cs and csha and _file_sha(cs) not in (None, csha):
            hard.append(f"direction-set: {d.get('id','?')} coded-slice hash 불일치")
        elif cs and _file_sha(cs) is None:
            hard.append(f"direction-set: {d.get('id','?')} coded-slice 파일 없음(실제 픽셀 필요)")
        for ref in (d.get("reference-cluster") or []):
            if any(g in (str(ref.get("name","")) + str(ref.get("signal",""))).lower() for g in _GENERIC):
                warn.append(f"direction-set: {d.get('id','?')} reference 형용사(generic-risk)")
    return (hard, warn)

def _lint_discovery(doc):
    hard = [f"direction-discovery: 필수 '{k}' 없음" for k in
            ["direction-input-brief-sha256", "findings", "constraints-restated"] if not doc.get(k)]
    return (hard, [])

def _lint_winner(doc):
    hard = [f"winner-prototype: 필수 '{k}' 없음" for k in
            ["selected-direction-ref", "selected-direction-sha256", "prototype-path", "prototype-sha256",
             "preview-receipt-ref", "revision"] if not doc.get(k)]
    if doc.get("prototype-path") and doc.get("prototype-sha256") and _file_sha(doc["prototype-path"]) not in (None, doc["prototype-sha256"]):
        hard.append("winner-prototype: prototype hash 불일치")
    return (hard, [])

def lint_selected_direction(selected_path, direction_set_path):
    """selected+direction-set BUNDLE 검증(Blocker 8)."""
    sd, ds = _load(selected_path), _load(direction_set_path)
    if "__err__" in sd or not isinstance(sd, dict): return ([f"selected-direction: 로드 실패"], [])
    hard, warn = [], []
    for k in ["direction-set-ref", "direction-set-sha256", "selected-direction-id", "parent-workflow-id",
              "product-decision-id", "direction-input-brief-sha256", "selection-acceptance-receipt"]:
        if not sd.get(k): hard.append(f"selected-direction: 필수 '{k}' 없음")
    if "secondary-influence-id" in sd: hard.append("selected-direction: secondary-influence-id 금지(평균 뒷문)")
    # direction-set 바인딩 hash
    if sd.get("direction-set-sha256") and _file_sha(direction_set_path) not in (None, sd["direction-set-sha256"]):
        hard.append("selected-direction: direction-set-sha256 불일치")
    set_ids = {d.get("id") for d in (ds.get("directions") or [])}
    sel = sd.get("selected-direction-id")
    if sel and sel not in set_ids: hard.append(f"selected-direction: 선택 ID {sel} 가 direction-set 에 없음(유령)")
    rej = sd.get("rejected-directions") or []
    rej_ids = {r.get("id") for r in rej if isinstance(r, dict)}
    for r in rej:
        if not (isinstance(r, dict) and str(r.get("reason") or "").strip()): hard.append("selected-direction: rejected reason 필수")
        if isinstance(r, dict) and r.get("id") not in set_ids: hard.append(f"selected-direction: rejected 유령 ID {r.get('id')}")
    # rejected  {selected} == 전체(정확히 덮음)
    if set_ids and ({sel} | rej_ids) != set_ids:
        hard.append(f"selected-direction: rejectedselected 가 전체 방향과 불일치(누락/여분)")
    lock = sd.get("locked-invariants") or []
    if not (isinstance(lock, list) and len(lock) >= 3): hard.append(f"selected-direction: locked-invariants >= 3(현재 {len(lock) if isinstance(lock,list) else 0})")
    ad = sd.get("adopted-elements") or []
    if isinstance(ad, list):
        if len(ad) > 1: hard.append("selected-direction: adopted-elements <= 1")
        for a in ad:
            if not (isinstance(a, dict) and a.get("element-id") and str(a.get("rationale") or "").strip()):
                hard.append("selected-direction: adopted-elements 는 element-id+rationale 필수")
            elif a.get("from-direction-id") not in set_ids:
                hard.append(f"selected-direction: adopted-elements from {a.get('from-direction-id')} 실존 안 함")
            elif any(v in str(a.get("element-id"))+str(a.get("rationale")) for v in _VAGUE):
                hard.append("selected-direction: adopted-elements 포괄표현 금지(원자 element-id만)")
    return (hard, warn)

if __name__ == "__main__":
    if sys.argv[1] == "--bundle":
        h, w = lint_selected_direction(sys.argv[2], sys.argv[3])
    else:
        h, w = lint_file(sys.argv[1], sys.argv[2])
    for x in h: print(f"HARD: {x}")
    for x in w: print(f"WARN: {x}")
    sys.exit(1 if h else 0)
  • Step 4: 통과 + 커밋

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: PASS(3).

git add .claude/hooks/lint_design_direction.py .claude/tests/test_design_direction.py
git commit -m "P2(A): lint_design_direction 정본(input-brief 필수9+금지6·direction-set 파일/hash·comparison-preview)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 4: lint — selected-direction BUNDLE 평균금지

Files: Modify .claude/tests/test_design_direction.py (bundle 테스트) Interfaces: Consumes lint_selected_direction(Task 3에서 구현됨). 이 task는 계약을 테스트로 고정.

  • Step 1: 테스트 추가 (print(f"\n 앞)
def _ds_file(ids=("DIR-1","DIR-2","DIR-3")):
    return _tmp({"direction-cycle-id": "C1", "representative-screen": {"id": "S1", "kind": "core-task"},
                 "directions": [{"id": i} for i in ids]})
def _sel_file(dsp, **over):
    d = {"direction-set-ref": dsp, "direction-set-sha256": _sha(dsp), "selected-direction-id": "DIR-2",
         "rejected-directions": [{"id": "DIR-1", "reason": "저밀도"}, {"id": "DIR-3", "reason": "클리셰"}],
         "locked-invariants": ["a","b","c"], "parent-workflow-id": "p", "product-decision-id": "PD",
         "direction-input-brief-sha256": "SHA", "selection-acceptance-receipt": "r"}
    d.update(over); return _tmp(d)
dsp = _ds_file()
check("valid selected bundle -> ok", not L.lint_selected_direction(_sel_file(dsp), dsp)[0])
check("ghost selected id -> Hard Fail", any("유령" in h for h in L.lint_selected_direction(_sel_file(dsp, **{"selected-direction-id": "DIR-9"}), dsp)[0]))
check("rejected not covering all -> Hard Fail", any("불일치" in h for h in L.lint_selected_direction(_sel_file(dsp, **{"rejected-directions": [{"id": "DIR-1", "reason": "x"}]}), dsp)[0]))
check("secondary-influence-id -> Hard Fail", any("secondary" in h for h in L.lint_selected_direction(_sel_file(dsp, **{"secondary-influence-id": "DIR-1"}), dsp)[0]))
check("locked<3 -> Hard Fail", any("locked" in h for h in L.lint_selected_direction(_sel_file(dsp, **{"locked-invariants": ["a"]}), dsp)[0]))
check("adopted from ghost -> Hard Fail", any("adopted" in h for h in L.lint_selected_direction(_sel_file(dsp, **{"adopted-elements": [{"from-direction-id": "DIR-9", "element-id": "t", "rationale": "r"}]}), dsp)[0]))
check("adopted vague -> Hard Fail", any("adopted" in h for h in L.lint_selected_direction(_sel_file(dsp, **{"adopted-elements": [{"from-direction-id": "DIR-1", "element-id": "분위기", "rationale": "감성"}]}), dsp)[0]))
check("direction-set hash mismatch -> Hard Fail", any("불일치" in h for h in L.lint_selected_direction(_sel_file(dsp, **{"direction-set-sha256": "WRONG"}), dsp)[0]))
  • Step 2: 통과 확인 (Task 3에서 이미 구현 → GREEN이어야)

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: PASS. (실패하면 Task 3 lint_selected_direction 수정)

  • Step 3: 커밋
git add .claude/tests/test_design_direction.py
git commit -m "P2(A): selected-direction bundle 평균금지 테스트(유령/커버리지/secondary/adopted/hash)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Phase B — 역할 + gen_agents (fam-design 진입점 보존, count 계약)

Task 5: 역할 2종 + capability-families (router-entrypoint 플래그)

Files: Modify roles.yaml, role-profiles.yaml, role-working-methods.yaml, capability-families.yaml Interfaces: Produces role-id DES-DIRECTOR(lead)·DES-VISUAL(worker). FAM-DESIGN lead-role-id: DES-DIRECTOR + router-entrypoint: true(Task 6이 소비) + members 5.

  • Step 1: roles.yaml — DES-INTERNAL 다음
    - { role-id: DES-DIRECTOR, role-name: 디자인 디렉터 AI, role-type: lead, parent-role: EXEC-CPO, default-team-type: Stream-aligned, is-human-proxy: false, is-decision-maker: false, is-execution-agent: false }
    - { role-id: DES-VISUAL, role-name: 비주얼 디자이너 AI, role-type: worker, parent-role: DES-DIRECTOR, default-team-type: Stream-aligned, is-human-proxy: false, is-decision-maker: false, is-execution-agent: true }
  • Step 2: capability-families.yaml FAM-DESIGN — 줄 43 교체
    - { family-id: FAM-DESIGN, collaboration-default: fan-out, lead-role-id: DES-DIRECTOR, router-entrypoint: true, member-role-ids: [DES-DIRECTOR, DES-PROD, DES-PLATFORM, DES-INTERNAL, DES-VISUAL], carries-lenses: [LENS-CUSTOMER], audit-capable: false, default-team-types: [Stream-aligned, Platform], instantiation-priority: standard, invocation-triggers: "UI/UX 디자인, 디자인 방향 발산·수렴, 프로토타입, 디자인시스템, 인터널툴 디자인", exclusions: "프론트 구현 -> FAM-ENG-FRONTEND" }
  • Step 3: role-profiles.yaml + role-working-methods.yaml — DES-PROD 항목 형식 복제로 DES-DIRECTOR·DES-VISUAL 추가. DES-DIRECTOR = 발산 프레이밍 + 3안 원본 종합 수렴 + 패널 종합(단독 평가자 아님). DES-VISUAL = 방향별 아트디렉션·reference cluster·signature interaction·design-craft(제약>묘사·6집중·anti-generic). working-method는 기존 DES-PROD 키 집합 그대로.

  • Step 4: 확인 + 커밋 (gen_agents는 Task 6)

Run: python3 -c "import yaml; f=[x for x in yaml.safe_load(open('org-os/00-role-registry/capability-families.yaml'))['capability-families']['families'] if x['family-id']=='FAM-DESIGN'][0]; assert f['lead-role-id']=='DES-DIRECTOR' and f.get('router-entrypoint') and len(f['member-role-ids'])==5; print('OK')" Expected: OK

git add org-os/00-role-registry/
git commit -m "P2(B): DES-DIRECTOR(lead)+DES-VISUAL + FAM-DESIGN lead-role-id·router-entrypoint·members5

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 6: gen_agents — router+lead+workers 공존 + count 갱신

Files: Modify .claude/hooks/gen_agents.py, .claude/tests/test_enforcement.py Interfaces: Produces fam-design router 보존 + des-director lead + des-visual worker 생성. count: workers 43·leads 3·routers 10·total 72. role-working-methods 75.

  • Step 1: gen_agents if lead: 분기 수정 — 줄 530~ if lead: 블록 시작에 router 조건 추가
        lead = fam.get("lead-role-id")
        if lead:
            # router-entrypoint 인 fan-out family(예: FAM-DESIGN)는 커맨드 진입점(fam-<id>)이
            # 필요하므로 router 도 생성한다. (기존: lead 있으면 router 생략 -> fam-design 진입점 소멸 버그)
            if fam.get("router-entrypoint"):
                rname, rcontent = build_router_agent(fam, profiles, wm_map)
                agents.append((rname, rcontent, "router"))
            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.get("member-role-ids", []):
                if rid == lead:
                    continue
                # ... 기존 worker 생성 로직 그대로 ...

(build_router_agent는 멤버 관점을 나열하되 lead 를 fan-out 멤버 목록에서 제외 — router 계약 문구에 "종합은 DES-DIRECTOR synthesis-lead"를 명시하도록 build_router_agent에 lead 인지 한 줄 추가는 선택. 최소 변경: 멤버 그대로 두어도 동작.)

  • Step 2: count 단언 갱신 — gen_agents 내부 assert(있으면)와 test_enforcement.py

gen_agents.py 하단 count 검증(lead_n/router_n/worker_n/len(agents))이 하드코딩 값과 비교하면 72/43/3/10으로 갱신. test_enforcement.py에서:

check("role-working-methods.yaml covers 75 roles",
      len((_yaml.safe_load(open(os.path.join(ROOT, "org-os/00-role-registry/role-working-methods.yaml")))["role-working-methods"])) == 75)

그리고 agent 총수/router/lead/worker 카운트를 단언하는 test가 있으면 72/10/3/43으로. fam-design router 존재 단언 유지.

  • Step 3: 재생성 + 확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/hooks/gen_agents.py && ls .claude/agents/fam-design.md .claude/agents/des-director.md .claude/agents/des-visual.md && grep -l "design-craft\|제약" .claude/agents/des-visual.md Expected: 세 파일 존재(fam-design router 보존), des-visual에 design-craft 블록.

  • Step 4: 커밋

Run: CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_enforcement.py 2>&1 | tail -3 Expected: role-working-methods 75 + count 단언 green.

git checkout -- .claude/tests/fixtures/ 2>/dev/null
git add .claude/hooks/gen_agents.py .claude/agents/ .claude/tests/test_enforcement.py
git commit -m "P2(B): gen_agents router+lead+workers 공존(fam-design 진입점 보존) + count 70->72

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Phase C — child plan / state graph (초기stage · finalize · cycle model)

Task 7: plan별 초기 stage (_initial_stage)

Files: Modify .claude/hooks/state_engine.py, .claude/tests/test_design_direction.py Interfaces: Produces _initial_stage(plan)_plan_stages(plan)[0]. _default_ledger가 이를 사용. namespaced 첫 stage 시작 가능.

  • Step 1: 실패 테스트 추가 (subprocess로 init 후 stage 확인)
def _se(*a, ws=None):
    env = {**os.environ, "CLAUDE_PROJECT_DIR": ROOT}
    if ws: env["ORGOS_WORKSPACE"] = ws
    return subprocess.run([sys.executable, os.path.join(HOOKS, "state_engine.py"), *a], capture_output=True, text=True, env=env)
with isolated_ws() as ws:
    wf = f"dd-{uuid.uuid4().hex[:8]}"
    _se("init", "--workflow", wf, "--plan", "design-direction", "--parent-workflow", "pc",
        "--product-decision", "PD", "--direction-input-brief", "org-os/06-agent-work/design-direction-spec.yaml", ws=ws)
    cur = _se("current", "--workflow", wf, ws=ws).stdout
    check("design-direction init stage = design-direction-intake", "design-direction-intake" in cur)

(Task 8~에서 init 바인딩 인자를 구현하기 전이므로 이 테스트는 Task 10 이후 완전 통과. 여기선 _initial_stage 단위만 우선.)

SE = _load("state_engine", "state_engine.py")
check("_initial_stage design-direction", SE._initial_stage("design-direction") == "design-direction-intake")
check("_initial_stage cascade = intake", SE._initial_stage("cascade") == "intake")
  • Step 2: 실패 확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: FAIL(_initial_stage 없음).

  • Step 3: 구현_plan_stages 다음에
def _initial_stage(plan):
    """plan 의 첫 stage. namespaced plan(design-direction)은 자기 첫 stage 에서 시작."""
    stages = _plan_stages(plan)
    return stages[0] if stages else INITIAL_STAGE

_default_ledger(줄 236) 수정:

        "stage": _initial_stage(plan),
  • Step 4: 통과 확인(단위) + 커밋

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py 2>&1 | grep _initial_stage Expected: _initial_stage 2건 PASS. (init stage 통합 테스트는 Task 10 후 GREEN)

git checkout -- .claude/tests/fixtures/ 2>/dev/null
git add .claude/hooks/state_engine.py .claude/tests/test_design_direction.py
git commit -m "P2(C): _initial_stage(plan) — _default_ledger 가 plan 첫 stage 에서 시작(namespaced 지원)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 8: plan + 전이 9종(finalize 포함) + condition-catalog

Files: Modify execution-plans.yaml, state-transition-rules.yaml, test Interfaces: Produces design-direction plan(8 stages, terminal design-direction-approved) + 9 전이(역전이 2 + finalize). condition-catalog 항목.

  • Step 1: 실패 테스트 추가
plans = yaml.safe_load(open(os.path.join(ROOT, "org-os/06-agent-work/execution-plans.yaml")))["execution-plans"]["plans"]
dd = plans.get("design-direction", {})
check("plan has 8 namespaced stages incl finalize",
      "design-direction-finalize" in dd.get("stages", []) and len(dd.get("stages", [])) == 8)
trans = yaml.safe_load(open(os.path.join(ROOT, "org-os/00-role-registry/state-transition-rules.yaml")))["state-transition-rules"]["workflow-stage-transitions"]["transitions"]
ddt = [t for t in trans if str(t.get("from","")).startswith("design-direction")]
check("design-direction transitions == 9", len(ddt) == 9)
check("finalize->approved exists", any(t.get("from")=="design-direction-finalize" and t.get("to")=="design-direction-approved" for t in trans))
check("critique->finalize (not approved)", any(t.get("from")=="design-direction-critique" and t.get("to")=="design-direction-finalize" for t in trans))
  • Step 2: 실패 확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: FAIL.

  • Step 3: execution-plans.yaml — venture-bootstrap 다음
    design-direction:
      description: >
        제품 cascade 종속 child. direction-input-brief(불변)을 입력으로 discovery→3안 독립발산→
        단일수렴→승자 prototype→비평 재작업 루프→finalize→approved. /design-system·design→spec 선행.
      stages: [design-direction-intake, design-direction-discovery, design-direction-divergence,
               design-direction-decision, design-direction-prototype, design-direction-critique,
               design-direction-finalize, design-direction-approved]
      terminal-stage: design-direction-approved
      default-tier: standard
      parent-binding: [parent-workflow-id, product-decision-id, direction-input-brief-sha256]
      outputs: "approved-direction 불변 report(completion-records/<child>/) + 부모 원장 design-direction-approval"
  • Step 4: state-transition-rules.yaml — condition-catalog + transitions

condition-catalog에 추가:

      parent-binding-present: "child 원장 parent-workflow-id+product-decision-id+direction-input-brief-ref 존재(실존 부모·accepted product-decision 대조)"
      direction-input-brief-valid: "direction-input-brief lint Hard Fail 0(필수9+금지6)"
      direction-discovery-present: "direction-discovery 아티팩트 존재(불변 brief 분석)"
      directions-diverged: "active cycle direction-set: >=3안·producer-run/pkg 상이·동일 대표화면·각 coded-slice 실존/hash·comparison-preview receipt(§9 독립성)"
      selected-direction-accepted: "active cycle selected-direction bundle lint 통과 + selection acceptance 이벤트"
      winner-prototype-present: "active cycle winner-prototype 존재 + preview receipt"
      critique-revision-requested: "active cycle design-review-panel synthesis.verdict=minor-revision"
      concept-rejection-recorded: "active cycle design-review-panel synthesis.verdict=concept-flaw"
      direction-critique-passed: "패널 pass(필수lens·producer≠reviewer·synthesis=pass) + winner preview receipt"
      approved-direction-valid: "approved-direction 불변 report 생성·schema/lint 통과·전 참조 hash 일치"
      approval-receipt-bound: "approved-direction acceptance 이벤트가 child workflow+report-id+report-sha256 에 정확 바인딩"
      parent-approval-link-recorded: "부모 원장 design-direction-approval(report-ref/sha256/child-workflow-id) 기록됨(register CLI)"

transitions에 9종 추가:

      # ── design-direction(제품 cascade 종속 child) — 전이 OPS-ORCH 단독 ──
      - { from: design-direction-intake, to: design-direction-discovery, allowed-by: [OPS-ORCH], required-conditions: [parent-binding-present, direction-input-brief-valid] }
      - { from: design-direction-discovery, to: design-direction-divergence, allowed-by: [OPS-ORCH], required-conditions: [direction-discovery-present] }
      - { from: design-direction-divergence, to: design-direction-decision, allowed-by: [OPS-ORCH], required-conditions: [directions-diverged] }
      - { from: design-direction-decision, to: design-direction-prototype, allowed-by: [OPS-ORCH], required-conditions: [selected-direction-accepted] }
      - { from: design-direction-prototype, to: design-direction-critique, allowed-by: [OPS-ORCH], required-conditions: [winner-prototype-present] }
      - { from: design-direction-critique, to: design-direction-prototype, allowed-by: [OPS-ORCH], required-conditions: [critique-revision-requested] }
      - { from: design-direction-critique, to: design-direction-divergence, allowed-by: [OPS-ORCH], required-conditions: [concept-rejection-recorded] }
      - { from: design-direction-critique, to: design-direction-finalize, allowed-by: [OPS-ORCH], required-conditions: [direction-critique-passed] }
      - { from: design-direction-finalize, to: design-direction-approved, allowed-by: [OPS-ORCH], required-conditions: [approved-direction-valid, approval-receipt-bound, parent-approval-link-recorded] }

unified-stage-graph 주석에 design-direction 8 stage(finalize 포함) 추가.

  • Step 5: 통과 + 커밋

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: plan/전이 테스트 PASS.

git add org-os/06-agent-work/execution-plans.yaml org-os/00-role-registry/state-transition-rules.yaml .claude/tests/test_design_direction.py
git commit -m "P2(C): design-direction plan(8 stage·finalize) + 전이 9종(역전이2+finalize) + condition-catalog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 9: cycle model — active pointer + facts/predicates/helpers

Files: Modify state_engine.py, test Interfaces: Produces _active(led, key)(active cycle report-id 해석), _active_artifact(led, arts, design_type), _directions_diverged, _selected_direction_ok, _critique_panel_ok; facts 10종; predicate 9키; _PROTECTED_FACTS 확장. active cycle 만 판정(오래된 pass 재사용 차단, Blocker 6).

  • Step 1: 실패 테스트 추가
SE = _load("state_engine", "state_engine.py")
for k in ["parent-binding-present","direction-discovery-present","directions-diverged","selected-direction-accepted",
          "winner-prototype-present","critique-revision-requested","concept-rejection-recorded","direction-critique-passed"]:
    check(f"predicate {k}", k in SE._PREDICATES)
for fk in ["directions_diverged","selected_direction_accepted","design_direction_approved","parent_binding_present"]:
    check(f"protected {fk}", fk in SE._PROTECTED_FACTS)
# active cycle: 오래된 pass 패널이 남아도 현재 cycle 아니면 무시
led = {"design-direction-active": {"cycle-id": "C2", "review-panel-report-id": "RP-C2"},
       "artifacts": [
         {"design-type": "design-review-panel", "report-id": "RP-C1", "direction-cycle-id": "C1",
          "design-review-panel": {"synthesis": {"verdict": "pass", "role-id": "DES-DIRECTOR"}, "reviews": []}},
         {"design-type": "design-review-panel", "report-id": "RP-C2", "direction-cycle-id": "C2",
          "design-review-panel": {"synthesis": {"verdict": "minor-revision", "role-id": "DES-DIRECTOR"}, "reviews": []}}]}
f = SE._facts("wf-cycle", led, None)
check("stale pass panel ignored, active=minor-revision", f.get("critique_revision_requested") and not f.get("direction_critique_passed"))
  • Step 2: 실패 확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: FAIL.

  • Step 3: helpers 구현_venture_decision_receipt_ok 다음
def _dd_active(led):
    return led.get("design-direction-active") or (led.get("facts") or {}).get("design-direction-active") or {}

def _active_artifact(led, arts, design_type, id_key):
    """active cycle pointer 가 가리키는 report-id 의 아티팩트만 반환(오래된 것 무시, Blocker 6)."""
    want = _dd_active(led).get(id_key)
    for a in (arts or []):
        if isinstance(a, dict) and a.get("design-type") == design_type:
            if want is None or a.get("report-id") == want:
                return a
    return None

def _dd_input_brief_ref(led):
    return led.get("direction-input-brief-ref") or (led.get("facts") or {}).get("direction-input-brief-ref")

def _current_input_brief_sha(led):
    ref = _dd_input_brief_ref(led)
    if not ref: return None
    p = ref if os.path.isabs(ref) else os.path.join(ROOT, ref)
    return hashlib.sha256(open(p, "rb").read()).hexdigest() if os.path.exists(p) else None

def _directions_diverged(led, arts):
    a = _active_artifact(led, arts, "direction-set", "direction-set-report-id")
    ds = (a or {}).get("direction-set") or a
    if not isinstance(ds, dict): return False
    dirs = ds.get("directions") or []
    if len(dirs) < 3: return False
    runs = [d.get("producer-run-id") for d in dirs]
    pkgs = [d.get("context-package-id") for d in dirs]
    if None in runs or len(set(runs)) != len(runs): return False
    if None in pkgs or len(set(pkgs)) != len(pkgs): return False
    if not (ds.get("representative-screen") or {}).get("id"): return False
    cp = ds.get("comparison-preview") or {}
    if not (cp.get("receipt-ref") and cp.get("gallery-path")): return False
    for d in dirs:
        cs = d.get("coded-slice")
        if not cs: return False
        ap = cs if os.path.isabs(cs) else os.path.join(ROOT, cs)
        if not os.path.exists(ap): return False   # 실제 픽셀
    return True

def _selected_direction_ok(led, arts):
    sd = _active_artifact(led, arts, "selected-direction", "selected-direction-report-id")
    dset = _active_artifact(led, arts, "direction-set", "direction-set-report-id")
    if not (sd and dset and sd.get("path") and dset.get("path")): return False
    try:
        import lint_design_direction as LDD
        hard, _ = LDD.lint_selected_direction(
            sd["path"] if os.path.isabs(sd["path"]) else os.path.join(ROOT, sd["path"]),
            dset["path"] if os.path.isabs(dset["path"]) else os.path.join(ROOT, dset["path"]))
        return not hard
    except Exception:
        return False

def _critique_panel_ok(led, arts):
    a = _active_artifact(led, arts, "design-review-panel", "review-panel-report-id")
    panel = (a or {}).get("design-review-panel") or a
    if not isinstance(panel, dict): return False
    required = {"product-fit","usability","distinctiveness","systematizability","market-memorability","implementability"}
    reviews = panel.get("reviews") or []
    if not required.issubset({r.get("lens") for r in reviews}): return False
    ds = _active_artifact(led, arts, "direction-set", "direction-set-report-id") or {}
    producer_runs = {d.get("producer-run-id") for d in ((ds.get("direction-set") or ds).get("directions") or [])}
    if any(r.get("reviewer-run-id") in producer_runs for r in reviews): return False
    syn = panel.get("synthesis") or {}
    return syn.get("verdict") == "pass" and str(syn.get("role-id","")).upper() == "DES-DIRECTOR"

def _panel_verdict(led, arts):
    a = _active_artifact(led, arts, "design-review-panel", "review-panel-report-id")
    return ((a or {}).get("design-review-panel") or a or {}).get("synthesis", {}).get("verdict") if a else None
  • Step 4: _facts 추가 — venture-bootstrap facts 다음
    # --- design-direction facts (P2) ---
    _pd_accepted = f.get("decision_packet_accepted")
    f["parent_binding_present"] = bool(led.get("parent-workflow-id") and led.get("product-decision-id") and _dd_input_brief_ref(led))
    try:
        import lint_design_direction as _LDD
        _ib = _dd_input_brief_ref(led); _ibp = (_abs_ib := (_ib if (_ib and os.path.isabs(_ib)) else (os.path.join(ROOT, _ib) if _ib else None)))
        f["direction_input_brief_valid"] = bool(_ibp and os.path.exists(_ibp) and not _LDD.lint_file(_ibp, "direction-input-brief")[0])
    except Exception:
        f["direction_input_brief_valid"] = False
    f["direction_discovery_present"] = bool(_active_artifact(led, arts, "direction-discovery", "direction-discovery-report-id"))
    f["directions_diverged"] = _directions_diverged(led, arts)
    f["selected_direction_accepted"] = _selected_direction_ok(led, arts) and _al_has_accepted(wf)
    _wp = _active_artifact(led, arts, "winner-prototype", "prototype-report-id")
    f["winner_prototype_present"] = bool(_wp and (_wp.get("winner-prototype") or _wp).get("preview-receipt-ref")) and _has_preview_receipt(wf)
    _v = _panel_verdict(led, arts)
    f["critique_revision_requested"] = (_v == "minor-revision")
    f["concept_rejection_recorded"] = (_v == "concept-flaw")
    f["direction_critique_passed"] = _critique_panel_ok(led, arts) and _has_preview_receipt(wf)
    f["design_direction_approved"] = _has_direction_approval(wf, led)   # Task 11
  • Step 5: _PROTECTED_FACTS + _PREDICATES 추가

_PROTECTED_FACTS에:

    "parent_binding_present", "direction_input_brief_valid", "direction_discovery_present",
    "directions_diverged", "selected_direction_accepted", "winner_prototype_present",
    "critique_revision_requested", "concept_rejection_recorded", "direction_critique_passed",
    "design_direction_approved",

_PREDICATES에:

    "parent-binding-present": lambda f: (bool(f.get("parent_binding_present")), "parent 바인딩 없음"),
    "direction-input-brief-valid": lambda f: (bool(f.get("direction_input_brief_valid")), "input-brief lint 실패(필수/금지)"),
    "direction-discovery-present": lambda f: (bool(f.get("direction_discovery_present")), "direction-discovery 아티팩트 없음"),
    "directions-diverged": lambda f: (bool(f.get("directions_diverged")), "3안 독립성/실픽셀/비교렌더 실패"),
    "selected-direction-accepted": lambda f: (bool(f.get("selected_direction_accepted")), "selected bundle lint 실패 또는 미승인"),
    "winner-prototype-present": lambda f: (bool(f.get("winner_prototype_present")), "winner-prototype/preview receipt 없음"),
    "critique-revision-requested": lambda f: (bool(f.get("critique_revision_requested")), "active verdict != minor-revision"),
    "concept-rejection-recorded": lambda f: (bool(f.get("concept_rejection_recorded")), "active verdict != concept-flaw"),
    "direction-critique-passed": lambda f: (bool(f.get("direction_critique_passed")), "패널 pass 미충족"),

(approved-direction-valid / approval-receipt-bound / parent-approval-link-recorded predicate는 Task 11.)

  • Step 6: 통과 + 커밋

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: PASS(cycle·predicate). (_has_direction_approval 미정의로 import 에러 나면 Task 11 stub 먼저: def _has_direction_approval(wf, led): return False 임시 배치 후 Task 11에서 완성.)

git checkout -- .claude/tests/fixtures/ 2>/dev/null
git add .claude/hooks/state_engine.py .claude/tests/test_design_direction.py
git commit -m "P2(C): cycle model(active pointer) + facts/helpers(독립성·실픽셀·패널) + predicate 9 + PROTECTED

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Phase D — trusted binding (parent 검증 · dedup · approval 등록)

Task 10: init 바인딩 + parent 검증 + dedup

Files: Modify state_engine.py(init_ledger, CLI init), test Interfaces: Produces init_ledger(..., parent_workflow, product_decision, direction_input_brief) — 부모 실존 + product-decision accepted 검증 후 바인딩. find_child_direction_workflow(parent, product_decision, brief_sha) dedup.

  • Step 1: 실패 테스트 추가
with isolated_ws() as ws:
    r = _se("init", "--workflow", f"dd-{uuid.uuid4().hex[:8]}", "--plan", "design-direction", ws=ws)
    check("design-direction init without binding -> reject", r.returncode != 0)
with isolated_ws() as ws:
    # 부모 원장 없음 -> 거부
    r = _se("init", "--workflow", f"dd-{uuid.uuid4().hex[:8]}", "--plan", "design-direction",
            "--parent-workflow", "ghost-parent", "--product-decision", "PD",
            "--direction-input-brief", "org-os/06-agent-work/design-direction-spec.yaml", ws=ws)
    check("nonexistent parent -> reject", r.returncode != 0)
  • Step 2: 실패 확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: FAIL.

  • Step 3: init_ledger 확장 (부모 검증 포함)
def init_ledger(wf, plan=DEFAULT_PLAN, tier=DEFAULT_TIER, mode=DEFAULT_MODE, overwrite=False,
                parent_workflow=None, product_decision=None, direction_input_brief=None):
    led = _default_ledger(wf, plan, tier, mode)
    if plan == "design-direction":
        if not (parent_workflow and product_decision and direction_input_brief):
            raise ValueError("design-direction: --parent-workflow --product-decision --direction-input-brief 필수")
        try:
            parent_led = _load_ledger(parent_workflow)
        except Exception:
            raise ValueError(f"design-direction: 부모 workflow '{parent_workflow}' 원장 없음")
        if not _al_has_accepted(parent_workflow) and product_decision not in str(parent_led):
            raise ValueError(f"design-direction: 부모에 accepted product-decision '{product_decision}' 없음")
        led["parent-workflow-id"] = parent_workflow
        led["product-decision-id"] = product_decision
        led["direction-input-brief-ref"] = direction_input_brief
        p = direction_input_brief if os.path.isabs(direction_input_brief) else os.path.join(ROOT, direction_input_brief)
        if os.path.exists(p):
            led["direction-input-brief-sha256"] = hashlib.sha256(open(p, "rb").read()).hexdigest()
    # ... 기존 저장 로직(원장 파일 write) ...
    _save_ledger(wf, led)   # 실제 저장 함수명에 맞춘다(Task 0 확인)
    return led

def find_child_direction_workflow(parent_workflow, product_decision, brief_sha):
    base = _state_root()   # 실제 state 디렉토리 헬퍼명(Task 0 확인)
    if not os.path.isdir(base): return None
    for wf in os.listdir(base):
        try: led = _load_ledger(wf)
        except Exception: continue
        if led.get("plan") != "design-direction": continue
        if led.get("parent-workflow-id") == parent_workflow and led.get("product-decision-id") == product_decision:
            return {"workflow-id": wf, "stage": led.get("stage"),
                    "stale": led.get("direction-input-brief-sha256") != brief_sha}
    return None
  • Step 4: CLI init 인자 연결
        init_ledger(wf, plan=_argval(args, "--plan", DEFAULT_PLAN), tier=_argval(args, "--tier", DEFAULT_TIER),
                    mode=_argval(args, "--mode", DEFAULT_MODE),
                    parent_workflow=_argval(args, "--parent-workflow"),
                    product_decision=_argval(args, "--product-decision"),
                    direction_input_brief=_argval(args, "--direction-input-brief"))

(ValueError → 비영 종료. 기존 init try/except 관례에 맞춘다.)

  • Step 5: 통과 + 커밋

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: PASS.

git checkout -- .claude/tests/fixtures/ 2>/dev/null
git add .claude/hooks/state_engine.py .claude/tests/test_design_direction.py
git commit -m "P2(D): init 바인딩 + 부모 실존/accepted product-decision 검증 + dedup 조회

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 11: register-direction-approval trusted CLI + _has_direction_approval 8점

Files: Modify state_engine.py(신규 CLI + validator), test Interfaces: Produces register_direction_approval(parent, child, report, report_sha256)(원자적 부모 원장 기록, 전 검증) + _has_direction_approval(wf, led)(exact 8점, fail-open 없음, live staleness) + predicates(approved-direction-valid/approval-receipt-bound/parent-approval-link-recorded/design-direction-approved).

  • Step 1: 실패 테스트 추가
# fail-closed: 참조 없음
check("no approval link -> not approved", not SE._has_direction_approval("wfx", {"plan": "cascade"}))
# staleness helper
approved = {"parent-workflow-id": "p", "product-decision-id": "PD", "direction-input-brief-sha256": "OLD",
            "child-workflow-id": "c", "selected-direction-sha256": "S", "winner-prototype-sha256": "W"}
check("brief mismatch stale", not SE._approval_brief_matches(approved, "NEW"))
check("brief match ok", SE._approval_brief_matches(approved, "OLD"))
# register CLI: 없는 child -> 거부
with isolated_ws() as ws:
    r = _se("register-direction-approval", "--parent-workflow", "p", "--child-workflow", "ghost",
            "--report", "/nope.yaml", "--report-sha256", "x", ws=ws)
    check("register with ghost child -> reject", r.returncode != 0)
  • Step 2: 실패 확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: FAIL.

  • Step 3: _has_direction_approval + register 구현
def _approval_brief_matches(approved_doc, current_sha):
    return bool(current_sha) and approved_doc.get("direction-input-brief-sha256") == current_sha

def _dd_load_report(path):
    ap = path if os.path.isabs(path) else os.path.join(_workspace_root(), path)  # 실제 ws 헬퍼명
    return ap if os.path.exists(ap) else None

def _has_direction_approval(wf, led):
    """§7 exact 8점 + live staleness. fail-open 없음. wf 는 부모 workflow, led 는 부모 원장."""
    link = led.get("design-direction-approval") or {}
    report_ref, report_sha, child = link.get("report-ref"), link.get("report-sha256"), link.get("child-workflow-id")
    if not (report_ref and report_sha and child):
        return False
    # 1) child 실존 + 부모-자식 관계
    try:
        child_led = _load_ledger(child)
    except Exception:
        return False
    if child_led.get("parent-workflow-id") != wf:
        return False
    # 2) child 가 terminal(approved) stage
    if child_led.get("stage") != "design-direction-approved":
        return False
    # 3) report 파일 실존 + hash 일치
    ap = _dd_load_report(report_ref)
    if not ap or hashlib.sha256(open(ap, "rb").read()).hexdigest() != report_sha:
        return False
    doc = _load_yaml(ap).get("approved-direction") or _load_yaml(ap)
    # 4) parent/product/child 일치
    if doc.get("parent-workflow-id") != wf or doc.get("child-workflow-id") != child:
        return False
    if doc.get("product-decision-id") != led.get("product-decision-id-of-child", child_led.get("product-decision-id")):
        return False
    # 5) 현재 accepted product-decision 이 supersede 안 됨(부모 acceptance_log 대조)
    if not _product_decision_current(wf, doc.get("product-decision-id")):
        return False
    # 6) live input-brief hash 일치(부모의 현재 brief)
    current_sha = _current_input_brief_sha(child_led)
    if not _approval_brief_matches(doc, current_sha):
        return False
    # 7) 참조 아티팩트 hash 검증(selected/winner-prototype)
    for ref_key, sha_key in [("selected-direction-ref", "selected-direction-sha256"),
                             ("winner-prototype-ref", "winner-prototype-sha256")]:
        rp = _dd_load_report(doc.get(ref_key) or "")
        if not rp or hashlib.sha256(open(rp, "rb").read()).hexdigest() != doc.get(sha_key):
            return False
    # 8) acceptance receipt 가 child+report-id+report-sha256 에 정확 바인딩(wf None 불허 -> child 정확)
    if not _direction_approval_receipt_ok(child, report_ref, report_sha):
        return False
    return True

def _direction_approval_receipt_ok(child_wf, report_ref, report_sha):
    try:
        import acceptance_log as AL
        rid_expected = os.path.basename(report_ref)
        for ev in reversed(list(AL.read_events())):
            if ev.get("decision") != "accepted": continue
            if ev.get("workflow-id") != child_wf: continue        # None 불허 — 정확 일치
            if ev.get("report-sha256") != report_sha: continue
            return True
        return False
    except Exception:
        return False

def _product_decision_current(parent_wf, pd_id):
    """product-decision 이 supersede 되지 않았는지(부모 acceptance_log)."""
    try:
        import acceptance_log as AL
        superseded = set()
        current = False
        for ev in AL.read_events():
            if ev.get("workflow-id") not in (None, parent_wf): continue
            if ev.get("supersedes"): superseded.add(ev.get("supersedes"))
            if ev.get("decision") == "accepted" and pd_id and pd_id in str(ev.get("accepted-report-id") or ev.get("report-id") or ""):
                current = True
        return current and pd_id not in superseded
    except Exception:
        return False

def register_direction_approval(parent, child, report, report_sha256):
    """trusted CLI 백엔드 — 전 검증 후 부모 원장 design-direction-approval 원자 기록."""
    child_led = _load_ledger(child)                 # 없으면 예외
    if child_led.get("parent-workflow-id") != parent:
        raise ValueError("child 가 parent 의 자식이 아님")
    if child_led.get("stage") not in ("design-direction-finalize", "design-direction-approved"):
        raise ValueError("child 가 finalize/approved stage 아님")
    ap = _dd_load_report(report)
    if not ap or hashlib.sha256(open(ap, "rb").read()).hexdigest() != report_sha256:
        raise ValueError("report 경로/hash 무효")
    parent_led = _load_ledger(parent)
    existing = parent_led.get("design-direction-approval")
    if existing and existing.get("child-workflow-id") not in (None, child):
        raise ValueError("기존 active approval 과 충돌")
    parent_led["design-direction-approval"] = {"report-ref": report, "report-sha256": report_sha256, "child-workflow-id": child}
    _save_ledger(parent, parent_led)
    return True
  • Step 4: CLI + predicate + facts 배선

CLI(dispatch에 추가):

    if cmd == "register-direction-approval":
        try:
            register_direction_approval(_argval(args,"--parent-workflow"), _argval(args,"--child-workflow"),
                                        _argval(args,"--report"), _argval(args,"--report-sha256"))
            print("registered"); sys.exit(0)
        except Exception as e:
            print(f"거부: {e}", file=sys.stderr); sys.exit(2)
    if cmd == "check-direction-approved":
        led = _load_ledger(wf); ok = _has_direction_approval(wf, led)
        print(f"{wf}: direction-approved={'YES' if ok else 'NO'}"); sys.exit(0 if ok else 3)

predicate(finalize→approved 조건 3종은 register+validator로 충족되므로 facts 파생):

    "approved-direction-valid": lambda f: (bool(f.get("design_direction_approved")), "approved-direction 검증 실패"),
    "approval-receipt-bound": lambda f: (bool(f.get("design_direction_approved")), "acceptance receipt 바인딩 실패"),
    "parent-approval-link-recorded": lambda f: (bool(f.get("design_direction_approved")), "부모 approval-link 미기록"),
    "design-direction-approved": lambda f: (bool(f.get("design_direction_approved")), "approved-direction 8점/staleness 실패 — 자기신고 불가"),

(단순화: 세 finalize 조건 모두 design_direction_approved 복합 파생을 공유 — 셋 다 참이어야 approved 이므로 동치. 원한다면 각각 분리 파생 가능.)

  • Step 5: 통과 + 커밋

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: PASS.

git checkout -- .claude/tests/fixtures/ 2>/dev/null
git add .claude/hooks/state_engine.py .claude/tests/test_design_direction.py
git commit -m "P2(E): _has_direction_approval exact 8점(fail-open 제거·live staleness·supersede) + register CLI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Phase F — 커맨드 + 부모 cascade gate

Task 12: /design-direction + /design-review 커맨드

Files: Create .claude/commands/design-direction.md, design-review.md Interfaces: Produces 커맨드 prose(각 스테이지 record+transition+acceptance 명시). cycle-id·active pointer 갱신·resume 명시.

  • Step 1: design-direction.md — 절차(요지):

    1. init --plan design-direction(바인딩 3종). dedup: 동일 binding running→resume, approved→재사용, stale→신규 cycle.
    2. -discovery: DES-DIRECTOR가 불변 brief 분석direction-discovery 아티팩트(brief 수정 금지) → active pointer 갱신 → transition.
    3. -divergence: DES-VISUAL 3 격리 run(producer-run-id/context-package 상이·형제 must-read 금지) → 동일 대표화면 slice → 갤러리 패키지 preview_ui.py 1회 → direction-set(cycle-id·comparison-preview) → transition.
    4. -decision: DES-DIRECTOR 3안 원본 종합 → selected-direction(bundle, 평균금지) → acceptance_log append accepted → transition.
    5. -prototype: 승자 핵심흐름 coded → preview_ui.pywinner-prototype(revision) → transition.
    6. -critique: /design-review 패널 → verdict로 transition(pass→finalize / minor→prototype[revision++] / concept→divergence[새 cycle]).
    7. -finalize: approved-direction 불변 report 생성 → acceptance_log append accepted --report-sha256state_engine register-direction-approval(부모 원장 기록) → transition→approved. report-header(BLUF)·evidence·권한(npm/chrome 로컬) 명시.
  • Step 2: design-review.md — 6-lens 패널(DES-PROD/UX-RESEARCHER/DES-VISUAL(비생산 run)/DES-PLATFORM/GTM-PMM/ENG-FE) 각 격리 → design-review-panel(cycle-id·각 report-sha256) → DES-DIRECTOR synthesis. producer-run-id ≠ reviewer-run-id 명시.

  • Step 3: 참조 무결성 + 커밋

Run: python3 .claude/hooks/lint_refs.py 2>&1 | tail -3 Expected: 통과(DES-DIRECTOR/DES-VISUAL 에이전트 실존 — Task 6 재생성).

git add .claude/commands/design-direction.md .claude/commands/design-review.md
git commit -m "P2(F): /design-direction(cycle·resume) + /design-review(패널·producer≠reviewer) 커맨드

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 13: 부모 cascade gate — _is_ui_bearing + design→spec 조건 + /design·/design-system·ceo-intake

Files: Modify state_engine.py, state-transition-rules.yaml, collaboration-map.yaml, design.md, design-system.md, ceo-intake.md, test Interfaces: Produces _is_ui_bearing(led) + design-direction-gate-satisfied 조건(부모 design→spec) + collaboration-map direction-gate. /design-system만 막던 우회(spec/build 직행) 봉인(Blocker 10).

  • Step 1: 실패 테스트 추가
check("ui-bearing explicit true", SE._is_ui_bearing({"deliverable-profile": {"ui-bearing": True}}))
check("ui-bearing explicit false", not SE._is_ui_bearing({"deliverable-profile": {"ui-bearing": False}, "build-families": ["FAM-ENG-FRONTEND"]}))
check("ui-bearing fallback FE", SE._is_ui_bearing({"build-families": ["FAM-ENG-FRONTEND"]}))
# design->spec gate: UI+standard, 승인 없음 -> 차단
ok, reasons = SE.can_transition_facts_only({"deliverable-profile": {"ui-bearing": True}, "tier": "standard",
                                            "design_direction_approved": False}, "design-direction-gate-satisfied") \
    if hasattr(SE, "can_transition_facts_only") else (None, None)
# 직접 predicate 평가
pred = SE._PREDICATES.get("design-direction-gate-satisfied")
f_block = {"tier": "standard", "_ui_bearing": True, "design_direction_approved": False}
f_pass = {"tier": "light", "_ui_bearing": True, "design_direction_approved": False}
check("gate blocks UI+standard w/o approval", not pred(f_block)[0])
check("gate passes UI+light", pred(f_pass)[0])
  • Step 2: 실패 확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py Expected: FAIL.

  • Step 3: _is_ui_bearing + gate predicate + facts
def _is_ui_bearing(led):
    profile = led.get("deliverable-profile") or {}
    if "ui-bearing" in profile:
        return bool(profile["ui-bearing"])
    kinds = str(led.get("deliverable-kind") or "")
    if any(k in kinds for k in ("ui", "screen", "frontend", "app")):
        return True
    fams = led.get("build-families") or ([led.get("build-family")] if led.get("build-family") else [])
    return "FAM-ENG-FRONTEND" in fams

_facts에:

    f["_ui_bearing"] = _is_ui_bearing(led)

_PREDICATES에:

    "design-direction-gate-satisfied": lambda f: (
        (not f.get("_ui_bearing")) or f.get("tier") not in ("standard", "heavy") or bool(f.get("design_direction_approved")),
        "UI-bearing standard/heavy: 승인된 design-direction 필요(우회 차단)"),

_PROTECTED_FACTS"_ui_bearing" 추가.

  • Step 4: 부모 design→spec 전이에 조건 추가 — state-transition-rules.yaml
      - { from: design, to: spec, allowed-by: [design-lead-role-agent], required-conditions: [design-accepted, design-direction-gate-satisfied] }

(기존 design→spec 전이 교체.)

  • Step 5: collaboration-map direction-gate + /design·/design-system·ceo-intake

collaboration-map.yaml design-to-build-contract에:

    direction-gate:
      requires: design-direction-approved
      enforced-by: state_engine._has_direction_approval
      applies-to: [FAM-ENG-FRONTEND]
      hard-if: "ui-bearing && tier in [standard, heavy]"
      also-gates: "design->spec 전이(design-direction-gate-satisfied) — /design-system 우회 차단"

design.md: UI-bearing(deliverable-profile 우선) && tier∈{standard,heavy}이면 유효 child 없을 때 init --plan design-direction 자동 spawn + dedup, child approved까지 design→spec 대기. design-system.md: 진입 시 check-direction-approved — 없으면 BlockedReport(standard/heavy), light 경고+상속. brief-phase=system-ready 요구. ceo-intake.md: decision-brief에 deliverable-profile: {ui-bearing, ui-kind, governance-tier} 명시(누락+standard/heavy+UI가능 → fail-closed).

  • Step 6: 통과 + 커밋

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py && python3 .claude/hooks/lint_refs.py 2>&1 | tail -2 Expected: PASS + refs OK.

git checkout -- .claude/tests/fixtures/ 2>/dev/null
git add .claude/hooks/state_engine.py org-os/00-role-registry/state-transition-rules.yaml org-os/06-agent-work/collaboration-map.yaml .claude/commands/ .claude/tests/test_design_direction.py
git commit -m "P2(F): _is_ui_bearing + design->spec gate(우회차단) + /design 자동spawn + /design-system 선행 + ceo-intake

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Phase G — doctor 배선 (커맨드 생성 이후 — Blocker 11)

Task 14: doctor check_design_direction_wiring

Files: Modify .claude/hooks/doctor.py Interfaces: Produces 스키마/spec/lint/커맨드/역할/plan/전이 존재 점검(커맨드가 이미 존재하므로 OK 성립).

  • Step 1: check_design_direction_wiring 추가 (check_venture_bootstrap_wiring 형식 복제)
def check_design_direction_wiring(report):
    sec = "design-direction(P2)"
    for f in ["org-os/06-agent-work/design-direction-spec.yaml", ".claude/hooks/lint_design_direction.py",
              ".claude/commands/design-direction.md", ".claude/commands/design-review.md",
              ".claude/agents/des-director.md", ".claude/agents/des-visual.md", ".claude/agents/fam-design.md"]:
        (report.ok if os.path.exists(os.path.join(REPO, f)) else report.fail)(sec, f"{'exists' if os.path.exists(os.path.join(REPO, f)) else 'missing'}: {f}")
    txt = open(os.path.join(REPO, "org-os/00-role-registry/roles.yaml")).read()
    for rid in ["DES-DIRECTOR", "DES-VISUAL"]:
        (report.ok if rid in txt else report.fail)(sec, f"role {rid}")
    plans = yaml.safe_load(open(os.path.join(REPO, "org-os/06-agent-work/execution-plans.yaml")))["execution-plans"]["plans"]
    (report.ok if "design-direction" in plans else report.fail)(sec, "design-direction plan")
  • Step 2: 체크 리스트 등록 — doctor 실행부에 check_design_direction_wiring 추가.

  • Step 3: 확인 + 커밋

Run: python3 .claude/hooks/doctor.py 2>&1 | grep -i "design-direction" Expected: design-direction(P2) OK 라인들.

git add .claude/hooks/doctor.py
git commit -m "P2(G): doctor check_design_direction_wiring(스키마·역할·plan·커맨드·fam-design router 보존)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Phase H — 격리 통합 테스트 + full green

Task 15: 통합 시나리오 테스트(workflow/hash/receipt 바인딩)

Files: Modify .claude/tests/test_design_direction.py Interfaces: Consumes 전 기능. Produces 아래 통합 케이스(격리 ws + uuid, subprocess CLI). 핵심 위험 = 통합 바인딩(단위 helper 아님).

  • Step 1: 통합 케이스 추가 — 각 케이스 with isolated_ws() as ws: + wf=f"...{uuid}"

필수 검증(각 1 case, subprocess _se 또는 in-process helper):

  1. 부모 workflow 부재 시 child init 거부.
  2. accepted product-decision 부재 시 거부.
  3. init stage == design-direction-intake.
  4. 동일 binding child 중복 생성 방지(find_child_direction_workflow running→resume).
  5. stale child(brief hash 상이) 표시.
  6. 오래된 review pass가 현재 cycle 승인 못 함(active pointer).
  7. selected acceptance가 다른 보고서 acceptance로 대체 불가(bundle+cycle).
  8. child receipt를 parent workflow receipt로 재사용 불가(_direction_approval_receipt_ok workflow-id 정확 일치).
  9. gallery preview로 critique 통과 불가(winner preview receipt 필요).
  10. finalize 아티팩트 부재 시 terminal 전이 거부.
  11. parent-approval-link 미기록 시 terminal 거부(register 안 함).
  12. live brief 변경 시 approval 무효(_approval_brief_matches).
  13. product-decision supersede 시 approval 무효(_product_decision_current).
  14. critique 역전이(minor→prototype / concept→divergence / pass→finalize).

(각 케이스는 helper 직접 호출 또는 CLI subprocess. 예시:)

# 8. receipt 재사용 불가
check("child receipt not reusable as parent", not SE._direction_approval_receipt_ok("child-A", "r.yaml", "SHA")  # 이벤트 없음
      )
# 12. staleness
check("live brief change invalidates", not SE._approval_brief_matches({"direction-input-brief-sha256": "OLD"}, "NEW"))
# 14. verdict 역전이
for v, key in [("minor-revision", "critique_revision_requested"), ("concept-flaw", "concept_rejection_recorded")]:
    led = {"design-direction-active": {"cycle-id": "C", "review-panel-report-id": "RP"},
           "artifacts": [{"design-type": "design-review-panel", "report-id": "RP", "direction-cycle-id": "C",
                          "design-review-panel": {"synthesis": {"verdict": v, "role-id": "DES-DIRECTOR"}, "reviews": []}}]}
    check(f"verdict {v} -> {key}", SE._facts(f"wf-{v}", led, None).get(key))
  • Step 2: 통과 확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_design_direction.py 2>&1 | tail -5 Expected: 전 케이스 PASS.

  • Step 3: 커밋
git checkout -- .claude/tests/fixtures/ 2>/dev/null
git add .claude/tests/test_design_direction.py
git commit -m "P2(H): 격리 통합 테스트(부모검증·dedup·cycle·receipt바인딩·staleness·supersede·역전이)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Task 16: full green + doctor + gen_agents 정합

Files: (검증 전용)

  • Step 1: gen_agents 재확인

Run: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/hooks/gen_agents.py 2>&1 | tail -2 && ls .claude/agents/fam-design.md .claude/agents/des-director.md .claude/agents/des-visual.md Expected: 성공 + 세 파일(fam-design router 보존).

  • Step 2: 전체 테스트

Run: CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py 2>&1 | tail -8 Expected: 전부 green(test_design_direction 포함, role-working-methods 75, count 72).

  • Step 3: doctor + lint_refs

Run: python3 .claude/hooks/doctor.py 2>&1 | tail -3 && python3 .claude/hooks/lint_refs.py 2>&1 | tail -2 Expected: OK.

  • Step 4: fixture 복원 + 최종 커밋
git checkout -- .claude/tests/fixtures/ 2>/dev/null
git add -A
git commit -m "P2(H): full green + doctor OK + gen_agents 정합(design-direction 완료)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"

Self-Review 결과 (rev2 — 12 Blocker 반영 확인)

  • B1 finalize stage: Task 8 — 8단계·9전이(critique→finalize→approved), finalize→approved 조건 3종. ✓
  • B2 초기 stage: Task 7 — _initial_stage(plan), _default_ledger 사용. ✓
  • B3 brief 불변: Global Constraints + Task 1(direction-discovery 아티팩트) + Task 8(discovery→divergence: direction-discovery-present). ✓
  • B4 fail-open 제거·exact 8점: Task 11 — if pass 제거, workflow-id 정확 일치, supersede·live hash·참조 hash 전부. ✓
  • B5 approval 등록 CLI: Task 11 — register-direction-approval trusted CLI(전 검증 후 원자 기록). ✓
  • B6 cycle model: Task 9 — design-direction-active pointer, _active_artifact, active cycle만 판정. ✓
  • B7 7 아티팩트: Task 1 — direction-discovery·winner-prototype 추가, comparison-preview·coded-slice hash·파일 실존. ✓
  • B8 schema/lint 강제·정본: Task 1/3/4 — lint 정본(JSON Schema 제거), input-brief 필수9, selected bundle(유령/커버리지/direction-set hash), YAML 인용 수정. ✓
  • B9 gen_agents 범위: Task 5/6 — router-entrypoint 플래그, router+lead+workers 공존, count 72/43/3/10 + 단언 갱신 + design-craft/lead 분기. ✓
  • B10 UI-bearing state engine·design→spec gate: Task 13 — _is_ui_bearing, design-direction-gate-satisfied(design→spec), /design-system 우회 차단. ✓
  • B11 task 순서: Phase G(doctor) < Phase F(커맨드) 뒤로 이동. ✓
  • B12 통합·격리 테스트: Task 15 — 14 통합 케이스, 격리 ws + uuid, "시간상" 문구 제거. ✓
  • Phase 0: Task 0 — P1 report-sha256 전제·baseline·gen_agents 스냅샷. ✓

구현 시 확인(Task 0에서 실측): 원장 로더/저장(_load_ledger/_save_ledger)·state 디렉토리(_state_root)·워크스페이스 루트(_workspace_root) 실제 함수명. product-decision-id-of-child 대조는 child 원장 값으로 단순화(위 코드 주 참조).