1364 lines
78 KiB
Markdown
1364 lines
78 KiB
Markdown
# 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: 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).
|
||
|
||
```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 원장 값으로 단순화(위 코드 주 참조).
|