Files
company-haness/docs/superpowers/plans/2026-07-12-p1-company-venture-bootstrap.md

1672 lines
87 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P1 — Company / Venture Bootstrap Implementation Plan
> **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:** 회사를 정의하기 전에 회사 문맥을 요구하는 순환의존을 끊는다 — `founder-context`를 입력으로 하는 별도 `venture-bootstrap` plan이 `company-context.yaml (status: provisional)`을 산출하고, 제품 cascade는 그것을 입력으로 소비한다.
**Architecture:** 5개 phase. (A) 데이터/계약 SoT, (B) `lint_company_context.py` 파일 린터, (C) state_engine에 `venture-bootstrap` plan·6 predicate, (D) 강제기 확장(validate_report·acceptance_log·commit_company_context·guard_tools), (E) 커맨드·문서·배선. 각 phase는 독립적으로 테스트 가능한 증분을 낸다. 모든 상태 전이는 OPS-ORCH가 집행하며, 공식 `company-context.yaml`은 candidate→원자적 교체로만 갱신된다.
**Tech Stack:** Python 3(표준 라이브러리 + `pyyaml`), 기존 하네스 hook 규약. 테스트는 pytest가 아니라 저장소 표준인 **standalone `check(name, ok)` 하니스**(`sys.exit(1 if failed else 0)`), `run_all.py`가 CI 진입점.
**Spec:** `docs/superpowers/specs/2026-07-12-p1-company-venture-bootstrap-design.md` (모든 § 참조는 이 스펙).
## Global Constraints
- **워크스페이스 필수:** 모든 hook/test 실행은 `CLAUDE_PROJECT_DIR="$PWD"` + `ORGOS_WORKSPACE=<ws>` 명시(미설정 시 `_workspace.py``WorkspaceNotSetError`). 테스트는 격리 fixture 워크스페이스 사용.
- **CI 게이트:** 매 phase 종료 시 `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py` **green 유지**(doctor + lint_refs + 모든 test_*.py).
- **테스트 규약:** pytest 금지. 새 테스트 파일은 `test_state_engine.py` 스켈레톤 복제 — `check(name, ok)`/`passed,failed` 카운터/`sys.exit(1 if failed else 0)`, 격리 워크스페이스 `FIX/<name>-ws`.
- **신뢰경계:** 에이전트/worker는 `state_engine.py transition`·원장·공식 `company-context.yaml`을 직접 쓰지 못한다(guard_tools). venture-bootstrap 모든 전이 `allowed-by: [OPS-ORCH]` 단독(§8.2).
- **공식 status 3-상태:** 공식 `company-context.yaml` `status ∈ {template, provisional, operating}`. `bootstrap`은 공식 status 아님 — candidate 파일의 `candidate-status: bootstrap` + workflow stage `company-context-commit`로만 표현(§7.3).
- **불변식:** 생성된 `.claude/agents/*.md`는 직접 편집 금지(role 변경은 `gen_agents.py` 재실행). 보고서는 불변(new_report). 커밋은 각 task 끝에서.
- **증거 등급:** `fact/decision/hypothesis`(정보 종류)와 `E1~E5`(증거 강도)는 독립 — 항목별 provenance(§7.1). 회사 문맥 인용 상한은 **항목 단위**(전체 보고서 강등 금지, §9.1).
- **커밋 트레일러:** 각 커밋 메시지 끝에 `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
---
## File Structure
**신규 생성**
- `org-os/01-company/founder-context.yaml` — 사람 입력(창업자 제약). `status: template|filled`.
- `org-os/06-agent-work/venture-option-spec.yaml` — opportunity-cluster·venture-option·validation-result 스키마.
- `org-os/06-agent-work/venture-validation-map.yaml` — 9-gate 역할 매핑 + opportunity-discovery 역할.
- `.claude/hooks/lint_company_context.py` — company-context 파일 린터(Hard Fail + Warning + `--migrate`).
- `.claude/hooks/commit_company_context.py` — candidate→공식 원자적 commit(trusted CLI).
- `.claude/commands/venture-validate.md`, `.claude/commands/company-bootstrap.md` — 신규 커맨드.
- `.claude/tests/test_company_context_lint.py` — 린터 단위테스트.
- `.claude/tests/test_venture_bootstrap.py` — plan/predicate/commit 단위테스트.
**수정**
- `org-os/01-company/company-context.yaml` — schema-version 2 재구조화(facts/decisions/hypotheses, 3-상태).
- `org-os/00-role-registry/state-transition-rules.yaml` — venture-bootstrap 전이 + condition-catalog.
- `org-os/06-agent-work/execution-plans.yaml``venture-bootstrap` plan.
- `.claude/hooks/state_engine.py` — 6 predicate + facts + `_PROTECTED_FACTS` + `company-context-ready` guard 서브커맨드.
- `.claude/hooks/validate_report.py` — status 어휘(operating) + read-compat + hypothesis anchor ceiling.
- `.claude/hooks/acceptance_log.py` + `.claude/schemas/acceptance-event.schema.json` — 선택 `report-sha256`.
- `.claude/hooks/guard_tools.py` — 공식 company-context.yaml 직접쓰기 보호.
- `.claude/hooks/doctor.py` — 신규 아티팩트·plan 배선 점검.
- `.claude/commands/ceo-intake.md``--plan venture-bootstrap` + founder-context 유도.
- `README.md`, `org-os/01-company/README.md`, `CLAUDE.md` — 흐름·정책 갱신.
---
# Phase A — 데이터 모델 & 계약 (declarative SoT)
## Task 1: `founder-context.yaml`
**Files:**
- Create: `org-os/01-company/founder-context.yaml`
**Interfaces:**
- Produces: 파일 경로 `org-os/01-company/founder-context.yaml`, 최상위 `status` 필드(`template|filled`). Task 10의 `_founder_context_filled()``status=="filled"`를 읽는다.
- [ ] **Step 1: 파일 생성**
```yaml
# founder-context.yaml — 창업자·사업 설계의 입력값(회사 정의 이전에 존재하는 실제 제약).
# company-context.yaml 이 아니다 — 순환을 끊는 유일 입력이다(§5). 사람이 직접 채운다.
# status: template(미채움) -> filled(채움). filled 여야 venture-bootstrap 의 founder-setup 통과.
schema-version: 1
status: template # template | filled
founder:
strengths: [] # 예: [backend, database, infrastructure]
available-time: "" # 예: "solo, 주 50h"
available-capital: ""
desired-business-size: "" # 예: "$1-5M ARR, solo-operable"
preferred-market: ""
distribution-capability: "" # self-serve? community-led? outbound?
sales-tolerance: "" # low | med | high
operation-tolerance: "" # low | med | high
risk-tolerance: "" # low | med | high
hard-constraints: [] # 예: [solo-operable, self-serve-distribution, no-enterprise-sales-dependency]
strategic-preferences: [] # 예: [recurring-revenue, technical-moat, global-developer-market]
```
- [ ] **Step 2: 유효성 확인**
Run: `python3 -c "import yaml; d=yaml.safe_load(open('org-os/01-company/founder-context.yaml')); assert d['status']=='template'; print('OK', d['schema-version'])"`
Expected: `OK 1`
- [ ] **Step 3: 커밋**
```bash
git add org-os/01-company/founder-context.yaml
git commit -m "P1(A): founder-context.yaml — 창업자 제약 입력(순환 해소 입력)"
```
---
## Task 2: 벤처 계약 두 파일 (`venture-option-spec.yaml`, `venture-validation-map.yaml`)
**Files:**
- Create: `org-os/06-agent-work/venture-option-spec.yaml`
- Create: `org-os/06-agent-work/venture-validation-map.yaml`
**Interfaces:**
- Produces: `venture-option.required`(kill-criteria 포함) 필드 목록, `venture-validation-map.gates[]`(gate/primary/auditor). `/venture-validate` 커맨드(Task 18)와 doctor(Task 20)가 참조.
- [ ] **Step 1: `venture-option-spec.yaml` 생성**
```yaml
# venture-option-spec.yaml — opportunity/venture-option 데이터 스키마(§6.1).
# 변경 이유 = 산출물 구조 변화. 역할 매핑은 venture-validation-map.yaml(변경 이유 다름).
venture-option-spec:
version: 1
opportunity-cluster: # opportunity-discovery 산출(제품명 이전, 문제 클러스터)
required: [id, problem-domain, target-user, triggering-event,
current-alternative, why-now, founder-fit]
venture-option: # venture-validation 산출(옵션별)
required: [id, customer, painful-job, current-alternative, wedge,
monetization, expected-price, reachable-customers,
rough-revenue-ceiling, acquisition-channel, build-cost,
operation-cost, founder-fit, defensibility, kill-criteria,
unresolved-assumptions]
notes:
- "'unknown' 은 허용값(모른다고 적을 수 있어야 한다) — 단 unresolved-assumptions 에 명시."
- "kill-criteria 는 필수 — 없으면 venture-validation→venture-decision 차단."
validation-result: # 게이트별 판정 스냅샷
required: [option-id, gate, verdict, evidence, dissent]
verdict-enum: [pass, fail, unknown]
```
- [ ] **Step 2: `venture-validation-map.yaml` 생성** (역할 보강 §6.2 반영)
```yaml
# venture-validation-map.yaml — 9-gate 역할 매핑(§6.2). 변경 이유 = 조직·협업 방식 변화.
# role-id 는 capability-families member-role-ids/lead 에 존재해야 한다(doctor 대조, Task 20).
venture-validation-map:
version: 1
synthesis-owner: EXEC-CEO # 종합(수렴)은 CEO, 최종 선택은 사람(HUMAN-001)
gates:
- { gate: problem-intensity, primary: [UX-RESEARCHER], auditor: [PROD-PM] }
- { gate: competition-alternatives, primary: [GTM-CI], auditor: [STR-ANALYST] }
- { gate: willingness-to-pay, primary: [GTM-PRICING], auditor: [EXEC-CFO] }
- { gate: revenue-unit-economics, primary: [EXEC-CFO, GTM-REVOPS], auditor: [GTM-PRICING] }
- { gate: tech-feasibility-moat, primary: [ARCH-TECH], auditor: [EXEC-CFO] }
- { gate: operability, primary: [CONSULT-OPS], auditor: [ARCH-TECH] }
- { gate: distribution, primary: [GTM-GROWTHPM, GTM-SALES], auditor: [EXEC-CFO] }
- { gate: founder-fit, primary: [EXEC-CEO], auditor: [ARCH-TECH] }
- { gate: kill-criteria, primary: [EXEC-CFO], auditor: [EXEC-CEO] }
opportunity-discovery-roles:
diverge: [EXEC-CEO, STR-ANALYST, PROD-PM, UX-RESEARCHER, GTM-PMM]
contrarian: [EXEC-CFO] # 왜 실패하는가 — 초기 아이디어의 경제구조 반증(§6.2)
```
> **role-id 확정(§15):** Step 2의 `EXEC-CFO`/`EXEC-CEO` 등은 확정 전 `capability-families.yaml`의 member-role-ids와 대조하라. Task 20의 doctor 체크가 이 파일의 모든 role-id를 등록 집합과 대조해 미등록이면 실패시킨다. 대조 명령:
> `python3 -c "import yaml; fams=yaml.safe_load(open('org-os/00-role-registry/capability-families.yaml'))['capability-families']['families']; reg={r for fam in fams for r in (fam.get('member-role-ids') or [])} | {fam.get('lead-role-id') for fam in fams}; m=yaml.safe_load(open('org-os/06-agent-work/venture-validation-map.yaml'))['venture-validation-map']; import itertools; used={x for g in m['gates'] for x in (g['primary']+g['auditor'])} | set(m['opportunity-discovery-roles']['diverge']+m['opportunity-discovery-roles']['contrarian']) | {m['synthesis-owner']}; print('UNREGISTERED:', sorted(used-reg))"`
> 출력이 `UNREGISTERED: []` 가 되도록 role-id를 등록 표기로 교정(예: `EXEC-CFO`가 미등록이면 실제 CFO member-role-id로 치환).
- [ ] **Step 3: 두 파일 YAML 파싱 확인**
Run: `python3 -c "import yaml; yaml.safe_load(open('org-os/06-agent-work/venture-option-spec.yaml')); yaml.safe_load(open('org-os/06-agent-work/venture-validation-map.yaml')); print('OK')"`
Expected: `OK`
- [ ] **Step 4: role-id 등록 대조(위 명령) → `UNREGISTERED: []` 확인 후 커밋**
```bash
git add org-os/06-agent-work/venture-option-spec.yaml org-os/06-agent-work/venture-validation-map.yaml
git commit -m "P1(A): 벤처 계약 2파일 — venture-option-spec + venture-validation-map(9-gate 역할)"
```
---
## Task 3: `company-context.yaml` 재구조화 (schema v2, 항목별 provenance)
**Files:**
- Modify: `org-os/01-company/company-context.yaml`
**Interfaces:**
- Produces: 최상위 `status ∈ {template, provisional, operating}`; `company.facts[]`/`company.strategic-decisions[]`/`company.hypotheses[]`(각 항목 id·provenance/accepted-by/validation-status), `company.validation-state`, `projects: []`. Task 4~7 린터, Task 10~11 predicate, Task 13~14 validate_report가 이 구조를 읽는다.
- [ ] **Step 1: 파일 재작성** (현 `status: demo`·자유서술 `company:` → schema v2 template)
```yaml
# company-context.yaml — 회사 + 프로젝트별 문맥 SoT (schema-version 2).
# 공식 status 3-상태: template | provisional | operating (bootstrap 아님 — §7.3).
# template : 초기(빈). 회사 미정의.
# provisional: venture-bootstrap 산출. 결정은 유효, 시장 가설은 검증 상태에 종속.
# operating : 실검증·실운영 데이터로 승격(operating 승격 acceptance event 필요).
# 전이: template ─(atomic commit)→ provisional ─(human promotion)→ operating.
# "작성 중"은 공식 status 가 아니라 candidate 파일의 candidate-status: bootstrap 로만 표현한다.
#
# 근거등급 정책(§7.1, finding #5): fact/decision/hypothesis 는 정보의 '종류', E1~E5 는 증거의 '강도' —
# 독립이다. 블록 단위 cap 을 두지 않고 항목별 provenance 를 둔다. company 네임스페이스 인용 상한은
# validate_report 가 항목 단위로 강제한다(status != operating 이면 company 인용은 E2/Med 상한;
# hypothesis 항목 인용은 status 무관 Med 상한). 외부·코드·테스트 증거는 원등급 유지.
schema-version: 2
status: template # template | provisional | operating
company:
facts: []
# 예:
# - id: FACT-001
# statement: "창업자는 백엔드·인프라 개발 역량을 보유한다."
# category: founder-capability
# provenance: [{ source-uri: org-os/01-company/founder-context.yaml, grade: E2 }]
# verified-at: "2026-07-12"
# status: active # active | retired
strategic-decisions: []
# 예:
# - id: DEC-001
# statement: "초기 고객은 소규모 백엔드·DB 운영팀으로 한정한다."
# decision-type: target-market
# accepted-by: HUMAN-001
# accepted-at: "2026-07-12"
# source-decision-id: VD-001
# supporting-evidence: [{ source-uri: completion-records/<wf>/exec-packet-*.report.yaml, grade: E2 }]
# status: active
hypotheses: []
# 예:
# - id: HYP-001
# statement: "대상 고객은 월 $79 이상 지불 의사가 있다."
# hypothesis-type: willingness-to-pay
# confidence: Med
# validation-status: untested # untested | validated | refuted
# evidence: [{ source-uri: completion-records/<wf>/pricing-*.report.yaml, grade: E2 }]
# promotion-criteria: ["유료 사전판매 5+"]
# falsification-criteria: ["2주 랜딩 유료전환 < 1%"]
validation-state:
stage: pre-traction # pre-traction | early-traction | operating
validated: []
open: []
refuted: []
# --- 프로젝트별 manifest (제품 lifecycle 소관, 기존 유지) --------------------
projects: []
```
- [ ] **Step 2: 파싱·구조 확인**
Run: `python3 -c "import yaml; d=yaml.safe_load(open('org-os/01-company/company-context.yaml')); assert d['schema-version']==2 and d['status']=='template'; assert set(['facts','strategic-decisions','hypotheses','validation-state']) <= set(d['company']); print('OK')"`
Expected: `OK`
- [ ] **Step 3: 커밋**
```bash
git add org-os/01-company/company-context.yaml
git commit -m "P1(A): company-context.yaml schema v2 — facts/decisions/hypotheses 항목별 provenance + 3-상태"
```
> 주의: 이 시점 이후 `validate_report`의 구 `populated` 판정이 깨질 수 있다(status가 `template`). 기존 테스트는 회사 인용을 상한하는 쪽이라 green 유지되지만, Phase D(Task 13)에서 read-compat/operating을 반영하기 전까지 `run_all`을 돌리면 회사-인용 테스트가 여전히 "상한됨"을 기대하므로 통과한다. Phase A 종료 시 `run_all` 확인:
- [ ] **Step 4: Phase A 회귀 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py`
Expected: 전체 PASS(green). 실패 시 원인 확인 후 수정.
---
# Phase B — `lint_company_context.py` (파일 린터)
새 테스트 파일 `test_company_context_lint.py`를 만들고, `check()` 하니스로 케이스를 추가한다. 린터는 **구조·참조·권한·상태 = Hard Fail, 의미 오분류 = Warning**(§9.2).
## Task 4: 린터 코어 + status/candidate Hard Fail
**Files:**
- Create: `.claude/hooks/lint_company_context.py`
- Create: `.claude/tests/test_company_context_lint.py`
**Interfaces:**
- Produces: `lint_file(path, is_candidate=False) -> (hard_fails: list[str], warnings: list[str])`; CLI `python3 lint_company_context.py [--candidate] [PATH]`(exit 0 = hard_fails 없음, exit 1 = 있음). Task 8(run_all/doctor), Task 11(company-context-lint-passed predicate), Task 16(committer)이 `lint_file`을 import해 쓴다.
- [ ] **Step 1: 실패 테스트 작성**`.claude/tests/test_company_context_lint.py`
```python
#!/usr/bin/env python3
"""lint_company_context.py 단위테스트. standalone(no pytest). exit 0 = all pass."""
import os, sys, tempfile
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
HOOKS = os.path.join(ROOT, ".claude", "hooks")
sys.path.insert(0, HOOKS)
os.environ["CLAUDE_PROJECT_DIR"] = ROOT
import lint_company_context as L # noqa: E402
passed, failed = 0, 0
def check(name, ok):
global passed, failed
if ok: passed += 1; print(f" PASS {name}")
else: failed += 1; print(f" FAIL {name}")
def _tmp(text):
fd, p = tempfile.mkstemp(suffix=".yaml"); os.write(fd, text.encode()); os.close(fd); return p
# 공식 파일에 status: bootstrap 이면 Hard Fail
p = _tmp("schema-version: 2\nstatus: bootstrap\ncompany: {facts: [], strategic-decisions: [], hypotheses: [], validation-state: {stage: pre-traction, validated: [], open: [], refuted: []}}\nprojects: []\n")
hf, wn = L.lint_file(p, is_candidate=False)
check("official status=bootstrap -> hard fail", any("bootstrap" in h or "status" in h for h in hf))
# 공식 파일에 candidate-status 잔존 -> Hard Fail
p = _tmp("schema-version: 2\nstatus: provisional\ncandidate-status: bootstrap\ncompany: {facts: [], strategic-decisions: [{id: DEC-1, statement: x, decision-type: t, accepted-by: HUMAN-001, accepted-at: '2026-07-12', source-decision-id: VD-1, supporting-evidence: []}], hypotheses: [], validation-state: {stage: pre-traction, validated: [], open: [], refuted: []}}\nprojects: []\n")
hf, wn = L.lint_file(p, is_candidate=False)
check("official candidate-status -> hard fail", any("candidate-status" in h for h in hf))
# 허용된 status 3종은 통과(구조 정상 최소본)
for st in ("template", "provisional", "operating"):
if st == "template":
body = "company: {facts: [], strategic-decisions: [], hypotheses: [], validation-state: {stage: pre-traction, validated: [], open: [], refuted: []}}"
else:
body = "company: {facts: [], strategic-decisions: [{id: DEC-1, statement: x, decision-type: t, accepted-by: HUMAN-001, accepted-at: '2026-07-12', source-decision-id: VD-1, supporting-evidence: []}], hypotheses: [], validation-state: {stage: pre-traction, validated: [], open: [], refuted: []}}"
p = _tmp(f"schema-version: 2\nstatus: {st}\n{body}\nprojects: []\n")
hf, wn = L.lint_file(p, is_candidate=False)
check(f"official status={st} -> no hard fail", hf == [])
sys.exit(1 if failed else 0)
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_company_context_lint.py`
Expected: FAIL — `ModuleNotFoundError: No module named 'lint_company_context'`
- [ ] **Step 3: 린터 코어 구현**`.claude/hooks/lint_company_context.py`
```python
#!/usr/bin/env python3
"""lint_company_context.py — company-context.yaml 내부 정합성 린터(§9.2).
구조·참조·권한·상태 = Hard Fail(exit 1). 의미상 오분류 가능성 = Warning(exit 0, stderr).
공식 파일: status ∈ {template, provisional, operating}, candidate-status 금지.
candidate 파일: candidate-status: bootstrap 허용.
CLI:
lint_company_context.py [--candidate] [PATH] # 기본 PATH = org-os/01-company/company-context.yaml
lint_company_context.py --migrate [PATH] # 구 스키마/어휘 1회 변환(Task 7)
"""
import os, sys
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DEFAULT_PATH = os.path.join(ROOT, "org-os", "01-company", "company-context.yaml")
OFFICIAL_STATUS = {"template", "provisional", "operating"}
def _load(path):
import yaml
with open(path, encoding="utf-8") as fh:
return yaml.safe_load(fh) or {}
def lint_file(path, is_candidate=False):
"""(hard_fails, warnings) 반환. 예외 없이 파싱 실패도 hard_fail 로."""
hard, warn = [], []
try:
doc = _load(path)
except Exception as e:
return ([f"파싱 실패: {e}"], [])
status = str(doc.get("status", "")).strip().lower()
# --- 상태·candidate 정합(Hard Fail) ---
if is_candidate:
# candidate 는 최종 목표 status(3-상태) + candidate-status: bootstrap
if status not in OFFICIAL_STATUS:
hard.append(f"candidate status '{status}' 는 {sorted(OFFICIAL_STATUS)} 밖")
if str(doc.get("candidate-status", "")).strip().lower() != "bootstrap":
hard.append("candidate 파일은 candidate-status: bootstrap 필요")
else:
if status == "demo" or status == "populated":
warn.append(f"구 어휘 status='{status}' — --migrate 로 변환 필요(deprecated)")
elif status not in OFFICIAL_STATUS:
hard.append(f"공식 status '{status}' 는 {sorted(OFFICIAL_STATUS)} 밖(특히 'bootstrap'은 공식 status 아님)")
if "candidate-status" in doc:
hard.append("공식 파일에 candidate-status 필드가 남아있음(commit 시 제거돼야 함)")
return (hard, warn)
def main(argv):
args = list(argv)
is_candidate = "--candidate" in args
if is_candidate: args.remove("--candidate")
if "--migrate" in args:
args.remove("--migrate")
path = args[0] if args else DEFAULT_PATH
return migrate(path)
path = args[0] if args else DEFAULT_PATH
hard, warn = lint_file(path, is_candidate=is_candidate)
for w in warn: sys.stderr.write(f"[lint_company_context] WARN: {w}\n")
for h in hard: sys.stderr.write(f"[lint_company_context] FAIL: {h}\n")
if hard:
return 1
print(f"[lint_company_context] OK ({path}) — hard-fails 0, warnings {len(warn)}")
return 0
def migrate(path): # Task 7 에서 구현. 지금은 no-op 성공.
print("[lint_company_context] --migrate 미구현(Task 7)"); return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
```
- [ ] **Step 4: 통과 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_company_context_lint.py`
Expected: PASS (모든 케이스).
- [ ] **Step 5: 커밋**
```bash
git add .claude/hooks/lint_company_context.py .claude/tests/test_company_context_lint.py
git commit -m "P1(B): lint_company_context 코어 — status/candidate 정합 Hard Fail"
```
---
## Task 5: 항목 구조 Hard Fail (id·provenance·권한·참조)
**Files:**
- Modify: `.claude/hooks/lint_company_context.py:lint_file`
- Modify: `.claude/tests/test_company_context_lint.py`
**Interfaces:**
- Consumes: `lint_file(path, is_candidate)` from Task 4.
- Produces: 동일 시그니처, Hard Fail 규칙 확장(항목 id/중복, fact provenance, decision accepted-by/at/source-decision-id, hypothesis validation-status/confidence/falsification, hypothesis-id-as-fact, nonexistent evidence path, provisional-needs-human-decision, operating-needs-promotion-event).
- [ ] **Step 1: 실패 테스트 추가**`test_company_context_lint.py``sys.exit` 직전에 삽입
```python
def _doc(status, facts="[]", decs="[]", hyps="[]"):
return (f"schema-version: 2\nstatus: {status}\ncompany:\n facts: {facts}\n"
f" strategic-decisions: {decs}\n hypotheses: {hyps}\n"
f" validation-state: {{stage: pre-traction, validated: [], open: [], refuted: []}}\nprojects: []\n")
# fact provenance 누락 -> hard fail
p = _tmp(_doc("operating", facts="[{id: FACT-1, statement: x, category: c, status: active}]"))
hf, _ = L.lint_file(p); check("fact w/o provenance -> hard", any("provenance" in h for h in hf))
# 중복 id -> hard fail
p = _tmp(_doc("operating",
facts="[{id: X, statement: a, category: c, provenance: [{source-uri: org-os/01-company/founder-context.yaml, grade: E2}], status: active}]",
hyps="[{id: X, statement: b, hypothesis-type: wtp, confidence: Med, validation-status: untested, evidence: [], falsification-criteria: [z]}]"))
hf, _ = L.lint_file(p); check("dup id across blocks -> hard", any("중복" in h or "dup" in h.lower() for h in hf))
# decision accepted-by 누락 -> hard fail
p = _tmp(_doc("provisional", decs="[{id: DEC-1, statement: x, decision-type: t, accepted-at: '2026-07-12', source-decision-id: VD-1, supporting-evidence: []}]"))
hf, _ = L.lint_file(p); check("decision w/o accepted-by -> hard", any("accepted-by" in h for h in hf))
# hypothesis falsification-criteria 누락 -> hard fail
p = _tmp(_doc("operating", hyps="[{id: H1, statement: x, hypothesis-type: wtp, confidence: Med, validation-status: untested, evidence: []}]"))
hf, _ = L.lint_file(p); check("hypothesis w/o falsification -> hard", any("falsification" in h for h in hf))
# provisional 인데 human 승인 decision 없음 -> hard fail
p = _tmp(_doc("provisional"))
hf, _ = L.lint_file(p); check("provisional w/o human decision -> hard", any("human" in h.lower() and "decision" in h.lower() for h in hf))
# 존재하지 않는 evidence 경로 -> hard fail
p = _tmp(_doc("operating", facts="[{id: F1, statement: x, category: c, provenance: [{source-uri: org-os/01-company/NOPE.yaml, grade: E2}], status: active}]"))
hf, _ = L.lint_file(p); check("nonexistent evidence path -> hard", any("NOPE" in h or "존재" in h for h in hf))
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_company_context_lint.py`
Expected: 새 케이스 FAIL(현재 코어는 상태만 검사).
- [ ] **Step 3: `lint_file`에 항목 규칙 추가** — 상태 검사 `return` 직전에 삽입
```python
company = doc.get("company") or {}
facts = company.get("facts") or []
decs = company.get("strategic-decisions") or []
hyps = company.get("hypotheses") or []
# 항목 id 수집(중복·hypothesis-id-as-fact 검사)
seen = {}
def _reg(idv, block):
if not idv:
hard.append(f"{block} 항목 id 누락")
return
if idv in seen:
hard.append(f"중복 id '{idv}' ({seen[idv]}{block})")
else:
seen[idv] = block
def _evidence_paths_exist(items, block):
for it in items:
for ev in (it.get("provenance") or it.get("supporting-evidence") or it.get("evidence") or []):
src = str((ev or {}).get("source-uri", "")).strip()
if not src or src.lower().startswith("http"):
continue
# glob(*) 은 검사 생략(경로 패턴). 구체 경로만 실존 확인.
if "*" in src:
continue
ap = src if os.path.isabs(src) else os.path.join(ROOT, src)
if not os.path.exists(ap):
hard.append(f"{block} 항목 evidence 경로 미존재: {src}")
for it in facts:
_reg(it.get("id"), "fact")
if not (it.get("provenance")):
hard.append(f"fact '{it.get('id')}' provenance 누락")
for it in decs:
_reg(it.get("id"), "decision")
for k in ("accepted-by", "accepted-at", "source-decision-id"):
if not it.get(k):
hard.append(f"decision '{it.get('id')}' {k} 누락")
hyp_ids = set()
for it in hyps:
_reg(it.get("id"), "hypothesis")
hyp_ids.add(it.get("id"))
for k in ("validation-status", "confidence", "falsification-criteria"):
if not it.get(k):
hard.append(f"hypothesis '{it.get('id')}' {k} 누락")
# hypothesis-id 를 fact/decision 근거(source-decision-id 또는 evidence source)에 쓰면 hard
for it in decs:
if it.get("source-decision-id") in hyp_ids and it.get("source-decision-id"):
hard.append(f"decision '{it.get('id')}' 가 hypothesis id 를 source-decision-id 로 사용")
_evidence_paths_exist(facts, "fact")
_evidence_paths_exist(decs, "decision")
_evidence_paths_exist(hyps, "hypothesis")
# 상태-권한 정합
if status == "provisional" and not is_candidate:
if not any(d.get("accepted-by") for d in decs):
hard.append("status=provisional 인데 human 승인(accepted-by) strategic-decision 이 하나도 없음")
if status == "operating" and not is_candidate:
# operating 승격은 별도 승격 이벤트(acceptance) 를 요구 — validation-state.stage 로 근사 검사.
if str((company.get("validation-state") or {}).get("stage", "")).lower() != "operating":
hard.append("status=operating 인데 validation-state.stage != operating(승격 근거 부재)")
```
- [ ] **Step 4: 통과 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_company_context_lint.py`
Expected: PASS (전체).
- [ ] **Step 5: 커밋**
```bash
git add .claude/hooks/lint_company_context.py .claude/tests/test_company_context_lint.py
git commit -m "P1(B): lint_company_context 항목 구조 Hard Fail(id·provenance·권한·참조)"
```
---
## Task 6: 의미 오분류 Warning
**Files:**
- Modify: `.claude/hooks/lint_company_context.py:lint_file`
- Modify: `.claude/tests/test_company_context_lint.py`
**Interfaces:**
- Consumes/Produces: `lint_file`(동일). Warning만 추가 — Hard Fail 0 유지.
- [ ] **Step 1: 실패 테스트 추가**
```python
# fact 문장이 추정 표현 -> warning(hard 아님)
p = _tmp(_doc("operating", facts="[{id: F1, statement: '고객은 지불할 것으로 예상된다', category: c, provenance: [{source-uri: org-os/01-company/founder-context.yaml, grade: E2}], status: active}]"))
hf, wn = L.lint_file(p)
check("estimative fact -> warning not hard", hf == [] and any("추정" in w or "예상" in w for w in wn))
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_company_context_lint.py`
Expected: 새 케이스 FAIL.
- [ ] **Step 3: Warning 규칙 추가**`lint_file``return (hard, warn)` 직전
```python
_ESTIMATIVE = ("예상", "일 것", "추정", "아마", "듯", "가능성이 높")
for it in facts:
s = str(it.get("statement", ""))
if any(t in s for t in _ESTIMATIVE):
warn.append(f"fact '{it.get('id')}' 문장이 추정 표현 포함 — hypothesis 여야 할 수 있음: {s[:40]}")
if any(t in s for t in ("시장 규모", "WTP", "지불 의사", "market size")):
warn.append(f"fact '{it.get('id')}' 가 시장/WTP 주장 — hypothesis 로 분류 검토")
for it in hyps:
if str(it.get("validation-status", "")).lower() == "untested" and not it.get("evidence"):
warn.append(f"hypothesis '{it.get('id')}' 미검증+근거 없음 — 장기 방치 주의")
```
- [ ] **Step 4: 통과 확인 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_company_context_lint.py`
Expected: PASS.
```bash
git add .claude/hooks/lint_company_context.py .claude/tests/test_company_context_lint.py
git commit -m "P1(B): lint_company_context 의미 오분류 Warning(추정 표현·시장/WTP·방치 가설)"
```
---
## Task 7: `--migrate` + 읽기 호환
**Files:**
- Modify: `.claude/hooks/lint_company_context.py:migrate`
- Modify: `.claude/tests/test_company_context_lint.py`
**Interfaces:**
- Produces: `migrate(path) -> int` — 구 `demo→template`, `populated→operating`, schema-version 없으면 2로 승격, `company:` 자유서술을 보존하며 facts/decisions/hypotheses/validation-state 골격 주입. exit 0 성공.
- [ ] **Step 1: 실패 테스트 추가**
```python
# 구 어휘 파일 migrate -> 신 어휘
p = _tmp("status: demo\ncompany:\n name: X\n mission: Y\nprojects: []\n")
rc = L.migrate(p)
import yaml as _y
d = _y.safe_load(open(p))
check("migrate demo->template + schema v2", rc == 0 and d["status"] == "template" and d.get("schema-version") == 2 and "facts" in d["company"])
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_company_context_lint.py`
Expected: 새 케이스 FAIL(migrate no-op).
- [ ] **Step 3: `migrate` 구현** (Task 4의 no-op 대체)
```python
def migrate(path):
"""구 스키마/어휘를 schema-version 2 + 3-상태로 1회 변환. 자유서술 company 는 보존."""
import yaml
try:
doc = _load(path)
except Exception as e:
sys.stderr.write(f"[lint_company_context] migrate 파싱 실패: {e}\n"); return 1
st = str(doc.get("status", "")).strip().lower()
doc["status"] = {"demo": "template", "populated": "operating"}.get(st, st if st in OFFICIAL_STATUS else "template")
doc["schema-version"] = 2
comp = doc.get("company")
if not isinstance(comp, dict):
comp = {}
# 자유서술 키(name/mission/constraints 등)는 보존하고 골격 블록만 보강
comp.setdefault("facts", [])
comp.setdefault("strategic-decisions", [])
comp.setdefault("hypotheses", [])
comp.setdefault("validation-state", {"stage": "pre-traction", "validated": [], "open": [], "refuted": []})
doc["company"] = comp
doc.pop("candidate-status", None)
with open(path, "w", encoding="utf-8") as fh:
yaml.safe_dump(doc, fh, allow_unicode=True, sort_keys=False)
print(f"[lint_company_context] migrated -> status={doc['status']}, schema-version=2")
return 0
```
- [ ] **Step 4: 통과 확인 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_company_context_lint.py`
Expected: PASS.
```bash
git add .claude/hooks/lint_company_context.py .claude/tests/test_company_context_lint.py
git commit -m "P1(B): lint_company_context --migrate(구 어휘->3-상태, schema v2, company 보존)"
```
---
## Task 8: 린터를 run_all.py + doctor.py에 배선
**Files:**
- Modify: `.claude/tests/run_all.py`
- Modify: `.claude/hooks/doctor.py`
**Interfaces:**
- Consumes: `lint_company_context.py` CLI (exit code), `test_company_context_lint.py`.
- Produces: run_all이 새 test 파일을 자동 수집(파일명 규약)하고, doctor가 공식 company-context.yaml에 대해 `lint_file`을 호출해 Hard Fail 0을 확인.
- [ ] **Step 1: run_all이 새 테스트를 수집하는지 확인**
Run: `grep -nE "glob|test_.*\.py|discover|for .*test" .claude/tests/run_all.py | head`
확인: run_all이 `test_*.py`를 자동 수집하면 추가 작업 불필요. 자동 수집이 아니면 명시 목록에 `test_company_context_lint.py`·`test_venture_bootstrap.py`(Task 9~) 추가.
- [ ] **Step 2: (자동수집 아닐 때만) 명시 목록에 추가** — run_all의 테스트 목록 배열에 두 파일명 삽입. (자동수집이면 이 step은 no-op 처리하고 넘어간다.)
- [ ] **Step 3: doctor에 company-context lint 점검 추가**`doctor.py`의 점검 함수 모음에 삽입(기존 점검 패턴을 따른다; 아래는 독립 호출로도 안전한 형태)
```python
def _check_company_context_lint():
"""공식 company-context.yaml 이 lint_company_context Hard Fail 0 인지."""
try:
import lint_company_context as L
path = os.path.join(ROOT, "org-os", "01-company", "company-context.yaml")
hard, warn = L.lint_file(path, is_candidate=False)
if hard:
return (False, "company-context.yaml lint Hard Fail: " + "; ".join(hard))
return (True, f"company-context.yaml lint OK (warnings {len(warn)})")
except Exception as e:
return (False, f"company-context lint 점검 오류: {e}")
```
그리고 doctor의 점검 실행 목록(기존에 각 `_check_*`를 호출·집계하는 곳)에 `_check_company_context_lint`를 등록한다. (doctor의 정확한 등록 지점은 `grep -nE "def _check|checks =|append.*_check|run.*check" .claude/hooks/doctor.py`로 확인.)
- [ ] **Step 4: doctor + run_all 확인**
Run: `python3 .claude/hooks/doctor.py; CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py`
Expected: doctor PASS(새 점검 포함), run_all green(새 lint 테스트 포함).
- [ ] **Step 5: 커밋**
```bash
git add .claude/tests/run_all.py .claude/hooks/doctor.py
git commit -m "P1(B): lint_company_context 를 run_all + doctor 에 배선"
```
---
# Phase C — 상태머신 (venture-bootstrap plan + predicates)
## Task 9: `venture-bootstrap` plan + 전이 규칙 (declarative)
**Files:**
- Modify: `org-os/06-agent-work/execution-plans.yaml`
- Modify: `org-os/00-role-registry/state-transition-rules.yaml`
- Create: `.claude/tests/test_venture_bootstrap.py`
**Interfaces:**
- Produces: plan `venture-bootstrap`(stages 7종), 전이 6개(모두 `allowed-by: [OPS-ORCH]`), condition-catalog 6 신규 키. Task 10~11 predicate가 이 조건 키를 구현한다. `SE.allowed_next`/`SE._find_transition`가 새 stage/전이를 인식한다.
- [ ] **Step 1: 실패 테스트 작성**`.claude/tests/test_venture_bootstrap.py` (state_engine 테스트 스켈레톤 복제)
```python
#!/usr/bin/env python3
"""venture-bootstrap plan + predicate 단위테스트. standalone. exit 0 = all pass."""
import os, sys, shutil
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
HOOKS = os.path.join(ROOT, ".claude", "hooks")
FIX = os.path.join(ROOT, ".claude", "tests", "fixtures")
os.makedirs(FIX, exist_ok=True)
WS = os.path.join(FIX, "venture-bootstrap-ws")
shutil.rmtree(WS, ignore_errors=True); os.makedirs(WS, exist_ok=True)
os.environ["CLAUDE_PROJECT_DIR"] = ROOT
os.environ["ORGOS_WORKSPACE"] = WS
sys.path.insert(0, HOOKS)
import state_engine as SE # noqa: E402
passed, failed = 0, 0
def check(name, ok):
global passed, failed
if ok: passed += 1; print(f" PASS {name}")
else: failed += 1; print(f" FAIL {name}")
# plan 로드에 venture-bootstrap 존재
plans = SE.load_plans().get("plans", {})
check("venture-bootstrap plan 존재", "venture-bootstrap" in plans)
vb = plans.get("venture-bootstrap", {})
check("stages 순서", vb.get("stages", [])[:3] == ["intake", "founder-setup", "opportunity-discovery"])
check("terminal-stage", vb.get("terminal-stage") == "bootstrap-complete")
# 전이 존재 + allowed-by OPS-ORCH 단독
t = SE._find_transition("founder-setup", "opportunity-discovery")
check("founder-setup->opportunity-discovery 전이 존재", bool(t))
check("전이 allowed-by = [OPS-ORCH] 단독", t and t.get("allowed-by") == ["OPS-ORCH"])
t2 = SE._find_transition("venture-decision", "company-context-commit")
check("venture-decision->company-context-commit 존재", bool(t2))
t3 = SE._find_transition("company-context-commit", "bootstrap-complete")
check("company-context-commit->bootstrap-complete 존재", bool(t3))
sys.exit(1 if failed else 0)
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: FAIL(plan/전이 부재).
- [ ] **Step 3: execution-plans.yaml에 plan 추가**`plans:` 아래에 삽입
```yaml
venture-bootstrap:
description: >
회사 수립(1회성). founder-context 를 입력으로 기회탐색→벤처검증→벤처결정→
company-context commit 을 거쳐 company-context.yaml(provisional) 을 산출한다.
제품 cascade 의 선행이며 별도 lifecycle 이다. 모든 전이는 OPS-ORCH 가 집행.
stages: [intake, founder-setup, opportunity-discovery, venture-validation,
venture-decision, company-context-commit, bootstrap-complete]
terminal-stage: bootstrap-complete
default-tier: standard
outputs: "org-os/01-company/company-context.yaml (status: provisional)"
```
- [ ] **Step 4: state-transition-rules.yaml에 전이 + 조건 추가**`workflow-stage-transitions.transitions:` 리스트에 6개 추가하고, `condition-catalog:`에 6개 키 추가. 또한 `unified-stage-graph`에 새 stage를 반영(문서적).
`transitions:` 에 추가:
```yaml
# ── venture-bootstrap(회사 수립, 별도 lifecycle) — 전이 집행은 OPS-ORCH 단독 ──
- { from: intake, to: founder-setup, allowed-by: [OPS-ORCH], required-conditions: [decision-brief-present] }
- { from: founder-setup, to: opportunity-discovery, allowed-by: [OPS-ORCH], required-conditions: [founder-context-present] }
- { from: opportunity-discovery, to: venture-validation, allowed-by: [OPS-ORCH], required-conditions: [opportunity-clusters-present] }
- { from: venture-validation, to: venture-decision, allowed-by: [OPS-ORCH], required-conditions: [venture-options-validated] }
- { from: venture-decision, to: company-context-commit, allowed-by: [OPS-ORCH], required-conditions: [venture-decision-accepted, human-acceptance-receipt-present] }
- { from: company-context-commit, to: bootstrap-complete, allowed-by: [OPS-ORCH], required-conditions: [company-context-provisional-committed, company-context-lint-passed, company-context-artifact-recorded] }
```
`condition-catalog:` 에 추가:
```yaml
founder-context-present: "org-os/01-company/founder-context.yaml status=filled"
opportunity-clusters-present: "opportunity-cluster 산출 ≥ 2(ledger.artifacts design-type=opportunity-cluster 또는 ledger.opportunity-clusters 길이)"
venture-options-validated: "venture-validation accepted 산출물 존재(각 옵션 required 필드+kill-criteria)"
venture-decision-accepted: "venture-decision accepted 산출물 존재(acceptance_log accepted)"
human-acceptance-receipt-present: "HUMAN-001 accepted 이벤트가 그 venture-decision report-id/report-sha256/workflow-id 에 바인딩"
company-context-provisional-committed: "공식 company-context.yaml status ∈ {provisional, operating}(commit_company_context 원자 교체)"
company-context-lint-passed: "lint_company_context 공식 파일 Hard Fail 0(live 평가)"
company-context-artifact-recorded: "ledger artifacts 에 design-type=company-context 등록(commit receipt)"
```
`unified-stage-graph`(문서 라인)에 stage를 병기(강제는 plan+transitions가 함): 주석으로 `# venture-bootstrap: founder-setup, opportunity-discovery, venture-validation, venture-decision, company-context-commit, bootstrap-complete` 추가.
- [ ] **Step 5: 통과 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: PASS (plan/전이 케이스). predicate 케이스는 아직 없음.
- [ ] **Step 6: 커밋**
```bash
git add org-os/06-agent-work/execution-plans.yaml org-os/00-role-registry/state-transition-rules.yaml .claude/tests/test_venture_bootstrap.py
git commit -m "P1(C): venture-bootstrap plan + 전이 6종(OPS-ORCH 단독) + condition-catalog"
```
---
## Task 10: predicates part 1 — founder / opportunity / venture-options
**Files:**
- Modify: `.claude/hooks/state_engine.py` (facts 빌더 `_facts`, `_PREDICATES`, helper)
- Modify: `.claude/tests/test_venture_bootstrap.py`
**Interfaces:**
- Consumes: `_facts(wf, led, ctx)` 패턴, `_present`, `_accepted_design_types`.
- Produces: facts 키 `founder_context_present`, `opportunity_cluster_count`, `venture_options_validated`; `_PREDICATES` 항목 `founder-context-present`/`opportunity-clusters-present`/`venture-options-validated`.
- [ ] **Step 1: 실패 테스트 추가**`test_venture_bootstrap.py``sys.exit` 직전
```python
def led_at(stage, **extra):
d = {"workflow-id": "vb1", "stage": stage, "plan": "venture-bootstrap", "tier": "standard", "artifacts": []}
d.update(extra); return d
# founder-context-present: 실제 파일 status 에 의존. filled 가정 불가하므로 조건 평가만 확인.
f = SE._facts("vb1", led_at("founder-setup"))
ok, _ = SE._eval_condition("founder-context-present", f)
check("founder-context-present 평가 가능", isinstance(ok, bool))
# opportunity-clusters-present: <2 면 차단, >=2 면 통과(ledger 명시 리스트)
f = SE._facts("vb1", led_at("opportunity-discovery", **{"opportunity-clusters": [{"id": "OC1"}]}))
ok, _ = SE._eval_condition("opportunity-clusters-present", f)
check("opportunity clusters 1개 -> 차단", ok is False)
f = SE._facts("vb1", led_at("opportunity-discovery", **{"opportunity-clusters": [{"id": "OC1"}, {"id": "OC2"}]}))
ok, _ = SE._eval_condition("opportunity-clusters-present", f)
check("opportunity clusters 2개 -> 통과", ok is True)
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: 새 케이스 FAIL(`미지의 조건`).
- [ ] **Step 3: helper + facts + predicates 추가**`state_engine.py`
`_COMPANY_CTX`/`FOUNDER_CTX` 경로 상수와 helper를 `_facts` 위쪽(예: `_present` 근처)에 추가:
```python
_FOUNDER_CTX = os.path.join(ROOT, "org-os", "01-company", "founder-context.yaml")
_COMPANY_CTX_PATH = os.path.join(ROOT, "org-os", "01-company", "company-context.yaml")
def _founder_context_filled():
try:
return str(_load_yaml(_FOUNDER_CTX).get("status", "")).strip().lower() == "filled"
except Exception:
return False
```
`_facts` 내부(예: `f["decision_brief_present"] = ...` 근처)에 facts 주입:
```python
# --- venture-bootstrap facts (P1) ---
f["founder_context_present"] = _founder_context_filled()
oc = led.get("opportunity-clusters") or led.get("opportunity_clusters") or []
oc_art = sum(1 for a in arts if isinstance(a, dict) and a.get("design-type") == "opportunity-cluster")
f["opportunity_cluster_count"] = max(len(oc) if isinstance(oc, list) else 0, oc_art)
f["venture_options_validated"] = bool(accepted_types & {"venture-validation", "venture-option"})
```
`_PREDICATES` 딕셔너리에 추가:
```python
"founder-context-present": lambda f: (bool(f.get("founder_context_present")), "founder-context.yaml status != filled"),
"opportunity-clusters-present": lambda f: (f.get("opportunity_cluster_count", 0) >= 2, f"opportunity-cluster ≥2 필요(현재 {f.get('opportunity_cluster_count', 0)})"),
"venture-options-validated": lambda f: (bool(f.get("venture_options_validated")), "venture-validation accepted 산출물 없음(kill-criteria 포함 옵션 검증 필요)"),
```
`_PROTECTED_FACTS`에 추가(자기신고 방지): `"founder_context_present", "venture_options_validated"`. (`opportunity_cluster_count`는 발산 카운트라 오버라이드 허용 — option_set_count와 동일 취급.)
- [ ] **Step 4: 통과 확인 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: PASS.
```bash
git add .claude/hooks/state_engine.py .claude/tests/test_venture_bootstrap.py
git commit -m "P1(C): predicates part1 — founder-context/opportunity-clusters/venture-options-validated"
```
---
## Task 11: predicates part 2 — decision·human-receipt(hash)·commit·lint·artifact
**Files:**
- Modify: `.claude/hooks/state_engine.py`
- Modify: `.claude/tests/test_venture_bootstrap.py`
**Interfaces:**
- Consumes: `acceptance_log.read_events`/`_resolve_report_path`(Task 15에서 report-sha256 지원), `lint_company_context.lint_file`(Task 4~7), `_evidence_receipts`, `record_artifact`.
- Produces: facts + `_PREDICATES` 항목 `venture-decision-accepted`/`human-acceptance-receipt-present`/`company-context-provisional-committed`/`company-context-lint-passed`/`company-context-artifact-recorded`. 모두 `_PROTECTED_FACTS`.
> 의존: 이 task는 Task 15(acceptance_log report-sha256)와 상호참조한다. 순서상 Task 11의 hash 검증 helper는 Task 15가 이벤트에 `report-sha256`를 넣어야 실동작한다. **권장 실행순서: Task 15 → Task 11.** (구현은 독립적이나 통합 테스트는 15 이후.)
- [ ] **Step 1: 실패 테스트 추가**
```python
# company-context-lint-passed: 공식 파일이 template(정상)이면 통과
f = SE._facts("vb1", led_at("company-context-commit"))
ok, _ = SE._eval_condition("company-context-lint-passed", f)
check("company-context-lint-passed 평가 가능", isinstance(ok, bool))
# company-context-provisional-committed: 공식 status=template 이면 False
ok, _ = SE._eval_condition("company-context-provisional-committed", f)
check("template 상태 -> committed False", ok is False)
# human-acceptance-receipt-present: 이벤트 없으면 False(boolean 자기신고 거부)
f2 = SE._facts("vb1", led_at("venture-decision", **{"facts": {"human_acceptance_receipt_present": True}}))
ok, _ = SE._eval_condition("human-acceptance-receipt-present", f2)
check("human boolean 자기신고 -> 거부(receipt 없음)", ok is False)
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: 새 케이스 FAIL.
- [ ] **Step 3: helper + facts + predicates 추가**
helper(state_engine.py):
```python
def _company_ctx_status():
try:
return str(_load_yaml(_COMPANY_CTX_PATH).get("status", "")).strip().lower()
except Exception:
return ""
def _company_ctx_lint_ok():
try:
import lint_company_context as L
hard, _ = L.lint_file(_COMPANY_CTX_PATH, is_candidate=False)
return not hard
except Exception:
return False
def _company_ctx_artifact_recorded(arts):
return any(isinstance(a, dict) and a.get("design-type") == "company-context" for a in (arts or []))
def _venture_decision_receipt_ok(wf):
"""HUMAN-001 accepted 이벤트가 venture-decision report 의 현재 해시와 일치 바인딩(§9.4).
이벤트에 report-sha256 이 있고(Task 15), report 파일이 실존하며 해시 일치할 때만 True."""
try:
import acceptance_log as AL, hashlib
for ev in reversed(list(AL.read_events())):
if ev.get("decision") != "accepted":
continue
if ev.get("workflow-id") not in (None, wf):
continue
if str(ev.get("role-id", "")).upper() != "HUMAN-001".upper():
continue
sha = ev.get("report-sha256")
rid = ev.get("accepted-report-id") or ev.get("report-id")
if not (sha and rid):
continue
path = AL._resolve_report_path(rid, wf)
if not path or not os.path.exists(path):
continue
actual = hashlib.sha256(open(path, "rb").read()).hexdigest()
if actual == sha:
return True
return False
except Exception:
return False
```
`_facts` 내부에 주입:
```python
f["venture_decision_accepted"] = bool(accepted_types & {"venture-decision", "ExecutiveDecisionPacket", "decision-packet"})
f["human_acceptance_receipt_present"] = _venture_decision_receipt_ok(wf)
_st = _company_ctx_status()
f["company_context_provisional_committed"] = _st in ("provisional", "operating")
f["company_context_lint_passed"] = _company_ctx_lint_ok()
f["company_context_artifact_recorded"] = _company_ctx_artifact_recorded(arts)
```
`_PREDICATES`에 추가:
```python
"venture-decision-accepted": lambda f: (bool(f.get("venture_decision_accepted")), "venture-decision accepted 산출물 없음"),
"human-acceptance-receipt-present": lambda f: (bool(f.get("human_acceptance_receipt_present")), "HUMAN-001 accepted 이벤트(report-sha256 바인딩) 없음 — boolean 자기신고 불가"),
"company-context-provisional-committed": lambda f: (bool(f.get("company_context_provisional_committed")), "공식 company-context.yaml status != provisional/operating"),
"company-context-lint-passed": lambda f: (bool(f.get("company_context_lint_passed")), "company-context lint Hard Fail 존재"),
"company-context-artifact-recorded": lambda f: (bool(f.get("company_context_artifact_recorded")), "company-context commit receipt(artifact) 없음"),
```
`_PROTECTED_FACTS`에 5개 키 추가: `"venture_decision_accepted", "human_acceptance_receipt_present", "company_context_provisional_committed", "company_context_lint_passed", "company_context_artifact_recorded"`.
- [ ] **Step 4: 통과 확인 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: PASS.
```bash
git add .claude/hooks/state_engine.py .claude/tests/test_venture_bootstrap.py
git commit -m "P1(C): predicates part2 — decision/human-receipt(hash)/committed/lint/artifact(전부 PROTECTED)"
```
---
## Task 12: `company-context-ready` seam predicate + guard 서브커맨드 (제품 cascade 진입)
**Files:**
- Modify: `.claude/hooks/state_engine.py` (predicate + CLI 서브커맨드)
- Modify: `.claude/tests/test_venture_bootstrap.py`
**Interfaces:**
- Produces: predicate `company-context-ready`(advisory, 기존 cascade 전이 조건에 **추가하지 않음** — 회귀 방지) + CLI `state_engine.py check-company-context-ready --workflow WF`(exit 0 ready / exit 2 not). 제품 커맨드(/ground·/decide)가 진입 시 호출.
- [ ] **Step 1: 실패 테스트 추가**
```python
# company-context-ready: 공식 status=template 이면 not ready
f = SE._facts("vb1", led_at("intake"))
ok, _ = SE._eval_condition("company-context-ready", f)
check("template -> cascade not ready", ok is False)
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: 새 케이스 FAIL.
- [ ] **Step 3: predicate + CLI 추가**
facts(이미 `company_context_provisional_committed` 존재하므로 재사용):
```python
f["company_context_ready"] = f.get("company_context_provisional_committed") and not f.get("blocker_open")
```
`_PREDICATES`:
```python
"company-context-ready": lambda f: (bool(f.get("company_context_ready")), "company-context 미준비(status provisional/operating 아님 또는 blocker)"),
```
`_PROTECTED_FACTS``"company_context_ready"` 추가.
CLI: state_engine의 argparse/명령 분기(기존 `guard` 처리부 근처)에 서브커맨드 추가:
```python
# (main 의 명령 분기에서)
if cmd == "check-company-context-ready":
wf = ns.workflow
f = _facts(wf, _load_ledger_safe(wf))
ok, reason = _eval_condition("company-context-ready", f)
if ok:
print("[state_engine] company-context READY")
return 0
sys.stderr.write(f"[state_engine] NOT READY: {reason}\n")
return 2
```
(정확한 argparse 등록은 기존 `guard` 서브커맨드 정의를 복제 — `grep -nE "add_parser|guard|def main" .claude/hooks/state_engine.py`로 위치 확인.)
- [ ] **Step 4: 통과 확인 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: PASS.
```bash
git add .claude/hooks/state_engine.py .claude/tests/test_venture_bootstrap.py
git commit -m "P1(C): company-context-ready seam predicate + check-company-context-ready guard(cascade 회귀 없음)"
```
- [ ] **Step 5: Phase C 회귀 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py`
Expected: green(기존 cascade 테스트 포함 — 새 조건을 기존 전이에 넣지 않았으므로 무회귀).
---
# Phase D — 강제기 확장
## Task 13: validate_report status 어휘(operating) + 읽기 호환
**Files:**
- Modify: `.claude/hooks/validate_report.py:_company_context_populated`
- Modify: `.claude/tests/test_enforcement.py`
**Interfaces:**
- Consumes: `_is_unpopulated_company_ref`(항목 단위 상한, 기존).
- Produces: `_company_context_populated()``operating`(및 구 `populated`)일 때만 True. `template/provisional`(및 구 `demo`)은 False(회사 인용 항목 E2 상한 유지). 외부/코드 증거는 무영향.
- [ ] **Step 1: 실패 테스트 추가**`test_enforcement.py`의 실행 케이스 영역에 추가(파일의 `check`/assert 규약을 따른다; 아래는 함수 단위 호출 예)
```python
# validate_report._company_context_populated 는 status=operating 에서만 True.
# 실제 SoT 파일을 건드리지 않도록 모듈 상수 VR._COMPANY_CTX 를 임시파일로 monkeypatch(크래시해도 원본 무손상).
import sys, os, tempfile, yaml
sys.path.insert(0, os.path.join(ROOT, ".claude", "hooks"))
import validate_report as VR
_orig_ctx = VR._COMPANY_CTX
def _set_ctx(status):
fd, p = tempfile.mkstemp(suffix=".yaml"); os.close(fd)
with open(p, "w", encoding="utf-8") as fh:
yaml.safe_dump({"schema-version": 2, "status": status,
"company": {"facts": [], "strategic-decisions": [], "hypotheses": [],
"validation-state": {"stage": "pre-traction", "validated": [], "open": [], "refuted": []}}, "projects": []},
fh, allow_unicode=True)
VR._COMPANY_CTX = p
def _restore():
VR._COMPANY_CTX = _orig_ctx
_set_ctx("operating")
check("operating -> populated True", VR._company_context_populated() is True)
_set_ctx("provisional")
check("provisional -> populated False(회사인용 상한 유지)", VR._company_context_populated() is False)
_set_ctx("template")
check("template -> populated False", VR._company_context_populated() is False)
_restore()
```
> `test_enforcement.py`가 `check()` 하니스가 아니라 다른 규약이면, 파일 상단 규약(assert/함수)에 맞춰 케이스를 삽입한다(`sed -n '1,40p' .claude/tests/test_enforcement.py`로 확인).
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_enforcement.py`
Expected: `provisional -> populated False`는 통과(현재 로직상 populated만 True)지만 `operating -> populated True`는 FAIL(현재는 'populated' 문자열만 True).
- [ ] **Step 3: `_company_context_populated` 수정**
```python
def _company_context_populated():
"""공식 company-context 가 '실데이터로 운영 중'이면 True → 회사 인용 상한 해제.
신 어휘 status=='operating' 만 True. 구 'populated' 는 읽기 호환(operating 취급).
template/provisional(및 구 'demo')은 False = 회사 인용 항목 E2/Med 상한 유지(§9.1)."""
try:
import yaml as _y # noqa: E402
doc = _y.safe_load(open(_COMPANY_CTX, encoding="utf-8")) or {}
st = str(doc.get("status", "")).strip().lower()
if st == "populated":
sys.stderr.write("[validate_report] WARN: status='populated' deprecated → 'operating'\n")
return True
return st == "operating"
except Exception:
return False
```
(`sys` import 확인 — 파일 상단에 이미 있으면 재사용.)
- [ ] **Step 4: 통과 확인 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_enforcement.py`
Expected: PASS.
```bash
git add .claude/hooks/validate_report.py .claude/tests/test_enforcement.py
git commit -m "P1(D): validate_report status 어휘 operating + populated 읽기호환(회사인용 상한은 항목단위 유지)"
```
---
## Task 14: validate_report hypothesis anchor ceiling
**Files:**
- Modify: `.claude/hooks/validate_report.py` (evidence 상한 로직)
- Modify: `.claude/tests/test_enforcement.py`
**Interfaces:**
- Consumes: evidence 항목의 `source-uri`(예: `org-os/01-company/company-context.yaml#HYP-001`).
- Produces: 회사 문맥 인용의 anchor가 `#HYP-`이면 그 항목 근거를 **E2/Med 상한**(status=operating이어도) — 가설 기반 회사 결론 상한. `#FACT-`/`#DEC-`는 기존 네임스페이스 규칙.
- [ ] **Step 1: 실패 테스트 추가**
```python
# _is_hypothesis_company_ref 는 source-uri anchor 문자열만 파싱한다(파일 접근 없음 → 파일 조작 불필요).
check("HYP anchor -> hypothesis ref True", VR._is_hypothesis_company_ref("org-os/01-company/company-context.yaml#HYP-001") is True)
check("FACT anchor -> hypothesis ref False", VR._is_hypothesis_company_ref("org-os/01-company/company-context.yaml#FACT-001") is False)
check("no anchor -> False", VR._is_hypothesis_company_ref("org-os/01-company/company-context.yaml") is False)
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_enforcement.py`
Expected: FAIL(`_is_hypothesis_company_ref` 부재).
- [ ] **Step 3: helper 추가 + 상한 로직 결선**
`_is_unpopulated_company_ref` 근처에 helper 추가:
```python
def _is_hypothesis_company_ref(src):
"""company-context 의 hypothesis 항목을 anchor(#HYP-...)로 인용하면 True.
가설 기반 회사 결론은 status=operating 이어도 E2/Med 상한(§9.1)."""
if not src:
return False
raw = str(src).strip().strip("'\"`").replace("\\", "/")
if "#" not in raw:
return False
path, _, anchor = raw.partition("#")
base = path.rsplit("/", 1)[-1]
return base == "company-context.yaml" and anchor.upper().startswith("HYP-")
```
evidence 상한 판정부(현재 `_is_unpopulated_company_ref(e.get("source-uri"))`로 E3+ 차단하는 곳, 스펙 §9.1 line ~474)에 OR 조건 추가 — hypothesis anchor면 populated 여부와 무관하게 상한:
```python
src = e.get("source-uri")
if gnum >= 3 and (_is_unpopulated_company_ref(src) or _is_hypothesis_company_ref(src)):
# ... 기존 E2 강등/경고 메시지 재사용, 사유에 'hypothesis 기반' 추가
```
(정확한 삽입 위치·메시지는 기존 블록을 그대로 확장; `grep -n "_is_unpopulated_company_ref" .claude/hooks/validate_report.py`로 호출부 확인.)
- [ ] **Step 4: 통과 확인 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_enforcement.py`
Expected: PASS.
```bash
git add .claude/hooks/validate_report.py .claude/tests/test_enforcement.py
git commit -m "P1(D): validate_report hypothesis anchor(#HYP-) ceiling — 가설기반 회사결론 Med/E2 상한"
```
---
## Task 15: acceptance_log `report-sha256` 바인딩
**Files:**
- Modify: `.claude/hooks/acceptance_log.py` (`build_event`, CLI append)
- Modify: `.claude/schemas/acceptance-event.schema.json`
- Modify: `.claude/tests/test_p1_acceptance.py`
**Interfaces:**
- Produces: `build_event(..., report_sha256=None)`이 지정 시 이벤트에 `report-sha256` 추가; CLI `append ... --report-sha256 <hex>`. schema에 optional `report-sha256`. Task 11의 `_venture_decision_receipt_ok`가 이 필드를 읽는다.
- [ ] **Step 1: 실패 테스트 추가**`test_p1_acceptance.py` 규약에 맞춰
```python
import acceptance_log as AL
ev = AL.build_event("r1", "accepted", workflow="wf1", role="HUMAN-001", report_sha256="abc123")
check("build_event report-sha256 포함", ev.get("report-sha256") == "abc123")
ev2 = AL.build_event("r1", "accepted", workflow="wf1")
check("report-sha256 미지정 시 부재", "report-sha256" not in ev2)
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_p1_acceptance.py`
Expected: FAIL(`build_event`가 report_sha256 인자 없음).
- [ ] **Step 3: `build_event` 시그니처 확장** (Task 4에서 본 정의)
`def build_event(..., report_sha256=None)` 추가하고 본문에:
```python
if report_sha256:
ev["report-sha256"] = report_sha256
```
CLI는 argparse가 아니라 커스텀 `_argval(args, "--flag")` 파서 + `def main()`(≈line 289)을 쓴다. `main()``append` 분기에서 `report_sha256=_argval(args, "--report-sha256")``build_event(...)` 호출 인자에 추가한다(기존 `workflow=_argval(args, "--workflow")` 라인들과 나란히). usage 문자열에도 `[--report-sha256 HEX]` 추가.
schema(`acceptance-event.schema.json`)의 `properties`에 추가:
```json
"report-sha256": {
"type": "string",
"description": "선택. 수락 대상 report 파일의 sha256 — venture-decision human-gate 위조·재사용 방지 바인딩(P1 §9.4)."
}
```
- [ ] **Step 4: 통과 확인 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_p1_acceptance.py`
Expected: PASS.
```bash
git add .claude/hooks/acceptance_log.py .claude/schemas/acceptance-event.schema.json .claude/tests/test_p1_acceptance.py
git commit -m "P1(D): acceptance_log report-sha256 바인딩(venture-decision human-gate 위조 방지)"
```
---
## Task 16: `commit_company_context.py` — candidate → 원자적 교체
**Files:**
- Create: `.claude/hooks/commit_company_context.py`
- Modify: `.claude/tests/test_venture_bootstrap.py`
**Interfaces:**
- Consumes: `lint_company_context.lint_file`, `state_engine.record_artifact`, `os.replace`.
- Produces: CLI `commit_company_context.py --workflow WF --candidate <path>`(lint→acceptance 검증→candidate-status 제거→os.replace→record_artifact). exit 0 성공/1 차단. 실패 시 공식 파일 무변경.
- [ ] **Step 1: 실패 테스트 추가**`test_venture_bootstrap.py`
```python
import subprocess, tempfile, yaml
def _write(path, obj):
with open(path, "w", encoding="utf-8") as fh: yaml.safe_dump(obj, fh, allow_unicode=True, sort_keys=False)
# lint 실패 candidate -> 공식 파일 무변경 + exit 1
official = os.path.join(ROOT, "org-os", "01-company", "company-context.yaml")
before = open(official, encoding="utf-8").read()
cand = os.path.join(WS, "bad.candidate.yaml")
_write(cand, {"schema-version": 2, "status": "provisional", "candidate-status": "bootstrap",
"company": {"facts": [], "strategic-decisions": [], "hypotheses": [],
"validation-state": {"stage": "pre-traction", "validated": [], "open": [], "refuted": []}}, "projects": []})
# provisional 인데 human decision 없음 -> lint hard fail
r = subprocess.run([sys.executable, os.path.join(HOOKS, "commit_company_context.py"), "--workflow", "vb1", "--candidate", cand],
capture_output=True, text=True, env={**os.environ})
check("lint 실패 candidate -> exit 1", r.returncode == 1)
check("공식 파일 무변경", open(official, encoding="utf-8").read() == before)
```
- [ ] **Step 2: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: FAIL(파일 부재).
- [ ] **Step 3: `commit_company_context.py` 구현**
```python
#!/usr/bin/env python3
"""commit_company_context.py — candidate → 공식 company-context.yaml 원자적 교체(trusted CLI, §9.3).
절차: schema/lint(Hard Fail 0) → (선택) venture-decision human acceptance 검증 → candidate-status 제거
→ 임시파일 write → os.replace(원자) → state_engine.record_artifact(design-type=company-context).
실패 시 공식 파일 **무변경**. OPS-ORCH 가 실행(에이전트는 guard_tools 로 공식 파일 직접쓰기 차단).
CLI: commit_company_context.py --workflow WF --candidate <path> [--require-human]
"""
import os, sys, argparse, tempfile
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OFFICIAL = os.path.join(ROOT, "org-os", "01-company", "company-context.yaml")
sys.path.insert(0, os.path.join(ROOT, ".claude", "hooks"))
def _fail(msg):
sys.stderr.write(f"[commit_company_context] FAIL: {msg}\n"); return 1
def main(argv):
ap = argparse.ArgumentParser()
ap.add_argument("--workflow", required=True)
ap.add_argument("--candidate", required=True)
ap.add_argument("--require-human", action="store_true")
ns = ap.parse_args(argv)
import yaml
if not os.path.exists(ns.candidate):
return _fail(f"candidate 없음: {ns.candidate}")
import lint_company_context as L
hard, warn = L.lint_file(ns.candidate, is_candidate=True)
for w in warn: sys.stderr.write(f"[commit_company_context] WARN: {w}\n")
if hard:
return _fail("candidate lint Hard Fail: " + "; ".join(hard))
if ns.require_human:
try:
import state_engine as SE
if not SE._venture_decision_receipt_ok(ns.workflow):
return _fail("HUMAN-001 acceptance receipt(report-sha256 바인딩) 없음")
except Exception as e:
return _fail(f"human acceptance 검증 오류: {e}")
# candidate → 공식: candidate-status 제거
with open(ns.candidate, encoding="utf-8") as fh:
doc = yaml.safe_load(fh) or {}
doc.pop("candidate-status", None)
# 최종 공식 형태 재-lint(안전)
tmp_fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(OFFICIAL), suffix=".tmp")
try:
with os.fdopen(tmp_fd, "w", encoding="utf-8") as fh:
yaml.safe_dump(doc, fh, allow_unicode=True, sort_keys=False)
hard2, _ = L.lint_file(tmp_path, is_candidate=False)
if hard2:
os.unlink(tmp_path)
return _fail("최종 공식형 lint Hard Fail: " + "; ".join(hard2))
os.replace(tmp_path, OFFICIAL) # 원자적 교체
except Exception as e:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
return _fail(f"원자 교체 실패(공식 파일 무변경): {e}")
# 아티팩트 등록(company-context-artifact-recorded predicate 근거)
try:
import state_engine as SE
SE.record_artifact(ns.workflow, design_type="company-context", path=OFFICIAL)
except Exception as e:
sys.stderr.write(f"[commit_company_context] WARN: record_artifact 실패: {e}\n")
print(f"[commit_company_context] OK — {OFFICIAL} (status={doc.get('status')})")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
```
- [ ] **Step 4: 통과 확인 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_venture_bootstrap.py`
Expected: PASS.
```bash
git add .claude/hooks/commit_company_context.py .claude/tests/test_venture_bootstrap.py
git commit -m "P1(D): commit_company_context — candidate→공식 원자적 교체(lint+human 게이트, 실패시 무변경)"
```
---
## Task 17: guard_tools — 공식 company-context.yaml 직접쓰기 보호
**Files:**
- Modify: `.claude/hooks/guard_tools.py`
- Modify: `.claude/tests/test_p1_permissions.py` (또는 test_p0_trust_boundary.py — 어느 파일이 guard_tools를 테스트하는지 `grep -l guard_tools .claude/tests/*.py`)
**Interfaces:**
- Produces: guard_tools가 Edit/Write 대상 경로가 `org-os/01-company/company-context.yaml`이면 **차단**(commit_company_context 경로만 허용). founder-context.yaml은 사람 편집이므로 별도 규칙(에이전트 쓰기 차단, 사람은 세션 밖).
- [ ] **Step 1: guard_tools 구조 확인**
Run: `grep -nE "def |PROTECT|immutable|report\.yaml|deny|block|path|Edit|Write" .claude/hooks/guard_tools.py | head -40`
확인: 기존에 `.report.yaml` 불변성/보호경로를 어떻게 매칭하는지(정규식/접미사/목록). 그 패턴에 공식 company-context 경로를 추가한다.
- [ ] **Step 2: 실패 테스트 추가** — guard_tools 테스트 파일의 규약에 맞춰(예: PreToolUse payload 시뮬레이션)
```python
# Write/Edit 로 공식 company-context.yaml 직접 수정 시도 -> 차단(exit != 0 또는 deny)
payload = {"tool_name": "Write", "tool_input": {"file_path": "org-os/01-company/company-context.yaml", "content": "x"}}
rc = run_guard(payload) # 테스트 파일의 기존 헬퍼 사용(guard_tools 호출 래퍼)
check("공식 company-context 직접 Write -> 차단", rc != 0)
```
> 테스트 파일에 guard 호출 헬퍼(`run_guard`)가 없으면, 같은 파일의 기존 케이스가 guard_tools를 호출하는 방식을 복제한다(예: `subprocess`로 hook 실행 + PreToolUse JSON stdin).
- [ ] **Step 3: 실패 확인**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_p1_permissions.py`
Expected: FAIL(현재 미보호).
- [ ] **Step 4: guard_tools에 보호경로 추가** — 기존 보호경로 매칭부에 삽입(정확한 형태는 Step 1의 패턴을 따름). 예시(접미사 매칭 방식일 때):
```python
_PROTECTED_SOT = ("org-os/01-company/company-context.yaml",)
# ... Edit/Write 분기에서:
norm = str(file_path).replace("\\", "/")
if any(norm.endswith(s) for s in _PROTECTED_SOT):
return _deny("공식 company-context.yaml 직접 수정 금지 — commit_company_context.py(원자 교체)만 허용(P1 §9.3/§9.5)")
```
(founder-context.yaml도 에이전트 쓰기 차단이 필요하면 동일 목록에 추가하되, 사람 입력 파일이므로 보호 메시지에 "사람이 세션 밖에서 편집" 명시.)
- [ ] **Step 5: 통과 확인 + Phase D 회귀 + 커밋**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/test_p1_permissions.py && CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py`
Expected: 둘 다 green.
```bash
git add .claude/hooks/guard_tools.py .claude/tests/test_p1_permissions.py
git commit -m "P1(D): guard_tools — 공식 company-context.yaml 직접쓰기 차단(commit CLI만 허용)"
```
---
# Phase E — 커맨드 & 문서 & 배선
## Task 18: 커맨드 — /ceo-intake 보강 + /venture-validate + /company-bootstrap
**Files:**
- Modify: `.claude/commands/ceo-intake.md`
- Create: `.claude/commands/venture-validate.md`
- Create: `.claude/commands/company-bootstrap.md`
**Interfaces:**
- Consumes: `state_engine.py guard/transition`, `context_package.py`, `venture-validation-map.yaml`, `commit_company_context.py`, converge 계약(collaboration-modes.yaml).
- Produces: 3개 커맨드 문서. lint_refs(Task 20)가 커맨드→에이전트/파일 참조 무결성을 검사한다.
- [ ] **Step 1: 기존 커맨드 형식 확인**
Run: `sed -n '1,30p' .claude/commands/decide.md`
확인: frontmatter·guard/transition 호출·context_package 지시·산출물 경로 규약. `/venture-validate`·`/company-bootstrap`는 이 규약을 그대로 따른다.
- [ ] **Step 2: `/ceo-intake` 보강**`ceo-intake.md`에 다음 절 추가(기존 내용 보존)
```markdown
## 회사 부트스트랩 진입(venture-bootstrap)
**회사/제품군을 처음 세우는** 경우에만 `--plan venture-bootstrap`을 명시한다(자동 선택 금지 — 기존 제품 cascade와 충돌 방지).
1. `org-os/01-company/founder-context.yaml``status`를 확인한다. `template`이면 **사람에게 채우도록 요청**하고(창업자 강점·시간·자본·유통역량·리스크 내성·hard-constraints), `status: filled`로 바뀌기 전에는 다음 단계로 진행하지 않는다(founder-setup 게이트가 `founder-context-present`를 강제).
2. Decision Brief를 작성하고 `plan=venture-bootstrap`, `tier`를 선언한다.
3. 상태 초기화 후 다음: `/venture-validate`.
기존 회사(공식 company-context.status ∈ {provisional, operating})면 이 절을 건너뛰고 제품 cascade(`/ground` 등)로 간다. 제품 커맨드는 진입 시 `python3 .claude/hooks/state_engine.py check-company-context-ready --workflow <wf>`로 회사 문맥 준비를 확인한다(template면 거부).
```
- [ ] **Step 3: `/venture-validate.md` 생성**
```markdown
---
description: 기회탐색(발산)→벤처검증(9-gate)로 opportunity-cluster와 검증된 venture-option을 산출한다. venture-bootstrap 2단계.
---
당신은 Orchestrator다. **venture-bootstrap: opportunity-discovery + venture-validation.** 회사 정의 이전이므로 company-context를 강근거로 쓰지 않는다(§7.1 상한). 입력: `org-os/01-company/founder-context.yaml`(must-read), `org-os/06-agent-work/venture-option-spec.yaml`, `org-os/06-agent-work/venture-validation-map.yaml`.
1. **guard:** `python3 .claude/hooks/state_engine.py guard --workflow <wf> --to opportunity-discovery`.
2. **opportunity-discovery(발산):** `venture-validation-map.opportunity-discovery-roles.diverge` 역할 + `contrarian`(CFO=경제구조 반증)로 **divergent** fan-out. 각 워커는 context_package로 spawn(mode=divergent, must-read=founder-context+venture-option-spec). 산출: opportunity-cluster ≥2(중복·완전성 검사). 각 cluster는 spec의 `opportunity-cluster.required` 필드 충족. 제품명 이전, **문제 클러스터**부터.
3. **전이:** OPS-ORCH가 `transition --to opportunity-discovery`(조건 `opportunity-clusters-present`).
4. **venture-validation:** 각 옵션 × 9-gate를 `venture-validation-map.gates`의 primary/auditor로 fan-out(dissent 보존). `unknown` 허용, `kill-criteria` 필수. 산출: venture-option 보고서(spec `venture-option.required` 충족) + validation-result. 종합은 EXEC-CEO가 하되 상태 전이는 하지 않는다.
5. **전이:** OPS-ORCH가 `transition --to venture-validation`(조건 `venture-options-validated`).
6. **다음:** `/company-bootstrap`.
산출물: `completion-records/<wf>/opportunity-clusters-*.report.yaml`, `venture-option-*.report.yaml`(불변, new_report). 모든 spawn은 context_package 컴파일러+validator를 거친다.
```
- [ ] **Step 4: `/company-bootstrap.md` 생성**
```markdown
---
description: 벤처결정(C-Level converge + 사람 승인)→company-context candidate→원자적 commit. venture-bootstrap 3단계.
---
당신은 Orchestrator다. **venture-bootstrap: venture-decision + company-context-commit.** `/venture-validate`의 검증된 옵션을 하나로 수렴해 회사 문맥을 확정한다. 재사용 단위는 `/decide` 명령이 아니라 **공통 converge 계약**(`org-os/06-agent-work/collaboration-modes.yaml``converge`: synthesis + report-header).
1. **guard:** `python3 .claude/hooks/state_engine.py guard --workflow <wf> --to venture-decision`.
2. **venture-decision(수렴):** C-Level(CPO·CFO·CTO·COO·CPTO)이 검증된 옵션을 **독립 평가**(divergent) → EXEC-CEO가 **converge** 종합해 **ExecutiveDecisionPacket**(선택 옵션·트레이드오프·기각사유·dissent 보존·go/no-go) 산출. `report-type: decision`/`design-type: venture-decision`.
3. **사람 승인(바인딩):** 사람이 하나를 선택하면 그 report의 sha256을 계산해 `python3 .claude/hooks/acceptance_log.py append --report-id <rid> --decision accepted --workflow <wf> --role HUMAN-001 --report-sha256 <sha>`. (boolean 자기신고 불가 — predicate가 report-sha256 바인딩을 검사.)
4. **전이:** OPS-ORCH가 `transition --to venture-decision`(조건 `venture-decision-accepted` + `human-acceptance-receipt-present`).
5. **company-context candidate 작성:** `<workspace>/completion-records/<wf>/company-context.candidate.yaml` 생성 — 선택된 결정을 `strategic-decisions`(accepted-by: HUMAN-001, source-decision-id=venture-decision id), 확정 사실을 `facts`(provenance), 시장 가정을 `hypotheses`(validation-status: untested, falsification-criteria). `status: provisional`, `candidate-status: bootstrap`.
6. **atomic commit:** `python3 .claude/hooks/commit_company_context.py --workflow <wf> --candidate <candidate-path> --require-human`. (lint Hard Fail 0 + human receipt 검증 통과 시에만 공식 파일 원자 교체. 실패 시 공식 파일 무변경.)
7. **전이(종료):** OPS-ORCH가 `transition --to company-context-commit``transition --to bootstrap-complete`(조건 `company-context-provisional-committed` + `company-context-lint-passed` + `company-context-artifact-recorded`).
8. **다음:** 제품 cascade(`/ground` …)가 이제 `company-context-ready`를 통과한다. 제품 intake는 `company-context-ref`·`venture-decision-id`·`company-decision-ids`를 참조한다(§11).
공식 `company-context.yaml`은 이 커맨드의 commit CLI로만 갱신된다(직접 Edit/Write는 guard_tools가 차단).
```
- [ ] **Step 5: lint_refs 확인 + 커밋**
Run: `python3 .claude/hooks/lint_refs.py`
Expected: 참조 무결성 PASS(커맨드가 참조하는 파일·hook 실존). 실패 시 경로 오타 교정.
```bash
git add .claude/commands/ceo-intake.md .claude/commands/venture-validate.md .claude/commands/company-bootstrap.md
git commit -m "P1(E): 커맨드 — /ceo-intake --plan 보강 + /venture-validate + /company-bootstrap"
```
---
## Task 19: 문서 — README / 01-company/README / CLAUDE.md
**Files:**
- Modify: `README.md`
- Modify: `org-os/01-company/README.md`
- Modify: `CLAUDE.md`
**Interfaces:** 없음(문서). lint_refs가 링크 무결성 검사.
- [ ] **Step 1: README에 부트스트랩 진입 경로 추가** — "어떤 커맨드부터?" 절에 삽입
```markdown
### 새 회사/제품을 처음 세울 때(venture-bootstrap)
company-context가 아직 `template`이면 제품 cascade 전에 회사부터 세운다:
1. `org-os/01-company/founder-context.yaml`을 채운다(status: filled).
2. `/ceo-intake --plan venture-bootstrap``/venture-validate`(기회탐색+9-gate 검증) → `/company-bootstrap`(C-Level 수렴 + 사람 승인 + company-context 원자 commit).
3. 완료되면 공식 company-context.status = `provisional`. 이제 `/ground`부터 제품 cascade를 탄다.
기존 회사(status ∈ {provisional, operating})면 곧장 `/ceo-intake``/ground`.
```
- [ ] **Step 2: `org-os/01-company/README.md` 갱신** — status 어휘 + facts/decisions/hypotheses 정책
```markdown
## company-context.yaml 상태·근거 정책 (P1)
- 공식 `status`는 3-상태: **template → provisional → operating** (`bootstrap`은 공식 status 아님 — 작성 중은 candidate 파일의 `candidate-status: bootstrap`로만 표현).
- `company`는 **facts / strategic-decisions / hypotheses**로 분리하며 **항목별 provenance**를 둔다(fact=provenance, decision=accepted-by/at/source-decision-id, hypothesis=validation-status/confidence/falsification-criteria). `fact/decision/hypothesis`(정보 종류)와 `E1~E5`(증거 강도)는 독립이다.
- 회사 인용 상한(finding #5)은 **항목 단위**: status != operating이면 company 인용 항목은 E2/Med 상한; hypothesis 항목 인용은 status 무관 Med 상한. 외부·코드·테스트 증거는 원등급 유지.
- 공식 파일은 `commit_company_context.py`(원자 교체)로만 갱신(직접 Edit/Write 차단). 내부 정합은 `lint_company_context.py`가 강제.
-`demo/populated`는 읽기 호환(deprecation warning) + `lint_company_context.py --migrate`로 1회 변환.
```
- [ ] **Step 3: `CLAUDE.md` 갱신** — 실행 흐름 절에 venture-bootstrap 추가 + finding #5 어휘 갱신
CLAUDE.md의 "Cascade 커맨드" 절 위에 삽입:
```markdown
### Venture Bootstrap(회사 수립 — 제품 cascade의 선행, 1회성)
```
/ceo-intake --plan venture-bootstrap → /venture-validate → /company-bootstrap
```
- 순환 해소: founder-context(사람 입력) → 기회탐색·9-gate 검증 → C-Level 수렴 + 사람 승인 → company-context.yaml(provisional) 원자 commit. 이후 제품 cascade가 이를 입력으로 소비.
- 공식 company-context status: template|provisional|operating(3-상태). 모든 전이는 OPS-ORCH 집행.
```
그리고 "회사 문맥 상한(#5)" 규칙 문장의 `status: populated``status: operating`(구 populated 읽기호환)으로, 항목별 provenance 정책을 반영.
- [ ] **Step 4: lint_refs + 커밋**
Run: `python3 .claude/hooks/lint_refs.py`
Expected: PASS.
```bash
git add README.md org-os/01-company/README.md CLAUDE.md
git commit -m "P1(E): 문서 — venture-bootstrap 진입 경로 + company-context 상태/근거 정책 + finding#5 어휘"
```
---
## Task 20: doctor 배선 + 전체 회귀 green
**Files:**
- Modify: `.claude/hooks/doctor.py`
**Interfaces:** doctor가 신규 아티팩트(founder-context·venture 계약·commit hook)·venture-bootstrap plan·venture-validation-map role-id 등록을 점검.
- [ ] **Step 1: doctor 점검 추가** — 기존 `_check_*` 패턴에 맞춰
```python
def _check_venture_bootstrap_wiring():
"""P1: 신규 SoT/hook 실존 + venture-bootstrap plan + validation-map role-id 등록."""
import yaml
missing = []
for p in ("org-os/01-company/founder-context.yaml",
"org-os/06-agent-work/venture-option-spec.yaml",
"org-os/06-agent-work/venture-validation-map.yaml",
".claude/hooks/lint_company_context.py",
".claude/hooks/commit_company_context.py"):
if not os.path.exists(os.path.join(ROOT, p)):
missing.append(p)
if missing:
return (False, "P1 신규 파일 누락: " + ", ".join(missing))
plans = yaml.safe_load(open(os.path.join(ROOT, "org-os/06-agent-work/execution-plans.yaml")))["execution-plans"]["plans"]
if "venture-bootstrap" not in plans:
return (False, "execution-plans 에 venture-bootstrap plan 없음")
# validation-map role-id 등록 대조
fams = yaml.safe_load(open(os.path.join(ROOT, "org-os/00-role-registry/capability-families.yaml")))["capability-families"]["families"]
reg = {r for fam in fams for r in (fam.get("member-role-ids") or [])} | {fam.get("lead-role-id") for fam in fams}
reg = {str(x).upper() for x in reg if x}
m = yaml.safe_load(open(os.path.join(ROOT, "org-os/06-agent-work/venture-validation-map.yaml")))["venture-validation-map"]
used = {x for g in m["gates"] for x in (g["primary"] + g["auditor"])} | set(m["opportunity-discovery-roles"]["diverge"] + m["opportunity-discovery-roles"]["contrarian"]) | {m["synthesis-owner"]}
unreg = sorted({u for u in used if str(u).upper() not in reg and not str(u).upper().startswith("HUMAN")})
if unreg:
return (False, "venture-validation-map 미등록 role-id: " + ", ".join(unreg))
return (True, "venture-bootstrap 배선 OK")
```
그리고 doctor의 점검 실행 목록에 `_check_venture_bootstrap_wiring` 등록.
- [ ] **Step 2: doctor 통과 확인**
Run: `python3 .claude/hooks/doctor.py`
Expected: PASS(새 점검 포함). `미등록 role-id`가 나오면 Task 2의 대조 명령으로 role-id 교정.
- [ ] **Step 3: 전체 회귀**
Run: `CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py`
Expected: **전부 green**(doctor + lint_refs + 모든 test_*.py, 신규 test_company_context_lint·test_venture_bootstrap 포함).
- [ ] **Step 4: 커밋**
```bash
git add .claude/hooks/doctor.py
git commit -m "P1(E): doctor — venture-bootstrap 배선·validation-map role-id 등록 점검 + 전체 green"
```
---
## Self-Review (스펙 대비)
**Spec coverage:**
- §5 founder-context → Task 1. §6 벤처계약 2파일 → Task 2. §7 company-context v2/3-상태 → Task 3. §8 plan/전이/OPS-ORCH단독/human-gate → Task 9,11. §9.1 validate_report 항목상한/operating → Task 13. §9.1 hypothesis anchor → Task 14. §9.2 lint Hard Fail/Warning → Task 4~6. §9.3 atomic commit → Task 16. §9.4 acceptance report-sha256 → Task 15. §9.5 guard → Task 17. §10 커맨드/converge → Task 18. §11 seam(company-context-ready) → Task 12. §12 migration → Task 7. §14 테스트 14종 → 각 Task의 TDD(2 human-boolean=Task11, 3/4 hash=Task11+15, 5/6 atomic=Task16, 7/8 cascade entry=Task12, 9 external-E4=Task13, 10 hypothesis=Task14, 11 demo/populated=Task7/13, 1 worker-transition=allowed-by 구조 Task9). §16 → 전체.
- 갭 점검: 테스트 #12(재실행 idempotent fan-out)·#13(candidate commit idempotent)·#14(decision 변경 시 이전 candidate 거부)는 커맨드/오케스트레이션 레벨 — Task 16의 commit은 hash 바인딩으로 #14를 커버(decision 바뀌면 receipt hash 불일치 → human 게이트 실패). #12/#13은 커맨드 실행 시 guard가 이미 완료된 stage 재전이를 거부하는 것으로 근사(별도 강제 테스트는 후속). → 스펙 §14에 "커버리지 근사" 주석과 일치.
**Placeholder scan:** "확인" 지시(grep으로 기존 등록 지점 찾기)는 실제 파일 구조 의존 지점으로, 정확한 삽입 위치를 찾는 실행 가능한 명령을 제시했다(placeholder 아님). role-id 교정은 실행 가능한 대조 명령 제공.
**Type consistency:** predicate 키(하이픈, 예 `founder-context-present`)와 facts 키(언더스코어, 예 `founder_context_present`)의 매핑은 `_eval_condition`/`_normkey` 규약과 일치. `lint_file(path, is_candidate)`·`build_event(..., report_sha256)`·`_venture_decision_receipt_ok(wf)`·`record_artifact(wf, design_type=, path=)` 시그니처는 Task 간 일관.
**실행 순서 주의:** Task 15(acceptance report-sha256) → Task 11(hash predicate) 순서 권장(상호참조). Phase 내 나머지는 순차.