78 KiB
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.pygreen. - 테스트 규약: pytest 금지.
check(name, ok)/카운터/sys.exit(1 if failed else 0). 통합 테스트는 subprocess로state_engine.pyCLI를 실제 호출(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 CLIregister-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: rejected∪selected 가 전체 방향과 불일치(누락/여분)")
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— 절차(요지):init --plan design-direction(바인딩 3종). dedup: 동일 binding running→resume, approved→재사용, stale→신규 cycle.-discovery: DES-DIRECTOR가 불변 brief 분석 →direction-discovery아티팩트(brief 수정 금지) → active pointer 갱신 → transition.-divergence: DES-VISUAL 3 격리 run(producer-run-id/context-package 상이·형제 must-read 금지) → 동일 대표화면 slice → 갤러리 패키지preview_ui.py1회 →direction-set(cycle-id·comparison-preview) → transition.-decision: DES-DIRECTOR 3안 원본 종합 →selected-direction(bundle, 평균금지) →acceptance_log append accepted→ transition.-prototype: 승자 핵심흐름 coded →preview_ui.py→winner-prototype(revision) → transition.-critique:/design-review패널 → verdict로 transition(pass→finalize / minor→prototype[revision++] / concept→divergence[새 cycle]).-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 재생성).
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):
- 부모 workflow 부재 시 child init 거부.
- accepted product-decision 부재 시 거부.
- init stage == design-direction-intake.
- 동일 binding child 중복 생성 방지(
find_child_direction_workflowrunning→resume). - stale child(brief hash 상이) 표시.
- 오래된 review pass가 현재 cycle 승인 못 함(active pointer).
- selected acceptance가 다른 보고서 acceptance로 대체 불가(bundle+cycle).
- child receipt를 parent workflow receipt로 재사용 불가(
_direction_approval_receipt_okworkflow-id 정확 일치). - gallery preview로 critique 통과 불가(winner preview receipt 필요).
- finalize 아티팩트 부재 시 terminal 전이 거부.
- parent-approval-link 미기록 시 terminal 거부(register 안 함).
- live brief 변경 시 approval 무효(
_approval_brief_matches). - product-decision supersede 시 approval 무효(
_product_decision_current). - 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-approvaltrusted CLI(전 검증 후 원자 기록). ✓ - B6 cycle model: Task 9 —
design-direction-activepointer,_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 원장 값으로 단순화(위 코드 주 참조).