1672 lines
87 KiB
Markdown
1672 lines
87 KiB
Markdown
# 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 내 나머지는 순차.
|