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

1364 lines
78 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 4~5 lint, Task 9~ predicate가 필드명 참조.
- [ ] **Step 1: 파일 생성** (YAML 문자열 인용 주의 — Blocker 8)
```yaml
# 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`
```bash
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:` 최상단에 필드 추가**
```yaml
brief-phase: pre-direction | system-ready # P2 S1 — direction 승인 전/후
approved-direction-ref: # system-ready 필수 — 승인 방향 불변 report 경로
approved-direction-sha256: # system-ready 필수 — staleness 대조
```
- [ ] **Step 2: `rules:`에 추가**
```yaml
- '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`
```bash
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`:
```python
#!/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)**
```python
#!/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).
```bash
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` 앞)
```python
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: 커밋**
```bash
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 다음
```yaml
- { 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 교체
```yaml
- { 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`
```bash
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 조건 추가
```python
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`에서:
```python
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.
```bash
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 확인)
```python
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` 단위만 우선.)
```python
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` 다음에
```python
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) 수정:
```python
"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)
```bash
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: 실패 테스트 추가**
```python
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 다음
```yaml
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에 추가:
```yaml
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종 추가:
```yaml
# ── 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.
```bash
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: 실패 테스트 추가**
```python
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` 다음
```python
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 다음
```python
# --- 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`에:
```python
"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`에:
```python
"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에서 완성.)
```bash
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: 실패 테스트 추가**
```python
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` 확장** (부모 검증 포함)
```python
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 인자 연결**
```python
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.
```bash
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: 실패 테스트 추가**
```python
# 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 구현**
```python
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에 추가):
```python
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 파생):
```python
"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.
```bash
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.py``winner-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-sha256``state_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 재생성).
```bash
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: 실패 테스트 추가**
```python
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**
```python
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`에:
```python
f["_ui_bearing"] = _is_ui_bearing(led)
```
`_PREDICATES`에:
```python
"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
```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`에:
```yaml
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.
```bash
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` 형식 복제)
```python
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 라인들.
```bash
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. 예시:)
```python
# 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: 커밋**
```bash
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 복원 + 최종 커밋**
```bash
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 원장 값으로 단순화(위 코드 주 참조).