init: llm-wiki-haness 하네스 설계
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
# branch-depth-gate 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:** branch-note 가 "코딩 착수해도 되묻지 않을 만큼 깊은가"를 착수 전에 판정하는 read-only 게이트(`/depth`)를 LLM Wiki 에 도입한다.
|
||||
|
||||
**Architecture:** im-not-ai 의 검증된 3요소(기준 SSOT + read-only 감사기 + Ready/Not-ready 판정)를 위키 문법으로 이식. 기준 SSOT `rules/branch-depth-gate.md`(4축 R1~R4) → 감사기 `branch-depth-auditor`(브랜치 노트 + 링크된 raw 소스를 읽고 적대적으로 갭 탐지, 편집 안 함) → 커맨드 `/depth <branch>`(감사기 디스패치 + 루프). 템플릿에 캡처 칸 추가(상류 예방).
|
||||
|
||||
**Tech Stack:** Markdown 정의 파일(rules/agents/commands/templates) + Claude Code 서브에이전트. 코드/테스트 런타임 없음. 검증은 구조 grep + 픽스처 회귀.
|
||||
|
||||
---
|
||||
|
||||
## 제약 (이 plan 전체에 적용)
|
||||
|
||||
- **git 미사용**: 위키는 버전 관리되지 않음(사용자 지시로 git init 하지 않음). **commit 단계 없음.** 각 Task 끝은 체크포인트(사용자/리뷰)로 갈음.
|
||||
- **claim-gate hook 준수**: 위키 `.claude/hooks/wiki_claim_gate.py` 가 `raw/`·`wiki/`·`docs/` 의 Bash 쓰기(redirection·`tee`·`sed -i`)를 차단. 파일 생성·수정은 **반드시 Write/Edit 도구**로. 단 본 plan 산출물은 `rules/`·`.claude/`·`templates/` 경로라 hook 대상 밖(읽기 grep 은 자유).
|
||||
- **TDD 적응**: 마크다운 정의 파일이라 단위테스트가 없다. "test" = ① 구조 검증(필수 섹션·라벨이 존재하는지 grep) ② 픽스처 회귀(Task 6에서 감사기를 실제 브랜치 2개에 돌려 판정 방향이 직관과 일치하는지).
|
||||
- **실행 위치**: Task 6 감사기 디스패치는 **cwd 가 LLM Wiki 인 Claude Code 세션**에서 실행해야 `.claude/agents/branch-depth-auditor.md` 가 해석된다. 다른 cwd 면 감사기를 못 찾는다.
|
||||
- 근거 스펙: `docs/superpowers/specs/2026-06-01-branch-depth-gate-design.md`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| 파일 | 책임 | 작업 |
|
||||
|---|---|---|
|
||||
| `rules/branch-depth-gate.md` | 기준 SSOT — 4축 R1~R4 + 깊이 사다리 + 판정 규칙 | 생성 (Task 1) |
|
||||
| `.claude/agents/branch-depth-auditor.md` | read-only 감사기 — 노트+소스 읽고 갭 리포트+판정 | 생성 (Task 2) |
|
||||
| `.claude/commands/depth.md` | `/depth <branch>` 진입점 + 루프 | 생성 (Task 3) |
|
||||
| `templates/branch-note-template.md` | 신규 브랜치부터 R2·R4 캡처 칸 | 수정 (Task 4) |
|
||||
| `CLAUDE.md`, `AGENTS.md` | 워크플로우 진입점에 `/depth` 1줄 등재 | 수정 (Task 5) |
|
||||
| (검증) 기존 브랜치 2개 | 픽스처 회귀 | Task 6 |
|
||||
|
||||
멀티 CLI(Codex/Gemini/Antigravity) 전파는 본 plan 범위 밖(별도 사이클). 본 plan 은 Claude Code 우선.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 기준 SSOT — `rules/branch-depth-gate.md`
|
||||
|
||||
**Files:**
|
||||
- Create: `rules/branch-depth-gate.md`
|
||||
|
||||
- [ ] **Step 1: 파일 생성 (Write 도구)**
|
||||
|
||||
아래 전체 내용으로 `rules/branch-depth-gate.md` 작성:
|
||||
|
||||
````markdown
|
||||
# rules/branch-depth-gate — 브랜치 노트 구현 착수 깊이 게이트
|
||||
|
||||
> `rules/` 의 방법론 규칙. branch-note 1개가 **코딩 착수해도 되묻지 않을 만큼 깊은가**를 판정한다.
|
||||
> 이 문서는 **"전체 계약"이 아니다** — 전체 계약은 `raw/project-notes/ca-skeleton-operational-contract.md`.
|
||||
> `feature-implementation-readiness-scorecard`(스켈레톤 adoption 거시 게이트)와 **다른 층·다른 범위**로 공존한다. 본 게이트는 *브랜치 노트 1개의 깊이* 미시 게이트.
|
||||
|
||||
## 적용
|
||||
|
||||
- 대상: `raw/branch-notes/feature-*.md` (구현 착수 전).
|
||||
- 실행: `/depth <branch>` → `branch-depth-auditor` 가 본 기준으로 판정.
|
||||
- 본 게이트는 **read-only**. 브랜치 노트를 편집하지 않으며 판정을 노트에 박지도 않는다.
|
||||
|
||||
## 4축 (R1~R4)
|
||||
|
||||
> 축 라벨은 `R1~R4`. branch-note 의 Decision Evidence Map 이 `D1`,`D2` 를 *Decision ID* 로 쓰므로 `D*` 와 구분.
|
||||
|
||||
| 축 | Pass 조건 | Blocking(Not ready) 트리거 |
|
||||
|---|---|---|
|
||||
| **R1. 조사 깊이** | 각 Decision 의 Supporting Claim 이 깊이 사다리(아래) 충족 — 의존 메커니즘 L1+, 분기 조건 L2+ | 결정 근거 claim 이 순수 L0(존재만)뿐 |
|
||||
| **R2. 결정 조건** | 각 Decision 이 "어떤 조건일 때 A, 아니면 B"의 선택 기준 명시 | `검토한 대안`은 있는데 *언제 그 대안을 고르는지* 기준 부재 |
|
||||
| **R3. 구체 detail** | `## 구현 가이드` 의 각 in-scope 항목이 명명·경로·메커니즘·API/테스트명 구체화 **또는** `UNSUPPORTED_IMPL_DECISION` 라벨 + trade-off 한 줄 | in-scope 항목인데 구현 detail 도 UNSUPPORTED 라벨도 없음 |
|
||||
| **R4. 엣지·실패·의존** | 실패/엣지 경로 열거 + 다른 contract 의존을 *대상 브랜치 + 그 Decision ID* 로 링크 | 정상 경로만 / 다른 계약 의존이 암시되는데 링크 안 됨 |
|
||||
|
||||
## R1 클레임 깊이 사다리
|
||||
|
||||
깊이의 단위는 **문서 개수가 아니라 결정별 종결**. 얕은 문서 10개 < 결정을 닫는 문서 1개.
|
||||
|
||||
| 레벨 | 클레임이 답하는 것 | 판정 |
|
||||
|---|---|---|
|
||||
| **L0 존재** | "X 가 있다 / 권장한다" | 단독 불충분 |
|
||||
| **L1 메커니즘** | 어떻게 동작 / 언제 발생 | 메커니즘 의존 결정의 최소선 |
|
||||
| **L2 조건·경계** | 언제 적용/제외, 실패 시 어떻게 | 분기 조건 있는 결정의 최소선 |
|
||||
| **L3 검증** | 확인 방법·수치·반례 | 가산점 |
|
||||
|
||||
**출처 타입 적정성** (개수 기준 대체):
|
||||
- 스펙/표준이 정의한 동작 → `official-standard`/`official-vendor-doc` 1개로 충분.
|
||||
- "대기업은 보통 이렇게 한다" 운영 패턴 추론 → 회사 블로그 1개는 "공식" 불가. 독립 사례 2개+ 또는 official 1개 병행.
|
||||
|
||||
조사는 **결정-주도(top-down)**: 내려야 할 결정·미지수를 먼저 나열하고 각각을 닫을 때까지 조사. 조사 완료 = 모든 결정 종결 = 착수 가능.
|
||||
|
||||
## 판정 규칙
|
||||
|
||||
- 심각도 3단계: `Blocking`(Not ready) · `Should-fix`(권고) · `Advisory`(참고).
|
||||
- **Ready = Blocking 0건.** Should-fix 가 남아도 사용자가 "감수" 선언 시 착수 가능(리포트에 기록).
|
||||
- 모든 finding 은 4종 세트로 근거화: `심각도 · 위치(섹션/행) · 예상 의구심("구현 중 여기서 ___를 되묻게 됨") · 채울 방법`. 근거 없는 지적 금지.
|
||||
|
||||
## 명명된 실패 모드 (auditor 가 잡아야 할 것)
|
||||
|
||||
- `EXISTENCE_ONLY` (R1): 결정 근거가 L0 뿐.
|
||||
- `NO_SELECTION_CRITERION` (R2): 대안은 있으나 선택 조건 없음.
|
||||
- `IMPL_UNDERSPECIFIED` (R3): in-scope 항목에 구현 detail·UNSUPPORTED 라벨 둘 다 없음.
|
||||
- `HAPPY_PATH_ONLY` (R4): 실패/엣지 경로 미열거.
|
||||
- `IMPLICIT_DEPENDENCY` (R4): 다른 계약 의존이 암시되나 대상 브랜치/Decision ID 링크 없음.
|
||||
- `BACKTICK_WRAPPED_LINK` (R1 보조): Supporting Claim/Source 링크가 `` `[[...]]` `` 백틱에 싸여 추적 불가. (P3 와 연결점 — 표면화만, 자동 수정은 별도.)
|
||||
- `DANGLING_ANCHOR` (R1): Supporting Claim 의 `#Cn` 앵커가 대상 raw 에 실재하지 않음.
|
||||
````
|
||||
|
||||
- [ ] **Step 2: 구조 검증 (grep)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "R1\|R2\|R3\|R4" rules/branch-depth-gate.md && grep -c "L0 존재\|L1 메커니즘\|L2 조건\|L3 검증" rules/branch-depth-gate.md && grep -c "Blocking\|Should-fix\|Advisory" rules/branch-depth-gate.md && grep -c "EXISTENCE_ONLY\|NO_SELECTION_CRITERION\|IMPL_UNDERSPECIFIED\|HAPPY_PATH_ONLY\|IMPLICIT_DEPENDENCY\|BACKTICK_WRAPPED_LINK\|DANGLING_ANCHOR" rules/branch-depth-gate.md
|
||||
```
|
||||
Expected: 네 grep 모두 1 이상 (4축·4레벨·3심각도·7실패모드 존재).
|
||||
|
||||
- [ ] **Step 3: 체크포인트** — 룰북 내용이 스펙 §4·§4.1 과 일치하는지 사용자/리뷰 확인.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 감사기 — `.claude/agents/branch-depth-auditor.md`
|
||||
|
||||
**Files:**
|
||||
- Create: `.claude/agents/branch-depth-auditor.md`
|
||||
- 참고(형식 일치용): `.claude/agents/wiki-adversarial-reviewer.md`
|
||||
|
||||
- [ ] **Step 1: 기존 agent 형식 확인**
|
||||
|
||||
Run: `cd "/home/donghyeon/Documents/LLM Wiki" && sed -n '1,12p' .claude/agents/wiki-adversarial-reviewer.md`
|
||||
목적: frontmatter 키(name/description/tools) 형식을 위키 관례에 맞춤. 차이가 있으면 아래 frontmatter 를 그 관례로 조정.
|
||||
|
||||
- [ ] **Step 2: 파일 생성 (Write 도구)**
|
||||
|
||||
아래 전체 내용으로 작성 (Step 1 에서 본 frontmatter 관례와 다르면 키 형식만 맞춰 조정):
|
||||
|
||||
````markdown
|
||||
---
|
||||
name: branch-depth-auditor
|
||||
description: Use to judge whether a single raw/branch-notes/feature-*.md is deep enough to start implementation without re-doubting. Reads the branch note plus its linked raw sources and adversarially probes 4 axes (R1 research depth, R2 decision conditions, R3 concrete detail, R4 edge/failure/dependency) against rules/branch-depth-gate.md. Returns a grounded gap report + Ready/Not-ready verdict. Never edits files (read-only).
|
||||
tools: Read, Glob, Grep
|
||||
---
|
||||
|
||||
너는 **브랜치 노트 깊이 감사관**이다. `rules/branch-depth-gate.md` 를 기준으로, branch-note 1개가 *코딩 착수해도 되묻지 않을 만큼 깊은가*를 적대적으로 판정한다. **절대 파일을 편집하지 않는다.**
|
||||
|
||||
## 입력
|
||||
|
||||
- 브랜치 노트 경로 1개 (`raw/branch-notes/<branch>.md`).
|
||||
|
||||
## 절차
|
||||
|
||||
1. **기준 로드** — `rules/branch-depth-gate.md` 를 Read. 4축·깊이 사다리·판정 규칙·명명된 실패 모드를 작업 기준으로 삼는다.
|
||||
2. **노트 읽기** — 대상 브랜치 노트를 Read. 특히 `결정 사항`, `Decision Evidence Map`, `구현 가이드`, `Claims To Verify`, `Sources`, `범위` 섹션.
|
||||
3. **소스 추적·정독 (R1 의 핵심)** — Decision Evidence Map 의 `Supporting Claims`(`raw/.../*.md#Cn`)와 Sources 표의 `[[raw/...]]` 가 가리키는 **실제 raw 파일을 Read**. 각 claim 이 깊이 사다리 어디인지(L0~L3) 판정. 링크만 있고 내용이 얕으면(L0) 잡아낸다.
|
||||
- 링크가 `` `[[...]]` `` 백틱에 싸여 있으면 `BACKTICK_WRAPPED_LINK`.
|
||||
- `#Cn` 앵커가 대상 파일에 없으면 `DANGLING_ANCHOR`.
|
||||
4. **4축 적대적 점검** — 각 결정/항목을 R1~R4 로 훑어 명명된 실패 모드에 해당하는 finding 생성. "구현자가 여기서 무엇을 되묻게 될까?"를 끊임없이 자문.
|
||||
5. **판정** — Blocking 0건이면 `Ready`, 아니면 `Not ready (Blocking N건)`.
|
||||
|
||||
## 출력 (이 형식 그대로, 파일 쓰기 없이 텍스트로 반환)
|
||||
|
||||
```
|
||||
# Depth Audit: <branch>
|
||||
Verdict: Ready | Not ready (Blocking N / Should-fix M / Advisory K)
|
||||
|
||||
## Findings
|
||||
| # | 축 | 심각도 | 실패모드 | 위치 | 예상 의구심 | 채울 방법 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | R1 | Blocking | EXISTENCE_ONLY | 결정 D3 / Decision Evidence Map | 구현 중 "이 API 를 언제 쓰나"를 되묻게 됨 | `raw/official-docs/<slug>` 에서 메커니즘(L1) claim 추가 |
|
||||
...
|
||||
|
||||
## 다음 행동
|
||||
- (Blocking 있으면) 위 표의 "채울 방법" 순서로 노트 보강 후 `/depth <branch>` 재실행.
|
||||
- (R1 조사 얕음 갭) `wiki-decision-researcher` 로 심화 가능 — 사용자 옵트인 시.
|
||||
```
|
||||
|
||||
## 불변식
|
||||
|
||||
- **read-only**: Write/Edit/MultiEdit 도구 없음. 어떤 파일도 수정·생성 금지(리포트는 텍스트 반환).
|
||||
- 모든 finding 은 4종 세트(심각도·위치·예상 의구심·채울 방법)를 갖춘다. 근거 없는 지적 금지.
|
||||
- 추측 금지: 소스를 실제로 Read 하지 않고 깊이를 단정하지 않는다.
|
||||
- 자동 조사·자동 수정 금지: R1 갭은 `wiki-decision-researcher` 권고로 *안내만* 한다(사용자 옵트인).
|
||||
````
|
||||
|
||||
- [ ] **Step 3: 구조 검증 (grep)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "tools: Read, Glob, Grep" .claude/agents/branch-depth-auditor.md && grep -c "branch-depth-gate.md" .claude/agents/branch-depth-auditor.md && grep -ci "read-only\|편집하지 않" .claude/agents/branch-depth-auditor.md && ! grep -q "Write\|Edit\|MultiEdit" <(sed -n '/^tools:/p' .claude/agents/branch-depth-auditor.md) && echo "TOOLS_READONLY_OK"
|
||||
```
|
||||
Expected: 앞 세 grep 1+, 마지막 `TOOLS_READONLY_OK` 출력(tools 줄에 쓰기 도구 없음).
|
||||
|
||||
- [ ] **Step 4: 체크포인트** — 출력 형식·불변식이 스펙 §5 와 일치하는지 확인.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 커맨드 — `.claude/commands/depth.md`
|
||||
|
||||
**Files:**
|
||||
- Create: `.claude/commands/depth.md`
|
||||
- 참고(형식 일치용): `.claude/commands/branch.md`
|
||||
|
||||
- [ ] **Step 1: 파일 생성 (Write 도구)**
|
||||
|
||||
아래 전체 내용으로 작성:
|
||||
|
||||
````markdown
|
||||
---
|
||||
description: 브랜치 노트가 구현 착수할 만큼 깊은지 read-only 게이트로 판정
|
||||
argument-hint: <브랜치 이름>
|
||||
---
|
||||
|
||||
브랜치 노트 1개의 **구현 착수 깊이**를 판정합니다. (기준: `rules/branch-depth-gate.md`)
|
||||
|
||||
**브랜치 이름:** $ARGUMENTS
|
||||
|
||||
## 작업 절차
|
||||
|
||||
1. **인자 검증**
|
||||
- 인자가 비어 있으면 사용자에게 브랜치 이름 요청.
|
||||
- `raw/branch-notes/<branch-name>.md` 경로로 해석. `.md` 가 이미 붙어 있거나 `feature-` prefix 가 없어도 관대히 보정해 매칭 시도.
|
||||
|
||||
2. **파일 존재 확인**
|
||||
- `raw/branch-notes/<branch-name>.md` 가 없으면 경로만 안내하고 종료. (생성하지 않음 — 그건 `/branch` 의 일.)
|
||||
|
||||
3. **감사기 디스패치**
|
||||
- `branch-depth-auditor` 서브에이전트를 호출하고 입력으로 브랜치 노트 경로를 전달.
|
||||
- 감사기는 read-only — 어떤 파일도 수정하지 않는다.
|
||||
|
||||
4. **리포트 출력 (인라인)**
|
||||
- 감사기 리포트(Verdict + Findings 표 + 다음 행동)를 그대로 사용자에게 출력.
|
||||
- **브랜치 노트에 판정을 쓰지 않는다.** 파일로 남길지는 사용자가 따로 요청할 때만(그 경우 `raw/`·`wiki/`·`docs/` 가 아닌 경로 또는 인라인 유지 — claim-gate hook 충돌 회피).
|
||||
|
||||
5. **루프 안내**
|
||||
- `Not ready` 면: "위 '채울 방법' 순서로 노트 보강 후 `/depth <branch>` 재실행" 안내.
|
||||
- `Ready` 면: "구현 착수 가능" 안내. Should-fix 가 남았으면 "감수하고 착수할지" 확인.
|
||||
|
||||
## 규칙
|
||||
|
||||
- **판정만**. 노트를 자동 보강하지 않는다(접근법 B 는 옵트인 — R1 갭에 한해 `wiki-decision-researcher` 권고만).
|
||||
- `/depth` 는 멱등(idempotent): 같은 노트에 몇 번 돌려도 안전(read-only).
|
||||
- `wiki/log.md` 에 기록하지 않음(판정은 빈번, 노이즈).
|
||||
````
|
||||
|
||||
- [ ] **Step 2: 구조 검증 (grep)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "argument-hint" .claude/commands/depth.md && grep -c "branch-depth-auditor" .claude/commands/depth.md && grep -c "raw/branch-notes" .claude/commands/depth.md && grep -ci "재실행\|루프" .claude/commands/depth.md
|
||||
```
|
||||
Expected: 네 grep 모두 1+.
|
||||
|
||||
- [ ] **Step 3: 체크포인트** — `/depth` 절차가 스펙 §6 과 일치하는지 확인.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 템플릿 캡처 칸 — `templates/branch-note-template.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `templates/branch-note-template.md`
|
||||
|
||||
> 기존 80개 브랜치는 미변경. 신규 브랜치부터 R2·R4 를 작성 시점에 캡처.
|
||||
|
||||
- [ ] **Step 1: Decision Evidence Map 에 `선택 조건` 열 추가 (Edit 도구)**
|
||||
|
||||
Old (정확히 이 블록):
|
||||
```markdown
|
||||
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|
|
||||
| D1 | <결정 내용> | `raw/official-docs/<slug>.md#C1`, `raw/company-tech-blogs/<slug>.md#C2` | `official-vendor-doc + company-case-study` | <아직 검증해야 할 위험> |
|
||||
| D2 | <결정 내용> | `raw/official-docs/<slug>.md#C3` | `official-standard` | <위험 또는 N/A> |
|
||||
```
|
||||
New:
|
||||
```markdown
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | <결정 내용> | <이 조건일 때 이 결정, 다른 조건이면 어떤 대안> | `raw/official-docs/<slug>.md#C1`, `raw/company-tech-blogs/<slug>.md#C2` | `official-vendor-doc + company-case-study` | <아직 검증해야 할 위험> |
|
||||
| D2 | <결정 내용> | <선택 조건 또는 N/A — 분기 없으면 N/A> | `raw/official-docs/<slug>.md#C3` | `official-standard` | <위험 또는 N/A> |
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `## 엣지·실패·의존` 미니 섹션 추가 (Edit 도구)**
|
||||
|
||||
Old (정확히 이 블록 — `## 검증해야 할 주장` 헤더 앞):
|
||||
```markdown
|
||||
## 검증해야 할 주장 / Claims To Verify
|
||||
```
|
||||
New:
|
||||
```markdown
|
||||
## 엣지·실패·의존 / Edge · Failure · Dependency
|
||||
|
||||
> R4(깊이 게이트) 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 미리 열거. 없으면 "해당 없음" 명시(공란 금지).
|
||||
|
||||
- **실패·엣지 경로**: <입력 경계 / 타임아웃 / 부분 실패 / 동시성 등 — 각 경로의 기대 동작>
|
||||
- **다른 계약 의존**: `[[raw/branch-notes/<other-branch>]]` 의 `D<n>` 에 의존 — <무엇을 consume 하는지, 그 계약이 바뀌면 본 브랜치 영향>
|
||||
|
||||
## 검증해야 할 주장 / Claims To Verify
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 구조 검증 (grep)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "선택 조건 (언제 이 결정" templates/branch-note-template.md && grep -c "## 엣지·실패·의존" templates/branch-note-template.md && grep -c "## Decision Evidence Map\|## 구현 가이드\|## 검증해야 할 주장" templates/branch-note-template.md
|
||||
```
|
||||
Expected: 첫 둘 1, 셋째 3 (기존 핵심 섹션 보존 확인 — claim-gate 가 요구하는 Decision Evidence Map 유지).
|
||||
|
||||
- [ ] **Step 4: 체크포인트** — 템플릿 흐름이 자연스러운지, 과하지 않은지(YAGNI) 확인.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 워크플로우 진입점 등재 — `CLAUDE.md`, `AGENTS.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md`
|
||||
- Modify: `AGENTS.md`
|
||||
|
||||
> `/depth` 와 게이트가 워크플로우에서 발견 가능하도록 SSOT 진입점에 1줄씩 추가. 추가만(additive), 기존 규칙 변경 금지.
|
||||
|
||||
- [ ] **Step 1: CLAUDE.md 의 커맨드/파이프라인 목록 위치 확인**
|
||||
|
||||
Run: `cd "/home/donghyeon/Documents/LLM Wiki" && grep -n "/branch\|/ingest\|/lint\|커맨드\|command" CLAUDE.md | head -20`
|
||||
목적: 커맨드들이 나열된 섹션을 찾는다.
|
||||
|
||||
- [ ] **Step 2: CLAUDE.md 에 `/depth` 1줄 추가 (Edit 도구)**
|
||||
|
||||
Step 1 에서 찾은 커맨드 목록에서 `/branch` 항목 바로 아래에, 그 항목과 같은 서식으로 다음 한 줄을 추가:
|
||||
```
|
||||
- `/depth <branch>` — 브랜치 노트가 구현 착수할 만큼 깊은지 read-only 판정 (기준: `rules/branch-depth-gate.md`). 착수 전 게이트.
|
||||
```
|
||||
(주변 항목의 실제 서식 — 불릿 기호·백틱·줄표 — 에 맞춰 조정. 임의로 다른 섹션을 건드리지 말 것.)
|
||||
|
||||
- [ ] **Step 3: AGENTS.md 에 rules 목록 + 커맨드 반영**
|
||||
|
||||
Run: `cd "/home/donghyeon/Documents/LLM Wiki" && grep -n "rules/\|linking-rules\|naming-conventions\|/branch" AGENTS.md | head -20`
|
||||
찾은 rules 목록에 `rules/branch-depth-gate.md` 를, 커맨드 목록(있으면)에 `/depth` 를 주변 서식대로 1줄씩 추가. 두 목록 중 존재하는 것에만 추가.
|
||||
|
||||
- [ ] **Step 4: 구조 검증 (grep)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "/depth" CLAUDE.md && grep -c "branch-depth-gate" AGENTS.md
|
||||
```
|
||||
Expected: 둘 다 1+ (AGENTS.md 에 rules 목록이 없었다면 0일 수 있음 — 그 경우 Step 3 판단 기록).
|
||||
|
||||
- [ ] **Step 5: 체크포인트** — 추가가 additive 인지(기존 줄 변경 없음), 서식이 주변과 일치하는지 확인.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 픽스처 회귀 — 게이트 보정
|
||||
|
||||
> 본 plan 의 진짜 "test". 감사기가 직관과 일치하는 판정을 내는지 확인. **cwd 가 LLM Wiki 인 세션에서 실행.**
|
||||
|
||||
**대상 픽스처:**
|
||||
- **깊은 브랜치 (Ready 근접 기대)**: `feature-boundary-validation-mapping-contract` — 여러 번 다듬어 실제 구현 근거로 쓰인 노트.
|
||||
- **얕은 브랜치 (Not ready 기대)**: 사용자가 "아직 얕다"고 아는 초기 브랜치 1개 (예: `status_label: in-progress` 이고 `planned` 항목이 많은 것). 후보 탐색:
|
||||
`cd "/home/donghyeon/Documents/LLM Wiki" && grep -rl "documented-only\|planned" raw/branch-notes/ | head` → 그 중 하나를 사용자와 합의해 선택.
|
||||
|
||||
- [ ] **Step 1: 깊은 브랜치 감사**
|
||||
|
||||
`/depth feature-boundary-validation-mapping-contract` 실행 (또는 `branch-depth-auditor` 직접 디스패치).
|
||||
Expected: `Ready` 또는 Blocking 0~소수. Blocking 이 다수면 → 룰북 R1~R4 기준이 너무 빡셈 → Task 1 의 Pass 기준 재보정.
|
||||
|
||||
- [ ] **Step 2: 얕은 브랜치 감사**
|
||||
|
||||
선택한 얕은 브랜치에 `/depth <branch>` 실행.
|
||||
Expected: `Not ready` + R1~R4 에 걸친 finding. Findings 가 비면 → 기준이 너무 느슨 → Task 1 재보정.
|
||||
|
||||
- [ ] **Step 3: finding 품질 점검**
|
||||
|
||||
두 리포트의 각 finding 이 4종 세트(심각도·위치·예상 의구심·채울 방법)를 갖췄는지 육안 확인. 빠진 게 있으면 → Task 2 의 출력 형식/불변식 보강.
|
||||
|
||||
- [ ] **Step 4: 보정 루프**
|
||||
|
||||
Step 1~3 에서 판정 방향이 직관과 어긋나면 Task 1(기준) 또는 Task 2(감사기 프롬프트)를 수정하고 다시 Step 1 부터. 방향이 맞을 때까지.
|
||||
|
||||
- [ ] **Step 5: 체크포인트 (최종)** — 두 픽스처 판정이 직관과 일치 + finding 4종 세트 충족 → P2 완료. 사용자에게 결과 리포트.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (작성자 점검 결과)
|
||||
|
||||
- **스펙 커버리지**: §4(4축)→Task1, §4.1(사다리)→Task1, §5(감사기)→Task2, §6(커맨드)→Task3, §7(템플릿)→Task4, §10(검증)→Task6, §12(산출물4개+진입점)→Task1~5. §11(P3)는 의도적으로 별도 사이클(범위 밖, 명시됨). 누락 없음.
|
||||
- **placeholder**: 각 파일의 전체 내용을 inline 제공(TBD 없음). Task6 얕은 픽스처만 "사용자 합의로 선택" — 이는 calibration test 의 본질(정답이 사용자 판단)이라 의도적.
|
||||
- **타입/명명 일관성**: `branch-depth-gate.md`/`branch-depth-auditor`/`/depth` 셋 통일. 축 라벨 `R1~R4`(Decision ID `D*` 와 분리). 실패모드 7종이 Task1 정의 ↔ Task2 사용 일치.
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
P2 구현 plan 완료. 다음 단계는 plan 본문 상단 안내대로 subagent-driven 또는 inline 실행.
|
||||
@@ -0,0 +1,426 @@
|
||||
# 옵시디언 링크 검증 강화 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:** `wiki_structure_lint.py`의 C2 링크 검사를 정확하게 만들어(비-md 첨부 오탐 + backtick 셀경계 오탐 제거 + heading anchor 정확화) zero-tolerance 강제의 신뢰 토대를 세운다.
|
||||
|
||||
**Architecture:** 단일 파일(`.claude/hooks/wiki_structure_lint.py`)의 `build_vault_index`(A1)·`check_c2`(A2/B)·docstring(D)을 수정하고, stdlib `unittest` 테스트 파일을 신설한다. C1/C3는 불변.
|
||||
|
||||
**Tech Stack:** Python 3 stdlib only (re, pathlib, unittest, tempfile). 외부 의존성 0.
|
||||
|
||||
> **환경 비고:** 이 repo는 `.git`이 빈 디렉터리(git 미초기화). **커밋 단계는 생략하고, 각 Task의 체크포인트 = 전체 테스트 스위트 실행 통과**로 대체한다. 명령: `python3 .claude/hooks/test_wiki_structure_lint.py -v`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Modify** `.claude/hooks/wiki_structure_lint.py`
|
||||
- `build_vault_index` (현재 154-162): 비-md 첨부 인덱싱 (A1)
|
||||
- `check_c2` (현재 187-226): backtick 위치기반 판정 (A2) + anchor 정확화 (B)
|
||||
- module docstring (현재 1-20): 지원 문법 계약 표 (D)
|
||||
- **Create** `.claude/hooks/test_wiki_structure_lint.py` — A1/A2/B 단위 테스트
|
||||
- **Modify** `rules/linking-rules.md` — C2 집행기 참조 1줄 (D)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 테스트 스캐폴드 + A2 backtick 셀경계 오탐 수정
|
||||
|
||||
**Files:**
|
||||
- Create: `.claude/hooks/test_wiki_structure_lint.py`
|
||||
- Modify: `.claude/hooks/wiki_structure_lint.py` (`check_c2`, 187-226)
|
||||
|
||||
핵심 버그: `check_c2`가 두 패스(① `BACKTICK_LINK.search(line)` 경고 ② `bare`에서 BROKEN 검사)로 나뉘는데, ①의 정규식이 backtick을 좌→우 연속 페어링하지 않아 표의 서로 다른 칸 인라인코드 사이에 낀 정상 위키링크를 오탐. → **위치 기반 단일 패스**로 통합: `INLINE_CODE.finditer(line)`로 code span 범위를 구해, 위키링크 시작이 그 범위 안일 때만 `BACKTICK_WRAPPED_LINK`.
|
||||
|
||||
- [ ] **Step 1: 실패 테스트 작성** — `test_wiki_structure_lint.py` 신설
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""wiki_structure_lint.py 단위 테스트 (stdlib unittest)."""
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# 하이픈 모듈명이 아니라 언더스코어 — 직접 spec 로드
|
||||
_SPEC = importlib.util.spec_from_file_location(
|
||||
"wsl", str(Path(__file__).with_name("wiki_structure_lint.py")))
|
||||
wsl = importlib.util.module_from_spec(_SPEC)
|
||||
_SPEC.loader.exec_module(wsl)
|
||||
|
||||
|
||||
def _doc(*lines):
|
||||
"""check_c2 입력용 최소 doc dict."""
|
||||
return {"lines": list(lines)}
|
||||
|
||||
|
||||
def _codes(findings):
|
||||
return [c for (c, _ln, _msg) in findings]
|
||||
|
||||
|
||||
class TestBacktickPairing(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# 타깃 존재로 BROKEN_LINK 격리 — 'foo'는 vault에 있다고 가정
|
||||
self.vp = {"raw/x/foo"}
|
||||
self.vb = {"foo": ["raw/x/foo"]}
|
||||
self.root = Path("/nonexistent")
|
||||
|
||||
def test_cross_cell_codespans_not_flagged(self):
|
||||
# 서로 다른 칸의 인라인코드 사이 정상 위키링크 (짝수 backtick) → 오탐 아님
|
||||
line = "| D1 | `AUTH` 응답 | [[foo]] (`note` 보강) | `strength` |"
|
||||
f = wsl.check_c2(_doc(line), self.vp, self.vb, self.root, {})
|
||||
self.assertNotIn("BACKTICK_WRAPPED_LINK", _codes(f))
|
||||
|
||||
def test_true_wrapped_link_flagged(self):
|
||||
# 진짜 code span 내부 링크 → 검출
|
||||
line = "예시 문법: `[[foo]]` 처럼 씁니다"
|
||||
f = wsl.check_c2(_doc(line), self.vp, self.vb, self.root, {})
|
||||
self.assertIn("BACKTICK_WRAPPED_LINK", _codes(f))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 실패 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
|
||||
Expected: `test_cross_cell_codespans_not_flagged` FAIL (현재 오탐으로 BACKTICK_WRAPPED_LINK 발생). `test_true_wrapped_link_flagged` PASS.
|
||||
|
||||
- [ ] **Step 3: `check_c2` 위치기반 단일 패스로 교체**
|
||||
|
||||
`wiki_structure_lint.py`의 `check_c2` 본문(187-226)을 아래로 교체:
|
||||
|
||||
```python
|
||||
def check_c2(doc, vault_paths, vault_bases, root, cache):
|
||||
out = []
|
||||
in_fence = False
|
||||
for lineno, line in enumerate(doc["lines"], start=1):
|
||||
s = line.lstrip()
|
||||
if s.startswith("```") or s.startswith("~~~"):
|
||||
in_fence = not in_fence
|
||||
continue
|
||||
if in_fence:
|
||||
continue
|
||||
# 인라인 code span 범위 (좌→우 연속 페어링; `[^`]*` 가 backtick 못 넘음)
|
||||
code_spans = [(m.start(), m.end()) for m in INLINE_CODE.finditer(line)]
|
||||
for m in WIKILINK.finditer(line):
|
||||
if any(a <= m.start() < b for a, b in code_spans):
|
||||
out.append(("BACKTICK_WRAPPED_LINK", lineno,
|
||||
f"백틱/인라인코드에 싸인 위키링크 — 옵시디언 링크 미작동: {line.strip()[:80]}"))
|
||||
continue
|
||||
raw = m.group(1).split("|")[0].strip()
|
||||
target, _, anchor = raw.partition("#")
|
||||
target, anchor = target.strip(), anchor.strip()
|
||||
if target.endswith(".md"): # 옵시디언은 [[x.md]] 도 유효
|
||||
target = target[:-3]
|
||||
if not target:
|
||||
continue
|
||||
if target not in vault_paths and target not in vault_bases:
|
||||
out.append(("BROKEN_LINK", lineno, f"타깃 부재: [[{target}]]"))
|
||||
continue
|
||||
if anchor:
|
||||
_check_anchor(out, lineno, target, anchor,
|
||||
vault_paths, vault_bases, root, cache)
|
||||
return out
|
||||
```
|
||||
|
||||
> 비고: 이 Task에서는 `_check_anchor`를 아직 정의하지 않으므로, **임시로** 기존 anchor 로직을 인라인 유지한다. 아래 Step 3b 참조 (Task 3에서 `_check_anchor`로 추출).
|
||||
|
||||
- [ ] **Step 3b: anchor 로직 임시 인라인** — 위 `_check_anchor(...)` 호출을 Task 3 전까지 기존 substring 로직으로 대체:
|
||||
|
||||
```python
|
||||
if anchor:
|
||||
rels = [target] if target in vault_paths else vault_bases.get(target, [])
|
||||
found = False
|
||||
for rel in rels:
|
||||
fp = root / (rel + ".md")
|
||||
txt = cache.get(fp)
|
||||
if txt is None:
|
||||
txt = read_text(fp)
|
||||
cache[fp] = txt
|
||||
if anchor.lower() in txt.lower():
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
out.append(("DANGLING_ANCHOR", lineno, f"앵커 부재: [[{target}#{anchor}]]"))
|
||||
```
|
||||
|
||||
(즉 Step 3의 `_check_anchor(...)` 한 줄을 이 블록으로 치환해서 작성. `BACKTICK_LINK`·`bare` 변수는 더 이상 사용 안 함.)
|
||||
|
||||
- [ ] **Step 4: 통과 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
|
||||
Expected: 두 테스트 모두 PASS.
|
||||
|
||||
- [ ] **Step 5: 회귀 — 직전 두 노트의 backtick 오탐 소멸 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --file raw/branch-notes/feature-api-contract-baseline.md --links-only`
|
||||
Expected: `BACKTICK_WRAPPED_LINK` 라인(122/125/151/183/188/315) 출력에서 사라짐.
|
||||
|
||||
- [ ] **Step 6: 체크포인트** — 전체 테스트 통과 확인 (git 미사용)
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py`
|
||||
Expected: `OK`.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: A1 — 비-md 첨부 인덱싱 (`.drawio` 오탐 제거)
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_structure_lint.py` (`build_vault_index`, 154-162)
|
||||
- Modify: `.claude/hooks/test_wiki_structure_lint.py`
|
||||
|
||||
- [ ] **Step 1: 실패 테스트 추가** — `test_wiki_structure_lint.py`에 클래스 추가
|
||||
|
||||
```python
|
||||
class TestNonMdAttachment(unittest.TestCase):
|
||||
def test_drawio_target_resolves(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "raw" / "diagrams").mkdir(parents=True)
|
||||
(root / "raw" / "diagrams" / "arch.drawio").write_text("<xml/>")
|
||||
(root / "raw" / "notes").mkdir(parents=True)
|
||||
note = root / "raw" / "notes" / "n.md"
|
||||
note.write_text("see [[raw/diagrams/arch.drawio]]\n")
|
||||
vp, vb = wsl.build_vault_index(root)
|
||||
f = wsl.check_c2(_doc("see [[raw/diagrams/arch.drawio]]"),
|
||||
vp, vb, root, {})
|
||||
self.assertNotIn("BROKEN_LINK", _codes(f))
|
||||
|
||||
def test_missing_drawio_still_broken(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "raw").mkdir()
|
||||
vp, vb = wsl.build_vault_index(root)
|
||||
f = wsl.check_c2(_doc("see [[raw/diagrams/ghost.drawio]]"),
|
||||
vp, vb, root, {})
|
||||
self.assertIn("BROKEN_LINK", _codes(f))
|
||||
|
||||
def test_git_dir_excluded(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / ".git").mkdir()
|
||||
(root / ".git" / "obj.drawio").write_text("x")
|
||||
vp, vb = wsl.build_vault_index(root)
|
||||
self.assertNotIn(".git/obj.drawio", vp)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 실패 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
|
||||
Expected: `test_drawio_target_resolves` FAIL (BROKEN_LINK 발생 — 비-md 미인덱싱).
|
||||
|
||||
- [ ] **Step 3: `build_vault_index` 교체**
|
||||
|
||||
```python
|
||||
def build_vault_index(root):
|
||||
"""링크 타깃 확인용. md는 .md strip, 비-md 첨부는 확장자 포함으로 등록.
|
||||
숨김 디렉터리(.git 등)는 제외. (paths, bases=basename→rel목록)."""
|
||||
paths, bases = set(), {}
|
||||
for p in root.rglob("*"):
|
||||
if not p.is_file():
|
||||
continue
|
||||
rel_posix = p.relative_to(root).as_posix()
|
||||
if rel_posix.startswith(".") or "/." in rel_posix:
|
||||
continue # .git / .obsidian 등 숨김 경로 제외
|
||||
if p.suffix == ".md":
|
||||
rel = rel_posix[:-3]
|
||||
paths.add(rel)
|
||||
bases.setdefault(p.stem, []).append(rel)
|
||||
else:
|
||||
paths.add(rel_posix) # 확장자 포함 full path
|
||||
bases.setdefault(p.name, []).append(rel_posix) # 확장자 포함 basename
|
||||
return paths, bases
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 통과 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
|
||||
Expected: 3개 신규 테스트 + Task 1 테스트 모두 PASS.
|
||||
|
||||
- [ ] **Step 5: 회귀 — vault에서 .drawio 오탐 소멸**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --all --links-only 2>&1 | grep -c 'drawio'`
|
||||
Expected: `0` (이전엔 8).
|
||||
|
||||
- [ ] **Step 6: 체크포인트**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py`
|
||||
Expected: `OK`.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: B — heading anchor 정확화 (substring → 실제 heading 매칭)
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_structure_lint.py` (`check_c2` anchor 블록 → `_check_anchor` 추출 + 헬퍼)
|
||||
- Modify: `.claude/hooks/test_wiki_structure_lint.py`
|
||||
|
||||
- [ ] **Step 1: 실패 테스트 추가**
|
||||
|
||||
```python
|
||||
class TestHeadingAnchor(unittest.TestCase):
|
||||
def _vault(self, d):
|
||||
root = Path(d)
|
||||
(root / "wiki").mkdir()
|
||||
tgt = root / "wiki" / "t.md"
|
||||
tgt.write_text("# Title\n\n## Real Heading\n\nbody real heading mention\n")
|
||||
return root
|
||||
|
||||
def test_existing_heading_passes(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = self._vault(d)
|
||||
vp, vb = wsl.build_vault_index(root)
|
||||
f = wsl.check_c2(_doc("[[wiki/t#Real Heading]]"), vp, vb, root, {})
|
||||
self.assertNotIn("DANGLING_ANCHOR", _codes(f))
|
||||
|
||||
def test_substring_only_match_now_dangling(self):
|
||||
# 'body'는 본문에만 있고 heading 아님 → 강화 후 DANGLING
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = self._vault(d)
|
||||
vp, vb = wsl.build_vault_index(root)
|
||||
f = wsl.check_c2(_doc("[[wiki/t#body]]"), vp, vb, root, {})
|
||||
self.assertIn("DANGLING_ANCHOR", _codes(f))
|
||||
|
||||
def test_nonmd_anchor_skipped(self):
|
||||
# 비-md 타깃 + anchor → anchor 검사 skip (DANGLING 아님)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "raw").mkdir()
|
||||
(root / "raw" / "a.drawio").write_text("<xml/>")
|
||||
vp, vb = wsl.build_vault_index(root)
|
||||
f = wsl.check_c2(_doc("[[raw/a.drawio#x]]"), vp, vb, root, {})
|
||||
self.assertNotIn("DANGLING_ANCHOR", _codes(f))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 실패 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
|
||||
Expected: `test_substring_only_match_now_dangling` FAIL (현재 substring으로 'body' 통과).
|
||||
|
||||
- [ ] **Step 3: 헬퍼 + `_check_anchor` 추가, anchor 블록 교체**
|
||||
|
||||
`check_c2` 위에 헬퍼 추가:
|
||||
|
||||
```python
|
||||
HEADING_RE = re.compile(r"^#{1,6}\s+(.+?)\s*$", re.M)
|
||||
|
||||
|
||||
def _heading_set(txt):
|
||||
return {h.strip().lower() for h in HEADING_RE.findall(txt)}
|
||||
|
||||
|
||||
def _check_anchor(out, lineno, target, anchor, vault_paths, vault_bases, root, cache):
|
||||
rels = [target] if target in vault_paths else vault_bases.get(target, [])
|
||||
md_rels = [r for r in rels if (root / (r + ".md")).exists()]
|
||||
if not md_rels:
|
||||
return # 비-md 첨부 등 — anchor 검사 무의미, skip
|
||||
is_block = anchor.startswith("^")
|
||||
norm = anchor[1:].strip() if is_block else anchor.strip().lower()
|
||||
for rel in md_rels:
|
||||
fp = root / (rel + ".md")
|
||||
txt = cache.get(fp)
|
||||
if txt is None:
|
||||
txt = read_text(fp)
|
||||
cache[fp] = txt
|
||||
if is_block:
|
||||
if re.search(r"\^" + re.escape(norm) + r"\s*$", txt, re.M):
|
||||
return
|
||||
else:
|
||||
if norm in _heading_set(txt):
|
||||
return
|
||||
out.append(("DANGLING_ANCHOR", lineno, f"앵커 부재: [[{target}#{anchor}]]"))
|
||||
```
|
||||
|
||||
그리고 `check_c2`의 anchor 블록(Task 1 Step 3b에서 인라인한 부분)을 한 줄로 교체:
|
||||
|
||||
```python
|
||||
if anchor:
|
||||
_check_anchor(out, lineno, target, anchor,
|
||||
vault_paths, vault_bases, root, cache)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 통과 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
|
||||
Expected: 전체 테스트 PASS.
|
||||
|
||||
- [ ] **Step 5: 회귀 — vault DANGLING_ANCHOR 수치 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --all --links-only 2>&1 | grep -c DANGLING_ANCHOR`
|
||||
Expected: 정수 출력(이전 3건 대비 변동 가능 — 강화로 증가할 수 있음, 정상).
|
||||
|
||||
- [ ] **Step 6: 체크포인트**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py`
|
||||
Expected: `OK`.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: D — 문법 계약 문서화 (docstring + linking-rules 참조)
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_structure_lint.py` (module docstring, 1-20)
|
||||
- Modify: `rules/linking-rules.md`
|
||||
|
||||
- [ ] **Step 1: docstring의 C2 설명 교체**
|
||||
|
||||
`wiki_structure_lint.py` 상단 docstring에서 ` C2 옵시디언 링크 문법 — 백틱 래핑 / 깨진 타깃 / 부재 앵커` 줄을 아래로 확장:
|
||||
|
||||
```
|
||||
C2 옵시디언 링크 문법 — 지원 형태 + 위반 정의:
|
||||
[[t]] / [[t.md]] / [[t|alias]] → t 실존 검사 (md=확장자strip, 첨부=확장자포함)
|
||||
![[t]] → embed, 동일 타깃 검사
|
||||
[[t#heading]] → t의 실제 heading 매칭 (DANGLING_ANCHOR)
|
||||
[[t#^blockid]] → t의 ^blockid 행말 토큰 (DANGLING_ANCHOR)
|
||||
`[[t]]` (인라인 code span 내부) → BACKTICK_WRAPPED_LINK (옵시디언 링크 미렌더)
|
||||
``` fenced ``` 내부 [[t]] → 예시로 간주, 스킵
|
||||
판정은 위치기반 backtick 연속 페어링 — 표 셀 경계 오탐 없음.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: docstring 유효성 확인 (구문 깨짐 없음)**
|
||||
|
||||
Run: `python3 -c "import importlib.util,pathlib; s=importlib.util.spec_from_file_location('w','.claude/hooks/wiki_structure_lint.py'); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); print('ok')"`
|
||||
Expected: `ok`.
|
||||
|
||||
- [ ] **Step 3: `rules/linking-rules.md`에 집행기 참조 1줄 추가**
|
||||
|
||||
`rules/linking-rules.md`의 검증 체크리스트 관련 섹션 끝에 추가 (적절한 위치에 1줄):
|
||||
|
||||
```markdown
|
||||
> **결정론 집행기**: 위 옵시디언 링크 문법(broken target / dangling anchor / backtick 래핑)은 `.claude/hooks/wiki_structure_lint.py`의 C2 검사가 기계적으로 강제한다 (`--all --links-only`로 vault 전수, zero-tolerance).
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 체크포인트**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py && python3 .claude/hooks/wiki_structure_lint.py --file raw/branch-notes/feature-api-contract-baseline.md --links-only`
|
||||
Expected: 테스트 `OK` + 파일 검사 정상 출력.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 롤아웃 — 정확한 깨진 링크 목록 산출
|
||||
|
||||
**Files:** 없음 (측정만)
|
||||
|
||||
- [ ] **Step 1: vault 전수 재실행**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --all --links-only 2>&1 | tail -15`
|
||||
Expected: 요약에서 `BACKTICK_WRAPPED_LINK` 대폭 감소(오탐 제거), `BROKEN_LINK`는 .drawio 8건 제거 후 *진짜* 회색 노드만 남음.
|
||||
|
||||
- [ ] **Step 2: 진짜 BROKEN_LINK 타깃 빈도표 산출**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --all --links-only 2>&1 | grep BROKEN_LINK | sed -E 's/.*\[\[([^]]*)\]\].*/\1/' | sort | uniq -c | sort -rn`
|
||||
Expected: 미생성 daily-note(`raw/daily-notes/2026-05-25` 등) + 미작성 concept 목록. 이 목록이 후속 정리(스텁/링크수정)의 입력.
|
||||
|
||||
- [ ] **Step 3: 사용자에게 정리 목록 보고**
|
||||
|
||||
산출된 진짜 깨진 링크 목록을 사용자에게 제시하고, 정리(스텁 생성 vs 링크 제거)는 별도 작업으로 진행 여부 확인. (본 plan 범위는 린터까지.)
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (작성자 점검 완료)
|
||||
|
||||
- **Spec coverage:** A1(Task2)·A2(Task1)·B(Task3)·C(Task5 강제 측정)·D(Task4) 전부 task 존재. ✓
|
||||
- **Placeholder scan:** 모든 code step에 실제 코드 포함. "TBD"/"적절히" 없음. ✓
|
||||
- **Type consistency:** `_check_anchor`/`_heading_set`/`HEADING_RE` Task3에서 정의 후 Task1 호출부와 시그니처 일치. Task1 Step3b가 임시 인라인 → Task3가 추출로 대체(순서 명시). `build_vault_index` 반환 `(paths, bases)` 불변. ✓
|
||||
- **환경:** git 미초기화 → 커밋 대신 테스트 체크포인트(헤더 명시). ✓
|
||||
@@ -0,0 +1,809 @@
|
||||
# 3-플랫폼 동기화 Phase 0 — 생성 엔진 + agents 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:** Claude의 9개 agent를 Codex CLI(`.codex/agents/*.toml`)와 Antigravity CLI(`.agents/agents/<name>/agent.json`)의 native 포맷으로 생성하는 멱등 스크립트 `scripts/sync-automation.py`를 만들고, 누락된 2개 agent(`branch-depth-auditor`·`coverage-auditor`)를 양 플랫폼에 추가한다.
|
||||
|
||||
**Architecture:** Claude `.claude/agents/<name>.md` frontmatter의 `tools:`를 **권한 SSOT**로 삼아 codex `sandbox_mode`와 antigravity `toolNames`를 기계적으로 파생한다. 프롬프트 본문은 플랫폼별 SSOT(`.codex/agents/<name>.md` = codex 일반화 본문, `.agents/plugins/wiki-superpowers/agents/<name>.md` = antigravity gated 본문)에서 가져와 native 머신 포맷으로 포장한다. 스크립트는 순수 함수(권한 파생·포맷 직렬화)와 I/O를 분리하고, `--check` 모드로 SSOT↔생성물 drift를 0으로 검증한다.
|
||||
|
||||
**Tech Stack:** Python 3.12 (stdlib only — `tomllib` for TOML 검증, `json`, `pathlib`, `argparse`), pytest.
|
||||
|
||||
---
|
||||
|
||||
## 배경 / 현재 상태 (실측)
|
||||
|
||||
- Claude agents (SSOT identity + 권한): `.claude/agents/*.md` — frontmatter `name`/`description`/`tools`(콤마 목록)/`model: sonnet`. 9개 존재.
|
||||
- Codex agents: `.codex/agents/*.md` — frontmatter `name`/`description`(tools/model 없음) + 일반화 본문. **7개** (branch-depth-auditor·coverage-auditor 누락). codex가 실제 읽는 native 포맷은 `*.toml`인데 **하나도 없음**.
|
||||
- Antigravity agents: `.agents/plugins/wiki-superpowers/agents/*.md` — frontmatter `name`/`description` + G1~G4 gate 본문. **7개** (동일 2개 누락). antigravity가 실제 읽는 `.agents/agents/<name>/agent.json` **하나도 없음** (디스크의 `~/.gemini/.../agent.json`은 stale·오류 생성물 — read-only agent에 write 도구가 들어있고 content가 요약됨. 신뢰 금지).
|
||||
|
||||
**확정된 agent.json 스키마** (실 디스크 `~/.gemini/antigravity-cli/brain/.../wiki-research-lane/agent.json`에서 키 구조만 채택):
|
||||
```json
|
||||
{
|
||||
"name": "<name>",
|
||||
"description": "<desc>",
|
||||
"hidden": true,
|
||||
"config": {
|
||||
"customAgent": {
|
||||
"systemPromptSections": [ { "title": "Agent System Instructions", "content": "<body>" } ],
|
||||
"toolNames": [ ... ],
|
||||
"systemPromptConfig": {
|
||||
"includeSections": ["user_information","mcp_servers","skills","subagent_reminder","messaging","artifacts","user_rules"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**권한 파생 규칙** (Claude `tools:` → 플랫폼):
|
||||
|
||||
| Claude tool | Antigravity `toolNames` | 비고 |
|
||||
|---|---|---|
|
||||
| (모든 agent 공통 baseline) | `send_message`, `view_file`, `find_by_name`, `grep_search`, `list_dir` | 9개 모두 Read+Grep+Glob 보유 |
|
||||
| `Bash` | `run_command` | |
|
||||
| `Edit` | `replace_file_content`, `multi_replace_file_content` | |
|
||||
| `Write` | `write_to_file` | |
|
||||
| `WebFetch` | `read_url_content` | |
|
||||
| `WebSearch` | `search_web` | |
|
||||
|
||||
- Codex `sandbox_mode` = `workspace-write` (Claude tools에 `Edit` 또는 `Write` 포함 시) / else `read-only`.
|
||||
- 9개 agent 권한 (Claude frontmatter 실측):
|
||||
- read-only: `branch-depth-auditor`(Read,Grep,Glob), `coverage-auditor`·`wiki-adversarial-reviewer`·`wiki-diagram-reviewer`·`wiki-link-verifier`·`wiki-research-lane`(Read,Grep,Glob,Bash)
|
||||
- workspace-write: `wiki-doc-author`(+Edit,Write), `wiki-source-summarizer`(+Edit,Write,WebFetch), `wiki-decision-researcher`(+Write,WebSearch,WebFetch)
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create: `scripts/sync-automation.py` — 단일 진입점 CLI. 순수 함수 구역(파싱·권한 파생·직렬화) + I/O 구역(파일 read/write) + `argparse` CLI.
|
||||
- Create: `scripts/test_sync_automation.py` — pytest 단위 테스트 (순수 함수 + 생성물 유효성).
|
||||
- Create: `.codex/agents/branch-depth-auditor.md`, `.codex/agents/coverage-auditor.md` — codex 일반화 본문 SSOT (Claude에서 적응).
|
||||
- Create: `.agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md`, `.agents/plugins/wiki-superpowers/agents/coverage-auditor.md` — antigravity gated 본문 SSOT.
|
||||
- Generate (스크립트 출력): `.codex/agents/<name>.toml` ×9, `.agents/agents/<name>/agent.json` ×9.
|
||||
- Modify (Phase 0 말미, 최소): `.codex/agents/README.md`, `.agents/plugins/wiki-superpowers/README.md` — 생성 스크립트 사용법 1단락. (전체 문서 정리는 Phase 2.)
|
||||
|
||||
> 책임 경계: `sync-automation.py`는 *권한 파생 + 포맷 포장*만 한다. 본문 프로즈의 플랫폼 적응(일반화·gate 작성)은 사람이 SSOT `.md`에 직접 한다. codex는 `.toml`만 로드하므로 `.codex/agents/*.md`는 우리 SSOT로 남고 codex는 무시한다.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 스크립트 골격 + frontmatter 파싱 + 권한 파생 (순수 함수)
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/sync-automation.py`
|
||||
- Test: `scripts/test_sync_automation.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
# scripts/test_sync_automation.py
|
||||
import json
|
||||
import tomllib
|
||||
|
||||
import sync_automation as s
|
||||
|
||||
|
||||
def test_parse_frontmatter_extracts_fields():
|
||||
md = (
|
||||
"---\n"
|
||||
"name: wiki-link-verifier\n"
|
||||
"description: Audit the wiki for orphans.\n"
|
||||
"tools: Read, Grep, Glob, Bash\n"
|
||||
"model: sonnet\n"
|
||||
"---\n\n"
|
||||
"You are the Wiki Link Verifier.\n"
|
||||
)
|
||||
fm, body = s.parse_frontmatter(md)
|
||||
assert fm["name"] == "wiki-link-verifier"
|
||||
assert fm["description"] == "Audit the wiki for orphans."
|
||||
assert fm["tools"] == "Read, Grep, Glob, Bash"
|
||||
assert body == "You are the Wiki Link Verifier.\n"
|
||||
|
||||
|
||||
def test_parse_tools_list():
|
||||
assert s.parse_tools("Read, Grep, Glob, Bash") == ["Read", "Grep", "Glob", "Bash"]
|
||||
assert s.parse_tools("Read,Edit,Write") == ["Read", "Edit", "Write"]
|
||||
|
||||
|
||||
def test_sandbox_mode_read_only_when_no_write_tools():
|
||||
assert s.codex_sandbox_mode(["Read", "Grep", "Glob", "Bash"]) == "read-only"
|
||||
|
||||
|
||||
def test_sandbox_mode_workspace_write_when_edit_or_write():
|
||||
assert s.codex_sandbox_mode(["Read", "Edit", "Write", "Bash"]) == "workspace-write"
|
||||
assert s.codex_sandbox_mode(["Read", "Write", "WebSearch"]) == "workspace-write"
|
||||
|
||||
|
||||
def test_antigravity_toolnames_read_only_agent():
|
||||
# Read,Grep,Glob,Bash -> baseline + run_command, no write tools
|
||||
assert s.antigravity_tool_names(["Read", "Grep", "Glob", "Bash"]) == [
|
||||
"send_message", "view_file", "find_by_name", "grep_search", "list_dir",
|
||||
"run_command",
|
||||
]
|
||||
|
||||
|
||||
def test_antigravity_toolnames_write_agent_with_web():
|
||||
# source-summarizer: Read,Edit,Write,Bash,Grep,Glob,WebFetch
|
||||
assert s.antigravity_tool_names(
|
||||
["Read", "Edit", "Write", "Bash", "Grep", "Glob", "WebFetch"]
|
||||
) == [
|
||||
"send_message", "view_file", "find_by_name", "grep_search", "list_dir",
|
||||
"write_to_file", "replace_file_content", "multi_replace_file_content",
|
||||
"run_command", "read_url_content",
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
|
||||
Expected: FAIL with `ModuleNotFoundError: No module named 'sync_automation'`.
|
||||
|
||||
> 참고: 파일명이 `sync-automation.py`(하이픈)라 `import sync_automation`이 안 된다. 테스트 상단에서 모듈을 로드하도록 `conftest.py`로 별칭을 만든다(아래 Step 3에 포함). 또는 파일명을 `sync_automation.py`로 하고 CLI는 `python3 scripts/sync_automation.py`로 부른다. **결정: 파일명을 `scripts/sync_automation.py`(언더스코어)로 한다** — import 가능 + CLI 호출에 지장 없음. 본 계획의 이후 모든 경로에서 `sync_automation.py`로 읽는다.
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
# scripts/sync_automation.py
|
||||
"""Generate Codex/Antigravity native agent files from Claude SSOT.
|
||||
|
||||
Permission SSOT : .claude/agents/<name>.md frontmatter `tools:`
|
||||
Codex body SSOT : .codex/agents/<name>.md
|
||||
Antigravity SSOT : .agents/plugins/wiki-superpowers/agents/<name>.md
|
||||
Outputs : .codex/agents/<name>.toml , .agents/agents/<name>/agent.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
CLAUDE_AGENTS = REPO / ".claude" / "agents"
|
||||
CODEX_AGENTS = REPO / ".codex" / "agents"
|
||||
ANTIGRAV_SSOT = REPO / ".agents" / "plugins" / "wiki-superpowers" / "agents"
|
||||
ANTIGRAV_OUT = REPO / ".agents" / "agents"
|
||||
|
||||
AGENT_NAMES = [
|
||||
"branch-depth-auditor",
|
||||
"coverage-auditor",
|
||||
"wiki-adversarial-reviewer",
|
||||
"wiki-decision-researcher",
|
||||
"wiki-diagram-reviewer",
|
||||
"wiki-doc-author",
|
||||
"wiki-link-verifier",
|
||||
"wiki-research-lane",
|
||||
"wiki-source-summarizer",
|
||||
]
|
||||
|
||||
ANTIGRAV_INCLUDE_SECTIONS = [
|
||||
"user_information", "mcp_servers", "skills",
|
||||
"subagent_reminder", "messaging", "artifacts", "user_rules",
|
||||
]
|
||||
|
||||
_ANTIGRAV_BASELINE = ["send_message", "view_file", "find_by_name", "grep_search", "list_dir"]
|
||||
_ANTIGRAV_EXTRA_ORDER = [
|
||||
("Write", ["write_to_file"]),
|
||||
("Edit", ["replace_file_content", "multi_replace_file_content"]),
|
||||
("Bash", ["run_command"]),
|
||||
("WebFetch", ["read_url_content"]),
|
||||
("WebSearch", ["search_web"]),
|
||||
]
|
||||
|
||||
|
||||
def parse_frontmatter(md: str) -> tuple[dict[str, str], str]:
|
||||
lines = md.splitlines(keepends=True)
|
||||
if not lines or lines[0].strip() != "---":
|
||||
raise ValueError("missing frontmatter open")
|
||||
fm: dict[str, str] = {}
|
||||
i = 1
|
||||
while i < len(lines) and lines[i].strip() != "---":
|
||||
line = lines[i].rstrip("\n")
|
||||
if ":" in line:
|
||||
key, _, val = line.partition(":")
|
||||
fm[key.strip()] = val.strip()
|
||||
i += 1
|
||||
if i >= len(lines):
|
||||
raise ValueError("missing frontmatter close")
|
||||
body = "".join(lines[i + 1:]).lstrip("\n")
|
||||
return fm, body
|
||||
|
||||
|
||||
def parse_tools(tools_csv: str) -> list[str]:
|
||||
return [t.strip() for t in tools_csv.split(",") if t.strip()]
|
||||
|
||||
|
||||
def codex_sandbox_mode(tools: list[str]) -> str:
|
||||
return "workspace-write" if ({"Edit", "Write"} & set(tools)) else "read-only"
|
||||
|
||||
|
||||
def antigravity_tool_names(tools: list[str]) -> list[str]:
|
||||
names = list(_ANTIGRAV_BASELINE)
|
||||
tset = set(tools)
|
||||
for claude_tool, mapped in _ANTIGRAV_EXTRA_ORDER:
|
||||
if claude_tool in tset:
|
||||
names.extend(mapped)
|
||||
return names
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
|
||||
Expected: PASS (6 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/sync_automation.py scripts/test_sync_automation.py
|
||||
git commit -m "feat(sync): frontmatter parse + permission derivation pure functions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Codex TOML 직렬화
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/sync_automation.py`
|
||||
- Test: `scripts/test_sync_automation.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
def test_codex_toml_is_valid_and_roundtrips():
|
||||
body = "You are the Wiki Link Verifier.\nLine two with `backticks` and 'quotes'.\n"
|
||||
out = s.render_codex_toml(
|
||||
name="wiki-link-verifier",
|
||||
description='Audit "the wiki" for orphans.',
|
||||
sandbox_mode="read-only",
|
||||
body=body,
|
||||
)
|
||||
parsed = tomllib.loads(out)
|
||||
assert parsed["name"] == "wiki-link-verifier"
|
||||
assert parsed["description"] == 'Audit "the wiki" for orphans.'
|
||||
assert parsed["sandbox_mode"] == "read-only"
|
||||
assert parsed["developer_instructions"].strip() == body.strip()
|
||||
|
||||
|
||||
def test_codex_toml_rejects_triple_single_quote_body():
|
||||
import pytest
|
||||
with pytest.raises(ValueError):
|
||||
s.render_codex_toml("n", "d", "read-only", "bad ''' body")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k codex_toml -v`
|
||||
Expected: FAIL with `AttributeError: module 'sync_automation' has no attribute 'render_codex_toml'`.
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
def _toml_basic_string(value: str) -> str:
|
||||
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def render_codex_toml(name: str, description: str, sandbox_mode: str, body: str) -> str:
|
||||
if "'''" in body:
|
||||
raise ValueError("body contains ''' which breaks TOML literal multiline string")
|
||||
return (
|
||||
f"name = {_toml_basic_string(name)}\n"
|
||||
f"description = {_toml_basic_string(description)}\n"
|
||||
f"sandbox_mode = {_toml_basic_string(sandbox_mode)}\n"
|
||||
f"developer_instructions = '''\n"
|
||||
f"{body.rstrip()}\n"
|
||||
f"'''\n"
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k codex_toml -v`
|
||||
Expected: PASS (2 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/sync_automation.py scripts/test_sync_automation.py
|
||||
git commit -m "feat(sync): codex TOML serialization with literal multiline body"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Antigravity agent.json 직렬화
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/sync_automation.py`
|
||||
- Test: `scripts/test_sync_automation.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
def test_antigravity_agent_json_schema():
|
||||
obj = s.build_antigravity_agent(
|
||||
name="wiki-link-verifier",
|
||||
description="Audit the wiki.",
|
||||
body="You are the Wiki Link Verifier.\n",
|
||||
tools=["Read", "Grep", "Glob", "Bash"],
|
||||
)
|
||||
assert obj["name"] == "wiki-link-verifier"
|
||||
assert obj["description"] == "Audit the wiki."
|
||||
assert obj["hidden"] is True
|
||||
ca = obj["config"]["customAgent"]
|
||||
assert ca["systemPromptSections"][0]["title"] == "Agent System Instructions"
|
||||
assert ca["systemPromptSections"][0]["content"] == "You are the Wiki Link Verifier.\n"
|
||||
assert "write_to_file" not in ca["toolNames"] # read-only agent
|
||||
assert ca["toolNames"][0] == "send_message"
|
||||
assert ca["systemPromptConfig"]["includeSections"] == s.ANTIGRAV_INCLUDE_SECTIONS
|
||||
|
||||
|
||||
def test_antigravity_json_render_is_valid_json():
|
||||
obj = s.build_antigravity_agent("n", "d", "body\n", ["Read", "Grep", "Glob"])
|
||||
text = s.render_json(obj)
|
||||
assert json.loads(text) == obj
|
||||
assert text.endswith("\n")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k antigravity_agent_json -v`
|
||||
Expected: FAIL with `AttributeError: ... 'build_antigravity_agent'`.
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
def build_antigravity_agent(name: str, description: str, body: str, tools: list[str]) -> dict:
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"hidden": True,
|
||||
"config": {
|
||||
"customAgent": {
|
||||
"systemPromptSections": [
|
||||
{"title": "Agent System Instructions", "content": body}
|
||||
],
|
||||
"toolNames": antigravity_tool_names(tools),
|
||||
"systemPromptConfig": {"includeSections": list(ANTIGRAV_INCLUDE_SECTIONS)},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def render_json(obj: dict) -> str:
|
||||
return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k antigravity_agent_json -v`
|
||||
Expected: PASS (2 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/sync_automation.py scripts/test_sync_automation.py
|
||||
git commit -m "feat(sync): antigravity agent.json builder + stable JSON render"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: CLI 배선 (generate / check) + 기존 7개 검증
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/sync_automation.py`
|
||||
- Test: `scripts/test_sync_automation.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
def test_load_agent_inputs_for_existing_agent():
|
||||
inp = s.load_agent_inputs("wiki-link-verifier")
|
||||
assert inp.tools == ["Read", "Grep", "Glob", "Bash"]
|
||||
assert inp.codex_description
|
||||
assert inp.antigrav_description
|
||||
assert "Wiki Link Verifier" in inp.codex_body
|
||||
assert inp.antigrav_body
|
||||
|
||||
|
||||
def test_generate_one_writes_both_outputs(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(s, "CODEX_AGENTS", tmp_path / "codex")
|
||||
monkeypatch.setattr(s, "ANTIGRAV_OUT", tmp_path / "antigrav")
|
||||
(tmp_path / "codex").mkdir()
|
||||
s.generate_one("wiki-link-verifier", check=False)
|
||||
toml_path = tmp_path / "codex" / "wiki-link-verifier.toml"
|
||||
json_path = tmp_path / "antigrav" / "wiki-link-verifier" / "agent.json"
|
||||
assert toml_path.exists()
|
||||
assert json_path.exists()
|
||||
assert tomllib.loads(toml_path.read_text())["sandbox_mode"] == "read-only"
|
||||
assert json.loads(json_path.read_text())["hidden"] is True
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k "load_agent_inputs or generate_one" -v`
|
||||
Expected: FAIL with `AttributeError: ... 'load_agent_inputs'`.
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentInputs:
|
||||
name: str
|
||||
tools: list[str]
|
||||
codex_description: str
|
||||
codex_body: str
|
||||
antigrav_description: str
|
||||
antigrav_body: str
|
||||
|
||||
|
||||
def load_agent_inputs(name: str) -> AgentInputs:
|
||||
claude_fm, _ = parse_frontmatter((CLAUDE_AGENTS / f"{name}.md").read_text())
|
||||
codex_fm, codex_body = parse_frontmatter((CODEX_AGENTS / f"{name}.md").read_text())
|
||||
ag_fm, ag_body = parse_frontmatter((ANTIGRAV_SSOT / f"{name}.md").read_text())
|
||||
return AgentInputs(
|
||||
name=name,
|
||||
tools=parse_tools(claude_fm["tools"]),
|
||||
codex_description=codex_fm["description"],
|
||||
codex_body=codex_body,
|
||||
antigrav_description=ag_fm["description"],
|
||||
antigrav_body=ag_body,
|
||||
)
|
||||
|
||||
|
||||
def _write_or_check(path: Path, content: str, check: bool, drift: list[str]) -> None:
|
||||
if check:
|
||||
current = path.read_text() if path.exists() else None
|
||||
if current != content:
|
||||
drift.append(str(path.relative_to(REPO)))
|
||||
else:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
|
||||
|
||||
def generate_one(name: str, check: bool, drift: list[str] | None = None) -> None:
|
||||
drift = drift if drift is not None else []
|
||||
inp = load_agent_inputs(name)
|
||||
toml_text = render_codex_toml(
|
||||
inp.name, inp.codex_description, codex_sandbox_mode(inp.tools), inp.codex_body
|
||||
)
|
||||
json_text = render_json(
|
||||
build_antigravity_agent(inp.name, inp.antigrav_description, inp.antigrav_body, inp.tools)
|
||||
)
|
||||
_write_or_check(CODEX_AGENTS / f"{name}.toml", toml_text, check, drift)
|
||||
_write_or_check(ANTIGRAV_OUT / name / "agent.json", json_text, check, drift)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description="Sync Claude agents -> Codex/Antigravity native files")
|
||||
ap.add_argument("target", choices=["agents"], help="what to sync")
|
||||
ap.add_argument("--check", action="store_true", help="fail (exit 2) if outputs drift from SSOT")
|
||||
ap.add_argument("--only", help="single agent name (default: all)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
names = [args.only] if args.only else AGENT_NAMES
|
||||
drift: list[str] = []
|
||||
for name in names:
|
||||
generate_one(name, check=args.check, drift=drift)
|
||||
if args.check and drift:
|
||||
print("DRIFT detected in:\n " + "\n ".join(drift), file=sys.stderr)
|
||||
return 2
|
||||
action = "checked" if args.check else "generated"
|
||||
print(f"{action} {len(names)} agents (codex .toml + antigravity agent.json)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run unit tests (still only 7 agents exist, so use --only)**
|
||||
|
||||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
|
||||
Expected: PASS (all tests). `load_agent_inputs`/`generate_one` use `wiki-link-verifier` which exists.
|
||||
|
||||
- [ ] **Step 5: Smoke-run on one existing agent**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
python3 scripts/sync_automation.py agents --only wiki-link-verifier
|
||||
python3 -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('.codex/agents/wiki-link-verifier.toml').read_text())['sandbox_mode'])"
|
||||
python3 -c "import json,pathlib; o=json.loads(pathlib.Path('.agents/agents/wiki-link-verifier/agent.json').read_text()); print(o['hidden'], 'write_to_file' in o['config']['customAgent']['toolNames'])"
|
||||
```
|
||||
Expected output:
|
||||
```
|
||||
generated 1 agents (codex .toml + antigravity agent.json)
|
||||
read-only
|
||||
True False
|
||||
```
|
||||
(`True False` = hidden true + write_to_file absent for read-only agent — confirms the stale-disk bug is fixed.)
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/sync_automation.py scripts/test_sync_automation.py .codex/agents/wiki-link-verifier.toml .agents/agents/wiki-link-verifier/agent.json
|
||||
git commit -m "feat(sync): CLI generate/check + AgentInputs loader; verify on link-verifier"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 누락 agent 2개의 codex SSOT 본문 작성 (`branch-depth-auditor`, `coverage-auditor`)
|
||||
|
||||
> codex 본문 = Claude 본문의 일반화: frontmatter에서 `tools:`/`model:` 제거(name+description만 유지), 본문의 `Read tool`/`Edit tool`/`Write tool`/`Bash tool` → `Read`/`Edit`/`Write`/`shell`, `CLAUDE.md` 단독 언급 → `CLAUDE.md (또는 AGENTS.md)`.
|
||||
|
||||
**Files:**
|
||||
- Create: `.codex/agents/branch-depth-auditor.md`
|
||||
- Create: `.codex/agents/coverage-auditor.md`
|
||||
|
||||
- [ ] **Step 1: Learn the generalization convention**
|
||||
|
||||
Read these two pairs side by side to learn the exact convention:
|
||||
- `.claude/agents/wiki-link-verifier.md` vs `.codex/agents/wiki-link-verifier.md`
|
||||
- Then Read `.claude/agents/branch-depth-auditor.md` and `.claude/agents/coverage-auditor.md` in full.
|
||||
|
||||
- [ ] **Step 2: Create `.codex/agents/branch-depth-auditor.md`**
|
||||
|
||||
Structure:
|
||||
```markdown
|
||||
---
|
||||
name: branch-depth-auditor
|
||||
description: <verbatim copy of the description line from .claude/agents/branch-depth-auditor.md>
|
||||
---
|
||||
|
||||
<Claude body copied verbatim, then these substitutions applied:
|
||||
"Read tool"->"Read", "Grep tool"->"Grep", "Glob tool"->"Glob", "Bash tool"->"shell",
|
||||
"Edit tool"->"Edit", "Write tool"->"Write";
|
||||
standalone "CLAUDE.md" reference -> "CLAUDE.md (또는 AGENTS.md)".
|
||||
Leave all rules/ and templates/ references, axes, and depth-ladder logic byte-identical.>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create `.codex/agents/coverage-auditor.md`** identically (verbatim description + generalized body).
|
||||
|
||||
- [ ] **Step 4: Verify frontmatter parses for both**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private/scripts && python3 -c "
|
||||
import sync_automation as s
|
||||
for n in ['branch-depth-auditor','coverage-auditor']:
|
||||
fm,body=s.parse_frontmatter(open(f'../.codex/agents/{n}.md').read())
|
||||
assert fm['name']==n and fm['description'] and body.strip(), n
|
||||
assert 'tool' not in fm and 'model' not in fm, 'frontmatter must be name+description only'
|
||||
print('ok', n)
|
||||
"
|
||||
```
|
||||
Expected:
|
||||
```
|
||||
ok branch-depth-auditor
|
||||
ok coverage-auditor
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .codex/agents/branch-depth-auditor.md .codex/agents/coverage-auditor.md
|
||||
git commit -m "feat(codex): add branch-depth-auditor + coverage-auditor SSOT bodies"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 누락 agent 2개의 antigravity gated SSOT 본문 작성
|
||||
|
||||
> antigravity 본문 = codex 본문 + G1~G4 Gemini hard gate. 기존 read-only gated agent를 템플릿으로 사용한다. tool 참조는 antigravity 어휘(`view_file`/`grep_search`/`run_command`)로 쓴다.
|
||||
|
||||
**Files:**
|
||||
- Create: `.agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md`
|
||||
- Create: `.agents/plugins/wiki-superpowers/agents/coverage-auditor.md`
|
||||
|
||||
- [ ] **Step 1: Study the gate template**
|
||||
|
||||
Read `.agents/plugins/wiki-superpowers/agents/wiki-link-verifier.md` in full (read-only agent — closest analog). Extract the exact section structure of: G1 Pre-Read Proof, G2 Post-Write Validator, G3 Output Schema + V Counter, G4 Enumerated STOP Conditions. Also read `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md` if a longer analytical example helps.
|
||||
|
||||
- [ ] **Step 2: Create `.agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md`**
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: branch-depth-auditor
|
||||
description: <verbatim copy from .claude/agents/branch-depth-auditor.md frontmatter>
|
||||
---
|
||||
|
||||
<body = the codex branch-depth-auditor body content (audit axes + L0~L3 depth ladder
|
||||
+ Ready/Not-ready verdict logic), PLUS the four gates:
|
||||
- G1 Pre-Read Proof: table quoting the first line of each Mandatory First Read
|
||||
(CLAUDE.md/AGENTS.md, rules/branch-depth-gate.md, the target branch note).
|
||||
- G2 Post-Write Validator: read-only -> state "read-only agent, no file writes;
|
||||
verification is the grep/sed verbatim-quote proofs inside the gap report (G3 V counter)".
|
||||
- G3 Output Schema + V Counter: {{ }} placeholder schema for the Ready/Not-ready
|
||||
verdict + per-axis gap rows; V = number of quote-grep commands actually run.
|
||||
- G4 Enumerated STOP Conditions: numbered list -> return NEEDS_CONTEXT
|
||||
(1: branch-note path missing/unreadable; 2: linked raw sources unreadable;
|
||||
3: wiki_structure_lint not yet passed; 4: target is not a feature-*.md branch note).
|
||||
Use Antigravity tool names in any tool reference: view_file, grep_search, run_command.>
|
||||
```
|
||||
Mirror the axes/ladder language from `.claude/agents/branch-depth-auditor.md`; do NOT invent new audit criteria.
|
||||
|
||||
- [ ] **Step 3: Create `.agents/plugins/wiki-superpowers/agents/coverage-auditor.md`**
|
||||
|
||||
Same structure. Coverage = completeness (covered-here / delegated / missing per required concern) with a 3-tier verdict and optional project mode. G4 STOP conditions: governing_docs missing, `## Coverage` section absent, links unresolved, target not a branch note. tool refs use `run_command` for grep-based concern classification + `view_file` for governing docs.
|
||||
|
||||
- [ ] **Step 4: Verify frontmatter parses + all four gates present**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
for n in branch-depth-auditor coverage-auditor; do
|
||||
f=.agents/plugins/wiki-superpowers/agents/$n.md
|
||||
echo "== $n =="
|
||||
grep -c "G1 Pre-Read Proof" "$f"
|
||||
grep -c "G4" "$f"
|
||||
done
|
||||
```
|
||||
Expected: each `grep -c` prints `1` or more (gates present). If `0`, the gate section is missing — add it.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md .agents/plugins/wiki-superpowers/agents/coverage-auditor.md
|
||||
git commit -m "feat(antigravity): add branch-depth-auditor + coverage-auditor gated SSOT bodies"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: 전체 9개 생성 + 스키마 검증 + drift 0 확인
|
||||
|
||||
**Files:**
|
||||
- Generate: `.codex/agents/*.toml` ×9, `.agents/agents/<name>/agent.json` ×9
|
||||
- Test: `scripts/test_sync_automation.py` (add a full-suite generation guard)
|
||||
|
||||
- [ ] **Step 1: Add a test asserting all 9 generate and pass schema checks**
|
||||
|
||||
```python
|
||||
def test_all_nine_agents_generate_valid_artifacts(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(s, "CODEX_AGENTS", tmp_path / "codex")
|
||||
monkeypatch.setattr(s, "ANTIGRAV_OUT", tmp_path / "antigrav")
|
||||
# copy SSOT bodies the generator reads from real repo (CODEX_AGENTS is also the source dir),
|
||||
# so point the source dir explicitly: regenerate using real source via a fresh load.
|
||||
(tmp_path / "codex").mkdir()
|
||||
# NOTE: generate_one reads codex body from s.CODEX_AGENTS; for this test we only verify
|
||||
# the antigravity side (json) which reads from ANTIGRAV_SSOT (unchanged real dir).
|
||||
for name in s.AGENT_NAMES:
|
||||
obj = s.build_antigravity_agent(
|
||||
name, "d",
|
||||
s.parse_frontmatter((s.ANTIGRAV_SSOT / f"{name}.md").read_text())[1],
|
||||
s.parse_tools(s.parse_frontmatter((s.CLAUDE_AGENTS / f"{name}.md").read_text())[0]["tools"]),
|
||||
)
|
||||
assert json.loads(s.render_json(obj))["name"] == name
|
||||
# read-only agents must NOT carry write tools
|
||||
ro = {"branch-depth-auditor", "coverage-auditor", "wiki-adversarial-reviewer",
|
||||
"wiki-diagram-reviewer", "wiki-link-verifier", "wiki-research-lane"}
|
||||
if name in ro:
|
||||
assert "write_to_file" not in obj["config"]["customAgent"]["toolNames"], name
|
||||
```
|
||||
|
||||
> 위 테스트는 `CODEX_AGENTS`/`ANTIGRAV_SSOT`의 9개 `.md`가 모두 존재해야 통과한다(Task 5·6 완료 후). codex toml 본문은 실 디렉토리에서 직접 읽으므로 통합 스모크(Step 3)로 검증한다.
|
||||
|
||||
- [ ] **Step 2: Run the unit suite**
|
||||
|
||||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
|
||||
Expected: PASS (all, including the new 9-agent guard).
|
||||
|
||||
- [ ] **Step 3: Generate all 9 and validate every artifact**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
python3 scripts/sync_automation.py agents
|
||||
echo "--- validate codex toml (9) ---"
|
||||
python3 -c "
|
||||
import tomllib, pathlib
|
||||
ns=['branch-depth-auditor','coverage-auditor','wiki-adversarial-reviewer','wiki-decision-researcher','wiki-diagram-reviewer','wiki-doc-author','wiki-link-verifier','wiki-research-lane','wiki-source-summarizer']
|
||||
for n in ns:
|
||||
o=tomllib.loads(pathlib.Path(f'.codex/agents/{n}.toml').read_text())
|
||||
assert o['name']==n and o['sandbox_mode'] in ('read-only','workspace-write') and o['developer_instructions'].strip()
|
||||
print('codex toml ok:', len(ns))
|
||||
"
|
||||
echo "--- validate antigravity json (9) ---"
|
||||
python3 -c "
|
||||
import json, pathlib
|
||||
ns=['branch-depth-auditor','coverage-auditor','wiki-adversarial-reviewer','wiki-decision-researcher','wiki-diagram-reviewer','wiki-doc-author','wiki-link-verifier','wiki-research-lane','wiki-source-summarizer']
|
||||
for n in ns:
|
||||
o=json.loads(pathlib.Path(f'.agents/agents/{n}/agent.json').read_text())
|
||||
ca=o['config']['customAgent']
|
||||
assert o['name']==n and o['hidden'] is True
|
||||
assert ca['systemPromptSections'][0]['content'].strip()
|
||||
assert ca['toolNames'][0]=='send_message'
|
||||
print('antigravity json ok:', len(ns))
|
||||
"
|
||||
```
|
||||
Expected:
|
||||
```
|
||||
generated 9 agents (codex .toml + antigravity agent.json)
|
||||
--- validate codex toml (9) ---
|
||||
codex toml ok: 9
|
||||
--- validate antigravity json (9) ---
|
||||
antigravity json ok: 9
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Confirm idempotency / drift-0**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
python3 scripts/sync_automation.py agents --check && echo "DRIFT-FREE"
|
||||
```
|
||||
Expected:
|
||||
```
|
||||
checked 9 agents (codex .toml + antigravity agent.json)
|
||||
DRIFT-FREE
|
||||
```
|
||||
(exit 0). If it prints `DRIFT detected`, re-run without `--check` and re-commit the generated files.
|
||||
|
||||
- [ ] **Step 5: Commit generated artifacts**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
git add scripts/test_sync_automation.py .codex/agents/*.toml .agents/agents/
|
||||
git commit -m "feat(sync): generate all 9 agents for codex (.toml) + antigravity (agent.json)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: 생성 스크립트 사용법 문서 1단락 (codex + antigravity README)
|
||||
|
||||
> 전체 문서 정리(CLAUDE.md의 구식 '수동 cat' 서술 제거 등)는 Phase 2. 여기서는 Phase 0가 만든 스크립트/산출물이 고아가 되지 않도록 최소 포인터만 추가한다.
|
||||
|
||||
**Files:**
|
||||
- Modify: `.codex/agents/README.md`
|
||||
- Modify: `.agents/plugins/wiki-superpowers/README.md`
|
||||
|
||||
- [ ] **Step 1: Add a "Native generation" note to `.codex/agents/README.md`**
|
||||
|
||||
기존 README 상단(또는 "Pattern" 섹션 위)에 다음 취지의 1단락 추가 (정확한 문구는 기존 톤에 맞춰 작성):
|
||||
- codex는 이제 native subagent를 `.codex/agents/*.toml`로 자동 등록한다(`developer_instructions`+`sandbox_mode`). `.md`는 사람이 편집하는 SSOT이고 `.toml`은 `python3 scripts/sync_automation.py agents`로 생성된다.
|
||||
- `.md` 편집 후 반드시 `sync_automation.py agents`를 다시 돌려야 `.toml`에 반영된다. CI/hook에서는 `--check`로 drift를 검사한다.
|
||||
|
||||
- [ ] **Step 2: Add the same note to `.agents/plugins/wiki-superpowers/README.md`**
|
||||
|
||||
기존 "⚠️ Loading model" 박스의 "Sync command" 줄을 실제 스크립트로 교체:
|
||||
- 구: `python3 .agents/scripts/convert-wiki-agents.py` (존재하지 않음)
|
||||
- 신: `python3 scripts/sync_automation.py agents` — `.agents/plugins/.../agents/*.md`(gated SSOT) + `.claude/agents/*.md`(권한 SSOT) → `.agents/agents/<name>/agent.json` 생성. `--check`로 drift 검사.
|
||||
|
||||
- [ ] **Step 3: Verify the dead reference is gone**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
grep -rn "convert-wiki-agents.py" .agents/ .codex/ CLAUDE.md || echo "no dead reference remaining in README scope"
|
||||
```
|
||||
Expected: `no dead reference remaining in README scope` (CLAUDE.md 본문의 언급은 Phase 2에서 처리하므로, 여기서 grep이 CLAUDE.md만 남기면 그 줄은 Phase 2 TODO로 남겨도 됨 — 단 README 2개에는 남지 않아야 함).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add .codex/agents/README.md .agents/plugins/wiki-superpowers/README.md
|
||||
git commit -m "docs(sync): point READMEs at scripts/sync_automation.py (replaces missing convert script)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 완료 기준 (Definition of Done)
|
||||
|
||||
- `scripts/sync_automation.py` + 테스트 통과(pytest green).
|
||||
- `.codex/agents/*.toml` 9개 + `.agents/agents/<name>/agent.json` 9개 생성·검증.
|
||||
- `sync_automation.py agents --check` 가 exit 0 (drift 0).
|
||||
- 누락 2개 agent(branch-depth-auditor·coverage-auditor)가 codex·antigravity 양쪽 SSOT + 생성물에 존재.
|
||||
- README 2개가 실제 생성 스크립트를 가리킴.
|
||||
- 검증된 부채 수정: read-only agent의 `agent.json`에 write 도구 없음(stale 디스크 버그 해소).
|
||||
|
||||
이후: **Phase 1**(commands → codex `.agents/skills/` + antigravity `.agents/workflows/`)는 별도 plan으로 작성한다. Phase 0의 `sync_automation.py`에 `commands` 타깃을 확장한다.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (작성자 체크)
|
||||
|
||||
- **Spec coverage**: 설계 §4 Phase 0(엔진+agents)의 모든 항목 — 생성 엔진, 9개 agent 생성, 누락 2개 추가, antigravity 로딩 경로 복구(agent.json+스크립트), codex toml 현대화 — 각각 Task 1~8에 매핑됨. Phase 1·2는 범위 밖(별도 plan).
|
||||
- **Placeholder scan**: 코드 스텝은 실제 코드 포함. Task 5·6의 본문 작성은 "기존 파일을 템플릿으로 verbatim 적응"이라는 결정론적 절차 + 검증 grep을 제공(프로즈 자체는 SSOT 적응이라 코드처럼 박제 불가하나, 입력 파일·치환 규칙·검증 명령을 명시).
|
||||
- **Type consistency**: 함수 시그니처 일관 — `parse_frontmatter`→`(fm,body)`, `antigravity_tool_names(tools)`, `render_codex_toml(name,description,sandbox_mode,body)`, `build_antigravity_agent(name,description,body,tools)`, `generate_one(name,check,drift)`. 모든 Task에서 동일 이름 사용. 모듈명 `sync_automation`(언더스코어)로 통일.
|
||||
- **알려진 한계**: Task 7 Step 1 테스트는 codex toml 본문을 실디렉토리에서 읽어 통합 스모크(Step 3)로 보완. Task 6 Step 4의 grep 카운트는 게이트 "존재"만 보장하고 의미적 정확성은 보장하지 않음 → 실제 antigravity 실행 스모크는 Phase 1 검증과 함께 수행 권장.
|
||||
@@ -0,0 +1,470 @@
|
||||
# 3-플랫폼 동기화 Phase 1 — commands 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:** Claude의 13개 슬래시 command(`.claude/commands/*.md`)를 Codex CLI **skills**(`.agents/skills/<cmd>/SKILL.md`)와 Antigravity CLI **workflows**(`.agents/workflows/<cmd>.md`)로 생성하는 기능을 `scripts/sync_automation.py` 에 `commands` 타깃으로 추가한다.
|
||||
|
||||
**Architecture:** Phase 0의 SSOT→생성 모델을 commands로 확장한다. SSOT = `.claude/commands/<cmd>.md`(frontmatter `description`+`argument-hint` + 본문). 생성기는 (1) frontmatter를 플랫폼별 헤더로 변환, (2) `$ARGUMENTS` 토큰을 자연어 각괄호 인자(`argument-hint` 값)로 치환, (3) 본문의 platform-neutral 절차는 그대로 보존, (4) codex skill / antigravity workflow 두 포맷으로 직렬화한다. command 본문은 agent 본문과 달리 Claude 전용 tool 표현(`Read tool` 등)을 쓰지 않고 절차적 prose + shell + 서브에이전트 이름(Phase 0에서 3 플랫폼에 모두 포팅됨)만 참조하므로 기계 변환이 충분하다.
|
||||
|
||||
**Tech Stack:** Python 3.12 (stdlib), pytest (venv: `/home/donghyeon/dev/llm-wiki-private/.venv/bin/python`).
|
||||
|
||||
---
|
||||
|
||||
## 배경 / 확정된 포맷 사실 (공식문서 + 리서치)
|
||||
|
||||
- **Codex skills** (`developers.openai.com/codex/skills`): 디스커버리 `<repo>/.agents/skills/<name>/SKILL.md`(repo 커밋). frontmatter 필수 2필드 `name`(폴더명과 일치 권장) + `description`(언제 발동). **인자 placeholder 없음 — 자연어로 전달**(`$ARGUMENTS`/`$1` 미지원). `/skills` picker 또는 `$name` 멘션으로 명시 호출 + description 의미 매칭.
|
||||
- **Antigravity workflows** (Google Codelabs / atamel.dev): `<repo>/.agents/workflows/<name>.md`, frontmatter **`description`만**(name=파일명), 본문은 prose 지시, 인자는 `<idea>` 식 각괄호 prose(placeholder 없음), `/<name> <args>` 호출.
|
||||
- Codelab 실제 예시(verbatim):
|
||||
```
|
||||
---
|
||||
description: Start the Autonomous AI Developer Pipeline sequence with a new idea
|
||||
---
|
||||
|
||||
When the user types `/startcycle <idea>`, orchestrate the development process ...
|
||||
```
|
||||
- **경로 규약 결정**: 이 repo는 이미 `.agents/`(복수) 규약(`.agents/agents/`, `.agents/hooks.json`)을 쓰고 antigravity가 그것을 로드 중 → **antigravity workflows도 `.agents/workflows/`(복수)** 로 통일. codex skills는 공식 고정 경로 `.agents/skills/`.
|
||||
- **알려진 충돌(미해결, 경험적 확인 필요)**: 일부 antigravity 빌드는 `.agents/skills/`도 skill로 읽어, codex command-skill이 antigravity에 semantic-trigger skill로 *이중 등록*될 수 있음(workflow + skill). 공식문서로 확정 불가 → **Task 1에서 실제 설치된 CLI로 경험적 확인** 후 필요 시 완화.
|
||||
|
||||
### 13개 command (SSOT: `.claude/commands/*.md`)
|
||||
`blogify` · `branch` · `branch-spec` · `coverage` · `daily` · `depth` · `ingest` · `interviewize` · `lint` · `migrate-claims` · `projectize` · `query` · `tag`
|
||||
|
||||
frontmatter 공통: `description:` + `argument-hint:`. 본문은 `$ARGUMENTS` 토큰 + 절차(`## 작업 절차`) + 규칙. 일부는 `.claude/hooks/wiki_structure_lint.py`(실제 repo 스크립트, 크로스플랫폼 실행 가능 — 유지)와 포팅된 서브에이전트(`branch-depth-auditor` 등) 참조.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `scripts/sync_automation.py` — `COMMAND_NAMES` 리스트, command frontmatter 파서 재사용(`parse_frontmatter`), `transform_command_body`, `render_codex_skill`, `render_antigravity_workflow`, `generate_command_one`, `main()` 의 `target` choices에 `commands` 추가.
|
||||
- Modify: `scripts/test_sync_automation.py` — command 변환 단위 테스트.
|
||||
- Create (생성물): `.agents/skills/<cmd>/SKILL.md` ×13 (codex), `.agents/workflows/<cmd>.md` ×13 (antigravity).
|
||||
- Create: `docs/superpowers/notes/2026-06-04-phase1-empirical-format-check.md` — Task 1 경험적 확인 결과 기록.
|
||||
- Modify (말미): `CLAUDE.md` §2 표 + `.codex/agents/README.md` — commands→skills/workflows 매핑 1단락.
|
||||
|
||||
> 책임 경계: 생성기는 frontmatter 변환 + `$ARGUMENTS` 치환 + 포맷 직렬화만. 본문 절차 prose는 platform-neutral이므로 보존. codex skill 본문에서 antigravity 전용 표기를 만들지 않고, antigravity workflow 본문에서 codex 전용 표기를 만들지 않는다(생성기가 플랫폼별로 분기).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 경험적 포맷·충돌 확인 + 결정 잠금
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/superpowers/notes/2026-04-phase1-empirical-format-check.md` (디렉토리 없으면 생성)
|
||||
|
||||
> 목적: 공식문서가 확정 못한 두 가지를 실제 설치된 CLI로 확인하고, 생성기 타깃 경로/인자 규약을 잠근다. CLI가 없으면 "가정 + 보류" 로 명시 기록(생성물은 가정대로 진행, 추후 검증).
|
||||
|
||||
- [ ] **Step 1: 설치 여부 확인**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
command -v codex && codex --version 2>&1 | head -1 || echo "codex: NOT INSTALLED"
|
||||
ls -d ~/.gemini/antigravity-cli 2>/dev/null && echo "antigravity-cli home present" || echo "antigravity: home absent"
|
||||
command -v agy 2>&1 || echo "agy (antigravity CLI) not on PATH"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: (codex 있으면) skill 디스커버리 경로 확인**
|
||||
|
||||
scratch skill을 만들어 codex가 `.agents/skills/`를 읽는지 확인:
|
||||
```bash
|
||||
mkdir -p /tmp/codex-skill-probe/.agents/skills/probe-skill
|
||||
printf -- '---\nname: probe-skill\ndescription: probe codex skill discovery\n---\nprobe body\n' > /tmp/codex-skill-probe/.agents/skills/probe-skill/SKILL.md
|
||||
# codex의 skills 목록 확인 (codex 설치 시): 예) `codex` TUI에서 /skills, 또는 가능한 CLI 서브커맨드
|
||||
codex --help 2>&1 | grep -iE "skill|prompt" || echo "no skill subcommand surfaced in --help"
|
||||
```
|
||||
결과(읽힘/안읽힘/불명)를 노트에 기록.
|
||||
|
||||
- [ ] **Step 3: (antigravity 있으면) `.agents/skills/` 이중 로드 여부 확인**
|
||||
|
||||
antigravity가 `.agents/skills/`를 skill로 읽는지(=codex command-skill 이중 등록 위험) 실제 빌드에서 확인. 가능한 방법: antigravity CLI의 skill/workflow 목록 출력 커맨드, 또는 `~/.gemini/antigravity-cli/` 로그/registry 확인. 결과를 노트에 기록.
|
||||
|
||||
- [ ] **Step 4: 결정 기록**
|
||||
|
||||
노트에 다음을 표로 확정(경험 결과 또는 "UNVERIFIED — 가정"):
|
||||
| 항목 | 결정 | 근거 |
|
||||
|---|---|---|
|
||||
| codex skill 경로 | `.agents/skills/<cmd>/SKILL.md` | 공식 디스커버리 경로 |
|
||||
| antigravity workflow 경로 | `.agents/workflows/<cmd>.md` | repo `.agents/` 복수 규약 일치 |
|
||||
| `$ARGUMENTS` 매핑 | argument-hint 각괄호 prose로 치환 | 양 플랫폼 placeholder 미지원 |
|
||||
| `.agents/skills/` antigravity 이중로드 | {{읽음→완화 필요 / 안읽음→무관 / UNVERIFIED}} | Step 3 결과 |
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
git add docs/superpowers/notes/
|
||||
git commit -m "docs(phase1): empirical format + skills-collision check, lock target paths"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: command 파싱 + skill/workflow 직렬화 (순수 함수, TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/sync_automation.py`
|
||||
- Test: `scripts/test_sync_automation.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
# append to scripts/test_sync_automation.py
|
||||
|
||||
_SAMPLE_CMD = (
|
||||
"---\n"
|
||||
"description: 브랜치 노트의 구현 착수 깊이 점검\n"
|
||||
"argument-hint: <브랜치 이름>\n"
|
||||
"---\n\n"
|
||||
"브랜치 노트 1개의 깊이를 점검합니다.\n\n"
|
||||
"**브랜치 이름:** $ARGUMENTS\n\n"
|
||||
"## 작업 절차\n1. `branch-depth-auditor` 서브에이전트를 디스패치한다.\n"
|
||||
)
|
||||
|
||||
|
||||
def test_transform_command_body_replaces_arguments():
|
||||
out = s.transform_command_body(_body_of(_SAMPLE_CMD), "<브랜치 이름>")
|
||||
assert "$ARGUMENTS" not in out
|
||||
assert "<브랜치 이름>" in out
|
||||
# platform-neutral 절차/에이전트 참조는 보존
|
||||
assert "branch-depth-auditor 서브에이전트를 디스패치" in out
|
||||
|
||||
|
||||
def test_render_codex_skill_frontmatter():
|
||||
out = s.render_codex_skill(
|
||||
name="depth",
|
||||
description="브랜치 노트의 구현 착수 깊이 점검",
|
||||
arg_hint="<브랜치 이름>",
|
||||
body="**브랜치 이름:** <브랜치 이름>\n\n## 작업 절차\n1. ...\n",
|
||||
)
|
||||
fm, body = s.parse_frontmatter(out)
|
||||
assert fm["name"] == "depth"
|
||||
assert fm["description"] # non-empty
|
||||
assert "$ARGUMENTS" not in body
|
||||
assert "## 작업 절차" in body
|
||||
|
||||
|
||||
def test_render_antigravity_workflow_frontmatter_and_invocation():
|
||||
out = s.render_antigravity_workflow(
|
||||
name="depth",
|
||||
description="브랜치 노트의 구현 착수 깊이 점검",
|
||||
arg_hint="<브랜치 이름>",
|
||||
body="## 작업 절차\n1. ...\n",
|
||||
)
|
||||
fm, body = s.parse_frontmatter(out)
|
||||
assert set(fm.keys()) == {"description"} # name comes from filename, not frontmatter
|
||||
# 호출 안내가 본문 상단에 있어야 함 (Codelab 패턴)
|
||||
assert "/depth <브랜치 이름>" in body
|
||||
assert "## 작업 절차" in body
|
||||
```
|
||||
|
||||
(헬퍼 `_body_of` 는 테스트 상단에 추가: `def _body_of(md): return s.parse_frontmatter(md)[1]`.)
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd /home/donghyeon/dev/llm-wiki-private && .venv/bin/python -m pytest scripts/test_sync_automation.py -k "command or codex_skill or workflow" -v`
|
||||
Expected: FAIL with `AttributeError: ... 'transform_command_body'`.
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
# add near COMMAND-related code in sync_automation.py
|
||||
|
||||
COMMAND_NAMES = [
|
||||
"blogify", "branch", "branch-spec", "coverage", "daily", "depth",
|
||||
"ingest", "interviewize", "lint", "migrate-claims", "projectize",
|
||||
"query", "tag",
|
||||
]
|
||||
|
||||
CLAUDE_COMMANDS = REPO / ".claude" / "commands"
|
||||
CODEX_SKILLS = REPO / ".agents" / "skills"
|
||||
ANTIGRAV_WORKFLOWS = REPO / ".agents" / "workflows"
|
||||
|
||||
|
||||
def transform_command_body(body: str, arg_hint: str) -> str:
|
||||
# Neither Codex skills nor Antigravity workflows support a $ARGUMENTS
|
||||
# placeholder; both take arguments as natural language. Replace the token
|
||||
# with the argument-hint's angle-bracket prose so the body still reads well.
|
||||
return body.replace("$ARGUMENTS", arg_hint)
|
||||
|
||||
|
||||
def render_codex_skill(name: str, description: str, arg_hint: str, body: str) -> str:
|
||||
# description drives implicit trigger; fold in the arg hint for clarity.
|
||||
desc = f"{description} (입력: {arg_hint})"
|
||||
return (
|
||||
"---\n"
|
||||
f"name: {name}\n"
|
||||
f"description: {desc}\n"
|
||||
"---\n\n"
|
||||
f"{body.rstrip()}\n"
|
||||
)
|
||||
|
||||
|
||||
def render_antigravity_workflow(name: str, description: str, arg_hint: str, body: str) -> str:
|
||||
invocation = f"사용자가 `/{name} {arg_hint}` 를 입력하면 아래 절차를 수행한다.\n\n"
|
||||
return (
|
||||
"---\n"
|
||||
f"description: {description}\n"
|
||||
"---\n\n"
|
||||
f"{invocation}{body.rstrip()}\n"
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd /home/donghyeon/dev/llm-wiki-private && .venv/bin/python -m pytest scripts/test_sync_automation.py -k "command or codex_skill or workflow" -v`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
git add scripts/sync_automation.py scripts/test_sync_automation.py
|
||||
git commit -m "feat(sync): command body transform + codex skill / antigravity workflow renderers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: command 로더 + CLI `commands` 타깃 (TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/sync_automation.py`
|
||||
- Test: `scripts/test_sync_automation.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
def test_load_command_inputs_for_existing_command():
|
||||
ci = s.load_command_inputs("depth")
|
||||
assert ci.name == "depth"
|
||||
assert ci.description
|
||||
assert ci.arg_hint.startswith("<") or "--" in ci.arg_hint
|
||||
assert "$ARGUMENTS" in ci.body or "작업 절차" in ci.body # raw body still has token
|
||||
|
||||
|
||||
def test_generate_command_writes_both(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(s, "CODEX_SKILLS", tmp_path / "skills")
|
||||
monkeypatch.setattr(s, "ANTIGRAV_WORKFLOWS", tmp_path / "workflows")
|
||||
s.generate_command_one("depth", check=False)
|
||||
skill = tmp_path / "skills" / "depth" / "SKILL.md"
|
||||
wf = tmp_path / "workflows" / "depth.md"
|
||||
assert skill.exists() and wf.exists()
|
||||
sfm, sbody = s.parse_frontmatter(skill.read_text())
|
||||
assert sfm["name"] == "depth" and "$ARGUMENTS" not in sbody
|
||||
wfm, wbody = s.parse_frontmatter(wf.read_text())
|
||||
assert "/depth" in wbody and "$ARGUMENTS" not in wbody
|
||||
|
||||
|
||||
def test_main_commands_target_check_is_clean_after_generate():
|
||||
# after generation (Task 4), --check must be drift-free
|
||||
rc = s.main(["commands", "--check"])
|
||||
assert rc == 0
|
||||
```
|
||||
|
||||
> 마지막 테스트는 Task 4 생성 이후에만 통과한다. Task 3 단계에서는 앞 두 테스트만 대상으로 실행하고, 세 번째는 Task 4 검증에서 green 확인.
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd /home/donghyeon/dev/llm-wiki-private && .venv/bin/python -m pytest scripts/test_sync_automation.py -k "load_command or generate_command" -v`
|
||||
Expected: FAIL with `AttributeError: ... 'load_command_inputs'`.
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class CommandInputs:
|
||||
name: str
|
||||
description: str
|
||||
arg_hint: str
|
||||
body: str
|
||||
|
||||
|
||||
def load_command_inputs(name: str) -> CommandInputs:
|
||||
fm, body = parse_frontmatter((CLAUDE_COMMANDS / f"{name}.md").read_text())
|
||||
return CommandInputs(
|
||||
name=name,
|
||||
description=fm.get("description", "").strip(),
|
||||
arg_hint=fm.get("argument-hint", "").strip(),
|
||||
body=body,
|
||||
)
|
||||
|
||||
|
||||
def generate_command_one(name: str, check: bool, drift: list[str] | None = None) -> None:
|
||||
drift = drift if drift is not None else []
|
||||
ci = load_command_inputs(name)
|
||||
tbody = transform_command_body(ci.body, ci.arg_hint)
|
||||
skill_text = render_codex_skill(ci.name, ci.description, ci.arg_hint, tbody)
|
||||
wf_text = render_antigravity_workflow(ci.name, ci.description, ci.arg_hint, tbody)
|
||||
_write_or_check(CODEX_SKILLS / name / "SKILL.md", skill_text, check, drift)
|
||||
_write_or_check(ANTIGRAV_WORKFLOWS / f"{name}.md", wf_text, check, drift)
|
||||
```
|
||||
|
||||
그리고 `main()` 을 확장: `target` choices에 `commands` 추가하고 분기.
|
||||
|
||||
```python
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description="Sync Claude agents/commands -> Codex/Antigravity native files")
|
||||
ap.add_argument("target", choices=["agents", "commands"], help="what to sync")
|
||||
ap.add_argument("--check", action="store_true", help="fail (exit 2) if outputs drift from SSOT")
|
||||
ap.add_argument("--only", help="single name (default: all)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if args.target == "agents":
|
||||
names = [args.only] if args.only else AGENT_NAMES
|
||||
gen = generate_one
|
||||
label = "agents (codex .toml + antigravity agent.json)"
|
||||
else:
|
||||
names = [args.only] if args.only else COMMAND_NAMES
|
||||
gen = generate_command_one
|
||||
label = "commands (codex skill + antigravity workflow)"
|
||||
|
||||
drift: list[str] = []
|
||||
for name in names:
|
||||
try:
|
||||
gen(name, check=args.check, drift=drift)
|
||||
except FileNotFoundError as e:
|
||||
print(f"ERROR: missing SSOT for '{name}': {e.filename}", file=sys.stderr)
|
||||
return 1
|
||||
if args.check and drift:
|
||||
print("DRIFT detected in:\n " + "\n ".join(drift), file=sys.stderr)
|
||||
return 2
|
||||
action = "checked" if args.check else "generated"
|
||||
print(f"{action} {len(names)} {label}")
|
||||
return 0
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd /home/donghyeon/dev/llm-wiki-private && .venv/bin/python -m pytest scripts/test_sync_automation.py -k "load_command or generate_command" -v`
|
||||
Expected: PASS. Also run the FULL suite to confirm the `main()` refactor didn't break agents:
|
||||
Run: `.venv/bin/python -m pytest scripts/test_sync_automation.py -q`
|
||||
Expected: all green. Then sanity: `python3 scripts/sync_automation.py agents --check` still exits 0.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
git add scripts/sync_automation.py scripts/test_sync_automation.py
|
||||
git commit -m "feat(sync): commands target — load + generate codex skills / antigravity workflows"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 13개 command 전체 생성 + 검증 + drift-0
|
||||
|
||||
**Files:**
|
||||
- Generate: `.agents/skills/<cmd>/SKILL.md` ×13, `.agents/workflows/<cmd>.md` ×13
|
||||
|
||||
- [ ] **Step 1: 생성 + 검증**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
python3 scripts/sync_automation.py commands
|
||||
echo "--- counts ---"
|
||||
echo "skills: $(ls .agents/skills/*/SKILL.md | wc -l) | workflows: $(ls .agents/workflows/*.md | wc -l)"
|
||||
echo "--- validate ---"
|
||||
.venv/bin/python -c "
|
||||
import sys; sys.path.insert(0,'scripts'); import sync_automation as s
|
||||
for n in s.COMMAND_NAMES:
|
||||
sfm,sbody=s.parse_frontmatter(open(f'.agents/skills/{n}/SKILL.md').read())
|
||||
assert sfm['name']==n and sfm['description'] and '\$ARGUMENTS' not in sbody, n
|
||||
wfm,wbody=s.parse_frontmatter(open(f'.agents/workflows/{n}.md').read())
|
||||
assert 'description' in wfm and f'/{n}' in wbody and '\$ARGUMENTS' not in wbody, n
|
||||
print('all 13 commands ok (skill name+desc, workflow desc+invocation, no \$ARGUMENTS)')
|
||||
"
|
||||
echo "--- no {{ }} placeholders leaked into workflows (antigravity G3) ---"
|
||||
! grep -l '{{' .agents/workflows/*.md || echo "WARNING: {{ }} found"
|
||||
```
|
||||
Expected: `skills: 13 | workflows: 13`, validation ok, no `{{` warning.
|
||||
|
||||
- [ ] **Step 2: drift-0**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
python3 scripts/sync_automation.py commands --check; echo "exit=$?"
|
||||
```
|
||||
Expected: `checked 13 commands ...` + `exit=0`.
|
||||
|
||||
- [ ] **Step 3: 전체 테스트(세 번째 main commands check 테스트 포함)**
|
||||
|
||||
Run: `.venv/bin/python -m pytest scripts/test_sync_automation.py -q`
|
||||
Expected: all green.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
git add .agents/skills/ .agents/workflows/
|
||||
git commit -m "feat(sync): generate 13 commands as codex skills + antigravity workflows"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 대표 command 실호출 스모크 (CLI 있으면)
|
||||
|
||||
> Phase 0 agents와 달리 commands는 사용자가 실제로 호출하는 표면이므로, 설치된 CLI에서 1~2개를 실호출해 형식이 맞는지 확인한다. CLI 미설치면 SKIP + 노트 기록.
|
||||
|
||||
- [ ] **Step 1: codex skill 인식 확인 (codex 있으면)**
|
||||
|
||||
`.agents/skills/depth/SKILL.md` 가 codex의 `/skills` 또는 `$depth` 로 인식되는지 확인. 인식 안 되면 frontmatter/경로를 Task 1 노트와 대조해 진단.
|
||||
|
||||
- [ ] **Step 2: antigravity workflow 인식 확인 (antigravity 있으면)**
|
||||
|
||||
`.agents/workflows/depth.md` 가 antigravity에서 `/depth` 로 등록되는지 확인. 동시에 Task 1의 이중로드 가설(코덱스 skill이 antigravity에 새는지)을 `/skills` 목록으로 재확인.
|
||||
|
||||
- [ ] **Step 3: 결과를 Task 1 노트에 추가 기록 + (필요 시) 완화**
|
||||
|
||||
- 이중로드가 실제로 발생하고 바람직하지 않으면: 완화안을 노트에 적고 사용자에게 에스컬레이션(예: codex skill만 두고 antigravity는 workflow만 쓰도록 build 설정, 또는 경로 분리). **이 단계에서 임의로 큰 구조 변경하지 말 것** — 결과만 보고.
|
||||
|
||||
- [ ] **Step 4: Commit (노트 갱신 시)**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
git add docs/superpowers/notes/
|
||||
git commit -m "docs(phase1): empirical smoke results for codex skills / antigravity workflows"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 문서 갱신 (commands→skills/workflows 매핑)
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md` (§2 디렉터리 역할 표 또는 자동화 섹션)
|
||||
- Modify: `.codex/agents/README.md`
|
||||
|
||||
- [ ] **Step 1: CLAUDE.md 에 1단락**
|
||||
|
||||
§2의 자동화 목록에 codex/antigravity의 command 등가물을 명시:
|
||||
- Codex: `.agents/skills/<cmd>/SKILL.md` (13개, `$cmd` 호출), 생성 `python3 scripts/sync_automation.py commands`.
|
||||
- Antigravity: `.agents/workflows/<cmd>.md` (13개, `/cmd` 호출).
|
||||
- 인자는 placeholder 없이 자연어(각괄호 prose).
|
||||
|
||||
- [ ] **Step 2: `.codex/agents/README.md` 의 "Native generation" 노트에 commands 줄 추가**
|
||||
|
||||
`sync_automation.py commands` 로 13개 command가 `.agents/skills/`(codex) + `.agents/workflows/`(antigravity)에 생성됨을 1줄 추가.
|
||||
|
||||
- [ ] **Step 3: 검증 + Commit**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
grep -c "sync_automation.py commands" CLAUDE.md .codex/agents/README.md
|
||||
git add CLAUDE.md .codex/agents/README.md
|
||||
git commit -m "docs(sync): document commands -> codex skills + antigravity workflows"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 완료 기준 (Definition of Done)
|
||||
|
||||
- `sync_automation.py commands` 타깃 동작 + 테스트 green.
|
||||
- `.agents/skills/<cmd>/SKILL.md` 13개 + `.agents/workflows/<cmd>.md` 13개 생성·검증.
|
||||
- `python3 scripts/sync_automation.py commands --check` exit 0 (drift 0).
|
||||
- `$ARGUMENTS` 토큰이 생성물에 0개, antigravity workflow에 `{{ }}` 0개.
|
||||
- Task 1/5 경험적 확인 결과가 노트에 기록(또는 CLI 미설치 시 가정으로 명시).
|
||||
- CLAUDE.md + codex README에 매핑 문서화.
|
||||
|
||||
이후: **Phase 2**(hooks → `.codex/hooks.json`, `AGENTS.md`, CLAUDE.md의 구식 codex "수동 cat" 서술 정리 — line 76)는 별도 plan.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (작성자 체크)
|
||||
|
||||
- **Spec coverage**: 설계 §4 Phase 1(commands → codex skills + antigravity workflows, 생성기 commands 타깃 확장)의 모든 요소가 Task 2~4에 매핑. 설계 §6 리스크의 `.agents/skills/` 충돌은 Task 1/5 경험적 확인으로 처리.
|
||||
- **Placeholder scan**: 코드 스텝은 실제 코드 포함. Task 1/5는 경험적 절차라 결과가 환경 의존 — 명령 + 기록 표 + "UNVERIFIED 시 가정 명시" 로 결정론화.
|
||||
- **Type consistency**: `transform_command_body(body, arg_hint)`, `render_codex_skill(name,description,arg_hint,body)`, `render_antigravity_workflow(name,description,arg_hint,body)`, `CommandInputs`, `load_command_inputs(name)`, `generate_command_one(name,check,drift)` — 전 Task 일관. `_write_or_check`/`parse_frontmatter`는 Phase 0 함수 재사용.
|
||||
- **알려진 한계**: command 본문이 platform-neutral하다는 가정에 의존(실측: `.claude/commands/*.md`는 `Read tool` 류 미사용, shell·서브에이전트명·repo 경로만 참조). 만약 특정 command가 Claude 전용 표기를 쓰면 그 command만 생성 후 hand-review 필요 — Task 4 검증의 `$ARGUMENTS`/`{{`grep으로 1차 포착.
|
||||
@@ -0,0 +1,298 @@
|
||||
# 3-플랫폼 동기화 Phase 2 (마지막) — hooks + 프로젝트 지침 + 정리 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax.
|
||||
|
||||
**Goal:** Claude의 결정론 훅(`wiki_structure_lint.py` 구조 린트 + `wiki_claim_gate.py` claim 게이트)을 Codex CLI(`.codex/hooks.json`)에 연결하고, 세 플랫폼이 동일한 프로젝트 지침(`CLAUDE.md`)을 읽도록 codex `AGENTS.md`/config를 구성하며, Phase 0~1에서 생긴 문서 잔여를 정리해 동기화를 완결한다.
|
||||
|
||||
**Architecture:** 훅 스크립트는 이미 멀티-variant 입력 리더(`tool_name` / `tool_call.name`)를 갖췄지만 *차단 규약*과 *출력 스키마*가 플랫폼마다 다르다(Claude: exit 2+stderr / Antigravity: `{decision:deny}` JSON / Codex: 경험적 확인 필요). 따라서 Phase 2는 **(1) 이식성 버그 선수정**(claim_gate 하드코딩 ROOT) → **(2) codex 훅 payload·차단 규약 경험적 캡처**(codex 설치됨) → **(3) `.codex/hooks.json` 배선 + 필요 시 codex-variant 어댑트** → **(4) 프로젝트 지침 DRY 구성** → **(5) CLAUDE.md 잔여 정리** 순으로 간다. 훅 *스크립트 SSOT* 는 `.claude/hooks/` 1벌을 유지하고 플랫폼별 config 가 그것을 가리킨다(스크립트 중복 금지).
|
||||
|
||||
**Tech Stack:** Python 3.12, codex-cli 0.136.0, agy(Antigravity), pytest(venv).
|
||||
|
||||
---
|
||||
|
||||
## 배경 / 현재 훅 지형 (실측)
|
||||
|
||||
| 훅 | 위치 | root 해석 | 입력 | 차단 규약 |
|
||||
|---|---|---|---|---|
|
||||
| `wiki_structure_lint.py` | `.claude/hooks/` | `DEFAULT_ROOT = SCRIPT.parents[2]` (동적·이식 OK) | `--hook`(stdin JSON→file_path, C2 non-blocking) / `--file` / `--all` | non-blocking(경고) |
|
||||
| `wiki_claim_gate.py` | `.claude/hooks/` | **`ROOT=Path("/home/donghyeon/Documents/LLM Wiki")` 하드코딩 — 이 repo 경로 아님(버그)** | stdin JSON (`tool_name`/`tool_call.name`/`hook_event_name`) | Claude: exit 2 + stderr |
|
||||
| `wiki_hard_gate.py` | `~/.gemini/antigravity-cli/hooks/` (global) | — | antigravity protobuf (`tool_call.{name,input}`) | Antigravity: stdout `{decision:"deny"}` |
|
||||
|
||||
- Claude `.claude/settings.json`: PreToolUse(claim_gate, matcher `*`) + PostToolUse(structure_lint `--hook`, matcher `Write|Edit|MultiEdit`) + SubagentStart/Stop(claim_gate). 명령은 `python3 "$CLAUDE_PROJECT_DIR"/.claude/hooks/<script>`.
|
||||
- Antigravity `.agents/hooks.json`: `wiki-hard-gate` PreToolUse(matcher `*`) → global `wiki_hard_gate.py`. 이미 claim/self-grep 게이트 역할 수행.
|
||||
- Codex hooks(공식, `developers.openai.com/codex/hooks`): `<repo>/.codex/hooks.json` 또는 `config.toml [hooks]`. 이벤트 PreToolUse/PostToolUse/Stop/SessionStart/SubagentStart/SubagentStop 등. matcher(regex)+command. **payload·차단 규약은 본 plan Task 2 에서 경험적 캡처.**
|
||||
- Codex 프로젝트 지침: `AGENTS.md`(root-down 연결). config `project_doc_fallback_filenames` 로 다른 파일명을 지침으로 인정 가능 → **CLAUDE.md 를 codex 지침으로 재사용(DRY)** 가능.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `.claude/hooks/wiki_claim_gate.py` — 하드코딩 ROOT → 동적(`SCRIPT.parents[2]` 또는 `$CLAUDE_PROJECT_DIR`/cwd). 모든 플랫폼 공용 버그픽스.
|
||||
- Create: `.codex/hooks.json` — structure_lint(PostToolUse) + claim_gate(PreToolUse) 배선.
|
||||
- Create: `.codex/config.toml` — `project_doc_fallback_filenames = ["CLAUDE.md"]` (codex 가 CLAUDE.md 를 지침으로 읽도록, DRY).
|
||||
- Create (조건부): `AGENTS.md`(root) — config fallback 이 동작 안 하면 thin pointer 대안.
|
||||
- Create: `docs/superpowers/notes/2026-06-04-phase2-codex-hook-schema.md` — codex 훅 payload·차단 규약 경험적 기록.
|
||||
- Modify (정리): `CLAUDE.md` §2 표(line 48 `.claude/commands/` 행에 codex/antigravity 등가 1줄) + 자동화 섹션 잔여 "7개" 카운트/구식 서술.
|
||||
|
||||
> 훅 *스크립트* 는 `.claude/hooks/` 1벌만 SSOT. codex `.codex/hooks.json` 은 절대경로/`$CODEX_PROJECT_DIR` 로 그 스크립트를 가리키며 복제하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: claim_gate 하드코딩 ROOT 버그픽스 (공용 선수정)
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_claim_gate.py`
|
||||
- Test: `.claude/hooks/` 기존 테스트가 있으면 거기, 없으면 `scripts/test_sync_automation.py` 밖의 별도 확인.
|
||||
|
||||
- [ ] **Step 1: 현재 ROOT 사용처 확인**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
grep -nE "ROOT" .claude/hooks/wiki_claim_gate.py
|
||||
```
|
||||
사용처: L20 정의, L81 `ROOT / p`(상대→절대), L149 `relative_to(ROOT)`.
|
||||
|
||||
- [ ] **Step 2: 동적 root 로 교체**
|
||||
|
||||
`structure_lint.py` 와 동일 패턴 적용. L20 을:
|
||||
```python
|
||||
ROOT = Path(__file__).resolve().parents[2] # .claude/hooks/<this> -> repo root
|
||||
```
|
||||
로 교체(환경변수 우선이 필요하면 `Path(os.environ.get("CLAUDE_PROJECT_DIR") or Path(__file__).resolve().parents[2])` — 단 codex 는 `CODEX_PROJECT_DIR`, antigravity 는 다른 변수일 수 있으므로 **파일 위치 기반이 가장 이식적**. 파일 위치 기반으로 간다).
|
||||
|
||||
- [ ] **Step 3: 동작 확인 (allow 경로 + relative_to 안전)**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
# 무해한 read 툴 이벤트는 allow(exit 0)
|
||||
echo '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"README.md"}}' | python3 .claude/hooks/wiki_claim_gate.py; echo "exit=$?"
|
||||
# repo 내부 경로가 relative_to 에서 깨지지 않음
|
||||
echo '{"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"raw/branch-notes/x.md","content":"---\ntitle: t\n---\n"}}' | python3 .claude/hooks/wiki_claim_gate.py; echo "exit=$?"
|
||||
```
|
||||
Expected: 첫 명령 `exit=0`. 둘째는 게이트 규칙에 따라 0 또는 2 — **ValueError/traceback 이 없어야** 함(하드코딩 ROOT 였으면 `relative_to` 에서 깨졌음).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add .claude/hooks/wiki_claim_gate.py
|
||||
git commit -m "fix(hooks): claim_gate uses dynamic repo root (was hardcoded stale path) — portable across platforms"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: codex 훅 payload·차단 규약 경험적 캡처
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/superpowers/notes/2026-06-04-phase2-codex-hook-schema.md`
|
||||
|
||||
> codex 훅이 PreToolUse 에 넘기는 JSON 구조와 *차단 방법*(exit code? stdout JSON `decision`?)을 실제로 캡처해야 `.codex/hooks.json` 을 정확히 쓸 수 있다.
|
||||
|
||||
- [ ] **Step 1: codex 훅 문서/도움말 확인**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
codex --help 2>&1 | grep -iE "hook" || echo "no hook in top help"
|
||||
codex exec --help 2>&1 | grep -iE "hook" || echo "no hook flag in exec"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 캡처용 probe 훅으로 실제 payload 덤프**
|
||||
|
||||
`.codex/hooks.json` 에 임시 probe(stdin 을 파일로 덤프 후 allow)를 걸고 codex 로 1개 툴을 실행시켜 payload 를 캡처:
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
mkdir -p .codex
|
||||
cat > /tmp/codex-hook-probe.py <<'PY'
|
||||
import sys, json, pathlib
|
||||
data = sys.stdin.read()
|
||||
pathlib.Path("/tmp/codex-hook-payload.json").write_text(data)
|
||||
sys.exit(0) # allow
|
||||
PY
|
||||
cat > .codex/hooks.json <<'JSON'
|
||||
{ "hooks": { "PreToolUse": [ { "matcher": "*",
|
||||
"hooks": [ { "type": "command", "command": "python3 /tmp/codex-hook-probe.py", "timeout": 30 } ] } ] } }
|
||||
JSON
|
||||
# codex 로 안전한 read-only 작업 한 번 실행 (sandbox read-only, 승인 never)
|
||||
codex exec -s read-only -C /home/donghyeon/dev/llm-wiki-private "list the files in the repo root with ls" 2>&1 | tail -5 || true
|
||||
echo "--- captured payload ---"; cat /tmp/codex-hook-payload.json 2>/dev/null || echo "no payload captured"
|
||||
```
|
||||
캡처된 JSON 의 필드(`tool_name`? `tool_call.name`? `hook_event_name`?)를 노트에 기록.
|
||||
|
||||
- [ ] **Step 3: 차단 규약 확인**
|
||||
|
||||
probe 를 `sys.exit(2)` + stderr 로 바꿔 codex 가 툴을 *차단*하는지, 아니면 stdout JSON(`{"decision":"deny"}`)을 요구하는지 확인(공식 hooks 문서 + 실측). 결과를 노트에 기록.
|
||||
|
||||
- [ ] **Step 4: 정리 + 노트 커밋**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
rm -f .codex/hooks.json # probe 제거 (Task 3 에서 정식 작성)
|
||||
git add docs/superpowers/notes/2026-06-04-phase2-codex-hook-schema.md
|
||||
git commit -m "docs(phase2): capture codex hook payload + block convention (empirical)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `.codex/hooks.json` 배선 (+ 필요 시 claim_gate codex-variant)
|
||||
|
||||
**Files:**
|
||||
- Create: `.codex/hooks.json`
|
||||
- Modify (조건부): `.claude/hooks/wiki_claim_gate.py` (codex payload·차단 variant — Task 2 결과가 Claude 규약과 다를 때만)
|
||||
|
||||
- [ ] **Step 1: `.codex/hooks.json` 작성**
|
||||
|
||||
Task 2 에서 확인한 이벤트명/matcher 로:
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{ "matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [ { "type": "command",
|
||||
"command": "python3 .codex/../.claude/hooks/wiki_claim_gate.py",
|
||||
"timeout": 30 } ] } ],
|
||||
"PostToolUse": [
|
||||
{ "matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [ { "type": "command",
|
||||
"command": "python3 .codex/../.claude/hooks/wiki_structure_lint.py --hook",
|
||||
"timeout": 30 } ] } ]
|
||||
}
|
||||
}
|
||||
```
|
||||
> command 경로는 Task 2 에서 codex 훅의 cwd 가 repo root 인지 확인 후 결정: repo-root 이면 `python3 .claude/hooks/...` (상대), 아니면 절대경로 `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/...`. **하드코딩 절대경로는 이식성 해치므로 codex 가 제공하는 project-dir 변수(`$CODEX_PROJECT_DIR` 등, Task 2 확인)를 우선 사용.** matcher 정규식은 codex 의 실제 tool 이름(Task 2)으로 맞춤 — codex tool 이름이 `shell`/`apply_patch` 등이면 그에 맞게 조정.
|
||||
|
||||
- [ ] **Step 2: claim_gate codex-variant (조건부)**
|
||||
|
||||
Task 2 가 "codex 차단 = exit 2 / 입력 = `tool_call.name`" 으로 Claude 와 호환이면 **스크립트 수정 불필요**(이미 멀티-variant 리더). 다르면(예: codex 가 stdout JSON 요구) `wiki_claim_gate.py` 에 codex 분기 추가:
|
||||
- 입력: `hook_event_name`/`tool_name` 없으면 codex 필드에서 추출.
|
||||
- 출력: codex 차단 규약에 맞춰 emit. **기존 Claude/antigravity 경로 회귀 없이** 분기.
|
||||
TDD: codex payload 샘플(Task 2 캡처)을 fixture 로 한 단위 테스트 추가.
|
||||
|
||||
- [ ] **Step 3: 실호출 검증**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
# codex 가 구조 위반 파일 저장을 차단/경고하는지 (read-only 가 아닌 workspace-write 로 한 번)
|
||||
codex exec -s workspace-write -C /home/donghyeon/dev/llm-wiki-private "create a file raw/branch-notes/zzz-hook-test.md with body 'no frontmatter'" 2>&1 | tail -8 || true
|
||||
ls raw/branch-notes/zzz-hook-test.md 2>&1 # claim_gate 가 frontmatter 없는 raw 를 막았는지
|
||||
rm -f raw/branch-notes/zzz-hook-test.md
|
||||
```
|
||||
기대: claim_gate 규칙에 따라 차단되거나 경고. 결과를 노트에 기록.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add .codex/hooks.json .claude/hooks/wiki_claim_gate.py docs/superpowers/notes/
|
||||
git commit -m "feat(codex): wire .codex/hooks.json (claim gate + structure lint) reusing .claude/hooks scripts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 프로젝트 지침 DRY 구성 (codex AGENTS.md / config)
|
||||
|
||||
**Files:**
|
||||
- Create: `.codex/config.toml`
|
||||
- Create (조건부): `AGENTS.md`
|
||||
|
||||
- [ ] **Step 1: config fallback 시도 (DRY 우선)**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
cat > .codex/config.toml <<'TOML'
|
||||
# codex 가 CLAUDE.md 를 프로젝트 지침으로 읽게 함 (SSOT 중복 방지)
|
||||
project_doc_fallback_filenames = ["CLAUDE.md"]
|
||||
TOML
|
||||
# 확인: codex 가 CLAUDE.md 를 지침으로 로드하는지
|
||||
codex exec -s read-only -C /home/donghyeon/dev/llm-wiki-private "이 저장소의 운영 규칙 문서 이름과 첫 섹션 제목을 말해줘" 2>&1 | tail -8 || true
|
||||
```
|
||||
codex 가 CLAUDE.md 내용을 인지하면 **AGENTS.md 불필요**(DRY 달성). 노트 기록.
|
||||
|
||||
- [ ] **Step 2: fallback 미동작 시 thin AGENTS.md (대안)**
|
||||
|
||||
config fallback 이 안 먹으면 root 에 thin pointer 작성(중복 본문 금지):
|
||||
```markdown
|
||||
# AGENTS.md
|
||||
|
||||
이 저장소의 운영 규칙 SSOT 는 `CLAUDE.md` 다. 모든 agent/명령은 `CLAUDE.md` 와 `rules/` 를 정독한다.
|
||||
(이 파일은 codex/antigravity 가 AGENTS.md 를 우선 탐색할 때의 포인터일 뿐, 규칙 본문을 중복하지 않는다.)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .codex/config.toml AGENTS.md 2>/dev/null; git add .codex/config.toml
|
||||
git commit -m "feat(codex): read CLAUDE.md as project instructions via config fallback (DRY, no duplication)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: antigravity hooks 커버리지 점검 (보강 여부 결정)
|
||||
|
||||
**Files:**
|
||||
- Modify (조건부): `.agents/hooks.json`
|
||||
|
||||
- [ ] **Step 1: hard_gate 가 structure-lint/claim-gate 를 이미 커버하는지 판정**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
grep -nE "frontmatter|Parent|Claim|structure|template|raw/branch|decision" ~/.gemini/antigravity-cli/hooks/wiki_hard_gate.py | head
|
||||
```
|
||||
`wiki_hard_gate.py` 가 이미 claim/구조 게이트를 antigravity 스키마로 수행 중이면(현재 그렇게 보임) **추가 배선 불필요** — 그 사실을 노트에 명시하고 종료. 빠진 검사(예: structure_lint 의 C2)가 있고 antigravity 에서 필요하면 PreToolUse 항목 추가(단 antigravity `Stop` 은 응답 텍스트 못 봄 → content gate 는 PreToolUse 만).
|
||||
|
||||
- [ ] **Step 2: (보강 시) `.agents/hooks.json` 갱신 + Commit. (불필요 시) 노트만 커밋.**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
git add docs/superpowers/notes/ .agents/hooks.json 2>/dev/null
|
||||
git commit -m "docs(phase2): antigravity hook coverage assessment (hard_gate already covers claim/structure)" || echo "nothing to commit"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: CLAUDE.md / 문서 잔여 정리
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md`
|
||||
|
||||
- [ ] **Step 1: §2 표 + 잔여 카운트 정리**
|
||||
|
||||
- §2 디렉터리 역할 표(line 48 부근 `.claude/commands/`)에 "codex `.agents/skills/` · antigravity `.agents/workflows/` 로 동기화(`scripts/sync_automation.py commands`)" 1줄.
|
||||
- 자동화 섹션의 "동일 7개 agent" → "9개 agent" (branch-depth-auditor·coverage-auditor 추가 반영). antigravity bullet 도 동일.
|
||||
- (Phase 0~1 에서 이미 codex bullet·sync 경로는 갱신됨 — 누락분만.)
|
||||
|
||||
- [ ] **Step 2: 최종 동기화 sanity**
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
python3 scripts/sync_automation.py agents --check && python3 scripts/sync_automation.py commands --check && echo "ALL DRIFT-0"
|
||||
.venv/bin/python -m pytest scripts/test_sync_automation.py -q | tail -2
|
||||
grep -rn "convert-wiki-agents" . --include=*.md 2>/dev/null | grep -v docs/superpowers || echo "no dead convert-script refs"
|
||||
```
|
||||
Expected: `ALL DRIFT-0`, 테스트 green, dead ref 없음.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add CLAUDE.md
|
||||
git commit -m "docs: CLAUDE.md final cleanup — 9 agents, commands cross-platform mapping (Phase 2)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 완료 기준 (Definition of Done)
|
||||
|
||||
- claim_gate 하드코딩 ROOT 버그 수정(이식성).
|
||||
- codex 훅 payload·차단 규약 경험적 캡처 + `.codex/hooks.json` 배선 동작(실호출 검증).
|
||||
- codex 가 CLAUDE.md 를 프로젝트 지침으로 읽음(config fallback) 또는 thin AGENTS.md.
|
||||
- antigravity 훅 커버리지 판정(보강 또는 "이미 커버" 명시).
|
||||
- CLAUDE.md 잔여(카운트·매핑) 정리, dead ref 0.
|
||||
- `agents`/`commands` 모두 drift-0, 테스트 green.
|
||||
|
||||
이로써 **3-플랫폼 동기화 전체 완료**: agents(9)·commands(13)·hooks·project-instructions 가 Codex·Antigravity 에 Claude 와 동등하게 구성됨. 이후: 브랜치 merge/PR (`finishing-a-development-branch`).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (작성자 체크)
|
||||
|
||||
- **Spec coverage**: 설계 §4 Phase 2 의 모든 항목 — codex hooks.json, AGENTS.md/지침, antigravity hooks 보강, CLAUDE.md 정리 — Task 1~6 에 매핑. 추가로 발견된 claim_gate 하드코딩 ROOT 버그를 Task 1 로 선수정(공용 이득).
|
||||
- **Placeholder scan**: codex 훅 payload/차단 규약은 *미지(unknown)* 라 Task 2 에서 경험적 캡처 후 Task 3 가 그 결과에 분기하도록 구성(추측 금지). config fallback 동작 여부도 Task 4 에서 실측 후 분기.
|
||||
- **Type/일관성**: 훅 스크립트 SSOT 1벌(`.claude/hooks/`) 원칙 유지, 플랫폼 config 가 가리키기만 함. codex command/agent 생성기(Phase 0~1)와 독립.
|
||||
- **알려진 한계**: codex 훅의 정확한 payload/차단 규약이 docs 로 확정 안 됨 → Task 2 경험적 캡처에 의존. codex 가 설치돼 있어 실측 가능(미설치였으면 BLOCKED). antigravity 훅은 이미 hard_gate 로 커버될 가능성이 높아 Task 5 는 "판정 우선, 보강은 조건부".
|
||||
@@ -0,0 +1,405 @@
|
||||
# wiki/projects 전용 템플릿 · source_type 분리 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:** `wiki/projects/` canonical 레이어에 전용 템플릿 `wiki-project-template.md` + 전용 `source_type: project`를 부여하고, 결정론 린터가 새 템플릿 기준으로 wiki/projects 문서를 검증하게 해 현재 FAIL 중인 17개 문서를 PASS로 복구한다.
|
||||
|
||||
**Architecture:** 린터(`wiki_structure_lint.py`)는 source_type 어휘를 하드코딩하지 않고 `templates/*-template.md`의 frontmatter `source_type`을 읽어 동적 매핑한다. 따라서 새 템플릿이 `source_type: project`를 선언하면 `project`가 자동으로 유효 타입이 되고, 템플릿의 `##` 헤더가 필수 섹션 검사 기준이 된다. 유일한 코드 변경은 린터 `classify()`에 named-hub(형제 폴더를 가진 `wiki/<cat>/<slug>.md`) C1 면제를 추가하는 것.
|
||||
|
||||
**Tech Stack:** Markdown 템플릿/문서, Python 3 (린터 hook), Obsidian wikilink, bash 검증.
|
||||
|
||||
**제약 (사용자 지시):** **커밋하지 않는다.** 각 Task 끝의 "체크포인트"는 git commit 대신 린터 회귀 확인으로 대체한다.
|
||||
|
||||
**참조 spec:** `docs/superpowers/specs/2026-06-04-wiki-project-template-source-type-split-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| 파일 | 책임 | 변경 |
|
||||
|---|---|---|
|
||||
| `templates/wiki-project-template.md` | wiki/projects canonical 슬라이스 출력 형식 정의 (`source_type: project`, 9 필수 섹션) | 신규 |
|
||||
| `.claude/hooks/wiki_structure_lint.py` | `classify()`에 named-hub C1/C3 면제 추가 | 수정 (유일 코드) |
|
||||
| `CLAUDE.md` | §2 템플릿 목록 / §4 source_type 어휘 / §5 신뢰도 표에 `project` 등록 | 수정 |
|
||||
| `rules/tag-taxonomy.md`, `rules/naming-conventions.md` | `project` 어휘 등록 | 수정 |
|
||||
| `wiki/projects/ca-tmpl.md` + `wiki/projects/ca-tmpl/*.md` (16) | `source_type: project-note → project` + 1개 섹션 보강 | 수정 (17 파일) |
|
||||
| `.claude/commands/ingest.md`, `.claude/commands/projectize.md` | wiki 추출 템플릿 참조 교정 | 수정 |
|
||||
| `rules/linking-rules.md` | 224행(nested) 참조 교정, 190행(raw) 유지 | 수정 |
|
||||
| `.claude/agents/wiki-research-lane.md` + `.codex/agents/wiki-research-lane.md` + `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md` | agent 본문 참조 교정 후 `scripts/sync_automation.py` 재생성 | 수정 |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 베이스라인 캡처 + 새 템플릿 생성 + 파일럿 슬라이스 1개 검증
|
||||
|
||||
**Files:**
|
||||
- Create: `templates/wiki-project-template.md`
|
||||
- Modify(파일럿): `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` frontmatter 1줄
|
||||
|
||||
- [ ] **Step 1: 현재 베이스라인 기록**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/donghyeon/dev/llm-wiki-private
|
||||
python3 .claude/hooks/wiki_structure_lint.py --all 2>&1 | tail -20
|
||||
```
|
||||
Expected: `== 요약: 482개 중 FAIL 238 / PASS 244 ==`, `source_type별 FAIL` 에 `project-note 20`. 이 숫자를 메모(검증 기준).
|
||||
|
||||
- [ ] **Step 2: 새 템플릿 생성**
|
||||
|
||||
Create `templates/wiki-project-template.md` (헤더 텍스트는 기존 슬라이스와 byte 동일 — `header_tokens` 매칭 필수):
|
||||
|
||||
````markdown
|
||||
---
|
||||
title:
|
||||
source_type: project
|
||||
status: draft
|
||||
confidence: unknown
|
||||
tags: []
|
||||
related_projects: []
|
||||
last_reviewed:
|
||||
---
|
||||
|
||||
# {{title}}
|
||||
|
||||
> Layer: `wiki/projects/` — canonical 실무 적용 문서(내 프로젝트 사실). 일반 개념은 `wiki/concepts/`, raw 프로젝트 hub는 `raw/project-notes/`(`project-template.md`) 사용.
|
||||
> 본 문서는 **하나의 토픽/결정 영역** 슬라이스다. 프로젝트 전체 hub(아키텍처·시퀀스·Cluster)는 `wiki/projects/<project>.md` named-hub(MOC)와 그 SSOT인 `raw/project-notes/` 가 담당한다.
|
||||
> 증거 등급(`actually-implemented`/`locally-verified`/`prod-verified`/`documented-only`/`planned`)을 섹션별로 분리해 외부 공개 가능 범위를 명확히 한다 (CLAUDE.md §6/§15).
|
||||
|
||||
## 프로젝트 컨텍스트
|
||||
|
||||
> 이 슬라이스가 다루는 결정/토픽의 배경. 문제 배경 + 검토한 선택지 + 결정 이유를 여기에 접어 서술(별도 필수 섹션 아님). 외부인이 "무엇을 왜 이렇게 했는가"를 1분에 이해할 수 있어야 함.
|
||||
|
||||
## 실제 구현 내용 (`actually-implemented`)
|
||||
|
||||
> 코드에 실제 존재하는 것만. 파일·클래스·task 단위로 구체적으로. 면접에서 "구현했다"고 말해도 되는 부분. 가능하면 ground-truth(레포 경로/커밋) 대조 근거를 함께.
|
||||
|
||||
## 로컬/dev 검증 (`locally-verified`)
|
||||
|
||||
> 로컬 또는 dev 환경에서 동작 확인한 부분. 어떻게 검증했는지(테스트 명령/로그/측정값)를 명시.
|
||||
|
||||
## 운영 검증 (`prod-verified`)
|
||||
|
||||
> 운영(prod) 환경에서 검증된 부분. 릴리즈 노트/운영 로그/모니터링/인시던트 근거. 없으면 "없음"이라고 명시.
|
||||
|
||||
## 문서/계획만 존재 (`documented-only` / `planned`)
|
||||
|
||||
> 설계/문서에만 있고 아직 구현 안 된 것. 면접·외부 공개에서 "구현했다"고 말하면 안 되는 부분. 후속 branch로 위임되는 항목은 링크.
|
||||
|
||||
## 면접에서 말할 수 있는 범위
|
||||
|
||||
> 자신 있게 / 적당히 / 답하면 안 되는 범위로 구분. 증거 등급과 일치해야 함.
|
||||
|
||||
## 과장 금지 지점
|
||||
|
||||
> 외부 설명 시 사실보다 부풀려지기 쉬운 표현. 자기 검열용.
|
||||
|
||||
## 관련 개념
|
||||
|
||||
> `[[wiki/concepts/...]]` 양방향 링크. 일반 개념과 본 프로젝트 사실을 연결.
|
||||
|
||||
## Sources
|
||||
|
||||
> 근거. 추출 출처 branch-note/raw, 그리고 ground-truth 레포. `[[raw/branch-notes/...]]`, `[[raw/project-notes/...]]` 등.
|
||||
````
|
||||
|
||||
- [ ] **Step 3: 템플릿이 `project` 타입을 매핑하는지 확인 (아직 문서 미변경)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl/clean-architecture-package-layout.md 2>&1 | head -3
|
||||
```
|
||||
Expected: 여전히 FAIL (이 문서는 아직 `source_type: project-note`라 `project-template.md`에 매핑되어 14개 MISSING_SECTION). 템플릿 추가만으로는 기존 문서 영향 없음 — 정상.
|
||||
|
||||
- [ ] **Step 4: 파일럿 1개 슬라이스 마이그레이션**
|
||||
|
||||
Edit `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` frontmatter: `source_type: project-note` → `source_type: project` (Write/Edit 도구 사용 — shell 직접 쓰기는 hook이 차단).
|
||||
|
||||
- [ ] **Step 5: 파일럿 PASS 검증**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl/clean-architecture-package-layout.md 2>&1 | head -5
|
||||
```
|
||||
Expected: `PASS wiki/projects/ca-tmpl/clean-architecture-package-layout.md` (MISSING_SECTION 0). 만약 일부 섹션이 여전히 누락으로 뜨면 = 템플릿 헤더 텍스트와 문서 헤더가 byte 불일치(위험 V1). 해당 헤더를 문서에서 복사해 템플릿을 정정 후 재실행.
|
||||
|
||||
- [ ] **Step 6: 체크포인트 (커밋 대신)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
Expected: `templates/wiki-project-template.md`(신규) + 파일럿 슬라이스 1개 수정. 커밋하지 않음.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 린터 `classify()` named-hub 면제 + hub 검증
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_structure_lint.py:52-62` (classify 함수), `:490` (호출부)
|
||||
- Modify: `wiki/projects/ca-tmpl.md` frontmatter 1줄
|
||||
|
||||
- [ ] **Step 1: hub 현재 상태 확인 (변경 전)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl.md 2>&1 | head -3
|
||||
```
|
||||
Expected: FAIL — `## 1. 프로젝트 개요 / Overview` 등 14개 MISSING_SECTION (hub이 raw `project-template`에 매핑됨).
|
||||
|
||||
- [ ] **Step 2: `classify()`에 named-hub 면제 추가**
|
||||
|
||||
`.claude/hooks/wiki_structure_lint.py`의 `classify` 함수(현재 52~62행)를 아래로 교체:
|
||||
|
||||
```python
|
||||
def classify(rel, root=None):
|
||||
"""문서를 검사 모드로 분류: 'full'(C1+C2+C3) | 'links'(C2만).
|
||||
- raw/wiki 의 *콘텐츠* 문서(2단계 이상, hub/log 아님) : 전체.
|
||||
- named-hub (wiki/<cat>/<slug>.md + 형제 폴더 <slug>/ 존재, linking-rules §12) : 링크만 (C1/C3 면제).
|
||||
- 그 외 전부 (rules/ · templates/ · docs/ · 최상위 CLAUDE.md 등 · hub/MOC/log/README) : 링크만.
|
||||
(템플릿 구조가 없거나 메타 문서이므로 C1/C3 면제, 그래프 ghost 방지용 C2 만.)
|
||||
"""
|
||||
parts = rel.split("/")
|
||||
base = parts[-1]
|
||||
# named-hub folder-note: <cat>/<slug>.md 에 형제 폴더 <slug>/ 가 있으면 MOC → 링크만
|
||||
if root is not None and len(parts) == 3 and parts[0] in ("raw", "wiki") and base.endswith(".md"):
|
||||
slug = base[:-3]
|
||||
if (root / parts[0] / parts[1] / slug).is_dir():
|
||||
return "links"
|
||||
if parts[0] in ("raw", "wiki") and len(parts) > 2 and base not in LINK_ONLY_BASENAMES:
|
||||
return "full"
|
||||
return "links"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 호출부에 `root` 전달**
|
||||
|
||||
`.claude/hooks/wiki_structure_lint.py`의 호출부(현재 490행) 교체:
|
||||
|
||||
```python
|
||||
mode = "links" if args.links_only else classify(rel, root)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: hub이 면제되어 PASS 되는지 확인 (아직 source_type 미변경)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl.md 2>&1 | head -3
|
||||
```
|
||||
Expected: `PASS wiki/projects/ca-tmpl.md` (형제 폴더 `wiki/projects/ca-tmpl/` 존재 → links 모드, C1 면제). source_type 변경 전이라도 면제만으로 14 FAIL 해소.
|
||||
|
||||
- [ ] **Step 5: hub frontmatter 마이그레이션**
|
||||
|
||||
Edit `wiki/projects/ca-tmpl.md` frontmatter: `source_type: project-note` → `source_type: project` (Write/Edit 도구).
|
||||
|
||||
- [ ] **Step 6: hub 재검증 + 회귀 (raw 3개 불변)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl.md 2>&1 | head -3
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/ca-skeleton-operational-contract.md 2>&1 | head -3
|
||||
```
|
||||
Expected: hub `PASS`. raw/project-notes 문서는 **상태 불변**(형제 폴더 없음 → 여전히 full 모드, 종전 FAIL/PASS 그대로). raw가 PASS→FAIL 또는 면제로 바뀌면 V2 위험 발생 — classify 조건의 `is_dir()` 가드 확인.
|
||||
|
||||
- [ ] **Step 7: 체크포인트**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git diff --stat .claude/hooks/wiki_structure_lint.py
|
||||
```
|
||||
Expected: classify 함수 + 호출부만 변경. 커밋하지 않음.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 나머지 15개 슬라이스 일괄 마이그레이션 + 누락 섹션 보강
|
||||
|
||||
**Files:**
|
||||
- Modify: `wiki/projects/ca-tmpl/*.md` 중 파일럿 제외 15개 frontmatter
|
||||
- Modify: `wiki/projects/ca-tmpl/api-error-envelope-design.md` — `## 문서/계획만 존재` 섹션 추가
|
||||
|
||||
- [ ] **Step 1: 대상 15개 목록 확인**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
ls -1 wiki/projects/ca-tmpl/*.md | grep -v clean-architecture-package-layout
|
||||
```
|
||||
Expected: 15개 경로(api-error-envelope-design, api-evolution-and-schema, config-and-adapter-templates, data-layer-persistence-cache-outbound, devops-ci-supply-chain-dx, idempotency-key-design, multi-tenancy-isolation-patterns, observability-log-metric-trace-runbook, privacy-file-domain-modeling, runtime-container-health-migration, sample-fixture-and-adoption, security-baseline-jwt-actuator-secrets, skeleton-governance-registry-verification-test-scorecard, transactional-outbox-pattern, transaction-boundary-abstraction).
|
||||
|
||||
- [ ] **Step 2: 15개 frontmatter 마이그레이션**
|
||||
|
||||
각 파일에 대해 Edit 도구로 `source_type: project-note` → `source_type: project`. (15개 개별 Edit. shell `sed` 금지 — hook 차단.)
|
||||
|
||||
- [ ] **Step 3: 누락 섹션 보강 (api-error-envelope-design.md)**
|
||||
|
||||
`wiki/projects/ca-tmpl/api-error-envelope-design.md`에 `## 운영 검증` 섹션 뒤(또는 `## 면접에서 말할 수 있는 범위` 앞)에 아래 섹션 추가:
|
||||
|
||||
```markdown
|
||||
## 문서/계획만 존재 (`documented-only` / `planned`)
|
||||
|
||||
- (해당 시점 기준 documented-only / planned 항목을 여기에. 없으면 "현재 없음 — 본 슬라이스 범위는 모두 구현·검증됨"으로 명시.)
|
||||
```
|
||||
|
||||
> 주: 실제 내용은 해당 문서의 사실에 맞게 채운다. 최소한 빈 플레이스홀더가 아니라 "현재 없음" 또는 실제 planned 항목이어야 한다(린터는 섹션 헤더 존재만 검사하지만, CLAUDE.md 과장 금지 정책상 내용 정직성 유지).
|
||||
|
||||
- [ ] **Step 4: 16개 슬라이스 전수 PASS 검증**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
for f in wiki/projects/ca-tmpl/*.md; do
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file "$f" 2>&1 | grep -E "^(PASS|FAIL)"
|
||||
done
|
||||
```
|
||||
Expected: 16줄 모두 `PASS`. FAIL이 있으면 해당 문서의 헤더 텍스트를 템플릿 9개와 대조(byte 일치 확인).
|
||||
|
||||
- [ ] **Step 5: 체크포인트**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git status --short wiki/projects/
|
||||
```
|
||||
Expected: hub 1 + nested 16 = 17개 수정. 커밋하지 않음.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: source_type 어휘 등록 (SSOT 문서)
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md` (§2 템플릿 목록, §4 메타데이터 어휘, §5 신뢰도 표)
|
||||
- Modify: `rules/tag-taxonomy.md:22`, `rules/naming-conventions.md` §2.11 근처
|
||||
|
||||
- [ ] **Step 1: CLAUDE.md §4 어휘에 `project` 추가**
|
||||
|
||||
`CLAUDE.md` §4의 `source_type 허용 어휘` 문장에 `project`를 추가. `concept | interview | portfolio | blog | llm-generated` 나열에 `project`를 포함시키고, "wiki/projects canonical 실무 적용 문서 → `source_type: project` (`wiki-project-template.md`)" 한 줄 설명 추가. `project-note`는 "raw 프로젝트 hub"로 의미 유지 명시.
|
||||
|
||||
- [ ] **Step 2: CLAUDE.md §5 신뢰도 표에 `project` 행 추가**
|
||||
|
||||
`| project-note | 포트폴리오 증거 후보 |` 행 아래(또는 `concept` 근처)에 추가:
|
||||
```markdown
|
||||
| `project` | wiki/projects canonical 실무 적용 문서 (검증된 내 프로젝트 사실, `verified` 지향) |
|
||||
```
|
||||
|
||||
- [ ] **Step 3: CLAUDE.md §2 템플릿 목록 갱신**
|
||||
|
||||
`[[templates/project-template]]` 설명을 `raw/project-notes/` **전용**으로 명확화하고, 그 아래에 추가:
|
||||
```markdown
|
||||
- [[templates/wiki-project-template]] — `wiki/projects/` canonical 실무 적용 문서 슬라이스 (raw hub에서 /ingest·/projectize 로 추출)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: rules/tag-taxonomy.md L1 Type 어휘에 `project` 추가**
|
||||
|
||||
`rules/tag-taxonomy.md:22`의 L1 Type 나열(`... project-note, concept, interview, portfolio, blog, meta`)에 `project`를 추가(`project-note` 와 별도).
|
||||
|
||||
- [ ] **Step 5: rules/naming-conventions.md 에 source_type 명시**
|
||||
|
||||
`rules/naming-conventions.md`의 `wiki/projects/<project-slug>/<topic>.md` (nested, §2.11 근처) 설명에 "frontmatter `source_type: project`" 한 줄 추가. 경로 규칙 자체는 불변.
|
||||
|
||||
- [ ] **Step 6: 검증 (어휘 문서가 그래프 깨짐 없는지)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file CLAUDE.md 2>&1 | grep -E "BROKEN|PASS|FAIL" | head
|
||||
git grep -n "source_type: project\b" CLAUDE.md rules/ | head
|
||||
```
|
||||
Expected: CLAUDE.md 링크 위반 0. `project` 어휘가 SSOT 문서에 등록됨.
|
||||
|
||||
- [ ] **Step 7: 체크포인트** — `git status --short`. 커밋하지 않음.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 커맨드 · 룰 · agent 참조 정합
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/commands/ingest.md`, `.claude/commands/projectize.md`
|
||||
- Modify: `rules/linking-rules.md:224` (190 유지)
|
||||
- Modify: `.claude/agents/wiki-research-lane.md`, `.codex/agents/wiki-research-lane.md`, `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md`
|
||||
- Run: `scripts/sync_automation.py`
|
||||
|
||||
- [ ] **Step 1: /ingest 템플릿 참조 교정**
|
||||
|
||||
`.claude/commands/ingest.md` 4단계 "프로젝트 (`wiki/projects/`): `templates/project-template.md`" → `templates/wiki-project-template.md`.
|
||||
|
||||
- [ ] **Step 2: /projectize 템플릿 참조 교정**
|
||||
|
||||
`.claude/commands/projectize.md:34` "`templates/project-template.md` 적용" → "`templates/wiki-project-template.md` 적용".
|
||||
|
||||
- [ ] **Step 3: linking-rules nested 참조 교정 (raw 유지)**
|
||||
|
||||
`rules/linking-rules.md:224` (`wiki/projects/<project-slug>/<topic>.md (nested)` 행)의 `project-template.md` → `wiki-project-template.md`. **190행(`raw/project-notes/` 행)은 변경하지 않음.**
|
||||
|
||||
- [ ] **Step 4: wiki-research-lane 3-port 본문 교정**
|
||||
|
||||
3개 파일 각각에서 "wiki-project 시 `project-template.md`" → "wiki-project 시 `wiki-project-template.md`":
|
||||
- `.claude/agents/wiki-research-lane.md` (47행 부근)
|
||||
- `.codex/agents/wiki-research-lane.md`
|
||||
- `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md`
|
||||
|
||||
- [ ] **Step 5: agent 머신 파일 재생성**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 scripts/sync_automation.py
|
||||
git diff --stat .codex/ .agents/agents/
|
||||
```
|
||||
Expected: `.codex/agents/wiki-research-lane.toml`·`.agents/agents/wiki-research-lane/agent.json` 에 참조 변경만 반영(다른 agent 파일 변동 없음). 스크립트 에러 0.
|
||||
|
||||
- [ ] **Step 6: 잔존 "wiki 추출 의미" 참조 0 확인**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git grep -n "project-template" -- ':!docs/superpowers/' ':!templates/wiki-project-template.md'
|
||||
```
|
||||
Expected: 결과의 모든 `project-template` 참조가 **raw/project-notes hub 의미**만 남음 (linking-rules:190, 그 외 raw 맥락). wiki/projects 추출 의미의 잔존 참조 0.
|
||||
|
||||
- [ ] **Step 7: 체크포인트** — `git status --short`. 커밋하지 않음.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 최종 전수 검증 (완료 정의)
|
||||
|
||||
- [ ] **Step 1: 전체 린터 회귀**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 .claude/hooks/wiki_structure_lint.py --all 2>&1 | tail -20
|
||||
```
|
||||
Expected:
|
||||
- 총 FAIL이 **238 → 221 근처로 감소** (project 17개 PASS 전환).
|
||||
- `source_type별 FAIL`에서 `project-note` 가 20 → 3(raw만)으로 감소, `project` 는 0(전부 PASS).
|
||||
- 신규 PASS→FAIL 역전 0건.
|
||||
|
||||
- [ ] **Step 2: project 17개 전수 PASS**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl.md 2>&1 | grep -E "^(PASS|FAIL)"
|
||||
for f in wiki/projects/ca-tmpl/*.md; do
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file "$f" 2>&1 | grep -E "^(PASS|FAIL)"
|
||||
done
|
||||
```
|
||||
Expected: 17줄 모두 `PASS`.
|
||||
|
||||
- [ ] **Step 3: raw/project-notes 불변 확인**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
for f in raw/project-notes/*.md; do
|
||||
echo -n "$f: "; python3 .claude/hooks/wiki_structure_lint.py --file "$f" 2>&1 | grep -Eo "^(PASS|FAIL)" | head -1
|
||||
done
|
||||
```
|
||||
Expected: 3개 모두 베이스라인(Task 1 Step 1)과 동일한 상태 — 본 작업으로 바뀌지 않음.
|
||||
|
||||
- [ ] **Step 4: 최종 변경 요약 (커밋 없이 사용자 검토용)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git status --short
|
||||
git diff --stat
|
||||
```
|
||||
Expected 변경 파일: 신규 템플릿 1, 린터 1, CLAUDE.md 1, rules 2, wiki/projects 17, commands 2, linking-rules 1, wiki-research-lane ×3 + 재생성 산출물. **커밋하지 않고** 사용자에게 결과 보고.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (작성자 체크)
|
||||
|
||||
- **Spec coverage:** 신규 템플릿(Task1) / 린터 classify 면제(Task2) / 17개 마이그레이션(Task1·2·3) / 어휘 등록(Task4) / 커맨드·rule·agent 참조(Task5) / 검증계획 §5(Task6) — 전부 태스크 매핑됨.
|
||||
- **Placeholder scan:** 누락 섹션 보강(Task3 Step3)은 "현재 없음" 또는 실제 항목으로 채우라고 명시 — 빈 플레이스홀더 금지 규칙 반영.
|
||||
- **Type/이름 일관성:** `wiki-project-template.md`, `source_type: project`, `classify(rel, root)` 시그니처가 전 태스크에서 일관.
|
||||
- **위험 반영:** V1(헤더 byte 불일치 → Task1 Step5 회귀), V2(면제 과적용 → Task2 Step6 + Task6 Step3 raw 불변 확인).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,746 @@
|
||||
# project-note 작성 파이프라인 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:** project-note 에 `/project`(스캐폴딩) + `/project-spec`(깊은 조사 오케스트레이터 + readiness 게이트) 2단 파이프라인을 추가해, 임의 프로젝트 hub 를 `ca-skeleton-operational-contract.md` 수준(caliber)으로 작성하게 한다.
|
||||
|
||||
**Architecture:** branch-note 의 `/branch`·`/branch-spec`·`branch-depth-gate`·`branch-depth-auditor` 4종을 project-note 용으로 미러링한다. 단 결정론 게이트는 *섹션명 매칭*이 아니라 *구조-불가지 proxy*(exemplar 가 template 섹션 구성을 안 따르기 때문)이며, caliber 판정은 신규 `project-readiness-auditor` LLM agent 가 R1~R4 로 한다. **Claude Code 전용** — Codex/Antigravity 포팅 없음.
|
||||
|
||||
**Tech Stack:** Markdown(rules/templates/commands/agents), Python 3 stdlib(`wiki_structure_lint.py` 확장 + `unittest`).
|
||||
|
||||
**설계 출처:** `docs/superpowers/specs/2026-06-05-project-note-pipeline-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| 파일 | 책임 | 신규/편집 |
|
||||
|---|---|---|
|
||||
| `rules/project-readiness-gate.md` | 4축(R1~R4)·깊이 사다리·proxy·실패 모드 정의 (방법론 SSOT) | 신규 |
|
||||
| `templates/project-template.md` | §8.0 Branch 분해/실행계획 표 추가 (핸드오프) | 편집 |
|
||||
| `.claude/hooks/wiki_structure_lint.py` | `"project"` 모드 + proxy 검사 함수 | 편집 |
|
||||
| `.claude/hooks/test_wiki_structure_lint.py` | project 모드 단위 테스트 | 편집 |
|
||||
| `.claude/agents/project-readiness-auditor.md` | hub 깊이 의미 판정 (read-only) | 신규 |
|
||||
| `.claude/commands/project.md` | 얇은 스캐폴딩 커맨드 | 신규 |
|
||||
| `.claude/commands/project-spec.md` | 깊은 조사 오케스트레이터 | 신규 |
|
||||
| `CLAUDE.md` | agent 목록 +1(Claude 전용 명시) · 커맨드 목록 +2 | 편집 |
|
||||
|
||||
Task 순서는 의존성 순(rules → template → linter → agent → commands → CLAUDE.md → dogfood). 각 Task 는 독립 커밋.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: rules/project-readiness-gate.md (방법론 SSOT)
|
||||
|
||||
**Files:**
|
||||
- Create: `rules/project-readiness-gate.md`
|
||||
|
||||
- [ ] **Step 1: 파일 작성**
|
||||
|
||||
아래 내용 그대로 생성:
|
||||
|
||||
````markdown
|
||||
# rules/project-readiness-gate — project-note 작성 완성도 게이트
|
||||
|
||||
> `rules/` 의 방법론 규칙. project-note(프로젝트 hub) 1개가 **다른 작업의 출발점이 될 만큼 깊고 근거 있는가**를 판정한다.
|
||||
> 기준선은 `raw/project-notes/ca-skeleton-operational-contract.md` 의 *caliber*(엄격성 수준)이다 — 그 노트의 *내용·섹션 구성을 복제하라는 게 아니다*. 프로젝트마다 내용도 섹션 조직도 다르며, 게이트는 *깊이·근거·분해 수준*만 강제한다.
|
||||
> `branch-depth-gate`(브랜치 1개 착수 깊이)와 다른 층: 본 게이트는 *프로젝트 hub* 미시 게이트.
|
||||
|
||||
## 적용
|
||||
|
||||
- 대상: `raw/project-notes/*.md`.
|
||||
- 실행: `/project-spec <slug> <목표>` 의 **내부 마지막 단계** (독립 `/project-readiness` 커맨드 없음) →
|
||||
1. **1차 결정론 검사** `.claude/hooks/wiki_structure_lint.py` (project 모드 — proxy + 링크)
|
||||
2. **2차 의미 판정** `project-readiness-auditor` (아래 4축 — 노트와 링크된 소스를 읽고 의미로 판정)
|
||||
- 본 게이트는 **read-only**. 노트를 편집하지 않으며 판정을 노트에 박지 않는다.
|
||||
|
||||
## 왜 결정론 계층이 *섹션명 매칭*이 아닌가
|
||||
|
||||
exemplar `ca-skeleton-operational-contract.md` 는 project-template §1~14 가 아니라 계약 특화 자기 구조(§1 목표 … §30 아키텍처 … §33 checklist)를 쓴다. "project-template 섹션 존재"를 강제하면 *exemplar 자신이 탈락*한다. 따라서 1차는 **구조-불가지 proxy**(섹션명 무관, 존재만)만 본다. *깊이/caliber* 는 전적으로 2차 auditor.
|
||||
|
||||
## 역할 분담 (결정론 proxy vs 의미)
|
||||
|
||||
| | 1차 린터(proxy, 존재) | 2차 감사기(LLM 의미, 깊이) |
|
||||
|---|---|---|
|
||||
| R1 문제·성공 구체성 | (해당 proxy 없음) | 측정가능 기준인가, 추상 표현("잘 동작")인가 |
|
||||
| R2 아키텍처·시퀀스 | `PROJECT_NO_DIAGRAM` (임베디드 다이어그램 0개) | 다이어그램이 컨퍼런스급인가, happy+error 시퀀스인가 |
|
||||
| R3 결정 근거성 | 링크 깨짐만 | 기술결정이 대안+외부근거로 뒷받침되나, 맨주장인가 |
|
||||
| R4 Branch 분해 | `PROJECT_NO_BRANCH_TABLE` (분해표 부재) | 각 branch 가 valid slug + 측정가능 목표조건인가 |
|
||||
|
||||
→ 2차 감사기는 **의미만** 본다(존재는 1차가 확인).
|
||||
|
||||
## 4축 (R1~R4)
|
||||
|
||||
> 축 라벨은 `R1~R4`. branch-depth-gate 와 동일 라벨 체계지만 *대상이 다르다*(branch 1개가 아니라 프로젝트 hub).
|
||||
|
||||
| 축 | Pass 조건 | Blocking(Not-ready) 트리거 |
|
||||
|---|---|---|
|
||||
| **R1. 문제·성공 구체성** | §문제정의가 구체 시나리오/수치, 성공기준이 측정가능 | 성공기준이 "잘 동작한다" 류 추상 표현뿐 |
|
||||
| **R2. 아키텍처·시퀀스 깊이** | 아키텍처 다이어그램 존재 + `wiki-diagram-reviewer` ≥95, 핵심 시퀀스가 happy+error path | 다이어그램 없음 / 시퀀스가 happy path 만 |
|
||||
| **R3. 결정 근거성** | 각 주요 기술결정이 검토 대안 + 외부근거 wikilink(official/회사블로그) 보유 | 기술결정이 근거 없는 맨주장 |
|
||||
| **R4. Branch 분해 실행가능성** | 각 자식 branch 가 naming-conventions 준수 slug + 측정가능 목표조건. 결정 *내용*은 hub 에 적지 않음 | 분해표 부재 / slug 또는 목표조건 누락 |
|
||||
|
||||
## 깊이 사다리 (R1~R4 공통)
|
||||
|
||||
| 레벨 | 항목이 답하는 것 | 판정 |
|
||||
|---|---|---|
|
||||
| **L0 존재** | "섹션이 있다 / 항목이 적혀 있다" | 단독 불충분 |
|
||||
| **L1 메커니즘** | 어떻게/왜 — 구체 시나리오·메커니즘·근거 링크 | 최소선 |
|
||||
| **L2 조건·경계** | 언제 적용/제외, 실패/대안, 측정 기준 | Ready 최소선 |
|
||||
| **L3 검증** | 측정값·다이어그램 점수·검증 등급 근거 | 가산점 |
|
||||
|
||||
## 판정 규칙
|
||||
|
||||
- 심각도 3단계: `Blocking`(Not-ready) · `Should-fix`(권고) · `Advisory`(참고).
|
||||
- **Ready = 4축 모두 L2+ (Blocking 0).** 이것이 "ca-skeleton caliber" 의 조작적 정의. Should-fix 가 남아도 사용자 "감수" 선언 시 진행 가능(리포트에 기록).
|
||||
- 모든 finding 은 4종 세트로 근거화: `심각도 · 위치(섹션/행) · 예상 문제("이 hub 를 출발점 삼는 다음 작업자가 여기서 ___를 되묻게 됨") · 채울 방법`. 근거 없는 지적 금지.
|
||||
|
||||
## 명명된 실패 모드
|
||||
|
||||
- `ABSTRACT_SUCCESS_CRITERION` (R1): 성공기준이 측정 불가 추상 표현.
|
||||
- `DIAGRAM_MISSING_OR_WEAK` (R2): 아키텍처 다이어그램 없음 또는 ≥95 미달.
|
||||
- `HAPPY_PATH_ONLY_SEQUENCE` (R2): 시퀀스에 error path 없음.
|
||||
- `UNSOURCED_TECH_DECISION` (R3): 기술결정에 대안·외부근거 없음.
|
||||
- `BRANCH_DECOMP_INCOMPLETE` (R4): 분해표 부재 또는 slug/목표조건 누락.
|
||||
|
||||
## proxy(1차 결정론) — `wiki_structure_lint.py` project 모드
|
||||
|
||||
- `PROJECT_NO_DIAGRAM` — 임베디드 다이어그램 0개(`![[....drawio` 임베드도 ```` ```mermaid ```` 블록도 없음). R2 존재 proxy.
|
||||
- `PROJECT_NO_BRANCH_TABLE` — Branch 분해표 부재(heading 토큰에 `branch`/`브랜치` 포함 + 그 아래 markdown 표). R4 존재 proxy.
|
||||
- `MISSING_FRONTMATTER` — project-template frontmatter 필수 키 누락(기존 검사 재사용).
|
||||
- C2 링크(BROKEN_LINK 등) — 그대로.
|
||||
|
||||
proxy 는 *존재* 만 본다. 임베디드 다이어그램이 컨퍼런스급인지, 분해표 row 가 측정가능한지는 2차 auditor 가 판정한다.
|
||||
````
|
||||
|
||||
- [ ] **Step 2: 링크 무결성 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --file rules/project-readiness-gate.md`
|
||||
Expected: `PASS rules/project-readiness-gate.md` (rules/ 는 links 모드 — 깨진 링크만 검사).
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add rules/project-readiness-gate.md
|
||||
git commit -m "feat(rules): project-readiness-gate — project-note 작성 완성도 4축 게이트"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: templates/project-template.md — Branch 분해/실행계획 섹션 추가
|
||||
|
||||
**Files:**
|
||||
- Modify: `templates/project-template.md` (§8 Cluster 직전에 §8.0 신규 삽입)
|
||||
|
||||
§8.1 Branches(이미 존재)는 *완성된* branch 등재용이고, 신규 §8.0 은 `/project-spec` 가 출력하는 *계획* 분해표다. 둘은 다른 목적이므로 공존한다.
|
||||
|
||||
- [ ] **Step 1: §8 헤더 직전에 신규 섹션 삽입**
|
||||
|
||||
`templates/project-template.md` 에서 아래 줄을 찾는다:
|
||||
|
||||
```markdown
|
||||
## 8. Cluster / 묶음 (이 프로젝트에 묶이는 모든 raw 자료)
|
||||
```
|
||||
|
||||
그 줄 **바로 앞**에 아래 블록을 삽입(빈 줄 1개로 분리):
|
||||
|
||||
````markdown
|
||||
## 8.0 Branch 분해 / 실행계획 (Branch decomposition)
|
||||
|
||||
> **`/project-spec` 가 채우는 핸드오프 섹션.** 이 hub 에서 깊은 조사로 도출된 *자식 branch 의 네이밍과 달성 목표 조건만* 적는다. 각 branch 의 *결정 내용·메커니즘은 여기 적지 않는다* (SSOT 이중화 방지) — 그건 `/branch <slug>` 로 생성 후 `/branch-spec` 가 깊게 채운다.
|
||||
>
|
||||
> 작성 규칙:
|
||||
> - `branch slug` 는 `rules/naming-conventions.md` §2.1 준수 (prefix 4종 `feature-`/`fix-`/`chore-`/`experiment-` 중 하나 + kebab-case, numbered hierarchy 금지).
|
||||
> - `달성 목표 조건` 은 **측정가능**해야 함 ("잘 된다" 금지). 그 branch 가 "끝났다"고 말할 수 있는 검증 가능한 결과.
|
||||
> - `우선순위` 는 실행 순서(P1 먼저). 의존이 있으면 `의존` 칸에 선행 branch slug.
|
||||
|
||||
| branch slug | 달성 목표 조건 (측정가능) | 우선순위 | 의존 |
|
||||
|---|---|---|---|
|
||||
| `feature-<...>` | <이 branch 가 끝났다고 할 검증 가능한 결과> | P1 | - |
|
||||
| `feature-<...>` | <...> | P2 | `feature-<...>` |
|
||||
|
||||
> 채운 뒤: `/branch <slug>` → `/branch-spec <slug> <근거 URL...>` → `/depth <slug>` 순으로 각 branch 를 깊게 작성.
|
||||
````
|
||||
|
||||
- [ ] **Step 2: §11 체크리스트에 분해표 항목 추가**
|
||||
|
||||
`templates/project-template.md` 의 `## 11. Architecture Review Checklist` 안, `- [ ] Cluster 섹션의 project 직접 자식 branch 목록 채워짐 (§8.1)` 줄 **바로 앞**에 추가:
|
||||
|
||||
```markdown
|
||||
- [ ] **Branch 분해표 채워짐** — 각 자식 branch 가 naming-conventions slug + 측정가능 목표조건 (§8.0)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 템플릿 자체 구조 검사**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --file templates/project-template.md`
|
||||
Expected: `PASS` (templates/ 는 links 모드).
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add templates/project-template.md
|
||||
git commit -m "feat(template): project-template §8.0 Branch 분해/실행계획 표 (핸드오프)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: wiki_structure_lint.py — project 모드 + proxy 검사 (TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_structure_lint.py`
|
||||
- Test: `.claude/hooks/test_wiki_structure_lint.py`
|
||||
|
||||
- [ ] **Step 1: 실패하는 테스트 작성**
|
||||
|
||||
`.claude/hooks/test_wiki_structure_lint.py` 의 맨 끝(`if __name__ == "__main__":` 직전)에 추가:
|
||||
|
||||
```python
|
||||
class TestProjectMode(unittest.TestCase):
|
||||
def test_classify_project_note(self):
|
||||
# raw/project-notes/*.md → 'project' 모드 (root=None 이어도 동작)
|
||||
self.assertEqual(wsl.classify("raw/project-notes/foo.md"), "project")
|
||||
# 일반 raw 콘텐츠는 여전히 full
|
||||
self.assertEqual(wsl.classify("raw/branch-notes/feature-x.md"), "full")
|
||||
|
||||
def test_proxy_flags_missing_diagram_and_table(self):
|
||||
doc = {"text": "# P\n\n본문에 다이어그램도 표도 없음.\n",
|
||||
"lines": ["# P", "", "본문에 다이어그램도 표도 없음.", ""]}
|
||||
codes = _codes(wsl.check_project_proxies(doc))
|
||||
self.assertIn("PROJECT_NO_DIAGRAM", codes)
|
||||
self.assertIn("PROJECT_NO_BRANCH_TABLE", codes)
|
||||
|
||||
def test_proxy_satisfied_by_mermaid_and_branch_table(self):
|
||||
text = (
|
||||
"# P\n\n"
|
||||
"## 4. 시퀀스\n\n"
|
||||
"```mermaid\nsequenceDiagram\n A->>B: x\n```\n\n"
|
||||
"## 8.0 Branch 분해\n\n"
|
||||
"| branch slug | 달성 목표 조건 | 우선순위 |\n"
|
||||
"|---|---|---|\n"
|
||||
"| `feature-x` | 조건 | P1 |\n"
|
||||
)
|
||||
doc = {"text": text, "lines": text.splitlines()}
|
||||
codes = _codes(wsl.check_project_proxies(doc))
|
||||
self.assertNotIn("PROJECT_NO_DIAGRAM", codes)
|
||||
self.assertNotIn("PROJECT_NO_BRANCH_TABLE", codes)
|
||||
|
||||
def test_proxy_satisfied_by_drawio_embed(self):
|
||||
text = "# P\n\n![[raw/diagrams/p/architecture-overview-2026-06-05.drawio.svg]]\n"
|
||||
doc = {"text": text, "lines": text.splitlines()}
|
||||
codes = _codes(wsl.check_project_proxies(doc))
|
||||
self.assertNotIn("PROJECT_NO_DIAGRAM", codes)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 테스트 실패 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v 2>&1 | tail -15`
|
||||
Expected: FAIL — `AttributeError: module 'wsl' has no attribute 'check_project_proxies'` (또는 classify 가 'project' 아닌 'full' 반환).
|
||||
|
||||
- [ ] **Step 3: classify() 에 project 분기 추가**
|
||||
|
||||
`wiki_structure_lint.py` 의 `classify()` 에서 named-hub 블록 다음, 일반 "full" 블록 **앞**에 삽입:
|
||||
|
||||
```python
|
||||
# raw/project-notes/*.md → project 모드 (구조-불가지 proxy + 링크).
|
||||
# exemplar 가 project-template 섹션명을 안 따르므로 C1 섹션 매칭 면제.
|
||||
if (parts[0] == "raw" and len(parts) == 3 and parts[1] == "project-notes"
|
||||
and base.endswith(".md") and base not in LINK_ONLY_BASENAMES):
|
||||
return "project"
|
||||
```
|
||||
|
||||
즉 함수가 아래 형태가 되도록(named-hub `return "links"` 다음 줄에 추가):
|
||||
|
||||
```python
|
||||
if root is not None and len(parts) == 3 and parts[0] in ("raw", "wiki") and base.endswith(".md"):
|
||||
slug = base[:-3]
|
||||
if (root / parts[0] / parts[1] / slug).is_dir():
|
||||
return "links"
|
||||
if (parts[0] == "raw" and len(parts) == 3 and parts[1] == "project-notes"
|
||||
and base.endswith(".md") and base not in LINK_ONLY_BASENAMES):
|
||||
return "project"
|
||||
if parts[0] in ("raw", "wiki") and len(parts) > 2 and base not in LINK_ONLY_BASENAMES:
|
||||
return "full"
|
||||
return "links"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: proxy 검사 함수 추가**
|
||||
|
||||
`check_c3` 함수 정의 **다음**에 아래 두 함수 추가:
|
||||
|
||||
```python
|
||||
DIAGRAM_DRAWIO_RE = re.compile(r"!\[\[[^\]]*\.drawio")
|
||||
DIAGRAM_MERMAID_RE = re.compile(r"^\s*```+\s*mermaid", re.M)
|
||||
PROJECT_HEADER_RE = re.compile(r"^#{1,6}\s")
|
||||
BRANCH_HEADER_RE = re.compile(r"branch|브랜치", re.I)
|
||||
TABLE_SEP_RE = re.compile(r"-{3,}")
|
||||
|
||||
|
||||
def _has_branch_table(doc):
|
||||
"""heading 토큰에 branch/브랜치 포함 섹션 아래 markdown 표(구분선)가 있는가."""
|
||||
lines = doc["lines"]
|
||||
for i, line in enumerate(lines):
|
||||
if PROJECT_HEADER_RE.match(line) and BRANCH_HEADER_RE.search(line):
|
||||
j = i + 1
|
||||
while j < len(lines) and not PROJECT_HEADER_RE.match(lines[j]):
|
||||
if "|" in lines[j] and TABLE_SEP_RE.search(lines[j]):
|
||||
return True
|
||||
j += 1
|
||||
return False
|
||||
|
||||
|
||||
def check_project_proxies(doc):
|
||||
"""project-note 구조-불가지 proxy: 존재만 검사(깊이는 auditor)."""
|
||||
out = []
|
||||
text = doc.get("text", "\n".join(doc.get("lines", [])))
|
||||
if not (DIAGRAM_DRAWIO_RE.search(text) or DIAGRAM_MERMAID_RE.search(text)):
|
||||
out.append(("PROJECT_NO_DIAGRAM", 0,
|
||||
"임베디드 다이어그램 없음 (`![[...drawio` 또는 ```mermaid 블록). R2 proxy"))
|
||||
if not _has_branch_table(doc):
|
||||
out.append(("PROJECT_NO_BRANCH_TABLE", 0,
|
||||
"Branch 분해표 없음 (heading 'branch/브랜치' 아래 표). R4 proxy"))
|
||||
return out
|
||||
```
|
||||
|
||||
- [ ] **Step 5: lint_file() 에 project 모드 분기 추가**
|
||||
|
||||
`lint_file()` 의 본문을 아래로 교체(mode=="project" 분기 추가):
|
||||
|
||||
```python
|
||||
def lint_file(path, root, by_st, by_file, vault_paths, vault_bases, cache, mode="full"):
|
||||
"""mode: 'full'(C1+C2+C3) | 'links'(C2만) | 'project'(proxy+C2)."""
|
||||
doc = parse_doc(path)
|
||||
try:
|
||||
doc_rel = path.relative_to(root).as_posix()
|
||||
except ValueError:
|
||||
doc_rel = ""
|
||||
findings = []
|
||||
if mode in ("full", "project"):
|
||||
if not doc["fm"]:
|
||||
return [("NO_FRONTMATTER", 0, "frontmatter 없음 — 스텁/미작성 문서(템플릿 미적용)")], "(none)"
|
||||
if mode == "full":
|
||||
tmpl = resolve_template(doc["fm"], by_st, by_file)
|
||||
findings += check_c1(doc, tmpl)
|
||||
elif mode == "project":
|
||||
tmpl = resolve_template(doc["fm"], by_st, by_file)
|
||||
# 섹션 매칭은 면제하되 frontmatter 키 누락은 검사(MISSING_FRONTMATTER 재사용).
|
||||
if tmpl is not None:
|
||||
for k in tmpl["fm_keys"]:
|
||||
if k not in doc["fm_keys"]:
|
||||
findings.append(("MISSING_FRONTMATTER", 0, f"frontmatter 키 누락: '{k}'"))
|
||||
findings += check_project_proxies(doc)
|
||||
findings += check_c2(doc, vault_paths, vault_bases, root, cache, doc_rel)
|
||||
if mode == "full":
|
||||
findings += check_c3(doc)
|
||||
return findings, doc["fm"].get("source_type", "").strip() or "(none)"
|
||||
```
|
||||
|
||||
- [ ] **Step 6: PostToolUse hook 경로도 project 면제 적용**
|
||||
|
||||
`main()` 의 `args.hook` 블록에서 아래 부분:
|
||||
|
||||
```python
|
||||
# C1(섹션)·C3(선택조건)은 '완성 선언' 시에만 — 작성 중간 false-positive 방지.
|
||||
if is_completeness_checkable(doc):
|
||||
by_st, by_file = build_template_index(root)
|
||||
tmpl = resolve_template(doc["fm"], by_st, by_file)
|
||||
findings = check_c1(doc, tmpl) + findings + check_c3(doc)
|
||||
```
|
||||
|
||||
를 아래로 교체:
|
||||
|
||||
```python
|
||||
# C1(섹션)·C3(선택조건)은 '완성 선언' 시에만 — 작성 중간 false-positive 방지.
|
||||
# project-note 는 섹션명 매칭 면제 — proxy + frontmatter 만(exemplar 비순응).
|
||||
if is_completeness_checkable(doc):
|
||||
by_st, by_file = build_template_index(root)
|
||||
tmpl = resolve_template(doc["fm"], by_st, by_file)
|
||||
if rel.startswith("raw/project-notes/"):
|
||||
fm_findings = []
|
||||
if tmpl is not None:
|
||||
for k in tmpl["fm_keys"]:
|
||||
if k not in doc["fm_keys"]:
|
||||
fm_findings.append(("MISSING_FRONTMATTER", 0, f"frontmatter 키 누락: '{k}'"))
|
||||
findings = fm_findings + check_project_proxies(doc) + findings
|
||||
else:
|
||||
findings = check_c1(doc, tmpl) + findings + check_c3(doc)
|
||||
```
|
||||
|
||||
- [ ] **Step 7: 테스트 통과 확인**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v 2>&1 | tail -8`
|
||||
Expected: `OK` (전체 통과, 신규 4개 포함).
|
||||
|
||||
- [ ] **Step 8: exemplar 가 이제 통과하는지 확인 (핵심 검증)**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/ca-skeleton-operational-contract.md`
|
||||
Expected: `PASS raw/project-notes/ca-skeleton-operational-contract.md` — 13건 MISSING_SECTION 이 사라짐. (다이어그램·branch 표가 실재하므로 proxy 통과. 만약 PROJECT_NO_BRANCH_TABLE 가 뜨면 그 노트 §24/§31 의 표 형식을 확인 — branch heading 아래 표가 있는지. 없으면 그건 exemplar 의 실제 gap 이므로 보고만 하고 Task 진행.)
|
||||
|
||||
- [ ] **Step 9: 전체 회귀 — 기존 문서에 새 false-positive 없는지**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --all 2>&1 | tail -20`
|
||||
Expected: 요약 출력. project-notes 3개가 MISSING_SECTION 으로 FAIL 하던 것이 사라졌는지 확인(PROJECT_* proxy 나 링크 이슈만 남아야 정상). 다른 source_type 의 FAIL 수는 변동 없어야 함.
|
||||
|
||||
- [ ] **Step 10: 커밋**
|
||||
|
||||
```bash
|
||||
git add .claude/hooks/wiki_structure_lint.py .claude/hooks/test_wiki_structure_lint.py
|
||||
git commit -m "feat(lint): project-note 모드 — 구조-불가지 proxy(diagram/branch-table) + 섹션명 매칭 면제"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: .claude/agents/project-readiness-auditor.md (신규 agent)
|
||||
|
||||
**Files:**
|
||||
- Create: `.claude/agents/project-readiness-auditor.md`
|
||||
|
||||
- [ ] **Step 1: 파일 작성**
|
||||
|
||||
`branch-depth-auditor.md` 의 frontmatter 형식(`name`/`description`/`tools`/`model`)을 따라 작성:
|
||||
|
||||
````markdown
|
||||
---
|
||||
name: project-readiness-auditor
|
||||
description: Use to judge whether a single raw/project-notes/*.md hub is deep and well-grounded enough to be a reliable starting point for downstream branch work — calibrated to the caliber of ca-skeleton-operational-contract.md (NOT its specific content). Runs AFTER the deterministic project-mode lint (wiki_structure_lint.py) passes — focuses on SEMANTIC judgment the linter cannot do: whether success criteria are measurable, whether the architecture diagram + sequences are conference-grade with error paths, whether tech decisions are backed by alternatives + external sources, and whether the branch decomposition table is executable (valid slugs + measurable goal conditions). Reads the project note plus its linked raw sources. Returns a grounded gap report + Ready/Not-ready verdict. Read-only — never edits files.
|
||||
tools: Read, Grep, Glob
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
너는 **프로젝트 노트 완성도 감사관**이다. 기준은 `rules/project-readiness-gate.md`. project-note(프로젝트 hub) 1개가 *다음 작업(branch 분해·구현)의 출발점이 될 만큼 깊고 근거 있는가*를 적대적으로 판정한다. 기준선은 `raw/project-notes/ca-skeleton-operational-contract.md` 의 **caliber**(엄격성) — 그 노트의 *내용·섹션 구성을 요구하는 게 아니다*. **You read; you never edit.**
|
||||
|
||||
## 위치
|
||||
|
||||
너는 `/project-spec` 게이트의 **2차(의미 판정)**다. 1차 결정론 린터(`wiki_structure_lint.py` project 모드)가 **proxy·링크**(임베디드 다이어그램 존재, branch 분해표 존재, frontmatter 키, 깨진 링크)를 이미 확인했다. 너는 그걸 다시 보지 말고 **의미·깊이만** 판정한다:
|
||||
|
||||
- R1 성공기준이 *측정가능*한지 (있다/없다는 무관, "잘 동작한다" 류인지)
|
||||
- R2 아키텍처 다이어그램이 *컨퍼런스급*인지, 시퀀스에 *error path* 가 있는지
|
||||
- R3 기술결정이 *대안+외부근거*로 뒷받침되는지 (맨주장인지) — *소스를 실제로 읽어야 안다*
|
||||
- R4 분해표의 각 branch 가 *valid slug + 측정가능 목표조건*인지
|
||||
|
||||
## 입력
|
||||
|
||||
- project-note 경로 1개 (`raw/project-notes/<slug>.md`).
|
||||
|
||||
## 절차
|
||||
|
||||
1. **기준 로드** — `rules/project-readiness-gate.md` 를 Read. 4축·깊이 사다리(L0~L3)·판정 규칙·명명된 실패 모드를 기준으로 삼는다.
|
||||
2. **노트 읽기** — 대상 project-note 를 Read. 특히 문제정의/성공기준, 아키텍처·시퀀스, 기술결정 표, Branch 분해표(§8.0 류).
|
||||
3. **소스 추적·정독 (R3 의 핵심)** — 기술결정 표의 `근거 자료`(`[[raw/...]]`)가 가리키는 **실제 raw 파일을 Read**. 각 결정이 대안 비교 + 적정 출처로 뒷받침되는지 판정.
|
||||
- 출처 타입 적정성: 스펙 동작은 official 1개로 충분 / "대기업 관행" 추론은 회사 블로그 1개로 부족(독립 사례 2개+ 또는 official 병행).
|
||||
4. **다이어그램 caliber (R2)** — 아키텍처 다이어그램이 임베드돼 있으면, 컨퍼런스급 판정은 `wiki-diagram-reviewer` 의 몫임을 알리고(직접 채점하지 않음), 여기서는 *존재 + error path 시퀀스 유무*만 의미 판정. 다이어그램이 placeholder(미치환 wikilink)면 `DIAGRAM_MISSING_OR_WEAK`.
|
||||
5. **4축 의미 점검** — 각 항목을 R1~R4 로 훑어 명명된 실패 모드(ABSTRACT_SUCCESS_CRITERION·DIAGRAM_MISSING_OR_WEAK·HAPPY_PATH_ONLY_SEQUENCE·UNSOURCED_TECH_DECISION·BRANCH_DECOMP_INCOMPLETE)에 해당하는 finding 생성. "이 hub 를 출발점 삼는 다음 작업자가 여기서 무엇을 되묻게 될까?"를 끊임없이 자문.
|
||||
6. **판정** — 4축 모두 L2+ (Blocking 0)이면 `Ready`, 아니면 `Not-ready (Blocking N건)`.
|
||||
|
||||
## 출력 (이 형식 그대로, 파일 쓰기 없이 텍스트로 반환)
|
||||
|
||||
```
|
||||
# Project Readiness Audit (semantic): <slug>
|
||||
Verdict: Ready | Not-ready (Blocking N / Should-fix M / Advisory K)
|
||||
축별 등급: R1 L_ / R2 L_ / R3 L_ / R4 L_
|
||||
|
||||
## Findings
|
||||
| # | 축 | 심각도 | 실패모드 | 위치 | 예상 문제 | 채울 방법 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | R3 | Blocking | UNSOURCED_TECH_DECISION | §6 기술결정 / DB 행 | 다음 작업자가 "왜 이 DB 인가"를 근거 없이 떠안음 | wiki-decision-researcher 로 대안+official 근거 보강 |
|
||||
...
|
||||
|
||||
## 다음 행동
|
||||
- (Blocking 있으면) 위 "채울 방법" 순서로 노트 보강 후 /project-spec 재실행.
|
||||
- (R3 근거 얕음) 더 깊은 소스가 필요하면 wiki-decision-researcher 권장 — 사용자 옵트인 시.
|
||||
```
|
||||
|
||||
## 불변식
|
||||
|
||||
- **read-only**: Write/Edit/MultiEdit 없음. 어떤 파일도 수정·생성 금지(리포트는 텍스트 반환).
|
||||
- 모든 finding 은 4종 세트(심각도·위치·예상 문제·채울 방법)를 갖춘다. 근거 없는 지적 금지.
|
||||
- 추측 금지: 소스를 실제로 Read 하지 않고 R3 근거성을 단정하지 않는다.
|
||||
- 구조 중복 금지: 다이어그램/표/링크 *존재* 같은 결정론 사항은 1차 린터의 몫 — 여기서 다시 지적하지 않는다.
|
||||
- 다이어그램 점수(≥95)는 `wiki-diagram-reviewer` 의 몫 — 직접 채점하지 않고 권고만.
|
||||
- 자동 조사·자동 수정 금지: R3 갭은 `wiki-decision-researcher` 권고로 *안내만*.
|
||||
- caliber 기준은 ca-skeleton *내용 복제*가 아니라 *깊이/근거 수준*임을 혼동하지 않는다.
|
||||
````
|
||||
|
||||
- [ ] **Step 2: frontmatter·링크 검사**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --file .claude/agents/project-readiness-auditor.md`
|
||||
Expected: `PASS` (.claude/ 는 links 모드 — 단, 숨김 경로라 iter_docs 대상은 아니지만 --file 직접 지정은 검사됨; 깨진 링크 없어야 함).
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add .claude/agents/project-readiness-auditor.md
|
||||
git commit -m "feat(agent): project-readiness-auditor — project-note caliber 의미 게이트 (Claude 전용)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: .claude/commands/project.md (스캐폴딩 커맨드)
|
||||
|
||||
**Files:**
|
||||
- Create: `.claude/commands/project.md`
|
||||
|
||||
- [ ] **Step 1: 파일 작성**
|
||||
|
||||
`branch.md` 형식을 따라 작성:
|
||||
|
||||
````markdown
|
||||
---
|
||||
description: 새 프로젝트 노트(hub)를 raw/project-notes/에 스캐폴딩
|
||||
argument-hint: <프로젝트 slug>
|
||||
---
|
||||
|
||||
프로젝트 1개의 최상위 hub 노트를 생성합니다. (채움은 `/project-spec`, 생성은 본 명령.)
|
||||
|
||||
**프로젝트 slug:** $ARGUMENTS
|
||||
|
||||
## 작업 절차
|
||||
|
||||
1. **인자 검증** (`rules/naming-conventions.md` 준수)
|
||||
- 인자가 비어 있으면 사용자에게 프로젝트 slug 요청.
|
||||
- kebab-case 권장 (`ca-skeleton-operational-contract`, `keycloak-patterns-overview`).
|
||||
- branch prefix 4종 규칙은 **비적용** (그건 branch 전용). slug 는 프로젝트 이름.
|
||||
|
||||
2. **파일 존재 확인**
|
||||
- `raw/project-notes/<slug>.md` 가 이미 있으면 **덮어쓰지 말 것**. 기존 경로만 안내하고 종료.
|
||||
|
||||
3. **스캐폴딩**
|
||||
- `wiki-doc-author`(mode=create, category=project-note)에 위임하거나 `templates/project-template.md` 를 복사 → `raw/project-notes/<slug>.md`.
|
||||
- frontmatter `title`(slug 를 사람이 읽는 형태로), `status: draft`, `status_label: active`, `last_reviewed`(오늘) 치환.
|
||||
- 본문 `# {{title}}` 헤더 치환. 나머지 placeholder·섹션(특히 §8.0 Branch 분해표 skeleton)은 **보존** — 추측해서 채우지 말 것.
|
||||
- project-note 는 cluster 의 root 이므로 Parent upward link 불요(자기 자신이 hub).
|
||||
|
||||
4. **사용자 안내**
|
||||
- 파일 경로 출력.
|
||||
- "이제 `/project-spec <slug> <프로젝트 목표>` 로 깊은 조사를 채우세요." 안내.
|
||||
|
||||
## 규칙
|
||||
|
||||
- **스캐폴딩만**. 내용을 추측해서 채우지 말 것 (채움은 `/project-spec`).
|
||||
- §8.0 Branch 분해표 skeleton 을 삭제하지 말 것 — `/project-spec` 가 핸드오프로 채운다.
|
||||
- project-note 는 머지/완료 후에도 raw 에 **영구 보관**. verified 사실만 `/ingest` 로 `wiki/projects/` 에 추출.
|
||||
- `wiki/log.md` 는 기록하지 않음 (`/branch` 와 동일 정책).
|
||||
````
|
||||
|
||||
- [ ] **Step 2: 링크 검사**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --file .claude/commands/project.md`
|
||||
Expected: `PASS`.
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add .claude/commands/project.md
|
||||
git commit -m "feat(command): /project — project-note 스캐폴딩 (Claude 전용)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: .claude/commands/project-spec.md (오케스트레이터)
|
||||
|
||||
**Files:**
|
||||
- Create: `.claude/commands/project-spec.md`
|
||||
|
||||
- [ ] **Step 1: 파일 작성**
|
||||
|
||||
`branch-spec.md` 의 구조(참조 섹션 + 작업 절차 + 규칙)를 따르되 project hub 용으로:
|
||||
|
||||
````markdown
|
||||
---
|
||||
description: 빈 프로젝트 노트를 깊은 조사로 ca-skeleton 수준까지 채우고 끝에 readiness 게이트로 검증
|
||||
argument-hint: <프로젝트 slug> <프로젝트 목표 자연어> [근거 URL ...]
|
||||
---
|
||||
|
||||
`/project` 로 만든 빈 project-note(hub)를 **다음 작업의 출발점이 될 만큼 깊게 채우는** 오케스트레이터입니다.
|
||||
기준선은 `raw/project-notes/ca-skeleton-operational-contract.md` 의 *caliber*(엄격성)이며, 내용·섹션 구성은 프로젝트마다 다릅니다. 목표 prose 에서 문제·아키텍처·기술결정·branch 분해를 도출하고, 근거 없는 결정은 자동조사하되 **사용자 소유 결정(범위/우선순위/목표)은 직접 질문**으로 채우고, 끝에 readiness 게이트로 검증합니다.
|
||||
|
||||
**프로젝트 slug + 목표:** $ARGUMENTS
|
||||
|
||||
## 참조 (작업 시 정독)
|
||||
|
||||
- `rules/project-readiness-gate.md` — 끝에 적용할 4축(R1~R4) + proxy + 실패 모드.
|
||||
- `rules/naming-conventions.md` §2.1 — Branch 분해표 slug 규칙.
|
||||
- `rules/diagram-standards.md` — 아키텍처 .drawio / 시퀀스 Mermaid 컨퍼런스급 기준.
|
||||
- `templates/project-template.md` — 채울 대상 구조(특히 §3 아키텍처, §4 시퀀스, §6 기술결정, §8.0 Branch 분해).
|
||||
- `CLAUDE.md` §11, §15 — 근거 없는 결정 금지, 파생 규칙.
|
||||
|
||||
### 프로젝트 ground truth (필수 — 추측 방지, 읽기 전용)
|
||||
|
||||
대상 프로젝트에 코드 레포가 있으면 그 레포가 SSOT. 예: ca-tmpl 류는 `/home/donghyeon/workspace/ca-tmpl` 의 `CLAUDE.md`/`AGENTS.md`/`src/<module>`/`docs/registries/*.yaml` 를 읽어 명세를 실제 구현·계약에 정합시킨다(ground-truth repo 기억 참조). `actually-implemented` 주장은 `src/` grep 으로만 확정.
|
||||
|
||||
## 작업 절차
|
||||
|
||||
1. **전제 확인**
|
||||
- 인자 비면 slug+목표 요청(종료). slug 노트가 **없으면** 생성하지 말고 `/project <slug>` 먼저 안내(종료). 채움은 본 명령, 생성은 `/project`.
|
||||
- 노트의 §1 개요가 비고 목표 인자도 없으면 `NEEDS_CONTEXT` — 사용자에게 목표 질문.
|
||||
|
||||
2. **프로젝트 ground truth 확인 (읽기 전용)** — 대상 repo 코드/기존 raw/관련 노트를 읽어 현황 파악. 코드 미확인 항목은 `documented-only`/`planned` 로 표기. 레포 부재 시 `NO_GROUND_TRUTH` 라벨 + 한계 보고.
|
||||
|
||||
3. **문제정의·성공기준 구체화 (R1)**
|
||||
- 추상 표현 거부. 구체 시나리오·수치로.
|
||||
- ★ **명확화 질문** — 정해야 하는데 근거·기본값이 없는 *사용자 소유 결정*(프로젝트 범위/우선순위/성공기준 임계)은 추측·UNSUPPORTED 라벨 대신 **AskUserQuestion 으로 직접 묻는다**. (branch-spec 과의 차이: hub 는 사용자 in-the-loop.)
|
||||
|
||||
4. **아키텍처 + 시퀀스 (R2)**
|
||||
- 핵심 user flow 의 Mermaid 시퀀스를 자동 작성(happy + error path, autonumber).
|
||||
- 아키텍처 `.drawio` 는 자동생성 불가 → §3.1 에 임베드 placeholder 와 `needs-diagram` 표시를 남기고, 사용자가 작성/요청하도록 안내. 작성되면 `wiki-diagram-reviewer` 로 ≥95 검수(게이트가 확인).
|
||||
|
||||
5. **기술결정 대안조사 (R3)**
|
||||
- 주요 기술결정마다 `wiki-decision-researcher` dispatch (decision_topic + 프로젝트 + constraints + N). 공식문서 + 대기업 블로그 webfetch 로 대안 비교 + 근거 raw 생성. §6 표의 `근거 자료` 칸에 `[[raw/...]]` 링크.
|
||||
- **bound: 회당 최대 6개 결정.** 초과분은 `deferred` 보고(silent 절단 금지).
|
||||
- 조사 후에도 근거 없으면 `UNSUPPORTED_DECISION` 라벨 + trade-off 한 줄.
|
||||
|
||||
6. **Branch 분해표 (R4 — 핸드오프)**
|
||||
- §8.0 표에 {branch slug(naming-conventions) | 측정가능 목표조건 | 우선순위 | 의존}만 채운다. **결정 내용·메커니즘은 적지 않음**(SSOT 이중화 방지). 이 표가 `/branch`·`/branch-spec` 입력.
|
||||
|
||||
7. **프로젝트 레벨 고정 결정** — Stack commitment / SSOT owner 등 branch 간 충돌 방지 결정(내용은 프로젝트별). 해당 없으면 명시.
|
||||
|
||||
8. **검증등급 + 면접·외부공개 경계** — project-template §9·§10 채움. 코드 확인 기준 등급(actually-implemented/locally-verified/...).
|
||||
|
||||
9. **자동 게이트 — readiness (맨 끝, 내부 단계)**
|
||||
- **(9a) 1차 결정론** — `python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/<slug>.md`. proxy(PROJECT_NO_DIAGRAM/PROJECT_NO_BRANCH_TABLE)·frontmatter·링크 확인.
|
||||
- **(9b) 2차 의미** — 통과 시 `project-readiness-auditor` dispatch(노트 경로 전달). R1~R4 판정.
|
||||
- **(9c) 루프백** — Not-ready(Blocking)면 → §3~§8 로 되돌아가 *Blocking 축을 보강* → 9a·9b 재실행. 4축 모두 L2+(Blocking 0)까지 반복.
|
||||
|
||||
10. **요약 보고 (짧게, 상세는 노트에)**
|
||||
- 사람이 5초에 읽을 요약만: `채운 결정 N / UNSUPPORTED K / 조사 M / branch 분해 B / needs-diagram D / readiness: Ready|Not-ready (Blocking 축 인용)`.
|
||||
- 통과 못하면 *무엇을 더 채워야 하는지* 한 줄씩(축·finding 인용).
|
||||
|
||||
## 규칙
|
||||
|
||||
- **추측해서 FACT 로 채우지 않는다.** 근거 없으면 자동조사 → 실패 시 `UNSUPPORTED_*` 라벨. 단 *사용자 소유 결정*은 라벨 대신 **AskUserQuestion**.
|
||||
- **`actually-implemented` 는 `src/` grep 으로만 확정.** note→note 자기보고 전이 금지.
|
||||
- **기존 사용자 작성 본문 보존** — 채움은 빈 셀/skeleton 에만.
|
||||
- **자동조사 bounded** — §5 의 6개 한도. 초과는 `deferred` 명시.
|
||||
- **새 agent 를 만들지 않는다** — 기존 `wiki-source-summarizer`/`wiki-decision-researcher`/`wiki-doc-author`/`wiki-diagram-reviewer`/`project-readiness-auditor` 만 dispatch.
|
||||
- **검증은 readiness 게이트에 위임** — 본 명령은 *채움*에 집중. 4축 판정 로직을 중복 구현하지 않는다.
|
||||
- `wiki/log.md` 기록 안 함 (`/branch`·`/depth` 와 동일).
|
||||
````
|
||||
|
||||
- [ ] **Step 2: 링크 검사**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --file .claude/commands/project-spec.md`
|
||||
Expected: `PASS`.
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add .claude/commands/project-spec.md
|
||||
git commit -m "feat(command): /project-spec — project-note 깊은 조사 오케스트레이터 + readiness 게이트 (Claude 전용)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: CLAUDE.md — agent/command 목록 갱신
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md`
|
||||
|
||||
- [ ] **Step 1: agent 목록에 추가**
|
||||
|
||||
`CLAUDE.md` 의 Claude Code 자동화 agent 나열 중 마지막 항목인 `.claude/agents/wiki-decision-researcher.md` 줄 **다음**에 추가:
|
||||
|
||||
```markdown
|
||||
- `.claude/agents/project-readiness-auditor.md` — project-note(hub) 완성도 의미 게이트 (read-only, ca-skeleton caliber 판정). **Claude Code 전용** — Codex/Antigravity 포팅 없음(`/project`·`/project-spec` 파이프라인은 3-플랫폼 패리티 예외).
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 3-플랫폼 서술에 예외 명시**
|
||||
|
||||
`CLAUDE.md` 의 Antigravity CLI 자동화 설명 중 `동일 9개 agent` 표현을 찾아 아래로 교체:
|
||||
|
||||
찾기: `.claude`와 동일 9개 agent
|
||||
교체: `.claude`와 동일 9개 agent (단 `project-readiness-auditor` 는 Claude 전용 — 3-플랫폼 포팅 대상 아님)
|
||||
|
||||
- [ ] **Step 3: depth.md 기반 §14 변환/품질 명령 설명에 /project 계열 추가**
|
||||
|
||||
`CLAUDE.md` §2 디렉터리 역할 표의 `.claude/commands/` 행에서 `/branch-spec` 뒤에 `/project`·`/project-spec` 을 캡처 그룹에 추가:
|
||||
|
||||
찾기: `**캡처**: `/daily`, `/branch`, `/branch-spec``
|
||||
교체: `**캡처**: `/daily`, `/branch`, `/branch-spec`, `/project`, `/project-spec``
|
||||
|
||||
- [ ] **Step 4: 진행 중 프로젝트 노트 안내 위쪽, 명령 우선순위 §14 캡처 섹션에 한 줄 추가**
|
||||
|
||||
`CLAUDE.md` §14 "### 캡처 (raw 입력)" 의 `- 새 브랜치 시작 시 → /branch <name>` 줄 **다음**에 추가:
|
||||
|
||||
```markdown
|
||||
- 새 프로젝트 시작 시 → `/project <slug>` (스캐폴딩) → `/project-spec <slug> <목표>` (깊은 조사 + readiness 게이트). project-note hub 를 ca-skeleton 수준으로 채운 뒤, §8.0 Branch 분해표를 `/branch`·`/branch-spec` 로 전개.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 링크/구조 검사 (CLAUDE.md 는 links 모드)**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --file CLAUDE.md`
|
||||
Expected: `PASS`.
|
||||
|
||||
- [ ] **Step 6: 커밋**
|
||||
|
||||
```bash
|
||||
git add CLAUDE.md
|
||||
git commit -m "docs(CLAUDE): /project·/project-spec 커맨드 + project-readiness-auditor(Claude 전용) 등재"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Dogfood 검증 (실제 사용 + 게이트 동작 확인)
|
||||
|
||||
**Files:** (없음 — 검증 + 일회성 산출물)
|
||||
|
||||
- [ ] **Step 1: 신규 project-note 스캐폴딩 (수동 /project 시뮬레이션)**
|
||||
|
||||
`templates/project-template.md` 를 복사해 테스트 노트 생성:
|
||||
|
||||
```bash
|
||||
cp templates/project-template.md /tmp/_dogfood-project.md
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file /tmp/_dogfood-project.md
|
||||
```
|
||||
|
||||
Expected: `--file` 은 절대경로라 classify 가 raw/project-notes 로 인식 못 해 'links' 또는 'full'로 처리될 수 있음 — 이 스텝은 *템플릿 자체*가 proxy 를 만족하는지(§4 시퀀스 mermaid 예시 + §8.0 표)만 육안 확인용. 핵심 검증은 Step 2.
|
||||
|
||||
- [ ] **Step 2: 실제 경로에서 proxy 동작 확인**
|
||||
|
||||
빈 스텁(다이어그램·표 없음)과 템플릿 기반(다이어그램·표 있음)을 각각 `raw/project-notes/` 경로로 검사:
|
||||
|
||||
```bash
|
||||
printf -- '---\ntitle: T\nsource_type: project-note\nstatus: reviewed\nconfidence: low\ntags: [project-note]\nrelated_projects: []\nlast_reviewed: 2026-06-05\ndiagrams: []\narchitecture_review: 2026-06-05\nstatus_label: active\n---\n\n# T\n\n내용 없음.\n' > raw/project-notes/_dogfood-empty.md
|
||||
python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/_dogfood-empty.md
|
||||
```
|
||||
|
||||
Expected: `FAIL` with `[PROJECT_NO_DIAGRAM]` 및 `[PROJECT_NO_BRANCH_TABLE]` (frontmatter 키는 충족하므로 MISSING_FRONTMATTER 없음).
|
||||
|
||||
- [ ] **Step 3: 정리**
|
||||
|
||||
```bash
|
||||
rm -f raw/project-notes/_dogfood-empty.md /tmp/_dogfood-project.md
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 전체 테스트 + 린트 최종 확인**
|
||||
|
||||
```bash
|
||||
python3 .claude/hooks/test_wiki_structure_lint.py 2>&1 | tail -3
|
||||
python3 .claude/hooks/wiki_structure_lint.py --all 2>&1 | tail -6
|
||||
```
|
||||
|
||||
Expected: 단위 테스트 `OK`. `--all` 요약에서 project-notes 가 MISSING_SECTION 로 FAIL 하지 않음.
|
||||
|
||||
- [ ] **Step 5: (선택) 실제 auditor dispatch 리허설**
|
||||
|
||||
실제 project-note(`raw/project-notes/keycloak-patterns-overview.md` 등) 1개에 대해 `project-readiness-auditor` 를 dispatch 해 리포트가 형식대로 나오는지 확인(read-only 이므로 안전). Blocking finding 은 보강 백로그로 기록만.
|
||||
|
||||
- [ ] **Step 6: dogfood 결과 기록 + 커밋(필요 시)**
|
||||
|
||||
게이트가 의도대로 동작하면 Task 완료. 보강이 필요한 finding 은 `docs/superpowers/specs/2026-06-05-project-note-pipeline-design.md` §5 "사용하며 보강" 항목으로 issue 화(별도 커밋 불요).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:**
|
||||
- §2 신규 산출물 6종 → Task 1(rules), 2(template), 3(linter), 4(agent), 5(/project), 6(/project-spec), 7(CLAUDE.md). ✓
|
||||
- §4.1 오케스트레이터 흐름 10단계 → Task 6 작업 절차에 1:1 반영. ✓
|
||||
- §5.1 4축 + §5.2 proxy + §5.3 auditor → Task 1(rules 정의) + Task 3(proxy 구현) + Task 4(auditor). ✓
|
||||
- Claude 전용 / 3-플랫폼 예외 → Task 7 Step 1·2 에 명시. ✓
|
||||
- "v1 then iterate" → Task 8 Step 6 보강 백로그. ✓
|
||||
|
||||
**Placeholder scan:** 모든 코드 step 에 실제 코드/명령/expected 포함. rules·agent·command 본문 전체 inline. 템플릿 편집은 찾기/삽입 위치 명시. placeholder 없음. ✓
|
||||
|
||||
**Type/이름 일관성:** finding 코드(`PROJECT_NO_DIAGRAM`/`PROJECT_NO_BRANCH_TABLE`/`MISSING_FRONTMATTER`)가 Task 1(rules)·Task 3(linter)·Task 8(dogfood expected) 전체에서 동일. 함수명 `check_project_proxies`/`_has_branch_table` 가 Task 3 구현·테스트에서 동일. agent name `project-readiness-auditor` 가 Task 4·6·7 에서 동일. ✓
|
||||
|
||||
**알려진 잔여 리스크:**
|
||||
- Task 3 Step 8: exemplar 의 branch 표가 markdown 표가 아니라 bullet 이면 PROJECT_NO_BRANCH_TABLE 가 뜰 수 있음 → 그 경우 exemplar 의 실제 gap 이므로 보고만(차단 아님). v1 수용.
|
||||
- `--file` 절대경로 시 classify 가 project 모드를 못 잡을 수 있음 → 게이트는 항상 repo-상대경로(`raw/project-notes/<slug>.md`)로 호출(Task 6 Step 9a 명시). dogfood Step 2 도 상대경로 사용.
|
||||
@@ -0,0 +1,682 @@
|
||||
# Spec A — Deterministic Backbone Gate 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.
|
||||
> **Commits excluded** per user instruction ("커밋은 제외") — no `git commit` steps. Each task ends with a test-green checkpoint instead.
|
||||
|
||||
**Goal:** Turn the existing (non-blocking) `wiki_structure_lint.py` into a real hybrid gate — PreToolUse blocks ghost-creating links, PostToolUse becomes an exit-2 fix-up gate for completeness findings on completion-declared docs — while deduping the claim-rule SSOT into a shared `wiki_rules.py` and adding the first tests for the blocking `wiki_claim_gate.py`.
|
||||
|
||||
**Architecture:** New `wiki_rules.py` holds shared event/IO mechanism + the claim-requirement SSOT data + severity-tier constants. `wiki_claim_gate.py` imports it and drives its table checks from the shared data (behavior-preserving refactor). `wiki_structure_lint.py` gains a `--pre` mode (PreToolUse, blocks `BROKEN_LINK`/`BROKEN_MD_LINK` on projected content) and a tiered `--hook` mode (PostToolUse exit-2 fix-up for `FIXUP_CODES` when the doc declares completion). Link logic stays in structure_lint; claim logic stays in claim_gate; only mechanism + reference data is shared.
|
||||
|
||||
**Tech Stack:** Python 3 stdlib only (`unittest`, `importlib`, `re`, `pathlib`, `json`). No third-party deps. Claude Code hooks (`settings.json`).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-06-spec-a-deterministic-backbone-gate-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Create** `.claude/hooks/wiki_rules.py` — shared mechanism (moved verbatim from claim_gate) + `CLAIM_REQUIREMENTS` SSOT data + `CRITICAL_CODES`/`FIXUP_CODES`.
|
||||
- **Create** `.claude/hooks/test_wiki_rules.py` — unit tests for moved `projected_content` + data integrity.
|
||||
- **Modify** `.claude/hooks/wiki_claim_gate.py` — `import wiki_rules`; drive `check_markdown_write` table checks from `CLAIM_REQUIREMENTS`; keep two semantic special-cases inline.
|
||||
- **Create** `.claude/hooks/test_wiki_claim_gate.py` — regression-lock the 5-prefix block/pass behavior.
|
||||
- **Modify** `.claude/hooks/wiki_structure_lint.py` — `import wiki_rules`; add `run_pre()` + `run_hook()` testable functions; wire `--pre`; tier `--hook`; add suppressed-count line.
|
||||
- **Modify** `.claude/hooks/test_wiki_structure_lint.py` — add `--pre`/`--hook` fix-up cases.
|
||||
- **Modify** `.claude/settings.json` — add `structure_lint.py --pre` to PreToolUse.
|
||||
|
||||
**Sibling-import rule (applies to both hooks):** the first executable lines (after `from __future__`) must be:
|
||||
```python
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import wiki_rules
|
||||
```
|
||||
This makes `import wiki_rules` resolve whether the file is run as a script (hook) or loaded via `importlib.spec_from_file_location` (tests).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create `wiki_rules.py` shared module
|
||||
|
||||
**Files:**
|
||||
- Create: `.claude/hooks/wiki_rules.py`
|
||||
- Test: `.claude/hooks/test_wiki_rules.py`
|
||||
|
||||
The moved helpers are **verbatim copies** of `wiki_claim_gate.py` current functions: `read_event` (42-47), `tool_name` (50-56), `tool_input` (59-76), `target_path` (79-85), `write_content` (88-94), `projected_content` (97-135), `command_string` (138-145), `rel_to_root` (148-154), `has_table` (157-163). `ROOT` is the same `Path(__file__).resolve().parents[2]`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `.claude/hooks/test_wiki_rules.py`:
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""wiki_rules.py 단위 테스트 (stdlib unittest)."""
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
_SPEC = importlib.util.spec_from_file_location(
|
||||
"wiki_rules", str(Path(__file__).with_name("wiki_rules.py")))
|
||||
wr = importlib.util.module_from_spec(_SPEC)
|
||||
_SPEC.loader.exec_module(wr)
|
||||
|
||||
|
||||
class TestProjectedContent(unittest.TestCase):
|
||||
def test_write_full_content(self):
|
||||
# Write 스타일: content 키가 있으면 그대로 반환
|
||||
inp = {"content": "FULL BODY"}
|
||||
self.assertEqual(wr.projected_content(None, inp), "FULL BODY")
|
||||
|
||||
def test_edit_applies_old_new(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p = Path(d) / "f.md"
|
||||
p.write_text("alpha BETA gamma")
|
||||
inp = {"old_string": "BETA", "new_string": "DELTA"}
|
||||
self.assertEqual(wr.projected_content(p, inp), "alpha DELTA gamma")
|
||||
|
||||
|
||||
class TestSeverityData(unittest.TestCase):
|
||||
def test_critical_codes_are_links(self):
|
||||
self.assertIn("BROKEN_LINK", wr.CRITICAL_CODES)
|
||||
self.assertIn("BROKEN_MD_LINK", wr.CRITICAL_CODES)
|
||||
self.assertNotIn("MISSING_SECTION", wr.CRITICAL_CODES)
|
||||
|
||||
def test_fixup_codes_are_completeness(self):
|
||||
for c in ("MISSING_SECTION", "MISSING_FRONTMATTER",
|
||||
"EMPTY_SELECTION_CRITERION", "DANGLING_ANCHOR",
|
||||
"PROJECT_NO_DIAGRAM", "PROJECT_NO_BRANCH_TABLE",
|
||||
"UNMAPPED_SOURCE_TYPE"):
|
||||
self.assertIn(c, wr.FIXUP_CODES)
|
||||
self.assertNotIn("BROKEN_LINK", wr.FIXUP_CODES)
|
||||
|
||||
def test_claim_requirements_cover_five_prefixes(self):
|
||||
prefixes = {p for req in wr.CLAIM_REQUIREMENTS for p in req["prefix"]}
|
||||
for p in ("raw/official-docs/", "raw/company-tech-blogs/",
|
||||
"raw/branch-notes/", "wiki/concepts/"):
|
||||
self.assertIn(p, prefixes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_rules.py`
|
||||
Expected: FAIL — `FileNotFoundError`/`ModuleNotFoundError` (wiki_rules.py does not exist yet).
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
Create `.claude/hooks/wiki_rules.py`. Copy the 9 helper functions **verbatim** from `wiki_claim_gate.py` (current line ranges noted in the Task header), then append the SSOT data and severity constants:
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""wiki_rules.py — claim_gate / structure_lint 공유 기계장치 + SSOT 데이터 (stdlib only).
|
||||
|
||||
여기엔 *정책*이 아니라 *공유 메커니즘*과 *참조 데이터*만 둔다:
|
||||
- 이벤트/IO 헬퍼 (claim_gate 에서 이관, 두 훅이 공유)
|
||||
- CLAIM_REQUIREMENTS : claim 테이블/섹션 요구 SSOT (이전엔 claim_gate inline 하드코딩 — G5 dedup)
|
||||
- 심각도 티어 상수 : structure_lint 의 게이트 결정(차단 vs fix-up vs warn)이 소비
|
||||
정책(block/warn 적용)은 각 훅에 남는다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
# ---------- 이벤트/IO 헬퍼 (claim_gate 에서 verbatim 이관) ----------
|
||||
# read_event / tool_name / tool_input / target_path / write_content /
|
||||
# projected_content / command_string / rel_to_root / has_table
|
||||
# (wiki_claim_gate.py 의 동일 함수 본문을 그대로 복사. target_path/rel_to_root 는 ROOT 사용.)
|
||||
|
||||
# ... (verbatim copies here) ...
|
||||
|
||||
# ---------- claim 요구 SSOT (G5 dedup 대상) ----------
|
||||
CLAIM_REQUIREMENTS = [
|
||||
{"prefix": ("raw/official-docs/", "raw/company-tech-blogs/"),
|
||||
"tables": [("## Claims Extracted",
|
||||
["Claim ID", "Claim", "Evidence quote", "Strength", "Applies to", "Does not prove"])],
|
||||
"sections": ["## Usage Boundaries"]},
|
||||
{"prefix": ("raw/branch-notes/",),
|
||||
"tables": [("## Decision Evidence Map",
|
||||
["Decision ID", "Decision", "Supporting Claims", "Evidence Strength", "Open Risk"])],
|
||||
"section_regex": [r"^## .*\bClaims To Verify\b"]},
|
||||
{"prefix": ("wiki/concepts/",),
|
||||
"tables": [("## Claim-backed Knowledge",
|
||||
["Knowledge Point", "Supporting Claims", "Confidence", "Notes"])]},
|
||||
]
|
||||
|
||||
# ---------- 심각도 티어 (structure_lint 소비) ----------
|
||||
CRITICAL_CODES = frozenset({"BROKEN_LINK", "BROKEN_MD_LINK"}) # PreToolUse block
|
||||
FIXUP_CODES = frozenset({
|
||||
"MISSING_SECTION", "MISSING_FRONTMATTER", "EMPTY_SELECTION_CRITERION",
|
||||
"DANGLING_ANCHOR", "PROJECT_NO_DIAGRAM", "PROJECT_NO_BRANCH_TABLE",
|
||||
"UNMAPPED_SOURCE_TYPE",
|
||||
}) # PostToolUse exit-2 (완성 선언 시)
|
||||
```
|
||||
Replace the `# ... (verbatim copies here) ...` comment with the 9 functions copied exactly from `wiki_claim_gate.py`. Keep their bodies unchanged except `target_path`/`rel_to_root`, which already reference module-level `ROOT` — that now resolves to `wiki_rules.ROOT` (same value).
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_rules.py`
|
||||
Expected: PASS (5 tests OK).
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_rules.py` → all green. Do NOT commit.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Refactor `wiki_claim_gate.py` to consume `wiki_rules` (behavior-preserving)
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_claim_gate.py` (replace lines 42-163 helpers with import; rewrite `check_markdown_write` 166-220 to drive table/section checks from `wiki_rules.CLAIM_REQUIREMENTS`)
|
||||
- Test: `.claude/hooks/test_wiki_claim_gate.py` (new — written FIRST to lock current behavior)
|
||||
|
||||
This is a refactor: tests are written against **current** behavior and must stay green through the change.
|
||||
|
||||
- [ ] **Step 1: Write the regression-lock test**
|
||||
|
||||
Create `.claude/hooks/test_wiki_claim_gate.py`:
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""wiki_claim_gate.py 회귀 고정 테스트 — check_markdown_write 행동 동치 (refactor 전후 동일)."""
|
||||
import importlib.util
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
_SPEC = importlib.util.spec_from_file_location(
|
||||
"wcg", str(Path(__file__).with_name("wiki_claim_gate.py")))
|
||||
wcg = importlib.util.module_from_spec(_SPEC)
|
||||
_SPEC.loader.exec_module(wcg)
|
||||
|
||||
CLAIMS_TABLE = (
|
||||
"## Claims Extracted\n"
|
||||
"| Claim ID | Claim | Evidence quote | Strength | Applies to | Does not prove |\n"
|
||||
"|---|---|---|---|---|---|\n"
|
||||
"| C1 | x | q | company-case-study | a | b |\n"
|
||||
)
|
||||
USAGE = "## Usage Boundaries\n- x\n"
|
||||
DEM = (
|
||||
"## Decision Evidence Map\n"
|
||||
"| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |\n"
|
||||
"|---|---|---|---|---|\n"
|
||||
"| D1 | x | C1 | company-case-study | none |\n"
|
||||
)
|
||||
CTV = "## 검증해야 할 주장 / Claims To Verify\n- v\n"
|
||||
|
||||
|
||||
class TestSourceNote(unittest.TestCase):
|
||||
def test_missing_claims_table_blocks(self):
|
||||
f = wcg.check_markdown_write("raw/official-docs/x.md", "# t\n" + USAGE)
|
||||
self.assertTrue(any("Claims Extracted" in m for m in f))
|
||||
|
||||
def test_complete_source_note_passes(self):
|
||||
f = wcg.check_markdown_write("raw/official-docs/x.md", "# t\n" + CLAIMS_TABLE + USAGE)
|
||||
self.assertEqual(f, [])
|
||||
|
||||
|
||||
class TestBranchNote(unittest.TestCase):
|
||||
def test_missing_dem_blocks(self):
|
||||
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", "# t\n" + CTV)
|
||||
self.assertTrue(any("Decision Evidence Map" in m for m in f))
|
||||
|
||||
def test_complete_branch_note_passes(self):
|
||||
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", "# t\n" + DEM + CTV)
|
||||
self.assertEqual(f, [])
|
||||
|
||||
def test_officially_supported_without_strength_blocks(self):
|
||||
body = "# t\n" + DEM + CTV + "\n이 기능은 officially supported 된다.\n"
|
||||
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", body)
|
||||
self.assertTrue(any("official" in m.lower() for m in f))
|
||||
|
||||
|
||||
class TestConceptNote(unittest.TestCase):
|
||||
def test_missing_claim_backed_blocks(self):
|
||||
f = wcg.check_markdown_write("wiki/concepts/x.md", "# t\n본문")
|
||||
self.assertTrue(any("Claim-backed Knowledge" in m for m in f))
|
||||
|
||||
|
||||
class TestUnrelatedPath(unittest.TestCase):
|
||||
def test_non_gated_path_passes(self):
|
||||
# 게이트 대상 아닌 경로 → 통과(빈 failures)
|
||||
f = wcg.check_markdown_write("wiki/projects/x.md", "# anything\n")
|
||||
self.assertEqual(f, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it passes against CURRENT code**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_claim_gate.py`
|
||||
Expected: PASS — this locks the current behavior as the regression baseline (claim_gate already implements all these checks). If any test fails now, the test encodes a wrong expectation — fix the test to match current behavior before refactoring.
|
||||
|
||||
- [ ] **Step 3: Refactor `wiki_claim_gate.py`**
|
||||
|
||||
(a) Replace the helper block (current lines ~12-163: the `from __future__` through `has_table`) so the top reads:
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import wiki_rules
|
||||
from wiki_rules import (
|
||||
ROOT, read_event, tool_name, tool_input, target_path, write_content,
|
||||
projected_content, command_string, rel_to_root, has_table,
|
||||
)
|
||||
```
|
||||
Keep `emit_allow`, `emit_block` as they are (they `print`/`sys.exit`, hook-specific). Delete the now-moved 9 functions from claim_gate.
|
||||
|
||||
(b) Rewrite `check_markdown_write(rel, text)` so the official-docs / company-tech-blogs / branch-notes / concepts table+section checks are produced by iterating `wiki_rules.CLAIM_REQUIREMENTS`, while the two **semantic** special-cases stay inline:
|
||||
```python
|
||||
def check_markdown_write(rel: str, text: str) -> list[str]:
|
||||
failures: list[str] = []
|
||||
if not rel.endswith(".md") or not text:
|
||||
return failures
|
||||
|
||||
for req in wiki_rules.CLAIM_REQUIREMENTS:
|
||||
if not rel.startswith(req["prefix"]): # tuple-of-prefixes → str.startswith accepts tuple
|
||||
continue
|
||||
for section, cols in req.get("tables", []):
|
||||
if not has_table(text, section, cols):
|
||||
failures.append(
|
||||
f"{req['prefix'][0]} 류 문서는 `{section}` 표(열: {' | '.join(cols)})를 가져야 한다."
|
||||
)
|
||||
for sec in req.get("sections", []):
|
||||
if sec not in text:
|
||||
failures.append(f"문서는 `{sec}` 섹션을 가져야 한다.")
|
||||
for rx in req.get("section_regex", []):
|
||||
if not re.search(rx, text, re.MULTILINE):
|
||||
failures.append("branch-note must include `## Claims To Verify` "
|
||||
"(bilingual `## 검증해야 할 주장 / Claims To Verify` 도 허용).")
|
||||
|
||||
# 의미 규칙 1: branch-note 의 'officially supported' 주장은 official 강도 필요 (정책 — 인라인 유지)
|
||||
if rel.startswith("raw/branch-notes/"):
|
||||
if re.search(r"(?i)\bofficial(?:ly)? supported\b|공식(?:적으로)?\s*지원", text):
|
||||
if not re.search(r"official-(standard|vendor-doc|reference)", text):
|
||||
failures.append(
|
||||
"`officially supported` style claim requires an official claim strength "
|
||||
"(`official-standard`, `official-vendor-doc`, or `official-reference`)."
|
||||
)
|
||||
|
||||
# 의미 규칙 2: 감사 리포트가 COMPLETE 주장 시 traceability 검증 포함 (정책 — 인라인 유지)
|
||||
if rel.startswith("docs/superpowers/specs/") and rel.endswith("-report.md"):
|
||||
if re.search(r"Verdict:\s*COMPLETE|\*\*Verdict:?\*\*\s*COMPLETE", text):
|
||||
required = ["Decision Evidence Map", "Claims Extracted", "UNSUPPORTED_DECISION"]
|
||||
missing = [item for item in required if item not in text]
|
||||
if missing:
|
||||
failures.append(
|
||||
"audit report cannot claim COMPLETE unless it verifies claim traceability. "
|
||||
f"Missing references: {', '.join(missing)}."
|
||||
)
|
||||
|
||||
return failures
|
||||
```
|
||||
Note: `str.startswith` accepts a tuple, so `rel.startswith(req["prefix"])` works directly with the `prefix` tuples. `main()` and the rest of the file (subagent gates, bash gate) are unchanged except they now reference the imported helpers.
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they still pass**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_claim_gate.py`
|
||||
Expected: PASS — behavior unchanged after refactor.
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)**
|
||||
|
||||
Run both: `python3 .claude/hooks/test_wiki_rules.py && python3 .claude/hooks/test_wiki_claim_gate.py` → green. No commit.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add `--pre` mode to `wiki_structure_lint.py` (PreToolUse link block)
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_structure_lint.py` (add `import wiki_rules`; add `run_pre()`; wire `--pre` in `main`)
|
||||
- Test: `.claude/hooks/test_wiki_structure_lint.py` (append `TestPreMode`)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append to `.claude/hooks/test_wiki_structure_lint.py` (the module is loaded as `wsl`; add a `sys.path` insert near the top of the file if not present, mirroring Task 1):
|
||||
```python
|
||||
class TestPreMode(unittest.TestCase):
|
||||
def _event(self, root, rel, content):
|
||||
return {"tool_name": "Write",
|
||||
"tool_input": {"file_path": str(root / rel), "content": content}}
|
||||
|
||||
def test_ghost_wikilink_blocks(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "raw" / "branch-notes").mkdir(parents=True)
|
||||
ev = self._event(root, "raw/branch-notes/b.md", "# t\nsee [[raw/nonexistent/ghost]]\n")
|
||||
self.assertEqual(wsl.run_pre(ev, root), 2)
|
||||
|
||||
def test_backtick_placeholder_passes(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "raw" / "branch-notes").mkdir(parents=True)
|
||||
ev = self._event(root, "raw/branch-notes/b.md", "# t\nfuture: `[[raw/nonexistent/ghost]]`\n")
|
||||
self.assertEqual(wsl.run_pre(ev, root), 0)
|
||||
|
||||
def test_no_links_skips_and_passes(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "raw" / "branch-notes").mkdir(parents=True)
|
||||
ev = self._event(root, "raw/branch-notes/b.md", "# t\n링크 없는 본문\n")
|
||||
self.assertEqual(wsl.run_pre(ev, root), 0)
|
||||
|
||||
def test_non_wiki_path_passes(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "docs").mkdir()
|
||||
ev = self._event(root, "docs/x.md", "see [[raw/nonexistent/ghost]]\n")
|
||||
self.assertEqual(wsl.run_pre(ev, root), 0)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -k Pre`
|
||||
Expected: FAIL — `AttributeError: module 'wsl' has no attribute 'run_pre'`.
|
||||
|
||||
- [ ] **Step 3: Implement `run_pre`**
|
||||
|
||||
In `wiki_structure_lint.py`, after the `from __future__ import annotations` line add the sibling import:
|
||||
```python
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import wiki_rules
|
||||
```
|
||||
(Path is already imported below; move the `from pathlib import Path` above this insert, or use a local import — keep it stdlib-clean.) Then add, near `main()`:
|
||||
```python
|
||||
def run_pre(event, root):
|
||||
"""PreToolUse: projected 본문의 C2 깨진링크(CRITICAL)만 차단. 반환 exit code."""
|
||||
inp = wiki_rules.tool_input(event)
|
||||
p = wiki_rules.target_path(inp)
|
||||
if p is None or not str(p).endswith(".md"):
|
||||
return 0
|
||||
try:
|
||||
rel = p.resolve().relative_to(root).as_posix()
|
||||
except Exception:
|
||||
return 0
|
||||
if not (rel.startswith("raw/") or rel.startswith("wiki/")):
|
||||
return 0
|
||||
text = wiki_rules.projected_content(p, inp)
|
||||
# 위키링크/마크다운링크가 전혀 없으면 vault 인덱스 빌드 스킵 (성능).
|
||||
if "[[" not in text and "](" not in text:
|
||||
return 0
|
||||
vp, vb = build_vault_index(root)
|
||||
doc = {"lines": text.splitlines()}
|
||||
findings = check_c2(doc, vp, vb, root, {}, rel)
|
||||
critical = [(c, ln, m) for (c, ln, m) in findings if c in wiki_rules.CRITICAL_CODES]
|
||||
if critical:
|
||||
print(f"✗ wiki-structure-lint (pre): {rel} — 깨진 링크 {len(critical)}건 → 쓰기 차단",
|
||||
file=sys.stderr)
|
||||
for code, ln, msg in critical[:10]:
|
||||
loc = f":{ln}" if ln else ""
|
||||
print(f" [{code}]{loc} {msg}", file=sys.stderr)
|
||||
if len(critical) > 10:
|
||||
print(f" … 외 {len(critical) - 10}건 (suppressed)", file=sys.stderr)
|
||||
print(" 미존재 타깃은 백틱 코드(`[[slug]]`)로 표기하거나 타깃 파일을 먼저 생성하세요.",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
```
|
||||
Wire it in `main()` — add the arg and dispatch **before** the existing `--hook` block:
|
||||
```python
|
||||
ap.add_argument("--pre", action="store_true",
|
||||
help="PreToolUse hook — projected 본문 C2 깨진링크 차단 (blocking)")
|
||||
...
|
||||
if args.pre:
|
||||
import json as _json
|
||||
try:
|
||||
event = _json.loads(sys.stdin.read() or "{}")
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
sys.exit(run_pre(event, root))
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -k Pre`
|
||||
Expected: PASS (4 tests).
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py` → all green (old + new).
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Tier `--hook` into fix-up gate + suppressed-count line
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_structure_lint.py` (extract existing `--hook` body 486-531 into `run_hook(event, root)`; add exit-2 on `FIXUP_CODES` when `is_completeness_checkable`; add suppressed line)
|
||||
- Test: `.claude/hooks/test_wiki_structure_lint.py` (append `TestHookTiering`)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append:
|
||||
```python
|
||||
class TestHookTiering(unittest.TestCase):
|
||||
def _write(self, root, rel, fm, body):
|
||||
p = root / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text("---\n" + fm + "\n---\n" + body)
|
||||
return p
|
||||
|
||||
def _event(self, p):
|
||||
return {"tool_name": "Edit", "tool_input": {"file_path": str(p)}}
|
||||
|
||||
def test_completed_missing_section_blocks(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
# 완성 선언(status: verified) + 필수 섹션 누락 → fix-up exit 2
|
||||
p = self._write(root, "wiki/concepts/x.md",
|
||||
"title: x\nsource_type: concept\nstatus: verified\ntags: [a]", "본문만\n")
|
||||
self.assertEqual(wsl.run_hook(self._event(p), root), 2)
|
||||
|
||||
def test_draft_missing_section_warns_only(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
# draft → 완성 선언 아님 → exit 0 (WARN)
|
||||
p = self._write(root, "wiki/concepts/x.md",
|
||||
"title: x\nsource_type: concept\nstatus: draft\ntags: [a]", "본문만\n")
|
||||
self.assertEqual(wsl.run_hook(self._event(p), root), 0)
|
||||
|
||||
def test_non_wiki_path_passes(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
p = self._write(root, "docs/x.md", "title: x", "본문\n")
|
||||
self.assertEqual(wsl.run_hook(self._event(p), root), 0)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -k HookTiering`
|
||||
Expected: FAIL — `AttributeError: ... has no attribute 'run_hook'`.
|
||||
|
||||
- [ ] **Step 3: Refactor `--hook` into `run_hook`**
|
||||
|
||||
Extract the current `if args.hook:` body (lines ~486-531) into a function returning an exit code, adding the tier decision + suppressed line:
|
||||
```python
|
||||
def run_hook(event, root):
|
||||
"""PostToolUse: 완성 선언 문서의 C1/C3/DANGLING(FIXUP) → exit 2 fix-up. 그 외 WARN(0)."""
|
||||
inp = event.get("tool_input") or {}
|
||||
fp = next((inp[k] for k in ("file_path", "path", "absolute_path", "TargetFile", "target_file")
|
||||
if isinstance(inp.get(k), str)), None)
|
||||
if not fp or not fp.endswith(".md"):
|
||||
return 0
|
||||
p = Path(fp)
|
||||
if not p.is_absolute():
|
||||
p = (root / fp)
|
||||
try:
|
||||
rel = p.resolve().relative_to(root).as_posix()
|
||||
except Exception:
|
||||
return 0
|
||||
if not (rel.startswith("raw/") or rel.startswith("wiki/")) or not p.exists():
|
||||
return 0
|
||||
vp, vb = build_vault_index(root)
|
||||
doc = parse_doc(p)
|
||||
findings = check_c2(doc, vp, vb, root, {}, rel) # C2 항상
|
||||
if is_completeness_checkable(doc):
|
||||
by_st, by_file = build_template_index(root)
|
||||
tmpl = resolve_template(doc["fm"], by_st, by_file)
|
||||
if rel.startswith("raw/project-notes/"):
|
||||
fm_findings = []
|
||||
if tmpl is not None:
|
||||
for k in tmpl["fm_keys"]:
|
||||
if k not in doc["fm_keys"]:
|
||||
fm_findings.append(("MISSING_FRONTMATTER", 0, f"frontmatter 키 누락: '{k}'"))
|
||||
findings = fm_findings + check_project_proxies(doc) + findings
|
||||
else:
|
||||
findings = check_c1(doc, tmpl) + findings + check_c3(doc)
|
||||
if not findings:
|
||||
return 0
|
||||
fixup = [f for f in findings if f[0] in wiki_rules.FIXUP_CODES]
|
||||
block = bool(fixup) and is_completeness_checkable(doc)
|
||||
sigil = "✗" if block else "⚠"
|
||||
print(f"{sigil} wiki-structure-lint: {rel} — 구조/링크 이슈 {len(findings)}건"
|
||||
+ (" → fix 필요" if block else ""), file=sys.stderr)
|
||||
for code, ln, msg in findings[:10]:
|
||||
loc = f":{ln}" if ln else ""
|
||||
print(f" [{code}]{loc} {msg}", file=sys.stderr)
|
||||
if len(findings) > 10:
|
||||
print(f" … 외 {len(findings) - 10}건 (suppressed)", file=sys.stderr)
|
||||
print(" 깨진 링크는 타깃 생성/수정(placeholder 는 `백틱 코드경로`). "
|
||||
"섹션/선택조건은 완성 선언 문서에만 검사됨.", file=sys.stderr)
|
||||
return 2 if block else 0
|
||||
```
|
||||
Replace the old `if args.hook:` block body with:
|
||||
```python
|
||||
if args.hook:
|
||||
import json as _json
|
||||
try:
|
||||
event = _json.loads(sys.stdin.read() or "{}")
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
sys.exit(run_hook(event, root))
|
||||
```
|
||||
Keep `build_vault_index`/`parse_doc`/`check_c1`/`check_c3`/`is_completeness_checkable`/`check_project_proxies`/`resolve_template`/`build_template_index` as-is (already module-level).
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -k HookTiering`
|
||||
Expected: PASS (3 tests).
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_structure_lint.py` → all green.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Wire `--pre` into `settings.json`
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/settings.json` (add a second PreToolUse entry)
|
||||
|
||||
- [ ] **Step 1: Edit `settings.json`**
|
||||
|
||||
Change the `PreToolUse` array from one matcher to two (leave PostToolUse / Subagent blocks untouched):
|
||||
```json
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{ "type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/wiki_claim_gate.py",
|
||||
"timeout": 30 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write|Edit|MultiEdit",
|
||||
"hooks": [
|
||||
{ "type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/wiki_structure_lint.py --pre",
|
||||
"timeout": 30 }
|
||||
]
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Validate JSON**
|
||||
|
||||
Run: `python3 -m json.tool .claude/settings.json > /dev/null && echo OK`
|
||||
Expected: `OK`.
|
||||
|
||||
- [ ] **Step 3: Checkpoint (no commit)** — JSON valid.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Full regression + acceptance smoke (spec §6)
|
||||
|
||||
**Files:** none (verification only)
|
||||
|
||||
- [ ] **Step 1: Run the full hook test suite**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 .claude/hooks/test_wiki_rules.py && \
|
||||
python3 .claude/hooks/test_wiki_claim_gate.py && \
|
||||
python3 .claude/hooks/test_wiki_structure_lint.py
|
||||
```
|
||||
Expected: all suites OK, zero failures.
|
||||
|
||||
- [ ] **Step 2: `--all` regression (gate wiring must not change findings)**
|
||||
|
||||
Run: `python3 .claude/hooks/wiki_structure_lint.py --all`
|
||||
Expected: a summary line `== 요약: N개 중 FAIL f / PASS p ==`. Compare `f` against a pre-change baseline (run the same on `git stash` of the hooks if unsure) — the FAIL set must be unchanged (the `--all` path is untouched by this spec).
|
||||
|
||||
- [ ] **Step 3: Acceptance criterion 1 — `--pre` blocks ghost, passes backtick (live stdin)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
echo '{"tool_name":"Write","tool_input":{"file_path":"raw/branch-notes/_smoke.md","content":"# t\nsee [[raw/nonexistent/ghost]]\n"}}' \
|
||||
| python3 .claude/hooks/wiki_structure_lint.py --pre; echo "exit=$?"
|
||||
echo '{"tool_name":"Write","tool_input":{"file_path":"raw/branch-notes/_smoke.md","content":"# t\nfuture: `[[raw/nonexistent/ghost]]`\n"}}' \
|
||||
| python3 .claude/hooks/wiki_structure_lint.py --pre; echo "exit=$?"
|
||||
```
|
||||
Expected: first `exit=2` with a stderr `[BROKEN_LINK]` line; second `exit=0`.
|
||||
|
||||
- [ ] **Step 4: Acceptance criterion 3 — `--hook` fix-up only on completion**
|
||||
|
||||
Create a temp completed concept doc missing required sections, pipe an Edit event, expect exit 2; flip `status: verified` → `status: draft`, expect exit 0. (Use a path under `raw/` or `wiki/` in the real repo or a temp `--root`.)
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import json, subprocess, tempfile, os
|
||||
from pathlib import Path
|
||||
d = tempfile.mkdtemp()
|
||||
root = Path(d); (root/"wiki"/"concepts").mkdir(parents=True)
|
||||
# copy templates so resolve_template works
|
||||
import shutil; shutil.copytree(".claude/hooks", root/".claude"/"hooks"); shutil.copytree("templates", root/"templates")
|
||||
p = root/"wiki"/"concepts"/"x.md"
|
||||
def run(status):
|
||||
p.write_text(f"---\ntitle: x\nsource_type: concept\nstatus: {status}\ntags: [a]\n---\n본문만\n")
|
||||
ev = json.dumps({"tool_name":"Edit","tool_input":{"file_path":str(p)}})
|
||||
r = subprocess.run(["python3", str(root/".claude"/"hooks"/"wiki_structure_lint.py"),
|
||||
"--hook", "--root", str(root)], input=ev, text=True, capture_output=True)
|
||||
print(status, "exit", r.returncode)
|
||||
run("verified"); run("draft")
|
||||
PY
|
||||
```
|
||||
Expected: `verified exit 2`, `draft exit 0`.
|
||||
|
||||
- [ ] **Step 5: Acceptance criterion 8 — happy path passes both gates**
|
||||
|
||||
Take a real completed wiki doc with valid links; run it through `--pre` (Write event) and `--hook` (Edit event). Both must `exit=0`. Confirms the gate doesn't block legitimate writes.
|
||||
|
||||
- [ ] **Step 6: Final checkpoint (no commit)**
|
||||
|
||||
All §6 acceptance criteria (1, 3, 4, 7, 8) demonstrated green. Report results to the user. Do NOT commit (per user instruction) — leave changes staged in the working tree for the user to review.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (completed by plan author)
|
||||
|
||||
- **Spec coverage:** §3 DD1 hybrid gate → Tasks 3+4+5. §3 DD2 lean SSOT → Tasks 1+2. §4.1 wiki_rules → Task 1. §4.2 `--pre` → Task 3. §4.3 `--hook` tiering + suppressed → Task 4. §4.4 settings → Task 5. §6 acceptance 1-8 → Task 6. G6 (claim_gate tests) → Task 2. No spec requirement left unmapped.
|
||||
- **Placeholder scan:** the only `...` is the explicit "copy these 9 functions verbatim from claim_gate lines X-Y" instruction with exact source line ranges — not a content gap. All test/impl steps carry runnable code.
|
||||
- **Type/name consistency:** `run_pre(event, root)` / `run_hook(event, root)` signatures match between Tasks 3/4 impl and their tests; `CRITICAL_CODES`/`FIXUP_CODES`/`CLAIM_REQUIREMENTS` names match between Task 1 (def) and Tasks 2/3/4 (use); `check_markdown_write(rel, text)` signature unchanged (Task 2 tests call it as today).
|
||||
@@ -0,0 +1,621 @@
|
||||
# Spec B — Judge Verdict Schema & Adversarial Quorum Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
> **Commits excluded** per user instruction — no `git commit` steps; each task ends with a test-green checkpoint.
|
||||
|
||||
**Goal:** Make judge-agent verdicts machine-validated at the SubagentStop boundary and make the adversarial-review kill-decision a deterministic quorum tally — without the Workflow tool (Claude subagents lack tool-layer schema enforcement).
|
||||
|
||||
**Architecture:** Extend the shared `wiki_rules.py` with `validate_verdict_block` (P1 schema + verdict↔count consistency) and `tally_quorum` (≥2 REJECT = KILL, default-refute, abstain≠pass). `wiki_claim_gate.subagent_stop_gate` validates any subagent output carrying a `wiki-verdict` marker (block on schema error; non-judge output untouched). New `wiki_quorum.py` CLI tallies N adversarial-review outputs. Five judge `.md` files emit the machine block; `wiki-adversarial-reviewer` also flips to default-refute. Deterministic core is unit-tested; agent prose is mirrored across 3 platforms by hand (the generator is absent).
|
||||
|
||||
**Tech Stack:** Python 3 stdlib (`re`, `json`, `argparse`, `unittest`, `subprocess`). Claude Code hooks. Markdown agent specs.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-06-spec-b-judge-verdict-schema-and-quorum-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Modify** `.claude/hooks/wiki_rules.py` — add `parse_verdict_block`, `validate_verdict_block`, `tally_quorum`, constants.
|
||||
- **Modify** `.claude/hooks/test_wiki_rules.py` — add `TestVerdictBlock`, `TestTallyQuorum`.
|
||||
- **Create** `.claude/hooks/wiki_quorum.py` — CLI over `tally_quorum`.
|
||||
- **Create** `.claude/hooks/test_wiki_quorum.py` — CLI integration tests.
|
||||
- **Modify** `.claude/hooks/wiki_claim_gate.py` — `subagent_stop_gate` calls `validate_verdict_block`.
|
||||
- **Modify** `.claude/hooks/test_wiki_claim_gate.py` — add `TestSubagentStopVerdict` (subprocess).
|
||||
- **Modify** `.claude/agents/wiki-adversarial-reviewer.md` — per-finding block + default-refute + quorum doc.
|
||||
- **Modify** `.claude/agents/{branch-depth-auditor,coverage-auditor,project-readiness-auditor,wiki-diagram-reviewer}.md` — standard verdict block.
|
||||
- **Mirror (manual)** the 4 shared judges into `.agents/plugins/wiki-superpowers/agents/<name>.md`, `.codex/agents/<name>.md`, `.codex/agents/<name>.toml`. (`project-readiness-auditor` is Claude-only — no mirror.)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `wiki_rules` — verdict block parse + validate
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_rules.py` (append functions + constants)
|
||||
- Test: `.claude/hooks/test_wiki_rules.py` (append `TestVerdictBlock`)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append to `.claude/hooks/test_wiki_rules.py` before the `if __name__` line:
|
||||
```python
|
||||
STD_OK = "리포트...\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: not-ready\nblocking: 2\nshould_fix: 1\nadvisory: 0\n```\n끝"
|
||||
STD_CONTRADICT = "```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 2\nshould_fix: 0\nadvisory: 0\n```"
|
||||
STD_BADVERDICT = "```wiki-verdict\nagent: x\nverdict: foo\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"
|
||||
ADV_OK = "```wiki-verdict\nagent: wiki-adversarial-reviewer\nfinding: 4.1.1 action: KEEP\nfinding: 4.2.1 action: REJECT\n```"
|
||||
ADV_BADACTION = "```wiki-verdict\nagent: wiki-adversarial-reviewer\nfinding: 4.1.1 action: NOPE\n```"
|
||||
ADV_EMPTY = "```wiki-verdict\nagent: wiki-adversarial-reviewer\n```"
|
||||
|
||||
|
||||
class TestVerdictBlock(unittest.TestCase):
|
||||
def test_no_marker_returns_none(self):
|
||||
parsed, errors = wr.validate_verdict_block("그냥 산문, 마커 없음")
|
||||
self.assertIsNone(parsed)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_standard_valid(self):
|
||||
parsed, errors = wr.validate_verdict_block(STD_OK)
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual(parsed["agent"], "branch-depth-auditor")
|
||||
self.assertEqual(parsed["kv"]["verdict"], "not-ready")
|
||||
|
||||
def test_standard_contradiction_flagged(self):
|
||||
_, errors = wr.validate_verdict_block(STD_CONTRADICT)
|
||||
self.assertTrue(any("blocking" in e for e in errors))
|
||||
|
||||
def test_standard_bad_verdict_flagged(self):
|
||||
_, errors = wr.validate_verdict_block(STD_BADVERDICT)
|
||||
self.assertTrue(any("verdict" in e for e in errors))
|
||||
|
||||
def test_adversarial_valid(self):
|
||||
parsed, errors = wr.validate_verdict_block(ADV_OK)
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual(len(parsed["findings"]), 2)
|
||||
|
||||
def test_adversarial_bad_action_flagged(self):
|
||||
_, errors = wr.validate_verdict_block(ADV_BADACTION)
|
||||
self.assertTrue(any("action" in e for e in errors))
|
||||
|
||||
def test_adversarial_empty_findings_flagged(self):
|
||||
_, errors = wr.validate_verdict_block(ADV_EMPTY)
|
||||
self.assertTrue(any("finding" in e for e in errors))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_rules.py -k Verdict`
|
||||
Expected: FAIL — `AttributeError: module 'wiki_rules' has no attribute 'validate_verdict_block'`.
|
||||
|
||||
- [ ] **Step 3: Implement in `wiki_rules.py`**
|
||||
|
||||
Append to `.claude/hooks/wiki_rules.py` (the `import re` must be added to the top import block — currently `wiki_rules.py` imports only `json, shlex, sys`):
|
||||
```python
|
||||
import re # (add to the existing import block at top of file)
|
||||
|
||||
VERDICT_FENCE_RE = re.compile(r"```wiki-verdict\s*\n(.*?)\n```", re.S)
|
||||
VALID_VERDICT = {"ready", "not-ready", "blocked"}
|
||||
VALID_ACTION = {"KEEP", "DOWNGRADE", "REJECT"}
|
||||
REFUTATIONS_REQUIRED = 2 # ≥2 REJECT → kill (deep-research 기본값)
|
||||
|
||||
|
||||
def parse_verdict_block(text):
|
||||
"""본문에서 wiki-verdict fenced 블록을 찾아 dict 로 파싱. 없으면 None."""
|
||||
m = VERDICT_FENCE_RE.search(text or "")
|
||||
if not m:
|
||||
return None
|
||||
out = {"agent": None, "kv": {}, "findings": []}
|
||||
for line in m.group(1).splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
fm = re.match(r"finding:\s*(\S+)\s+action:\s*(\S+)", line)
|
||||
if fm:
|
||||
out["findings"].append((fm.group(1), fm.group(2)))
|
||||
continue
|
||||
kv = re.match(r"([a-z_]+):\s*(.+)$", line)
|
||||
if kv:
|
||||
k, v = kv.group(1), kv.group(2).strip()
|
||||
if k == "agent":
|
||||
out["agent"] = v
|
||||
else:
|
||||
out["kv"][k] = v
|
||||
return out
|
||||
|
||||
|
||||
def validate_verdict_block(text):
|
||||
"""(parsed, errors). parsed None → 마커 없음(judge 아님, caller 통과).
|
||||
errors 비어있지 않으면 스키마 위반 → SubagentStop 차단."""
|
||||
parsed = parse_verdict_block(text)
|
||||
if parsed is None:
|
||||
return None, []
|
||||
errors = []
|
||||
if not parsed["agent"]:
|
||||
errors.append("wiki-verdict 블록에 `agent:` 누락")
|
||||
if parsed["agent"] == "wiki-adversarial-reviewer":
|
||||
if not parsed["findings"]:
|
||||
errors.append("adversarial verdict 블록에 `finding: <id> action: <act>` 행 ≥1 필요")
|
||||
for fid, act in parsed["findings"]:
|
||||
if act not in VALID_ACTION:
|
||||
errors.append(f"finding {fid}: action '{act}' 비허용(KEEP|DOWNGRADE|REJECT)")
|
||||
else:
|
||||
v = parsed["kv"].get("verdict")
|
||||
if v not in VALID_VERDICT:
|
||||
errors.append(f"verdict '{v}' 비허용(ready|not-ready|blocked)")
|
||||
blocking = None
|
||||
try:
|
||||
blocking = int(parsed["kv"].get("blocking", ""))
|
||||
int(parsed["kv"].get("should_fix", ""))
|
||||
int(parsed["kv"].get("advisory", ""))
|
||||
except ValueError:
|
||||
errors.append("blocking/should_fix/advisory 는 정수여야 함")
|
||||
if blocking is not None and v == "ready" and blocking != 0:
|
||||
errors.append("verdict=ready 인데 blocking≠0 (모순)")
|
||||
if blocking is not None and v == "not-ready" and blocking < 1:
|
||||
errors.append("verdict=not-ready 인데 blocking<1 (모순)")
|
||||
return parsed, errors
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_rules.py -k Verdict`
|
||||
Expected: PASS (7 tests).
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)** — `python3 .claude/hooks/test_wiki_rules.py` all green.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `wiki_rules` — quorum tally
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_rules.py` (append `tally_quorum`)
|
||||
- Test: `.claude/hooks/test_wiki_rules.py` (append `TestTallyQuorum`)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append:
|
||||
```python
|
||||
def _adv(*pairs):
|
||||
lines = "\n".join(f"finding: {fid} action: {act}" for fid, act in pairs)
|
||||
return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"
|
||||
|
||||
|
||||
class TestTallyQuorum(unittest.TestCase):
|
||||
def test_two_rejects_kill(self):
|
||||
blocks = [_adv(("A", "REJECT")), _adv(("A", "REJECT")), _adv(("A", "KEEP"))]
|
||||
per = wr.tally_quorum(blocks)
|
||||
self.assertEqual(per["A"]["decision"], "KILL")
|
||||
|
||||
def test_unanimous_keep(self):
|
||||
blocks = [_adv(("A", "KEEP")), _adv(("A", "KEEP")), _adv(("A", "KEEP"))]
|
||||
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "KEEP")
|
||||
|
||||
def test_reject_plus_downgrade_is_downgrade(self):
|
||||
blocks = [_adv(("A", "REJECT")), _adv(("A", "DOWNGRADE")), _adv(("A", "KEEP"))]
|
||||
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "DOWNGRADE")
|
||||
|
||||
def test_abstain_not_pass(self):
|
||||
# 한 블록만 KEEP, 나머지 둘은 A 를 누락(abstain) → 정족수 미달 → UNVERIFIED
|
||||
blocks = [_adv(("A", "KEEP")), _adv(("B", "KEEP")), _adv(("C", "KEEP"))]
|
||||
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "UNVERIFIED")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_rules.py -k Tally`
|
||||
Expected: FAIL — `AttributeError: ... 'tally_quorum'`.
|
||||
|
||||
- [ ] **Step 3: Implement in `wiki_rules.py`**
|
||||
|
||||
Append:
|
||||
```python
|
||||
def tally_quorum(block_texts, refutations_required=REFUTATIONS_REQUIRED):
|
||||
"""N개 adversarial verdict 블록 → per-finding 결정론 판정.
|
||||
|
||||
refute = DOWNGRADE 또는 REJECT (원 severity 반박).
|
||||
default-refute: 어떤 pass 가 finding 을 누락/malformed → abstain(non-KEEP).
|
||||
결정: reject≥req → KILL · (reject+downgrade)≥req → DOWNGRADE ·
|
||||
keep≥req → KEEP · 그 외(정족수 미달) → UNVERIFIED(통과 금지).
|
||||
"""
|
||||
parsed_all = [parse_verdict_block(t) for t in block_texts]
|
||||
all_fids = set()
|
||||
for p in parsed_all:
|
||||
if p:
|
||||
for fid, _ in p["findings"]:
|
||||
all_fids.add(fid)
|
||||
per = {}
|
||||
for fid in all_fids:
|
||||
keep = downgrade = reject = abstain = 0
|
||||
for p in parsed_all:
|
||||
act = None
|
||||
if p:
|
||||
for f, a in p["findings"]:
|
||||
if f == fid:
|
||||
act = a
|
||||
break
|
||||
if act == "KEEP":
|
||||
keep += 1
|
||||
elif act == "DOWNGRADE":
|
||||
downgrade += 1
|
||||
elif act == "REJECT":
|
||||
reject += 1
|
||||
else:
|
||||
abstain += 1
|
||||
if reject >= refutations_required:
|
||||
decision = "KILL"
|
||||
elif (reject + downgrade) >= refutations_required:
|
||||
decision = "DOWNGRADE"
|
||||
elif keep >= refutations_required:
|
||||
decision = "KEEP"
|
||||
else:
|
||||
decision = "UNVERIFIED"
|
||||
per[fid] = {"keep": keep, "downgrade": downgrade, "reject": reject,
|
||||
"abstain": abstain, "n": len(block_texts), "decision": decision}
|
||||
return per
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_rules.py -k Tally`
|
||||
Expected: PASS (4 tests).
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)** — `python3 .claude/hooks/test_wiki_rules.py` all green.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `wiki_quorum.py` CLI
|
||||
|
||||
**Files:**
|
||||
- Create: `.claude/hooks/wiki_quorum.py`
|
||||
- Test: `.claude/hooks/test_wiki_quorum.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `.claude/hooks/test_wiki_quorum.py`:
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""wiki_quorum.py CLI 통합 테스트."""
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
CLI = str(Path(__file__).with_name("wiki_quorum.py"))
|
||||
|
||||
|
||||
def _adv(*pairs):
|
||||
lines = "\n".join(f"finding: {fid} action: {act}" for fid, act in pairs)
|
||||
return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"
|
||||
|
||||
|
||||
class TestQuorumCLI(unittest.TestCase):
|
||||
def _files(self, d, *texts):
|
||||
paths = []
|
||||
for i, t in enumerate(texts):
|
||||
p = Path(d) / f"v{i}.md"
|
||||
p.write_text(t)
|
||||
paths.append(str(p))
|
||||
return paths
|
||||
|
||||
def test_kill_exits_1(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
paths = self._files(d, _adv(("A", "REJECT")), _adv(("A", "REJECT")), _adv(("A", "KEEP")))
|
||||
r = subprocess.run(["python3", CLI] + paths, capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 1)
|
||||
self.assertIn("KILL", r.stdout)
|
||||
|
||||
def test_all_keep_exits_0(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
paths = self._files(d, _adv(("A", "KEEP")), _adv(("A", "KEEP")), _adv(("A", "KEEP")))
|
||||
r = subprocess.run(["python3", CLI] + paths, capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertIn("KEEP", r.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_quorum.py`
|
||||
Expected: FAIL — `FileNotFoundError` (wiki_quorum.py missing) → subprocess returncode nonzero / can't run.
|
||||
|
||||
- [ ] **Step 3: Implement `wiki_quorum.py`**
|
||||
|
||||
Create `.claude/hooks/wiki_quorum.py`:
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""wiki_quorum.py — N개 adversarial verdict 블록의 결정론 quorum tally CLI.
|
||||
|
||||
사용:
|
||||
python3 wiki_quorum.py vote1.md vote2.md vote3.md
|
||||
cat votes.md | python3 wiki_quorum.py --stdin # '---' 구분 멀티블록
|
||||
exit: 1 if any KILL/UNVERIFIED, else 0.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import wiki_rules
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if "--stdin" in args:
|
||||
blob = sys.stdin.read()
|
||||
blocks = [b for b in blob.split("\n---\n") if "wiki-verdict" in b]
|
||||
else:
|
||||
blocks = []
|
||||
for a in args:
|
||||
try:
|
||||
blocks.append(Path(a).read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
print(f"파일 읽기 실패: {a} — {e}", file=sys.stderr)
|
||||
if not blocks:
|
||||
print("verdict 블록 입력 없음", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
per = wiki_rules.tally_quorum(blocks)
|
||||
print(f"== Quorum tally: N={len(blocks)} votes, {len(per)} findings ==")
|
||||
print("| finding | keep | down | reject | abstain | decision |")
|
||||
print("|---|---|---|---|---|---|")
|
||||
bad = 0
|
||||
for fid in sorted(per):
|
||||
r = per[fid]
|
||||
if r["decision"] in ("KILL", "UNVERIFIED"):
|
||||
bad += 1
|
||||
print(f"| {fid} | {r['keep']} | {r['downgrade']} | {r['reject']} | {r['abstain']} | {r['decision']} |")
|
||||
print(f"\nKILL/UNVERIFIED: {bad} / {len(per)}")
|
||||
sys.exit(1 if bad else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_quorum.py`
|
||||
Expected: PASS (2 tests).
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)** — green.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: `claim_gate.subagent_stop_gate` verdict enforcement
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_claim_gate.py` (`subagent_stop_gate`)
|
||||
- Test: `.claude/hooks/test_wiki_claim_gate.py` (append `TestSubagentStopVerdict`, subprocess-based)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append to `.claude/hooks/test_wiki_claim_gate.py`:
|
||||
```python
|
||||
import json as _json
|
||||
import subprocess as _sp
|
||||
|
||||
_GATE = str(Path(__file__).with_name("wiki_claim_gate.py"))
|
||||
|
||||
|
||||
def _run_stop(message):
|
||||
ev = {"hook_event_name": "SubagentStop", "last_assistant_message": message}
|
||||
return _sp.run(["python3", _GATE], input=_json.dumps(ev), capture_output=True, text=True)
|
||||
|
||||
|
||||
class TestSubagentStopVerdict(unittest.TestCase):
|
||||
def test_malformed_verdict_blocks(self):
|
||||
msg = "리뷰 끝.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"
|
||||
r = _run_stop(msg)
|
||||
self.assertEqual(r.returncode, 2)
|
||||
|
||||
def test_valid_verdict_allows(self):
|
||||
msg = "리뷰 끝.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"
|
||||
r = _run_stop(msg)
|
||||
self.assertEqual(r.returncode, 0)
|
||||
|
||||
def test_no_marker_allows(self):
|
||||
r = _run_stop("그냥 일반 subagent 출력, 마커 없음")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopVerdict`
|
||||
Expected: FAIL — `test_malformed_verdict_blocks` gets returncode 0 (gate not yet checking verdict).
|
||||
|
||||
- [ ] **Step 3: Implement — extend `subagent_stop_gate`**
|
||||
|
||||
In `.claude/hooks/wiki_claim_gate.py`, replace the `subagent_stop_gate` body's final `emit_allow()` with a verdict check before it:
|
||||
```python
|
||||
def subagent_stop_gate(event: dict) -> None:
|
||||
message = event.get("last_assistant_message") or ""
|
||||
if not isinstance(message, str):
|
||||
emit_allow()
|
||||
if re.search(r"Verdict:\s*COMPLETE|\bDONE\b|완료", message):
|
||||
missing = []
|
||||
for term in ("Claim ID", "Decision Evidence Map", "UNSUPPORTED_DECISION"):
|
||||
if term not in message:
|
||||
missing.append(term)
|
||||
if missing and not event.get("stop_hook_active"):
|
||||
emit_block(
|
||||
"Subagent output claims completion but does not report claim-traceability checks: "
|
||||
+ ", ".join(missing)
|
||||
)
|
||||
# judge 출력에 wiki-verdict 마커가 있으면 스키마 검증(없으면 judge 아님 → 통과).
|
||||
parsed, verr = wiki_rules.validate_verdict_block(message)
|
||||
if parsed is not None and verr and not event.get("stop_hook_active"):
|
||||
emit_block("judge verdict 블록 스키마 오류:\n- " + "\n- ".join(verr))
|
||||
emit_allow()
|
||||
```
|
||||
(`wiki_rules` is already imported in `wiki_claim_gate.py` from Spec A.)
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopVerdict`
|
||||
Expected: PASS (3 tests).
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)** — `python3 .claude/hooks/test_wiki_claim_gate.py` all green (old 11 + 3).
|
||||
|
||||
---
|
||||
|
||||
## Task 5: `wiki-adversarial-reviewer.md` — block + default-refute + quorum doc
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/agents/wiki-adversarial-reviewer.md`
|
||||
|
||||
- [ ] **Step 1: Add the machine verdict block to the Output section**
|
||||
|
||||
In the `## Output` markdown template (after the `## Aggregate metrics` block, before the closing ```), add — as part of the report the agent must emit:
|
||||
````
|
||||
## Machine verdict (필수 — SubagentStop 가 검증)
|
||||
|
||||
```wiki-verdict
|
||||
agent: wiki-adversarial-reviewer
|
||||
finding: 4.1.1 action: KEEP
|
||||
finding: 4.2.1 action: DOWNGRADE
|
||||
```
|
||||
(모든 Falsification Summary 행의 Finding ID 를 `finding: <id> action: KEEP|DOWNGRADE|REJECT` 로 1:1 반영.)
|
||||
````
|
||||
|
||||
- [ ] **Step 2: Flip to default-refute**
|
||||
|
||||
In the `## Severity Adjustment` and `Shortcut Trap` sections, change the uncertainty default. Replace the current `INSUFFICIENT_CONTEXT` guidance so that **uncertainty leans toward REJECT/DOWNGRADE, not KEEP**:
|
||||
- Add to `## Severity Adjustment`:
|
||||
> **Default-refute (deep-research 정렬):** 세 검사 중 하나라도 확신이 안 서면 KEEP 이 아니라 최소 DOWNGRADE. `INSUFFICIENT_CONTEXT` 는 "판단 보류 후 KEEP" 이 아니라 "근거 부족 → 그 finding 의 원 severity 를 신뢰할 수 없음 → DOWNGRADE 권고" 로 처리한다. KEEP 은 세 검사가 *적극적으로* 통과할 때만.
|
||||
|
||||
- [ ] **Step 3: Document the N=3 quorum flow**
|
||||
|
||||
Add a new section `## Quorum (opt-in N=3)`:
|
||||
````
|
||||
고위험 검증 시 controller 가 이 에이전트를 **독립적으로 N=3 병렬 dispatch** 하고, 각 출력의 `wiki-verdict` 블록을 `wiki_quorum.py` 에 투입한다:
|
||||
|
||||
```
|
||||
python3 .claude/hooks/wiki_quorum.py vote1.md vote2.md vote3.md
|
||||
```
|
||||
|
||||
`wiki_quorum.py` 가 per-finding 결정(KILL/DOWNGRADE/KEEP/UNVERIFIED)을 **결정론적으로** 계산한다(≥2 REJECT=KILL, abstain≠pass). controller 는 임계값을 못 바꾼다. 기본은 N=1(단일 패스).
|
||||
````
|
||||
|
||||
- [ ] **Step 4: Verify block present**
|
||||
|
||||
Run: `grep -c "wiki-verdict" .claude/agents/wiki-adversarial-reviewer.md`
|
||||
Expected: ≥1.
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Standard 4 judges — verdict block
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/agents/branch-depth-auditor.md`, `coverage-auditor.md`, `project-readiness-auditor.md`, `wiki-diagram-reviewer.md`
|
||||
|
||||
- [ ] **Step 1: Add the block to each judge's Output section**
|
||||
|
||||
In each agent's output template (`## 출력` / `## Output`), directly under the human `Verdict:` line, add the machine block. Use the agent's own name and map its verdict:
|
||||
````
|
||||
```wiki-verdict
|
||||
agent: <이 에이전트 name>
|
||||
verdict: ready|not-ready|blocked
|
||||
blocking: <N>
|
||||
should_fix: <M>
|
||||
advisory: <K>
|
||||
```
|
||||
````
|
||||
Per-agent mapping note to include inline:
|
||||
- `branch-depth-auditor`: `verdict=ready` ⟺ Blocking 0; else `not-ready`.
|
||||
- `coverage-auditor`: `verdict=ready` ⟺ missing(Blocking) 0; else `not-ready`. (`Covered`→ready, `Not-covered`→not-ready.)
|
||||
- `project-readiness-auditor`: `verdict=ready` ⟺ Blocking 0 (Ready); else `not-ready`.
|
||||
- `wiki-diagram-reviewer`: `verdict=ready` ⟺ 점수 ≥95 (PASS); `<95`→`not-ready`; BLOCKED→`blocked`. `blocking` = HARD-STOP 수.
|
||||
|
||||
- [ ] **Step 2: Verify all four have the block**
|
||||
|
||||
Run: `for a in branch-depth-auditor coverage-auditor project-readiness-auditor wiki-diagram-reviewer; do echo -n "$a: "; grep -c "wiki-verdict" .claude/agents/$a.md; done`
|
||||
Expected: each prints ≥1.
|
||||
|
||||
- [ ] **Step 3: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 7: 3-platform manual mirror (4 shared judges)
|
||||
|
||||
**Files (mirror the SAME block/edits made in Tasks 5-6):**
|
||||
- `.agents/plugins/wiki-superpowers/agents/{branch-depth-auditor,coverage-auditor,wiki-adversarial-reviewer,wiki-diagram-reviewer}.md`
|
||||
- `.codex/agents/{branch-depth-auditor,coverage-auditor,wiki-adversarial-reviewer,wiki-diagram-reviewer}.md`
|
||||
- `.codex/agents/{...}.toml` (the verdict block goes inside the `developer_instructions` string)
|
||||
- **NOT** `project-readiness-auditor` — Claude-only, no variants exist.
|
||||
|
||||
- [ ] **Step 1: Mirror the body changes**
|
||||
|
||||
For each of the 4 shared judges, copy the verdict-block addition (and for adversarial, the default-refute + quorum sections) from the `.claude/agents/<name>.md` into the three variant files. Keep each platform's frontmatter/format; only the body content is mirrored.
|
||||
|
||||
- [ ] **Step 2: Grep-verify parity**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
for a in branch-depth-auditor coverage-auditor wiki-adversarial-reviewer wiki-diagram-reviewer; do
|
||||
echo "== $a =="
|
||||
grep -l "wiki-verdict" .claude/agents/$a.md .agents/plugins/wiki-superpowers/agents/$a.md .codex/agents/$a.md .codex/agents/$a.toml
|
||||
done
|
||||
```
|
||||
Expected: all 4 files listed for each judge (16 total).
|
||||
|
||||
- [ ] **Step 3: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Full regression + acceptance smoke (spec §6)
|
||||
|
||||
**Files:** none (verification)
|
||||
|
||||
- [ ] **Step 1: Run all hook test suites**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
for t in test_wiki_rules test_wiki_claim_gate test_wiki_structure_lint test_wiki_quorum; do
|
||||
out=$(python3 .claude/hooks/$t.py 2>&1 | tail -1); echo "$t -> $out"
|
||||
done
|
||||
```
|
||||
Expected: all `OK`.
|
||||
|
||||
- [ ] **Step 2: Acceptance §6.4 — SubagentStop live (malformed blocks, valid passes)**
|
||||
|
||||
Run (printf to avoid JSON mangling; no `>` redirect to avoid the claim_gate bash-gate):
|
||||
```bash
|
||||
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 4\nshould_fix: 0\nadvisory: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py; echo "malformed exit=$?"
|
||||
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py; echo "valid exit=$?"
|
||||
```
|
||||
Expected: `malformed exit=2`, `valid exit=0`.
|
||||
|
||||
- [ ] **Step 3: Acceptance §6.5 — wiki_quorum CLI live**
|
||||
|
||||
Run (build 3 vote files in /tmp via a /tmp helper to avoid the bash-gate, then tally):
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
d = Path("/tmp/qsmoke"); d.mkdir(exist_ok=True)
|
||||
def adv(fid_acts):
|
||||
lines = "\n".join(f"finding: {f} action: {a}" for f,a in fid_acts)
|
||||
return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"
|
||||
(d/"v0.md").write_text(adv([("A","REJECT")]))
|
||||
(d/"v1.md").write_text(adv([("A","REJECT")]))
|
||||
(d/"v2.md").write_text(adv([("A","KEEP")]))
|
||||
print("wrote", d)
|
||||
PY
|
||||
python3 .claude/hooks/wiki_quorum.py /tmp/qsmoke/v0.md /tmp/qsmoke/v1.md /tmp/qsmoke/v2.md; echo "quorum exit=$?"
|
||||
rm -rf /tmp/qsmoke
|
||||
```
|
||||
Expected: table with finding `A … KILL`, `quorum exit=1`.
|
||||
|
||||
- [ ] **Step 4: Acceptance §6.7 — grep parity**
|
||||
|
||||
Run the Task 7 Step 2 grep + the Task 6 Step 2 grep. Expected: every judge file carries `wiki-verdict`.
|
||||
|
||||
- [ ] **Step 5: Final checkpoint (no commit)** — report all results. Leave changes in working tree.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (completed by plan author)
|
||||
|
||||
- **Spec coverage:** §4.1 block format → Tasks 5/6. §4.2 validate+tally → Tasks 1/2. §4.3 SubagentStop → Task 4. §4.4 wiki_quorum.py → Task 3. §4.5 agent edits + mirror → Tasks 5/6/7. §6 acceptance 1-7 → Tasks 1-4 tests + Task 8 smokes. No requirement unmapped.
|
||||
- **Placeholder scan:** agent-edit tasks give the exact block text + exact mapping per agent + exact mirror file list; no "TBD"/"similar to". Deterministic-core tasks carry full runnable code.
|
||||
- **Type/name consistency:** `parse_verdict_block`/`validate_verdict_block`/`tally_quorum` names match across Tasks 1-4 and the CLI; the `wiki-verdict` fence string is identical in tests, impl, agent blocks, and smokes; `REFUTATIONS_REQUIRED=2` matches the tally tests (REJECT×2 = KILL).
|
||||
@@ -0,0 +1,354 @@
|
||||
# Spec C — Funnel Stats & No-Silent-Truncation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax.
|
||||
> **Commits excluded** per user instruction — no `git commit` steps; each task ends with a test-green checkpoint.
|
||||
|
||||
**Goal:** Make coverage-bounding outputs report a balanced funnel (`found = processed + dropped`) with a mandatory `dropped_reason`, enforced at SubagentStop for the 4 key judge/research agents — so silent truncation becomes visible (audit gap G4).
|
||||
|
||||
**Architecture:** Extend the shared `wiki_rules.py` with `validate_stats_block` (funnel-balance + dropped-reason). `wiki_claim_gate.subagent_stop_gate` validates any subagent output carrying a `wiki-stats` marker (reuses the Spec B SubagentStop path). Add a no-silent-truncation contract to `rules/reporting-standards.md`; add a `## Stats` `wiki-stats` block to 4 agents + a `## Stats` funnel to `/ingest`. Deterministic core unit-tested; agent prose mirrored 3-platform by hand.
|
||||
|
||||
**Tech Stack:** Python 3 stdlib (`re`, `json`, `unittest`, `subprocess`). Claude Code hooks. Markdown agent/command specs.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-06-spec-c-funnel-stats-no-silent-truncation-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Modify** `.claude/hooks/wiki_rules.py` — `parse_stats_block`, `validate_stats_block`, `STATS_FENCE_RE`.
|
||||
- **Modify** `.claude/hooks/test_wiki_rules.py` — `TestStatsBlock`.
|
||||
- **Modify** `.claude/hooks/wiki_claim_gate.py` — `subagent_stop_gate` validates `wiki-stats`.
|
||||
- **Modify** `.claude/hooks/test_wiki_claim_gate.py` — `TestSubagentStopStats`.
|
||||
- **Modify** `rules/reporting-standards.md` — "No silent truncation" 절.
|
||||
- **Modify** `.claude/agents/{coverage-auditor,branch-depth-auditor,wiki-decision-researcher,wiki-research-lane}.md` — `## Stats` block.
|
||||
- **Modify** `.claude/commands/ingest.md` — `## Stats` funnel 계약.
|
||||
- **Mirror (manual)** the 4 agents → `.agents/plugins/wiki-superpowers/agents/<name>.md` (G3) + `.codex/agents/<name>.md`/`.toml`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `wiki_rules` — stats block parse + validate
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_rules.py`
|
||||
- Test: `.claude/hooks/test_wiki_rules.py` (append `TestStatsBlock`)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append to `test_wiki_rules.py` before `if __name__`:
|
||||
```python
|
||||
def _stats(found, processed, dropped, reason=None):
|
||||
body = f"agent: coverage-auditor\nfound: {found}\nprocessed: {processed}\ndropped: {dropped}"
|
||||
if reason is not None:
|
||||
body += f"\ndropped_reason: {reason}"
|
||||
return f"```wiki-stats\n{body}\n```"
|
||||
|
||||
|
||||
class TestStatsBlock(unittest.TestCase):
|
||||
def test_no_marker_returns_none(self):
|
||||
parsed, errors = wr.validate_stats_block("산문, 마커 없음")
|
||||
self.assertIsNone(parsed)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_balanced_ok(self):
|
||||
parsed, errors = wr.validate_stats_block(_stats(12, 10, 2, "2 out-of-scope"))
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual(parsed["agent"], "coverage-auditor")
|
||||
|
||||
def test_imbalance_flagged(self):
|
||||
_, errors = wr.validate_stats_block(_stats(12, 10, 0))
|
||||
self.assertTrue(any("불균형" in e for e in errors))
|
||||
|
||||
def test_dropped_without_reason_flagged(self):
|
||||
_, errors = wr.validate_stats_block(_stats(12, 10, 2))
|
||||
self.assertTrue(any("dropped_reason" in e for e in errors))
|
||||
|
||||
def test_non_integer_flagged(self):
|
||||
block = "```wiki-stats\nagent: x\nfound: many\nprocessed: 1\ndropped: 0\n```"
|
||||
_, errors = wr.validate_stats_block(block)
|
||||
self.assertTrue(any("정수" in e for e in errors))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_rules.py -k Stats`
|
||||
Expected: FAIL — `AttributeError: ... 'validate_stats_block'`.
|
||||
|
||||
- [ ] **Step 3: Implement in `wiki_rules.py`**
|
||||
|
||||
Append (after the `tally_quorum` function):
|
||||
```python
|
||||
STATS_FENCE_RE = re.compile(r"```wiki-stats\s*\n(.*?)\n```", re.S)
|
||||
|
||||
|
||||
def parse_stats_block(text):
|
||||
m = STATS_FENCE_RE.search(text or "")
|
||||
if not m:
|
||||
return None
|
||||
out = {"agent": None, "kv": {}}
|
||||
for line in m.group(1).splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
kv = re.match(r"([a-z_]+):\s*(.+)$", line)
|
||||
if kv:
|
||||
k, v = kv.group(1), kv.group(2).strip()
|
||||
if k == "agent":
|
||||
out["agent"] = v
|
||||
else:
|
||||
out["kv"][k] = v
|
||||
return out
|
||||
|
||||
|
||||
def validate_stats_block(text):
|
||||
"""(parsed, errors). parsed None → 마커 없음(통과). errors → SubagentStop 차단.
|
||||
funnel 균형(found=processed+dropped) + dropped>0 시 dropped_reason 필수 (no-silent-truncation)."""
|
||||
parsed = parse_stats_block(text)
|
||||
if parsed is None:
|
||||
return None, []
|
||||
errors = []
|
||||
if not parsed["agent"]:
|
||||
errors.append("wiki-stats 블록에 `agent:` 누락")
|
||||
nums = {}
|
||||
for k in ("found", "processed", "dropped"):
|
||||
try:
|
||||
nums[k] = int(parsed["kv"].get(k, ""))
|
||||
except ValueError:
|
||||
errors.append(f"wiki-stats `{k}` 는 정수여야 함 (funnel 필수 필드)")
|
||||
if len(nums) == 3:
|
||||
if nums["found"] != nums["processed"] + nums["dropped"]:
|
||||
errors.append(
|
||||
f"funnel 불균형: found({nums['found']}) ≠ processed({nums['processed']}) "
|
||||
f"+ dropped({nums['dropped']}) — 조용한 누락 의심"
|
||||
)
|
||||
if nums["dropped"] > 0 and not parsed["kv"].get("dropped_reason", "").strip():
|
||||
errors.append("dropped>0 인데 `dropped_reason` 누락 (no-silent-truncation 위반)")
|
||||
return parsed, errors
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_rules.py -k Stats`
|
||||
Expected: PASS (5 tests).
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)** — `python3 .claude/hooks/test_wiki_rules.py` all green.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `claim_gate.subagent_stop_gate` — wiki-stats enforcement
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/hooks/wiki_claim_gate.py` (`subagent_stop_gate`)
|
||||
- Test: `.claude/hooks/test_wiki_claim_gate.py` (append `TestSubagentStopStats`)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append (the `_run_stop` helper already exists from Spec B):
|
||||
```python
|
||||
class TestSubagentStopStats(unittest.TestCase):
|
||||
def test_imbalanced_stats_blocks(self):
|
||||
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 0\n```"
|
||||
self.assertEqual(_run_stop(msg).returncode, 2)
|
||||
|
||||
def test_balanced_stats_allows(self):
|
||||
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 10\nprocessed: 10\ndropped: 0\n```"
|
||||
self.assertEqual(_run_stop(msg).returncode, 0)
|
||||
|
||||
def test_dropped_without_reason_blocks(self):
|
||||
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 2\n```"
|
||||
self.assertEqual(_run_stop(msg).returncode, 2)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopStats`
|
||||
Expected: FAIL — `test_imbalanced_stats_blocks` returns 0 (gate not checking stats yet).
|
||||
|
||||
- [ ] **Step 3: Implement — extend `subagent_stop_gate`**
|
||||
|
||||
In `.claude/hooks/wiki_claim_gate.py`, add a stats check right after the verdict-block check (before the final `emit_allow()`):
|
||||
```python
|
||||
# judge verdict 검사(위) 다음 — wiki-stats 마커가 있으면 funnel 검증(없으면 통과).
|
||||
sparsed, serr = wiki_rules.validate_stats_block(message)
|
||||
if sparsed is not None and serr and not event.get("stop_hook_active"):
|
||||
emit_block("wiki-stats 블록 오류:\n- " + "\n- ".join(serr))
|
||||
emit_allow()
|
||||
```
|
||||
(Replace the existing trailing `emit_allow()` with the block above so the stats check precedes it.)
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopStats`
|
||||
Expected: PASS (3 tests).
|
||||
|
||||
- [ ] **Step 5: Checkpoint (no commit)** — `python3 .claude/hooks/test_wiki_claim_gate.py` all green.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `rules/reporting-standards.md` — No silent truncation 계약
|
||||
|
||||
**Files:**
|
||||
- Modify: `rules/reporting-standards.md`
|
||||
|
||||
- [ ] **Step 1: Append the contract section**
|
||||
|
||||
Add at the end of `rules/reporting-standards.md`:
|
||||
```markdown
|
||||
## No silent truncation (funnel 계약)
|
||||
|
||||
출력이 캡/슬라이스/top-N/skip 으로 coverage 를 bound 하면 **드롭한 수 + 이유**를 반드시 보고한다. funnel 은 균형해야 한다:
|
||||
|
||||
```
|
||||
found = processed + dropped
|
||||
```
|
||||
|
||||
- `found` = 식별한 총 항목. `processed` = 실제 판정한 수(결과 무관 — covered/missing/verified/promoted 모두 포함). `dropped` = 판정하지 않고 의도 제외(이유 필수).
|
||||
- **agent 출력**은 `wiki-stats` 블록으로 보고한다(SubagentStop 이 균형·dropped_reason 검증).
|
||||
- **command 출력**은 `## Stats` 절로 보고한다.
|
||||
- 침묵 누락은 "전부 다뤘다" 는 거짓 신호다 — 제3의 보고되지 않은 버킷을 두지 않는다.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify**
|
||||
|
||||
Run: `grep -c "No silent truncation" rules/reporting-standards.md`
|
||||
Expected: ≥1.
|
||||
|
||||
- [ ] **Step 3: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 4 agents — `## Stats` block (Claude)
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/agents/coverage-auditor.md`, `branch-depth-auditor.md`, `wiki-decision-researcher.md`, `wiki-research-lane.md`
|
||||
|
||||
- [ ] **Step 1: Add the block to each agent's Output section**
|
||||
|
||||
In each agent's Output section (for coverage-auditor / branch-depth-auditor, place it **directly after the `## Machine verdict` block** added in Spec B; for decision-researcher / research-lane place it at the end of the Output template), add — with the agent's own name and an example funnel:
|
||||
````
|
||||
## Stats (funnel — SubagentStop 가 균형·dropped_reason 검증)
|
||||
|
||||
리포트 끝에 기계 파싱용 funnel 을 **반드시** 방출한다. `found = processed + dropped` 균형 필수, `dropped>0` 면 `dropped_reason` 필수:
|
||||
|
||||
```wiki-stats
|
||||
agent: <이 에이전트 name>
|
||||
found: 12
|
||||
processed: 10
|
||||
dropped: 2
|
||||
dropped_reason: 2 out-of-scope (사유)
|
||||
```
|
||||
````
|
||||
Per-agent funnel 의미:
|
||||
- `coverage-auditor`: found=governing 관심사 수, processed=covered+delegated+missing, dropped=범위 밖(이유).
|
||||
- `branch-depth-auditor`: found=점검한 claim/결정 수, processed=판정 완료, dropped=범위 밖(이유).
|
||||
- `wiki-decision-researcher`: found=식별 후보 수, processed=archive 한 수, dropped=bound(N) 초과 제외(이유).
|
||||
- `wiki-research-lane`: found=슬라이스 파일 수, processed=정독+추출, dropped=무관/제외(이유).
|
||||
|
||||
- [ ] **Step 2: Verify all four**
|
||||
|
||||
Run: `for a in coverage-auditor branch-depth-auditor wiki-decision-researcher wiki-research-lane; do echo -n "$a: "; grep -c "wiki-stats" .claude/agents/$a.md; done`
|
||||
Expected: each ≥1.
|
||||
|
||||
- [ ] **Step 3: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 5: `/ingest` — `## Stats` funnel 계약
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/commands/ingest.md`
|
||||
|
||||
- [ ] **Step 1: Append the funnel contract**
|
||||
|
||||
Add a new section at the end of `.claude/commands/ingest.md`:
|
||||
```markdown
|
||||
## 출력: Stats funnel (no-silent-truncation)
|
||||
|
||||
작업 종료 시 `## Stats` 절을 보고한다 (`rules/reporting-standards.md` No silent truncation 계약):
|
||||
|
||||
```
|
||||
## Stats
|
||||
found: <식별한 promotable 항목 수>
|
||||
processed: <canonical 로 promote 한 수>
|
||||
dropped: <추출 안 한 수>
|
||||
dropped_reason: <항목별 제외 사유 (raw 보존 / 잡담 / abandoned / 등)>
|
||||
```
|
||||
|
||||
`found = processed + dropped` 균형 필수. daily/branch 특수처리에서 "추출 안 함" 으로 raw 에 남긴 항목도 `dropped` 에 카운트하고 사유를 적는다 — 무엇을 안 옮겼는지 보이게.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify**
|
||||
|
||||
Run: `grep -c "Stats funnel" .claude/commands/ingest.md`
|
||||
Expected: ≥1.
|
||||
|
||||
- [ ] **Step 3: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 3-platform manual mirror (4 agents)
|
||||
|
||||
**Files (mirror the `## Stats` block from Task 4):**
|
||||
- `.agents/plugins/wiki-superpowers/agents/{coverage-auditor,branch-depth-auditor,wiki-decision-researcher,wiki-research-lane}.md` — integrate into the G3 Output Schema (`{{ }}` placeholder style, framed "형식 외 응답 금지").
|
||||
- `.codex/agents/{...}.md` and `.codex/agents/{...}.toml` — same block as Claude (plain body, inside `developer_instructions` for `.toml`).
|
||||
|
||||
- [ ] **Step 1: Mirror**
|
||||
|
||||
For each of the 4 agents, copy the `## Stats` block into the variant files. Antigravity uses `{{ }}` placeholders (e.g. `found: {{N}}`); Codex uses the same concrete-example body as Claude. (Note: `wiki-decision-researcher` / `wiki-research-lane` — confirm they have Antigravity/Codex variants; mirror only those that exist.)
|
||||
|
||||
- [ ] **Step 2: Grep-verify parity**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
for a in coverage-auditor branch-depth-auditor wiki-decision-researcher wiki-research-lane; do
|
||||
echo "== $a =="
|
||||
grep -l "wiki-stats" .claude/agents/$a.md .agents/plugins/wiki-superpowers/agents/$a.md .codex/agents/$a.md .codex/agents/$a.toml 2>/dev/null
|
||||
done
|
||||
```
|
||||
Expected: every existing variant lists for each agent.
|
||||
|
||||
- [ ] **Step 3: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Full regression + acceptance smoke (spec §6)
|
||||
|
||||
**Files:** none (verification)
|
||||
|
||||
- [ ] **Step 1: All hook test suites**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
for t in test_wiki_rules test_wiki_claim_gate test_wiki_structure_lint test_wiki_quorum; do
|
||||
out=$(python3 .claude/hooks/$t.py 2>&1 | tail -1); echo "$t -> $out"
|
||||
done
|
||||
```
|
||||
Expected: all `OK`.
|
||||
|
||||
- [ ] **Step 2: Acceptance §6.2 — live SubagentStop stats gate**
|
||||
|
||||
Run (printf; no `>` redirect to avoid the bash-gate):
|
||||
```bash
|
||||
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py 2>/dev/null; echo "imbalance exit=$?"
|
||||
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-stats\nagent: coverage-auditor\nfound: 10\nprocessed: 10\ndropped: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py 2>/dev/null; echo "balanced exit=$?"
|
||||
```
|
||||
Expected: `imbalance exit=2`, `balanced exit=0`.
|
||||
|
||||
- [ ] **Step 3: Acceptance §6.3-6.5 — grep contracts**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -c "No silent truncation" rules/reporting-standards.md
|
||||
for a in coverage-auditor branch-depth-auditor wiki-decision-researcher wiki-research-lane; do echo -n "$a: "; grep -c wiki-stats .claude/agents/$a.md; done
|
||||
grep -c "Stats funnel" .claude/commands/ingest.md
|
||||
```
|
||||
Expected: reporting-standards ≥1; each agent ≥1; ingest ≥1.
|
||||
|
||||
- [ ] **Step 4: Final checkpoint (no commit)** — report results; leave changes in working tree.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (completed by plan author)
|
||||
|
||||
- **Spec coverage:** §4.2 validate_stats_block → Task 1. §4.3 SubagentStop → Task 2. §4.4 reporting-standards → Task 3. §4.5 agents → Tasks 4+6. §4.6 ingest → Task 5. §6 acceptance 1-6 → Task 1/2 tests + Task 7 smokes. No gap.
|
||||
- **Placeholder scan:** deterministic-core tasks carry full code; agent/command tasks give exact block text + per-agent mapping + exact mirror file list. No "TBD"/"similar to".
|
||||
- **Type/name consistency:** `parse_stats_block`/`validate_stats_block` names match Tasks 1-2 and tests; the `wiki-stats` fence + funnel fields (`found`/`processed`/`dropped`/`dropped_reason`) identical across spec, impl, tests, agent blocks, smokes; `found = processed + dropped` invariant consistent everywhere.
|
||||
@@ -0,0 +1,218 @@
|
||||
# Spec D — research-fanout Workflow Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax.
|
||||
> **Commits excluded** per user instruction. **OPT-IN BOUNDARY:** Tasks marked `[OPT-IN]` require the `Workflow` tool (explicit opt-in / "ultracode") to *run*; Task 4 (prose edit) needs no opt-in and can run anytime.
|
||||
|
||||
**Goal:** Provide a Claude-only `research-fanout` Workflow that runs the bounded per-decision alternatives-research burst (branch-spec §5) as a real deep-research-style pipeline — parallel `agent({schema})` + cap + deferred + funnel stats — without disturbing the 3-platform prose command.
|
||||
|
||||
**Architecture:** A standalone Workflow script (`.claude/workflows/research-fanout.js`) that takes `args.decisions = [{topic, parentBranch, constraints, n}]`, caps at 6, fans out autonomous schema-enforced web research in parallel, and returns comparison matrices + `branchNoteInput` + funnel `stats`. The prose `/branch-spec §5` gains one optional line pointing to it (Claude only). Codex/Antigravity keep sequential dispatch — no parity change.
|
||||
|
||||
**Tech Stack:** Claude Code `Workflow` tool (JS script, not TypeScript), `agent()`/`parallel()`/`phase()`/`log()`. WebSearch/WebFetch inside agents.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-06-spec-d-research-fanout-workflow-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Create** `.claude/workflows/research-fanout.js` — the Workflow script (spec §4). `[OPT-IN]`
|
||||
- **Modify** `.claude/commands/branch-spec.md` — one optional line in §5 (Claude-only Workflow call). (no opt-in)
|
||||
|
||||
> Invocation note: at opt-in, run via `Workflow({name: 'research-fanout', args: {...}})` if the file is registered as a named workflow, or `Workflow({scriptPath: '.claude/workflows/research-fanout.js', args: {...}})`. Confirm the registration path the first run (the Workflow tool persists inline scripts and reports the path).
|
||||
|
||||
---
|
||||
|
||||
## Task 1 `[OPT-IN]`: Author the Workflow script
|
||||
|
||||
**Files:**
|
||||
- Create: `.claude/workflows/research-fanout.js`
|
||||
|
||||
- [ ] **Step 1: Write the script verbatim from spec §4**
|
||||
|
||||
Create `.claude/workflows/research-fanout.js` with exactly the spec §4 script:
|
||||
```js
|
||||
export const meta = {
|
||||
name: 'research-fanout',
|
||||
description: 'Bounded parallel alternatives-research for N branch decisions (autonomous, schema-enforced)',
|
||||
phases: [
|
||||
{ title: 'Scope', detail: 'validate + cap decisions (max 6), split deferred' },
|
||||
{ title: 'Research', detail: 'parallel autonomous web research per decision, schema-enforced' },
|
||||
{ title: 'Synthesize', detail: 'comparison matrices + funnel stats + branch-note DEM input' },
|
||||
],
|
||||
}
|
||||
|
||||
const MAX_DECISIONS = 6
|
||||
|
||||
const ALT_SCHEMA = {
|
||||
type: "object", required: ["decision", "alternatives", "recommendation", "confidence"],
|
||||
properties: {
|
||||
decision: { type: "string" },
|
||||
alternatives: { type: "array", minItems: 2, maxItems: 5, items: {
|
||||
type: "object", required: ["name", "pros", "cons", "sources"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
pros: { type: "string" },
|
||||
cons: { type: "string" },
|
||||
sources: { type: "array", items: { type: "string" } },
|
||||
quote: { type: "string" },
|
||||
sourceType: { enum: ["official-doc", "company-tech-blog", "personal-blog", "unknown"] },
|
||||
},
|
||||
}},
|
||||
recommendation: { type: "string" },
|
||||
confidence: { enum: ["high", "medium", "low"] },
|
||||
unsupported: { type: "boolean" },
|
||||
},
|
||||
}
|
||||
|
||||
phase("Scope")
|
||||
const decisions = (args && Array.isArray(args.decisions)) ? args.decisions : []
|
||||
if (!decisions.length) {
|
||||
return { error: "No decisions. Pass args.decisions = [{topic, parentBranch, constraints, n}]." }
|
||||
}
|
||||
const capped = decisions.slice(0, MAX_DECISIONS)
|
||||
const deferred = decisions.slice(MAX_DECISIONS)
|
||||
log(`${decisions.length} decisions → research ${capped.length}, defer ${deferred.length}`)
|
||||
|
||||
const researchPrompt = (d) =>
|
||||
"## Alternatives Researcher (autonomous)\n\n" +
|
||||
"Branch decision: \"" + d.topic + "\"\n" +
|
||||
"Parent branch: " + (d.parentBranch || "(none)") + "\n" +
|
||||
"Constraints: " + (d.constraints || "(none)") + "\n\n" +
|
||||
"## Task\n" +
|
||||
"1. WebSearch official docs + 대기업 기술블로그 for " + (d.n || 3) + " viable alternatives.\n" +
|
||||
"2. WebFetch each; extract Pros/Cons + a verbatim quote + source URL + sourceType.\n" +
|
||||
"3. company-tech-blog 만으로 '공식 best practice' 승격 금지(독립 사례 2+ 또는 official 병행).\n" +
|
||||
"4. 근거가 한쪽으로 명확하면 그대로 recommendation. 가짜 5:5 균형 금지.\n" +
|
||||
"5. 조사 후에도 근거 부족하면 unsupported=true.\n\nStructured output only."
|
||||
|
||||
phase("Research")
|
||||
const results = (await parallel(
|
||||
capped.map(d => () =>
|
||||
agent(researchPrompt(d), { label: "research:" + d.topic.slice(0, 30), phase: "Research", schema: ALT_SCHEMA })
|
||||
.then(r => r ? { ...r, topic: d.topic } : null)
|
||||
)
|
||||
)).filter(Boolean)
|
||||
|
||||
phase("Synthesize")
|
||||
const failures = capped.length - results.length
|
||||
const stats = {
|
||||
found: decisions.length,
|
||||
processed: results.length,
|
||||
dropped: deferred.length + failures,
|
||||
dropped_reason: [
|
||||
deferred.length ? `${deferred.length} over cap(${MAX_DECISIONS})` : null,
|
||||
failures ? `${failures} research failed/skipped` : null,
|
||||
].filter(Boolean).join("; ") || "none",
|
||||
}
|
||||
log(`done: ${results.length} researched, ${stats.dropped} dropped`)
|
||||
return {
|
||||
matrices: results,
|
||||
deferred: deferred.map(d => d.topic),
|
||||
unsupported: results.filter(r => r.unsupported).map(r => r.topic),
|
||||
stats,
|
||||
branchNoteInput: results.map(r => ({
|
||||
decision: r.topic, recommendation: r.recommendation, confidence: r.confidence,
|
||||
sources: r.alternatives.flatMap(a => a.sources), unsupported: !!r.unsupported,
|
||||
})),
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Syntax sanity (no opt-in needed — node parse only)**
|
||||
|
||||
Run: `node --check .claude/workflows/research-fanout.js && echo "syntax OK"`
|
||||
Expected: `syntax OK`. (Note: `node --check` validates JS syntax; the Workflow runtime — `agent`/`parallel`/`phase`/`log`/`args` globals — is provided by the Workflow tool, not node, so this only checks parse-ability, not execution.)
|
||||
If `node` is unavailable, skip and rely on the Workflow tool's own parse at run time.
|
||||
|
||||
- [ ] **Step 3: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 2 `[OPT-IN]`: Run — happy path (2-3 decisions)
|
||||
|
||||
**Files:** none (Workflow execution)
|
||||
|
||||
- [ ] **Step 1: Invoke the Workflow with a small decisions list**
|
||||
|
||||
Via the `Workflow` tool:
|
||||
```
|
||||
Workflow({ name: 'research-fanout', args: { decisions: [
|
||||
{ topic: "outbox polling vs CDC for transactional outbox", parentBranch: "feature-domain-event-outbox-contract", constraints: "Spring Boot, Postgres", n: 3 },
|
||||
{ topic: "idempotency key storage: dedicated table vs redis", parentBranch: "feature-idempotency-key", constraints: "at-least-once delivery", n: 3 }
|
||||
]}})
|
||||
```
|
||||
(If `name` resolution fails, use `scriptPath: '.claude/workflows/research-fanout.js'`.)
|
||||
|
||||
- [ ] **Step 2: Verify acceptance §6.1, §6.4**
|
||||
|
||||
Expected return: `matrices` length 2; each matrix has `alternatives` ≥2 with `sources` arrays + `recommendation` + `confidence`; `stats.found == 2` and `stats.processed + stats.dropped == 2`.
|
||||
|
||||
- [ ] **Step 3: Verify §6.3 — empty input graceful**
|
||||
|
||||
Invoke `Workflow({ name: 'research-fanout', args: { decisions: [] }})`.
|
||||
Expected: `{ error: "No decisions. ..." }` (no crash).
|
||||
|
||||
- [ ] **Step 4: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 3 `[OPT-IN]`: Run — cap + deferred (8 decisions)
|
||||
|
||||
**Files:** none
|
||||
|
||||
- [ ] **Step 1: Invoke with 8 decisions**
|
||||
|
||||
`Workflow({ name: 'research-fanout', args: { decisions: [ /* 8 objects, each {topic, n:2} */ ]}})`.
|
||||
|
||||
- [ ] **Step 2: Verify acceptance §6.2**
|
||||
|
||||
Expected: `matrices` length ≤6; `deferred` length 2 (the 7th, 8th topics); `stats.dropped >= 2` and `stats.dropped_reason` contains `over cap(6)`; `stats.found == 8` and `processed + dropped == 8`.
|
||||
|
||||
- [ ] **Step 3: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 4 (no opt-in): Add the optional call line to `/branch-spec §5`
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude/commands/branch-spec.md` (§5, after the bound line)
|
||||
|
||||
- [ ] **Step 1: Add the Claude-only option line**
|
||||
|
||||
In `.claude/commands/branch-spec.md`, in step 5 (after the line ` - 조사는 **개수가 아니라 근거** ...`), add:
|
||||
```markdown
|
||||
- **(옵션, Claude 전용)** 결정 수가 많거나 빠른 병렬 조사를 원하면 `research-fanout` Workflow 를 호출한다 (`Workflow({name:'research-fanout', args:{decisions:[{topic,parentBranch,constraints,n}, ...]}})`, ultracode/opt-in 필요). 반환된 `matrices`/`branchNoteInput` 으로 §7 Decision Evidence Map 을 채우고 `stats`/`deferred` 를 §9 요약에 반영한다. **autonomous 조사라 user-approval 이 없으므로** 승인-gated 가 필요하면 기존 `wiki-decision-researcher` 순차 dispatch 를 쓴다. Codex/Antigravity 는 항상 순차 dispatch (Workflow 는 Claude 전용).
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify**
|
||||
|
||||
Run: `grep -c "research-fanout" .claude/commands/branch-spec.md`
|
||||
Expected: ≥1.
|
||||
|
||||
- [ ] **Step 3: 3-platform note (no edit)**
|
||||
|
||||
`/branch-spec` 의 Codex/Antigravity variant 에는 이 줄을 **미러하지 않는다** (Workflow 는 Claude 전용). 미러 생략이 의도임을 확인만 한다 — variant 는 기존 순차 dispatch 유지.
|
||||
|
||||
- [ ] **Step 4: Checkpoint (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Task 5 `[OPT-IN]`: Full acceptance smoke
|
||||
|
||||
**Files:** none
|
||||
|
||||
- [ ] **Step 1: Re-confirm §6.1-6.6**
|
||||
|
||||
- §6.1/6.4 (Task 2), §6.2 (Task 3), §6.3 (Task 2 Step 3) — done.
|
||||
- §6.5: inspect a Task 2 return — `branchNoteInput[i]` has `{decision, recommendation, sources, unsupported}` usable for prose §7 fill.
|
||||
- §6.6: confirm `/branch-spec` prose sequential path (without Workflow) is unchanged — read §5, verify the original `wiki-decision-researcher` dispatch line is intact and the new line is clearly "옵션".
|
||||
|
||||
- [ ] **Step 2: Report results; leave changes in working tree (no commit).**
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (completed by plan author)
|
||||
|
||||
- **Spec coverage:** §4 script → Task 1. §4.1 prose line → Task 4. §6 acceptance 1-6 → Tasks 2/3/5. §7 opt-in boundary → `[OPT-IN]` tags + Task 4 non-opt-in. No gap.
|
||||
- **Placeholder scan:** Task 1 carries the full verbatim script; Task 4 carries the exact line; run tasks give exact `Workflow({...})` invocations + expected returns. The only intentional deferral is execution (opt-in), explicitly flagged — not a placeholder.
|
||||
- **Type/name consistency:** `decisions`/`topic`/`parentBranch`/`constraints`/`n` arg shape consistent across script, invocations, and `branchNoteInput`; `stats.{found,processed,dropped,dropped_reason}` matches the C-spec funnel contract; `MAX_DECISIONS=6` matches the "8→6+2 deferred" acceptance.
|
||||
- **Note:** Task 4 is the only non-opt-in change; if the user never opts into Workflow, the prose command still gains a (currently-dormant) Claude-only pointer that degrades gracefully to the existing sequential path.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,241 @@
|
||||
# 자금흐름 관측 시스템 — 완성까지의 마스터 빌드아웃 플랜
|
||||
|
||||
> **이 플랜의 약속:** 아래 Phase 0~6 을 전부 완수하면, "전체 주식시장 돈의 흐름 → 분야 → 대장주 → 추종주 → 분야간 상승·하락 연관"을 매일 체계적으로 관측하는 구조가 *완성*된다. 더 이상 즉흥적으로 하나씩 늘리지 않는다 — **목표 구조가 유한하게 정의돼 있고, 단계로 채운다.**
|
||||
>
|
||||
> 프로젝트 hub: `[[raw/project-notes/invest-money-flow-system]]` · 설계: `[[docs/superpowers/specs/2026-06-08-invest-field-map-design]]`
|
||||
|
||||
---
|
||||
|
||||
## 0. 완성 정의 / Definition of Done (전체 시스템)
|
||||
|
||||
시스템이 "완성됐다"고 말할 수 있는 **측정가능 조건 6개**:
|
||||
|
||||
1. **분야 지도**에 거시 자산군(L0) + 한국 주요 섹터/테마(L1) 가 **목표 분류표(§2)대로 전부 카드로 존재**한다.
|
||||
2. 모든 **산업 섹터 카드**가 *대장주 1~3 + 추종주 2~5* 표를 가진다.
|
||||
3. **분야간 로테이션 지도**(`field-rotation`)가 존재하고, "risk-on/off · 금리 · 달러 · 경기 사이클"별 *돈이 빠지고 들어가는 연쇄*가 행으로 명시된다.
|
||||
4. 모든 관계 행에 `[검증]/[가설]` 라벨. **`[검증]` ≥ 10개** (관측·research로 승급된 실제 관계).
|
||||
5. **매일 `/invest-daily` 가 분야 관찰**(대장주↔추종주 + 로테이션 예측 vs 실측)을 채점한다 — 14일+ 연속 운영 기록.
|
||||
6. 검증된 분야부터 **매수신호 규칙**이 전략과 연동된다(`[[wiki/invest-strategy/strategy]]`).
|
||||
|
||||
> 1~3 = 구조, 4~6 = 운영. 구조 먼저(Phase 1~2), 운영 누적(Phase 3~5), 신호(Phase 6).
|
||||
|
||||
---
|
||||
|
||||
## 1. 문서 관리 규칙 / Document Management (전체 SSOT 맵)
|
||||
|
||||
> "어디에 무엇을 두는가"를 고정한다. 이게 흔들리면 또 piecemeal·중복이 된다.
|
||||
|
||||
| 레이어 | 위치 | 무엇 | 명령 |
|
||||
|---|---|---|---|
|
||||
| **프로젝트 hub** | `raw/project-notes/invest-money-flow-system.md` | 전체 설계·아키텍처·로드맵. 모든 것의 entry point | (수동/`/project-spec`) |
|
||||
| **분야 카드(개념)** | `wiki/invest-concepts/field-<slug>.md` | 분야 노드: drivers·연결·대장주/추종주·관찰지표 | `/invest-ingest` |
|
||||
| **분야 지도 허브** | `wiki/invest-concepts/field-map.md` | 카드 목차(2층) | (수동) |
|
||||
| **로테이션 지도** | `wiki/invest-concepts/field-rotation.md` | 분야간 돈 흐름 연쇄 | `/invest-ingest` |
|
||||
| **전략 규칙** | `wiki/invest-strategy/strategy.md` | 매매·리스크 규칙 | `/invest-ingest` |
|
||||
| **활성 계획** | `wiki/invest-plan/active-plan.md` | 현재 포지션 계획 | `/invest-plan` |
|
||||
| **일일 관측(증거)** | `raw/invest-daily/YYYY-MM-DD.md` | 매일 예측 vs 실측 채점 | `/invest-daily` |
|
||||
| **심층 조사(증거)** | `raw/invest-research/<slug>.md` | 관계·종목 3표 검증 | `/invest-research` |
|
||||
| **매매 원장(사실)** | `raw/invest-ledger/ledger.md` | 실제 거래 | `/invest-decide` |
|
||||
| **cluster 색인** | `wiki/invest/invest-hub.md` | 가벼운 목차(폴더 안내) | (수동) |
|
||||
|
||||
**규칙:**
|
||||
- **명명**: 분야 카드 = `field-<영문-kebab>`. 로테이션 = `field-rotation`. 한글 슬러그 금지(일관성).
|
||||
- **링크**: 카드 `## Parent` → `field-map` → `invest-hub` + 본 프로젝트 hub. 조사(research)는 프로젝트 hub §8.2 로 연결.
|
||||
- **근거 규율**: 모든 관계/종목 행에 `[검증]/[가설]`. `[가설]` = 외부 사용·단정 금지. 검증은 `/invest-research`(3표) → `/invest-ingest` 로만 `[검증]` 승급.
|
||||
- **링크 순환 주의**: 새 카드 만들 때 *대상이 아직 없는* wikilink 는 백틱으로 감싸고(린터 무시), 대상 생성 후 백틱 제거(설계 spec 참조).
|
||||
|
||||
---
|
||||
|
||||
## 2. 완성 목표 구조 / Target Structure (유한 목록)
|
||||
|
||||
### 2.1 4층 + 로테이션 모델
|
||||
|
||||
```
|
||||
L0 거시 자산군 ──인과/상관──> L1 산업 섹터/테마 ──대장주 견인──> L2 대장주 ──동조 낙수──> L3 추종주
|
||||
└──────────────── 분야간 로테이션(돈이 빠져 옮겨감) ────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 거시 자산군 L0 (목표 8) — 현재 6 ✅
|
||||
|
||||
| slug | 분야 | 상태 |
|
||||
|---|---|---|
|
||||
| `field-dollar` | 달러 | ✅ |
|
||||
| `field-us-rates` | 미 10Y 금리 | ✅ |
|
||||
| `field-oil` | 원유 | ✅ |
|
||||
| `field-gold` | 금 | ✅ |
|
||||
| `field-us-equity` | 미국 주식(S&P500) | ✅ |
|
||||
| `field-bitcoin` | 비트코인 | ✅ |
|
||||
| `field-krw-rates` | 한국 금리·원화채 | ⬜ Phase 1 |
|
||||
| `field-em-china` | 신흥국·중국(위험선호 척도) | ⬜ Phase 1 |
|
||||
|
||||
### 2.3 한국 산업 섹터/테마 L1 (목표 ~16) — 현재 6 ✅ + 빅테크AI
|
||||
|
||||
| slug | 분야 | 대장주(예시·가설) | 상태 |
|
||||
|---|---|---|---|
|
||||
| `field-semiconductors` | 반도체 | 엔비디아·SK하이닉스 | ✅ |
|
||||
| `field-bigtech-ai` | 빅테크/AI(글로벌) | 매그니피센트7 | ✅ |
|
||||
| `field-secondary-battery` | 2차전지 | LG엔솔·에코프로비엠 | ✅ |
|
||||
| `field-defense` | 방산 | 한화에어로스페이스 | ✅ |
|
||||
| `field-shipbuilding` | 조선 | HD현대중공업 | ✅ |
|
||||
| `field-bio-pharma` | 바이오·제약 | 삼성바이오·셀트리온 | ✅ |
|
||||
| `field-internet-platform` | 인터넷·플랫폼 | 네이버·카카오 | ✅ |
|
||||
| `field-auto` | 자동차·부품 | 현대차·기아 | ⬜ Phase 1 |
|
||||
| `field-financials` | 금융(은행·증권·보험) | KB·신한·삼성생명 | ⬜ Phase 1 |
|
||||
| `field-steel-materials` | 철강·소재 | POSCO홀딩스 | ⬜ Phase 1 |
|
||||
| `field-chem-refining` | 화학·정유 | LG화학·S-Oil | ⬜ Phase 1 |
|
||||
| `field-nuclear-power` | 원자력·전력설비 | 두산에너빌리티 | ⬜ Phase 1 |
|
||||
| `field-robotics` | 로봇·자동화 | 두산로보틱스·레인보우 | ⬜ Phase 1 |
|
||||
| `field-game` | 게임 | 크래프톤·엔씨 | ⬜ Phase 1 |
|
||||
| `field-entertainment` | 엔터·미디어·콘텐츠 | 하이브·JYP | ⬜ Phase 1 |
|
||||
| `field-cosmetics-consumer` | 화장품·소비재 | 아모레·LG생건 | ⬜ Phase 1 |
|
||||
| `field-telecom-utility` | 통신·유틸리티(방어주) | SKT·한전 | ⬜ Phase 1 |
|
||||
|
||||
> 위 17개가 **한국 시장 "전체 큰 분야"의 목표 집합**(필요시 가감). 이걸 다 채우면 "분야가 9개뿐"이 아니라 시장 전 분야가 지도에 들어온다.
|
||||
|
||||
### 2.4 분야간 로테이션 지도 `field-rotation` (Phase 2 핵심)
|
||||
|
||||
> 당신이 말한 "각 분야별 상승·하락 연관" = 돈이 *어디서 빠져 어디로 가는가*. 1장의 로테이션 카드에 4개 축으로 정리(전부 `[가설]`로 시작, 검증 대상).
|
||||
|
||||
| 축 | 조건 | 빠지는 쪽(↓) | 들어가는 쪽(↑) | 검증/가설 |
|
||||
|---|---|---|---|---|
|
||||
| 위험선호 | risk-on | 금·달러·방어주(통신) | 반도체·2차전지·코인·성장주 | `[가설]` |
|
||||
| 위험회피 | risk-off | 성장주·코인 | 금·달러·방산·통신·유틸 | `[가설]` |
|
||||
| 금리 | 금리↑ | 성장주(바이오·인터넷·2차전지) | 금융·가치·경기방어 | `[가설]` |
|
||||
| 금리 | 금리↓ | 금융 | 성장주·바이오·부동산 | `[가설]` |
|
||||
| 달러 | 달러↑ | 신흥국·원자재·금 | 미국자산 | `[가설]` |
|
||||
| 경기 | 회복초입 | 방어주 | 반도체·조선·철강·화학(경기민감) | `[가설]` |
|
||||
| 경기 | 둔화 | 경기민감 | 통신·유틸·필수소비(방어) | `[가설]` |
|
||||
|
||||
각 행은 관련 field 카드로 wikilink. `/invest-daily` 가 매일 "오늘 이 로테이션이 실제로 일어났나" 채점.
|
||||
|
||||
---
|
||||
|
||||
## 3. 단계별 로드맵 / Phased Roadmap
|
||||
|
||||
> 각 Phase = 산출물 + 측정가능 완료조건 + 사용하는 명령. 순서대로.
|
||||
|
||||
### Phase 0 — 토대 (✅ 완료)
|
||||
- **산출물**: 카드 템플릿 + field-map 허브 + 거시 6 + 한국 섹터 6 + 대장주/추종주 칸 + invest-daily 분야관찰 + 프로젝트 hub.
|
||||
- **완료조건**: 13카드 링크 무결성 OK, 모두 `[가설]` 라벨. ✅
|
||||
|
||||
### Phase 1 — 분야 분류 완성 (구조)
|
||||
- **산출물**: §2.2 거시 +2(`field-krw-rates`·`field-em-china`), §2.3 한국 섹터 +10 카드(자동차·금융·철강·화학·원자력·로봇·게임·엔터·화장품·통신유틸). 각 대장주/추종주 `[가설]` 스캐폴드.
|
||||
- **완료조건**: field-map 에 거시 8 + 섹터 17 = **25 카드 등재**, 전부 링크 OK + `[가설]` 라벨.
|
||||
- **명령/방식**: 카드 템플릿 복사 → `[가설]` 빠른 스캐폴드(이번 한국 섹터 6개와 동일 방식). 배치로 5~6장씩.
|
||||
|
||||
### Phase 2 — 분야간 로테이션 지도 (구조의 마지막 조각)
|
||||
- **산출물**: `wiki/invest-concepts/field-rotation.md` (§2.4 표) + field-map 에 등재 + 각 행이 관련 카드 wikilink.
|
||||
- **완료조건**: 4축(위험선호/금리/달러/경기) 로테이션이 행으로 명시, 각 행 관련 카드 연결, `[가설]` 라벨.
|
||||
- **여기까지 = 목표 *구조* 완성.** 이후는 운영.
|
||||
|
||||
### Phase 3 — 일일 관측 운영 (데이터 누적)
|
||||
- **산출물**: `/invest-daily` 를 매일(또는 거래일) 실행 → 분야 관찰에 대장주↔추종주 + 로테이션 예측 vs 실측 채점 누적.
|
||||
- **완료조건**: `raw/invest-daily/` 에 **14개+ 일일 노트**, 각 노트 분야관찰 채워짐.
|
||||
- **명령**: `/invest-daily` (Phase 0에서 명령 보강 완료 — 분야관찰 채우기 포함).
|
||||
|
||||
### Phase 4 — 검증 승급 (가설 → 사실)
|
||||
- **산출물**: 관측에서 반복 확인된 관계/대장주-추종주 동조를 `/invest-research`(3표)로 검증 → `/invest-ingest`로 카드 `[가설]`→`[검증]`.
|
||||
- **완료조건**: 시스템 전체 `[검증]` ≥ 10개.
|
||||
- **명령**: `/invest-research "<관계>"` → `/invest-ingest <research경로>`. 우선순위: 본인 보유/관심 섹터부터.
|
||||
|
||||
### Phase 5 — 매수신호 규칙 (운영→실행 연동)
|
||||
- **산출물**: `[검증]` 충분한 분야부터 "언제부터 모으나" 트리거를 전략에 추가(예: 대장주 추세 + 추종주 미반영 = 관찰 강화 신호). **여전히 타이밍 노림 금지(전략 ③)** — 신호는 *관찰 우선순위*이지 매매 자동화 아님.
|
||||
- **완료조건**: strategy 에 신호 규칙 섹션 + 근거 링크.
|
||||
- **명령**: 수동 + `/invest-ingest`.
|
||||
|
||||
### Phase 6 — (선택) 글로벌/미국 섹터 확장
|
||||
- 미국 섹터(에너지·금융·헬스케어 등) 카드 추가. 한국 구조 완성 후.
|
||||
|
||||
---
|
||||
|
||||
## 4. 진행 추적 / Progress Tracker
|
||||
|
||||
| Phase | 상태 | 다음 액션 |
|
||||
|---|---|---|
|
||||
| 0 토대 | ✅ 완료 | — |
|
||||
| 1 분야 완성 | ✅ 완료 (25카드: 거시 8 + 섹터 17) | — |
|
||||
| 2 로테이션 지도 | ✅ 완료 (`field-rotation` 4축) | — |
|
||||
| 3 일일 관측 | ⬜ 0/14일 ← **다음** | `/invest-daily` 매일 |
|
||||
| 4 검증 승급 | ⬜ 0/10 `[검증]` | 관심 섹터 `/invest-research` |
|
||||
| 5 매수신호 | ⬜ | Phase 4 후 |
|
||||
| 6 글로벌 | ⬜(선택) | Phase 2 후 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 의사결정 지원 트랙 (매수 후보·금액·규칙체크)
|
||||
|
||||
> ⚠️ **정직 고지 (가장 중요):** 이 시스템은 **"오늘 이거 사면 오른다"는 매수 추천을 하지 않는다.** 그건 면허 자문도, 예측도 아니다(strategy §고지 + ③ 타이밍/FOMO 금지). 대신 *"내 계획·위험예산에 비춰 다음 매수가 무엇/얼마인지, 규칙을 어기진 않는지"* 를 점검한다. **상승 예측으로 거르지 않고, 계획 적합성·위험예산으로 거른다.**
|
||||
|
||||
### 6.1 매일 의사결정 워크플로
|
||||
|
||||
```
|
||||
1. /invest-daily → 시장 + 분야 관찰(예측 vs 실측)
|
||||
2. (후보 점검 — 추천 아님) → 활성계획(active-plan)의 목표배분·다음 매수 트리거와 대조:
|
||||
"내 계획상 다음에 살 건 무엇/얼마인가? 지금이 그 조건인가?"
|
||||
급등 추격·패닉 매수는 ③ 가드가 빨간 플래그
|
||||
3. /invest-decide "매수…" → 실제 매수 시 원장 기록 + 전략규칙 위반 자동 차단
|
||||
```
|
||||
|
||||
- **"그날의 종목과 금액"** = *추천*이 아니라 *내 계획에서 도출되는 다음 행동*(예: active-plan 4단계 = TIGER 360750 일시매수). 계획에 없는 종목을 "오늘 사라"고 들이미는 건 ❌.
|
||||
- 후보가 새로 생기면 반드시 `/invest-research` 로 근거를 먼저(선근거 원칙, 전략 ③).
|
||||
|
||||
### 6.2 완료조건
|
||||
- active-plan 의 "다음 매수 트리거"가 항상 채워져 있고, `/invest-decide` 가 규칙위반을 0건 통과시킨다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 차트·기술적 분석 리터러시 트랙 (극한 학습, 정직판)
|
||||
|
||||
> ⚠️ **정직 고지:** **"더 떨어질지 / 지지받고 안 떨어질지"를 확실히 아는 방법은 없다.** 차트(기술적 분석, TA)는 *크리스탈볼이 아니라*, 가격·거래량의 과거 패턴으로 **확률적 시나리오(가설)** 를 세우는 도구다. 이 wiki의 기존 근거(`[[raw/invest-research/2026-06-05-passive-diversification-behavior]]`)는 *잦은 매매·타이밍이 수익을 해친다*고 본다. 그래서 TA는 **"리터러시(읽는 눈)"로 배우되, 예측 보장으로 쓰지 않는다.**
|
||||
|
||||
### 7.1 무엇을 기르나 (리터러시)
|
||||
- 차트를 *읽을 줄* 알기: 추세(상승/하락/횡보), **지지·저항**, 거래량, 이동평균(20·60·120일), 캔들, RSI·MACD 같은 보조지표가 *무엇을 의미하는지*.
|
||||
- "지지선에서 반등할 수도 / 깨고 더 빠질 수도" 양쪽 **시나리오를 `[가설]`로** 세우는 법 (한쪽 단정 ❌).
|
||||
|
||||
### 7.2 명명된 실패 모드 (반드시 함께 학습)
|
||||
- **후행성**: 대부분 지표는 과거의 요약 → 미래 보장 아님.
|
||||
- **자기실현/소멸**: 많이 알려진 패턴은 효과가 사라지거나 역이용됨.
|
||||
- **과최적화·사후해석**: 지나고 보면 다 맞아 보임(생존편향).
|
||||
- **확증편향**: 보고 싶은 시나리오만 차트에서 찾음.
|
||||
|
||||
### 7.3 구조·커맨드
|
||||
- TA 개념은 `wiki/invest-concepts/` 에 개념 카드로(예: `concept-support-resistance`, `concept-moving-average`) — *정의 + 한계 + 실패모드* 포함.
|
||||
- 특정 종목 차트 상황은 `/invest-research "<종목> 지지·저항·하방 시나리오(2026-MM-DD)"` → 다출처 + **상방/하방/지지 시나리오 각각 `[가설]`** + 명시적 "예측 아님".
|
||||
- 매일 쌓인 일일 노트·조사는 `/query "…"` 로 질문(예: "최근 2주 반도체 대장주 추세와 지지선 관측 요약").
|
||||
|
||||
### 7.4 완료조건
|
||||
- TA 리터러시 개념 카드 ≥ 5장(각 §실패모드 포함), 종목 차트 조사 시 항상 양방향 `[가설]` + "예측 아님" 표기.
|
||||
|
||||
---
|
||||
|
||||
## 8. 주식 마스터리 커리큘럼 (극한으로 알기)
|
||||
|
||||
> "주식을 극한으로 알고 싶다" = 한 분야가 아니라 **8개 지식 도메인**을 개념 카드(`wiki/invest-concepts/`) + 조사(`raw/invest-research/`)로 채우는 장기 학습 트랙.
|
||||
|
||||
| # | 도메인 | 무엇 | 현황 |
|
||||
|---|---|---|---|
|
||||
| 1 | 거시·자금흐름 | 분야→대장주→추종주→로테이션 | 🟡 field-map(이 플랜 §2~3) |
|
||||
| 2 | 펀더멘털 | 재무제표·실적·산업구조·해자 | ⬜ |
|
||||
| 3 | 밸류에이션 | PER·PBR·EV/EBITDA·DCF·상대가치 | ⬜ |
|
||||
| 4 | 기술적 분석 리터러시 | §7 (정직판) | ⬜ |
|
||||
| 5 | 행동재무·심리 | 패닉·FOMO·행동격차 | 🟡 strategy ③ 근거 |
|
||||
| 6 | 리스크·포트폴리오 | MDD·분산·상관·켈리 | 🟡 strategy ① 근거 |
|
||||
| 7 | 세제·계좌 | 양도세·ISA·금융소득종합과세 | ✅ 조사됨(research 2건) |
|
||||
| 8 | 시장 미시구조 | 호가·수급·유동성·공매도 | ⬜ |
|
||||
|
||||
- 각 도메인은 `/invest-research` 로 권위 출처 조사 → `wiki/invest-concepts/` 개념 카드로 정제. **모든 주장 `[검증]/[가설]` + 출처.**
|
||||
- 우선순위: 2 펀더멘털 → 3 밸류에이션 → 4 TA리터러시 → 8 미시구조 (1·5·6·7 은 부분 완료).
|
||||
|
||||
---
|
||||
|
||||
## 9. 이 플랜대로 하면 (요약)
|
||||
|
||||
- **Phase 1~2 끝나면** = 원한 *구조*("전체 분야 → 대장주 → 추종주 → 분야간 연관")가 **완전히** 카드로 존재.
|
||||
- **Phase 3~4** = 그 구조가 매일 관측·검증으로 *살아 움직이며* 뇌피셜이 사실로 익는다.
|
||||
- **트랙 §6** = "오늘 뭘 얼마 살지"를 *계획·위험예산*으로(추천 아님) 점검.
|
||||
- **트랙 §7** = 차트 *읽는 눈*(지지·저항·하방 시나리오) — 단, **예측 보장 아님 + 실패모드** 정직판.
|
||||
- **§8 커리큘럼** = 펀더멘털·밸류에이션·미시구조까지 8도메인으로 "극한" 학습.
|
||||
|
||||
> 즉 더 이상 "조금씩 즉흥적으로"가 아니라, **유한한 목표(25카드 + 로테이션 + 8도메인)** 를 단계로 채우는 것. 끝이 정의돼 있다.
|
||||
>
|
||||
> ⚠️ **단, 시스템이 길러주는 건 "더 잘 *결정*하는 눈"이지 "미래를 *맞추는* 능력"이 아니다.** 어떤 차트·조사도 "더 떨어질지"를 확실히 답하지 못한다 — 그걸 약속하는 건 거짓말. 이 정직함이 시스템의 핵심.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,451 @@
|
||||
# 하네스 정합성 스위프 (Tier A) 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:** 문서·설정이 실제 repo 상태와 어긋난 4개 지점(A1 Antigravity 훅 경로 / A2 Codex 로딩 문서 오류 / A3 wiki-doc-author 계약 결함 3종 / A4 인벤토리 숫자)을 아키텍처 변경 없이 정합화한다.
|
||||
|
||||
**Architecture:** 순수 config·문서·프롬프트 텍스트 편집 + 결정론적 검증(grep / `json.tool` / `tomllib` / `pytest`). 새 기능 코드는 없다. "test" 사이클은 *현재상태 assert(grep) → 편집 → 정합 assert(grep/syntax) → commit*. A3 은 3개 플랫폼 계열 5개 표현 파일을 **하나의 commit 으로** 원자 정합화(부분 정합 = 새 드리프트).
|
||||
|
||||
**Tech Stack:** git, Python 3.12(`json.tool`, `tomllib`, `pytest`), grep, bash. Antigravity CLI(A1 E2E 확인용, user-gated).
|
||||
|
||||
**Spec:** [docs/superpowers/specs/2026-07-14-harness-correctness-sweep-design.md](../specs/2026-07-14-harness-correctness-sweep-design.md) (rev.3)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
모든 task 는 아래를 암묵적으로 포함한다. 값은 spec 에서 verbatim 복사.
|
||||
|
||||
- **브랜치:** main 이 현재 dirty(무관한 미완 작업 존재). 반드시 `harness-correctness-sweep` 브랜치에서 작업.
|
||||
- **스테이징 격리:** `git add -A` / `git add .` **절대 금지**. 각 commit 은 그 task 의 명시된 파일만 `git add <경로>`. (무관한 dirty 파일 오염 방지.)
|
||||
- **git-diff allowlist (이 10파일 + 조건부 launcher + 본 plan/spec 외 변경 금지):**
|
||||
```
|
||||
.agents/hooks.json
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
.codex/agents/README.md
|
||||
.agents/plugins/wiki-superpowers/README.md
|
||||
.claude/agents/wiki-doc-author.md
|
||||
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md
|
||||
.codex/agents/wiki-doc-author.md
|
||||
.codex/agents/wiki-doc-author.toml
|
||||
.agents/agents/wiki-doc-author/agent.json
|
||||
# 조건부(A1 launcher 채택 시에만): .agents/hooks/run-wiki-hook.py
|
||||
```
|
||||
- **A1 portable-only:** 새 하드코딩 절대경로 채택 금지. 우선순위 — env var → `git rev-parse` → launcher self-locate. 절대경로는 임시 진단 외 최종안 아님.
|
||||
- **A3 부분변경 = `Status = BLOCKED`** (고정, "또는 PARTIAL" 금지 — `PARTIAL` 은 doc-author output schema 에 미정의; `DONE | NEEDS_CONTEXT | BLOCKED` 뿐).
|
||||
- **A3 카테고리 = 8** (branch-note, error-note, interview-prep, job-posting, blog-topic, lecture-note, project-note, daily-note). **validator 개수 하드코딩 제거**(8→7 로 바꾸지 **않음**).
|
||||
- **A3 migrate 본문-보존 계약 불변** — 건드리지 않는다.
|
||||
- **A4 historical 제외:** `docs/superpowers/plans/*`·`notes/*` 의 동일 stale 문구는 dated 기록이라 **정정 대상 아님**. "14개"/"7개" 는 다의어 — 자동 치환 금지, inventory 문맥만 수동 판정.
|
||||
- **임시 진단/테스트 파일은 최종 `git diff` 잔존 0.**
|
||||
|
||||
### 공유 스니펫 — A3 새 계약 텍스트 (D1)
|
||||
|
||||
A3 의 5개 표현 파일이 **동일 계약**으로 수렴해야 하는 canonical 문구. 각 파일의 포맷(md 산문 / toml 문자열 / json 이스케이프 문자열)에 맞춰 적용하되 의미는 동일.
|
||||
|
||||
- **STOP 조건(파일수 STOP 대체):**
|
||||
> target document + 그 Parent hub 외의 파일을 생성·수정하려는 요청 — 1 dispatch = 1 논리적 문서(허용 write set: target 1개 + 그 Parent hub 의 `## Cluster` 링크 유지만; 다른 raw/rule/template/derived 문서 수정 금지)
|
||||
- **"What you are NOT" 라인 대체:**
|
||||
> - target document + 그 Parent hub 외 파일 수정 금지 (1 dispatch = 1 논리적 문서: target 1개 + Parent hub Cluster 링크 유지만 허용)
|
||||
- **description 절 대체** (`Writes exactly 1 file and reports validation.` →):
|
||||
> Creates or migrates one target document (also maintaining its Parent hub Cluster link) and reports validation.
|
||||
- **부분변경 규칙(신규 1줄, Shortcut Trap 또는 작업 절차 말미):**
|
||||
> target 또는 Parent hub 중 일부만 변경되고 나머지가 실패하면 DONE 금지 → **Status = BLOCKED**, 변경 성공 파일 + 실패 단계 모두 보고 (자동 rollback 미구현).
|
||||
|
||||
---
|
||||
|
||||
## Task 0: 브랜치 + baseline 캡처 (setup)
|
||||
|
||||
**Files:** (변경 없음 — 브랜치 생성 + 읽기 전용 baseline)
|
||||
|
||||
- [ ] **Step 1: main 에서 작업 브랜치 생성**
|
||||
|
||||
```bash
|
||||
git rev-parse --abbrev-ref HEAD # main 확인
|
||||
git checkout -b harness-correctness-sweep
|
||||
```
|
||||
Expected: `Switched to a new branch 'harness-correctness-sweep'`
|
||||
|
||||
- [ ] **Step 2: A4 권위 카운트 baseline 기록 (이 값이 문서 정정의 근거)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
echo "templates=$(find templates -maxdepth 1 -type f -name '*.md' | wc -l)"
|
||||
echo "rules=$(find rules -maxdepth 1 -type f -name '*.md' | wc -l)"
|
||||
echo "claude_agents=$(find .claude/agents -maxdepth 1 -type f -name '*.md' | wc -l)"
|
||||
echo "antigravity_exec_agents=$(find .agents/agents -mindepth 2 -maxdepth 2 -type f -name 'agent.json' | wc -l)"
|
||||
echo "agent_prompt_ssot=$(find .agents/plugins/wiki-superpowers/agents -maxdepth 1 -type f -name '*.md' | wc -l)"
|
||||
echo "commands=$(find .claude/commands -maxdepth 1 -type f -name '*.md' | wc -l)"
|
||||
```
|
||||
Expected: `templates=27 rules=14 claude_agents=12 antigravity_exec_agents=10 agent_prompt_ssot=10 commands=23`
|
||||
(값이 다르면 STOP — spec 숫자 재확정 필요.)
|
||||
|
||||
- [ ] **Step 3: 현재 dirty 파일 목록 기록 (오염 감시 기준선)**
|
||||
|
||||
Run: `git status --short`
|
||||
기대: 무관한 기존 변경(ca-tmpl 삭제, experiment-nplus1 수정, error 노트들, llm-wiki-harness.xml)이 보임. 이 파일들은 **이번 작업에서 절대 stage 하지 않는다.**
|
||||
|
||||
(commit 없음 — setup only.)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: A1 — Antigravity 훅 경로 정합 (portable) + JSON 검증
|
||||
|
||||
**Files:**
|
||||
- Modify: `.agents/hooks.json` (4개 command entry)
|
||||
- (조건부, E2E 실패 시에만) Create: `.agents/hooks/run-wiki-hook.py`
|
||||
|
||||
**결정 배경 (spec §4 A1):** 대상 절대경로 `/home/donghyeon/dev/llm-wiki-private/...` 는 존재하지 않는 경로(dead). 새 절대경로로 바꾸면 이동·clone 시 재사망 → portable 필수. 이 세션(Claude Code)에서 Antigravity 실제 실행 cwd 를 관측할 수 없으므로, **Codex 가 이미 쓰는 검증된 상대경로 형태**(`.codex/hooks.json` 참조)를 기본값으로 채택하고, 실제 firing 은 user 가 Antigravity 세션에서 확인한다. E2E 확인 전까지 A1 verdict = **PARTIAL**.
|
||||
|
||||
- [ ] **Step 1: 현재 dead 경로 확인 (assert 실패상태)**
|
||||
|
||||
Run: `grep -n '/home/donghyeon/dev/llm-wiki-private' .agents/hooks.json`
|
||||
Expected: 4개 line hit (claim-gate Pre/Stop, structure-gate Pre/Post). 그리고:
|
||||
```bash
|
||||
ls /home/donghyeon/dev/llm-wiki-private 2>&1 # → No such file or directory (dead 확인)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 4개 command 를 Codex-미러 상대경로로 수정**
|
||||
|
||||
`.agents/hooks.json` 의 4개 `command` 문자열을 절대경로 → 상대경로로. (앞부분 `python3 ` + 뒷부분 flags 는 보존, 경로만 교체.)
|
||||
|
||||
| Gate.Event | 현재 | 수정 후 |
|
||||
|---|---|---|
|
||||
| wiki-claim-gate.PreToolUse | `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/wiki_claim_gate.py --antigravity` | `python3 .claude/hooks/wiki_claim_gate.py --antigravity` |
|
||||
| wiki-claim-gate.Stop | `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/wiki_claim_gate.py --antigravity` | `python3 .claude/hooks/wiki_claim_gate.py --antigravity` |
|
||||
| wiki-structure-gate.PreToolUse | `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/wiki_structure_lint.py --pre --antigravity` | `python3 .claude/hooks/wiki_structure_lint.py --pre --antigravity` |
|
||||
| wiki-structure-gate.PostToolUse | `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/wiki_structure_lint.py --hook --antigravity` | `python3 .claude/hooks/wiki_structure_lint.py --hook --antigravity` |
|
||||
|
||||
`wiki-hard-gate`(global `~/.gemini/...`)는 **건드리지 않는다.**
|
||||
|
||||
- [ ] **Step 3: 정합 assert (grep) + JSON 구문 검증**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -c '/home/donghyeon/dev/llm-wiki-private' .agents/hooks.json # 기대: 0
|
||||
grep -c 'python3 .claude/hooks/' .agents/hooks.json # 기대: 4
|
||||
python3 -m json.tool .agents/hooks.json >/dev/null && echo "JSON OK"
|
||||
```
|
||||
Expected: `0`, `4`, `JSON OK`
|
||||
|
||||
- [ ] **Step 4: commit**
|
||||
|
||||
```bash
|
||||
git add .agents/hooks.json
|
||||
git commit -m "fix(hooks): repair dead Antigravity hook paths (absolute→portable relative)
|
||||
|
||||
wiki-claim-gate/wiki-structure-gate pointed at /home/donghyeon/dev/... which
|
||||
does not exist. Mirror Codex's proven repo-root-relative form.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: E2E 확인 (USER-GATED — Antigravity 세션 필요)**
|
||||
|
||||
이 세션에서 Antigravity 를 구동할 수 없으므로, 사용자에게 아래를 요청하고 결과를 받는다. spec §5 A1 표 6행:
|
||||
|
||||
| Hook | 발생 이벤트 | 기대 |
|
||||
|---|---|---|
|
||||
| claim gate PreToolUse | Antigravity 에서 Write/Edit | hook 실행 로그 |
|
||||
| claim gate Stop | agent 정상 종료 | 종료 hook 로그 |
|
||||
| structure gate PreToolUse | Markdown Write/Edit | pre 검사 실행 |
|
||||
| structure gate PostToolUse | Markdown 쓰기 완료 | post 검사 실행 |
|
||||
| 차단 검증 | 규칙 위반 임시 입력 | BLOCK 전달 |
|
||||
| 정상 검증 | 규칙 만족 임시 입력 | PASS 후 계속 |
|
||||
|
||||
- **firing 확인됨** → A1 = DONE.
|
||||
- **firing 안 됨(cwd ≠ repo root)** → 상대경로 무효 → Step 6(fallback) 진행.
|
||||
- **확인 불가(Antigravity 세션 없음)** → A1 = **PARTIAL**, 나머지 task 진행, 사용자 확인 시 재개.
|
||||
|
||||
- [ ] **Step 6: (조건부 fallback — Step 5 에서 firing 실패 시에만) launcher 도입**
|
||||
|
||||
Antigravity 가 repo-root cwd 가 아니고 workspace-root 환경변수도 없으면, `.agents/hooks.json` 이 참조할 수 있는 경로 형태(Antigravity 가 resolve 가능한 형태 — 진단으로 확정)로 launcher 를 두고, launcher 가 자기 위치에서 repo root 를 찾아 실제 hook 을 exec 한다. 진단(pwd/env)이 선행되어야 하며 결과에 따라 형태 확정. 이 fallback 을 쓰면 `.agents/hooks/run-wiki-hook.py` 를 allowlist 에 포함하고 별도 commit. **Step 5 firing 확인 전에는 착수하지 않는다.**
|
||||
|
||||
---
|
||||
|
||||
## Task 2: A2 — CLAUDE.md Codex 로딩 서술 정정
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md:82` (§2 Hooks bullet 내 codex fallback 절)
|
||||
|
||||
- [ ] **Step 1: 현재 오류 문구 확인 (assert)**
|
||||
|
||||
Run: `grep -n 'codex 가 본 CLAUDE.md 를 프로젝트 지침으로 읽음(AGENTS.md 중복 불필요)' CLAUDE.md`
|
||||
Expected: line 82 hit.
|
||||
|
||||
- [ ] **Step 2: 문구 교체 (Codex 로만 증거 한정, Antigravity 단정 금지)**
|
||||
|
||||
`CLAUDE.md:82` 의 다음 절을 교체:
|
||||
|
||||
old:
|
||||
```
|
||||
`.codex/config.toml` 의 `project_doc_fallback_filenames = ["CLAUDE.md"]` 로 codex 가 본 CLAUDE.md 를 프로젝트 지침으로 읽음(AGENTS.md 중복 불필요).
|
||||
```
|
||||
new:
|
||||
```
|
||||
`.codex/config.toml` 의 `project_doc_fallback_filenames = ["CLAUDE.md"]` 는 해당 디렉터리에 `AGENTS.md` 가 **없을 때만** 쓰이는 fallback 이다 — 루트 `AGENTS.md` 가 존재하는 현재 구조에선 발동하지 않으므로, Codex 는 `AGENTS.md`(CLAUDE.md 의 ≤150줄 요약)를 진입점으로 로드한다. CLAUDE.md 는 그 요약이 가리키는 모든 모델 공통 **운영-규칙 SSOT** 로 유지된다(Codex 가 CLAUDE.md 를 자동 지침으로 직접 선택한다고 가정 금지).
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 정합 assert**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -c 'AGENTS.md 중복 불필요' CLAUDE.md # 기대: 0
|
||||
grep -c '없을 때만' CLAUDE.md # 기대: ≥1 (새 문구)
|
||||
```
|
||||
Expected: `0`, `≥1`
|
||||
|
||||
- [ ] **Step 4: commit**
|
||||
|
||||
```bash
|
||||
git add CLAUDE.md
|
||||
git commit -m "docs(CLAUDE): correct Codex project-doc loading (AGENTS.md preempts CLAUDE.md fallback)
|
||||
|
||||
Root AGENTS.md exists, so project_doc_fallback_filenames never fires; Codex
|
||||
loads AGENTS.md, not CLAUDE.md. Scope claim to Codex only.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: A3 — wiki-doc-author 계약 정합 (5표현 원자 commit)
|
||||
|
||||
**Files (모두 한 commit):**
|
||||
- Modify: `.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md` (SSOT, 먼저)
|
||||
- Modify: `.claude/agents/wiki-doc-author.md`
|
||||
- Modify: `.codex/agents/wiki-doc-author.md`
|
||||
- Modify: `.codex/agents/wiki-doc-author.toml`
|
||||
- Modify: `.agents/agents/wiki-doc-author/agent.json`
|
||||
|
||||
**Interfaces (계약 불변식 — 5파일 수렴 목표):** target=1 · Parent hub edit 허용 · unrelated edit 금지 · category=8 · validator 개수 비고정 · migrate 본문보존 유지 · 부분변경→BLOCKED.
|
||||
|
||||
**결함 분포 (spec §4 A3):**
|
||||
|
||||
| 파일 | D1(파일수 STOP+What NOT+desc) | D2(category 7→8) | D3((8개 모두)→비고정) |
|
||||
|---|---|---|---|
|
||||
| plugin SSOT md | 有 (L78, L253, desc) | 有 (L73) | 편집 시 확인 |
|
||||
| Claude md | 有 (L73, L240, desc) | 無(이미 8) | 無 |
|
||||
| Codex md | 有 (What NOT L191, desc, STOP 재확인) | 無(이미 8) | 편집 시 확인 |
|
||||
| Codex toml | 有 (What NOT L190, desc, STOP 재확인) | 無(이미 8) | 편집 시 확인 |
|
||||
| agent.json | 有 (STOP#7, What NOT, desc) | 有 (STOP#2 + STOP-check table row2) | 有 ("(8개 모두)") |
|
||||
|
||||
각 파일은 **먼저 Read** 해서 정확한 old 문자열을 확보한 뒤 편집(사본마다 표현이 다름). 적용할 새 문구는 Global Constraints §"공유 스니펫 — A3 새 계약 텍스트 (D1)" 참조.
|
||||
|
||||
- [ ] **Step 1: plugin SSOT 편집 (`.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md`)**
|
||||
|
||||
Read 후 적용:
|
||||
- **D1** — L78 파일수 STOP(`2개 이상 파일 만들기 / 수정 (1 dispatch = 1 file 위반)`) → 공유 스니펫 STOP 문구. L253 "What NOT"(`여러 파일 동시 생성·수정 금지 (1 dispatch = 1 file)`) → 공유 스니펫 What-NOT 문구. description(`Writes exactly 1 file and reports validation.`) → 공유 스니펫 description. 부분변경 규칙 1줄 추가(Shortcut Trap 또는 작업 절차 말미).
|
||||
- **D2** — L73 `Category ∉ 7 허용` → `Category ∉ 8 허용`.
|
||||
- **D3** — Post-Write Validator 블록에 `(N개 모두)` 식 숫자 하드코딩이 있으면 `(위 Validator 의 모든 적용 가능 항목)` 로. 없으면 skip.
|
||||
|
||||
- [ ] **Step 2: Claude md 편집 (`.claude/agents/wiki-doc-author.md`)**
|
||||
|
||||
Read 후 적용:
|
||||
- **D1** — L73 STOP #5(`5. 2개 이상 파일 생성·수정 요청 — 1 dispatch = 1 file`) → 공유 스니펫 STOP. L240 What-NOT(`여러 파일을 한 번에 생성·수정 금지 (1 dispatch = 1 file)`) → 공유 스니펫 What-NOT. L3 description → 공유 스니펫 description. 부분변경 규칙 1줄 추가.
|
||||
- D2/D3 없음(이미 8, validator 하드코딩 없음).
|
||||
|
||||
- [ ] **Step 3: Codex md 편집 (`.codex/agents/wiki-doc-author.md`)**
|
||||
|
||||
Read 후 적용:
|
||||
- **D1** — 파일수 STOP 조건(있으면) + What-NOT(L191 `여러 파일을 한 번에 생성·수정 금지 (1 dispatch = 1 file)`) + description → 공유 스니펫. 부분변경 규칙 1줄 추가.
|
||||
- D3 — validator 숫자 하드코딩 있으면 제거.
|
||||
|
||||
- [ ] **Step 4: Codex toml 편집 (`.codex/agents/wiki-doc-author.toml`)**
|
||||
|
||||
Read 후 적용 (프롬프트가 TOML 문자열 — 따옴표/이스케이프 보존):
|
||||
- **D1** — 파일수 STOP + What-NOT(L190) + description(L2) → 공유 스니펫. 부분변경 규칙 1줄 추가.
|
||||
- D3 — validator 숫자 하드코딩 있으면 제거.
|
||||
|
||||
- [ ] **Step 5: agent.json 편집 (`.agents/agents/wiki-doc-author/agent.json`)**
|
||||
|
||||
Read 후 적용 (프롬프트가 JSON 이스케이프 문자열 — `\n`·`\"` 이스케이프 **보존**):
|
||||
- **D1** — 임베디드 프롬프트의 STOP #7(`2개 이상 파일 만들기 / 수정 (1 dispatch = 1 file 위반)`) + What-NOT(`여러 파일 동시 생성·수정 금지 (1 dispatch = 1 file)`) + top-level `description`(`Writes exactly 1 file and reports validation.`) → 공유 스니펫. 부분변경 규칙 1줄 추가.
|
||||
- **D2** — STOP #2(`Category ∉ 7 허용`) 및 STOP-check 표 row2(`Category in 7 allowed`) → `8`.
|
||||
- **D3** — output schema 의 `... (8개 모두)` → `(위 Validator 의 모든 적용 가능 항목)`.
|
||||
|
||||
- [ ] **Step 6: 잔존-문구 grep (5표현 전부, 기대 0)**
|
||||
|
||||
Run (spec §5 A3 넓힌 패턴):
|
||||
```bash
|
||||
grep -RInE '1 dispatch = 1 file|Writes exactly 1 file|Write 1 file|write the file once|2개 이상 파일|여러 파일을 한 번에 생성.?수정 금지|여러 파일 동시 생성.?수정 금지|Category ∉ 7|Category in 7 allowed|8개 모두' \
|
||||
.claude/agents/wiki-doc-author.md \
|
||||
.codex/agents/wiki-doc-author.md \
|
||||
.codex/agents/wiki-doc-author.toml \
|
||||
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md \
|
||||
.agents/agents/wiki-doc-author/agent.json
|
||||
```
|
||||
Expected: **0 hit**. hit 남으면 해당 파일 재편집.
|
||||
|
||||
> **판정 주의:** `write the file once` / `one file at a time` 같은 *도입 문장*이 잡히면 — **파일 수를 계약으로 단정하는 경우**(예: "you write **the file** once"가 '단일 파일만'을 함의)만 새 write-set 계약과 어긋나므로 정정한다. 단순히 target 문서 1개를 쓴다는 서술은 새 계약과 일치하므로 유지 가능. 즉 이 grep 은 *탐지 그물*이고, 제거 대상은 "target+Parent 2파일"과 모순되는 file-count 단정뿐이다.
|
||||
|
||||
- [ ] **Step 7: 새 계약 문구 존재 + 구문 검증**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
# 새 write-set 계약 문구가 5표현 전부에 존재
|
||||
grep -Rl '1 논리적 문서' \
|
||||
.claude/agents/wiki-doc-author.md .codex/agents/wiki-doc-author.md \
|
||||
.codex/agents/wiki-doc-author.toml \
|
||||
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md \
|
||||
.agents/agents/wiki-doc-author/agent.json | wc -l # 기대: 5
|
||||
# 구문
|
||||
python3 -m json.tool .agents/agents/wiki-doc-author/agent.json >/dev/null && echo "json OK"
|
||||
python3 -c 'import tomllib; tomllib.load(open(".codex/agents/wiki-doc-author.toml","rb")); print("toml OK")'
|
||||
# category=8 반영 (7 잔존 0)
|
||||
grep -REc 'Category ∉ 7|Category in 7 allowed' .agents/plugins/wiki-superpowers/agents/wiki-doc-author.md .agents/agents/wiki-doc-author/agent.json # 기대: 0
|
||||
```
|
||||
Expected: `5`, `json OK`, `toml OK`, `0`
|
||||
|
||||
- [ ] **Step 8: 의미 패리티 표 확인 (수기 — grep 으로 안 잡히는 계약 동등성)**
|
||||
|
||||
각 파일을 훑어 아래 6행이 5파일 전부 PASS 인지 확인(문장은 달라도 계약값 동일):
|
||||
|
||||
| Contract | Claude md | plugin SSOT | Codex md | Codex toml | agent.json |
|
||||
|---|---|---|---|---|---|
|
||||
| target document 1개 | | | | | |
|
||||
| Parent hub edit 허용 | | | | | |
|
||||
| unrelated 문서 edit 금지 | | | | | |
|
||||
| 허용 category = 8 | | | | | |
|
||||
| validator 개수 비고정 | | | | | |
|
||||
| migrate 본문 보존 유지 | | | | | |
|
||||
|
||||
1개라도 FAIL 이면 해당 파일 재편집 후 Step 6~8 반복.
|
||||
|
||||
- [ ] **Step 9: commit (5파일 원자)**
|
||||
|
||||
```bash
|
||||
git add .agents/plugins/wiki-superpowers/agents/wiki-doc-author.md \
|
||||
.claude/agents/wiki-doc-author.md \
|
||||
.codex/agents/wiki-doc-author.md \
|
||||
.codex/agents/wiki-doc-author.toml \
|
||||
.agents/agents/wiki-doc-author/agent.json
|
||||
git commit -m "fix(wiki-doc-author): replace '1 file' contradiction with explicit write set (5 platform copies)
|
||||
|
||||
D1: '1 dispatch=1 file' vs mandatory Parent-hub edit → explicit allowed write
|
||||
set (target + its Parent hub only); partial write → BLOCKED. D2: category 7→8.
|
||||
D3: drop hardcoded validator count. Syntax + semantic parity verified.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: A4 — live 문서 인벤토리 숫자 정정
|
||||
|
||||
**Files:**
|
||||
- Modify: `AGENTS.md` (L23, L24, L50)
|
||||
- Modify: `CLAUDE.md` (L79 — 9→10 agent + scripts 문구)
|
||||
- Modify: `.codex/agents/README.md` (L9 — scripts 문구)
|
||||
- Modify: `.agents/plugins/wiki-superpowers/README.md` (L11 scripts, L46 rules 7→14)
|
||||
|
||||
**주의:** `CLAUDE.md:81`("생성기 부재(2026-06-06 확인)")은 여전히 참 — **변경 안 함**. historical(`docs/.../plans|notes`) 제외.
|
||||
|
||||
- [ ] **Step 1: 현재 stale 문구 확인 (assert, live 문서만)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -nE '정의 \(14개|7개 rule|7 agents|동일 9개 agent|디렉터리 자체 부재|the .scripts. directory itself is absent|동일한 7개 rule 파일' \
|
||||
AGENTS.md CLAUDE.md .codex/agents/README.md .agents/plugins/wiki-superpowers/README.md
|
||||
```
|
||||
Expected: AGENTS.md(23,24,50), CLAUDE.md(79), .codex/agents/README.md(9), plugin README(11,46) hit.
|
||||
|
||||
- [ ] **Step 2: 정정 적용 (각 파일 Read 후 정확 문자열 교체)**
|
||||
|
||||
| 파일:줄 | old | new |
|
||||
|---|---|---|
|
||||
| AGENTS.md:23 | `카테고리별 출력 형식 정의 (14개, ` | `카테고리별 출력 형식 정의 (27개, ` |
|
||||
| AGENTS.md:24 | `**3개 모델 공유 SSOT** — 7개 rule ` | `**3개 모델 공유 SSOT** — 14개 rule ` |
|
||||
| AGENTS.md:50 | `7 agents (` … `) 가 정의되어 있음.` | `10 agents (7 core + branch-depth-auditor·coverage-auditor·wiki-consistency-auditor) 가 정의되어 있음.` |
|
||||
| CLAUDE.md:79 | `.claude/`와 동일 9개 agent` | `.claude/`와 동일 10개 agent` |
|
||||
| CLAUDE.md:79 | `(위 7개 + `branch-depth-auditor`·`coverage-auditor`) 의` | `(위 7개 + `branch-depth-auditor`·`coverage-auditor`·`wiki-consistency-auditor`) 의` |
|
||||
| CLAUDE.md:79 | `(2026-06-06 확인; `scripts/` 디렉터리 자체 부재)` | `(2026-06-06 확인; `scripts/` 는 존재하나 `sync_automation.py` 만 부재 — 2026-07-14 재확인)` |
|
||||
| .codex/agents/README.md:9 | `(2026-06-06 확인; `scripts/` 디렉터리 자체 부재)` | `(2026-06-06 확인; `scripts/` 는 존재하나 `sync_automation.py` 만 부재 — 2026-07-14 재확인)` |
|
||||
| plugin README:11 | `(the `scripts/` directory itself is absent)` | `(the `scripts/` dir now exists — deep-research — but `sync_automation.py` is still absent, re-checked 2026-07-14)` |
|
||||
| plugin README:46 | `동일한 7개 rule 파일을 공유한다` | `동일한 14개 rule 파일을 공유한다` |
|
||||
|
||||
> AGENTS.md:50 은 이름 나열이 길면 위처럼 count+대표 3개로 축약. 정확한 old 문자열은 Read 로 확인 후 교체.
|
||||
|
||||
- [ ] **Step 3: 정합 assert (live 문서 stale 잔존 0)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -nE '정의 \(14개|— 7개 rule|^본 skill.*7 agents|동일 9개 agent|디렉터리 자체 부재|directory itself is absent|동일한 7개 rule 파일' \
|
||||
AGENTS.md CLAUDE.md .codex/agents/README.md .agents/plugins/wiki-superpowers/README.md
|
||||
```
|
||||
Expected: **0 hit**.
|
||||
|
||||
- [ ] **Step 4: 카운트 재검증 (문서값 == 실측)**
|
||||
|
||||
Run: Task 0 Step 2 의 `find` 블록 재실행 → 결과가 `27/14/12/10/10/23` 이고 문서 정정값과 일치 확인.
|
||||
|
||||
- [ ] **Step 5: commit**
|
||||
|
||||
```bash
|
||||
git add AGENTS.md CLAUDE.md .codex/agents/README.md .agents/plugins/wiki-superpowers/README.md
|
||||
git commit -m "docs: correct stale inventory counts (templates 14→27, rules 7→14, agents 9→10) + scripts-dir claim
|
||||
|
||||
Live docs only; historical plans/notes left as dated records. 2026-07-14
|
||||
manual snapshot (to be automated by B1 generator).
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 전역 검증 + verdict
|
||||
|
||||
**Files:** (변경 없음 — 검증만)
|
||||
|
||||
- [ ] **Step 1: 구문 검증 전체**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python3 -m json.tool .agents/hooks.json >/dev/null && echo "hooks.json OK"
|
||||
python3 -m json.tool .agents/agents/wiki-doc-author/agent.json >/dev/null && echo "agent.json OK"
|
||||
python3 -c 'import tomllib; tomllib.load(open(".codex/agents/wiki-doc-author.toml","rb")); print("toml OK")'
|
||||
```
|
||||
Expected: 3 × OK.
|
||||
|
||||
- [ ] **Step 2: 기존 hook test 회귀**
|
||||
|
||||
Run: `python3 -m pytest .claude/hooks -q`
|
||||
Expected: 전부 PASS (test_wiki_structure_lint / _consistency_check / _claim_gate / _rules / _quorum 등). 실패 시 원인 조사 — 이번 편집이 hook 스크립트 로직을 안 건드렸으므로 무관 실패면 기록.
|
||||
|
||||
- [ ] **Step 3: git-diff allowlist 이탈 감시**
|
||||
|
||||
Run: `git diff --name-only main...HEAD`
|
||||
Expected: 아래 집합의 부분집합만 (+ launcher 는 A1 Step 6 채택 시에만):
|
||||
```
|
||||
.agents/hooks.json
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
.codex/agents/README.md
|
||||
.agents/plugins/wiki-superpowers/README.md
|
||||
.claude/agents/wiki-doc-author.md
|
||||
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md
|
||||
.codex/agents/wiki-doc-author.md
|
||||
.codex/agents/wiki-doc-author.toml
|
||||
.agents/agents/wiki-doc-author/agent.json
|
||||
docs/superpowers/specs/2026-07-14-harness-correctness-sweep-design.md
|
||||
docs/superpowers/plans/2026-07-14-harness-correctness-sweep.md
|
||||
```
|
||||
이 집합 밖 파일이 나오면 STOP + 사유 확인 (무관한 dirty 파일이 stage 됐는지 점검). 임시 진단/테스트 파일 잔존 0 확인.
|
||||
|
||||
- [ ] **Step 4: verdict 판정 (spec §8)**
|
||||
|
||||
- A2·A3·A4: 각 assert PASS + 구문 통과 + 패리티 표 전 PASS → **DONE**.
|
||||
- A1: Antigravity firing 관측됨 → DONE / 직접 실행만 확인 → **PARTIAL** / 계약·구문 불일치 → BLOCKED.
|
||||
- 전체: allowlist 한정 + pytest PASS 확인.
|
||||
|
||||
- [ ] **Step 5: 사용자 보고**
|
||||
|
||||
Status(A1 PARTIAL/DONE, A2·A3·A4 DONE) + 변경 파일 + A1 E2E 잔여(사용자 Antigravity 확인 필요 여부) + 다음 후보(백로그 B1~B4) 요약. 커밋은 브랜치에 있음 — merge/PR 여부는 사용자 결정.
|
||||
|
||||
---
|
||||
|
||||
## 완료 기준 요약 (spec §8 대응)
|
||||
|
||||
- **A1:** hooks.json 4개 command portable 수정 + JSON OK + (E2E firing 확인 시 DONE, 아니면 PARTIAL). 새 절대경로 0.
|
||||
- **A2:** "AGENTS.md 중복 불필요" 0건, Antigravity 단정 없음, "운영-규칙 SSOT" 표기.
|
||||
- **A3:** 5표현 잔존-문구 0 + 새 write-set 계약 존재 + category 8 + validator 비고정 + JSON/TOML OK + 패리티 표 PASS + migrate 계약 불변.
|
||||
- **A4:** live 문서 stale 0 + 실측값 일치 + 두 "10" 디렉터리 구분 + 스냅샷 명시. historical 미변경.
|
||||
- **전역:** `git diff --name-only` allowlist 한정 + `pytest .claude/hooks` PASS + 임시파일 잔존 0.
|
||||
Reference in New Issue
Block a user