264 lines
13 KiB
Markdown
264 lines
13 KiB
Markdown
---
|
||
title: "Spec B — judge 출력 verdict 스키마 강제 + adversarial quorum (비-Workflow)"
|
||
source_type: llm-generated
|
||
status: draft
|
||
confidence: medium
|
||
tags: [harness, claude-code, hooks, agents, design, automation]
|
||
last_reviewed: 2026-06-06
|
||
---
|
||
|
||
# Spec B — judge verdict 스키마 강제 + adversarial quorum
|
||
|
||
> 상위 감사: [[2026-06-06-harness-audit-report]] §3 G3·G7. Spec A 다음 슬라이스.
|
||
> 확정 결정: **(1) 비-Workflow(hook+script)**, **(2) quorum은 adversarial-reviewer만 opt-in N=3**, **(3) 설계 승인 완료**.
|
||
|
||
## 1. 문제 (감사에서)
|
||
|
||
- **G7 (P1)**: judge 출력이 산문 markdown("Verdict: … | findings 표"). 경계에서 검증 안 됨. **Claude 일반 subagent 엔 tool-layer schema 강제가 없다**(Workflow `agent({schema})` 만 가능) → deep-research 방식 직접 적용 불가.
|
||
- **G3 (P3)**: 단일 judge, N-vote/default-refute/abstain≠pass 없음. `wiki-adversarial-reviewer` 의 `INSUFFICIENT_CONTEXT` 가 불확실성을 **PASS 쪽으로** — deep-research(불확실→refute)의 정반대.
|
||
|
||
## 2. 핵심 원리 / 비목표
|
||
|
||
- **무게중심을 hook/script(`.claude/hooks/`, 단일 SSOT)에 둔다.** 결정론 가치(파싱·검증·tally)는 테스트 가능. dispatch 는 controller(알려진 한계 — Spec D).
|
||
- `scripts/sync_automation.py` 부재(2026-06-06) → agent `.md` 편집은 **수기 3-플랫폼 미러** 비용. 따라서 agent 변경 최소화, 강제는 hook/script 로.
|
||
- **비목표**: tool-layer schema(Workflow) → Spec D. depth/coverage/readiness 의 quorum → 범위 밖(이미 1차 결정론 린터 보유, single-judge 유지). branch-spec/project-spec Workflow 변환 → Spec D.
|
||
|
||
## 3. 설계 결정 (확정)
|
||
|
||
### DD1 — 비-Workflow 메커니즘
|
||
스키마는 **SubagentStop 검증 훅**으로, quorum kill 은 **결정론 tally 스크립트**로 강제. dispatch 는 controller 가 하되 *검증*과 *kill 결정*은 기계적.
|
||
|
||
### DD2 — quorum 범위
|
||
`wiki-adversarial-reviewer` 만. 기본 N=1, 고위험 검증 시 N=3 opt-in 병렬 dispatch → `wiki_quorum.py` tally.
|
||
|
||
### DD3 — verdict 블록은 *추가*(replace 아님)
|
||
사람용 Verdict line/표는 유지하고, 기계 파싱용 `wiki-verdict` fenced 블록을 **병기**. 5 judge 모두.
|
||
|
||
## 4. 아키텍처
|
||
|
||
```
|
||
.claude/hooks/
|
||
wiki_rules.py ← MOD. validate_verdict_block() + tally_quorum() + 상수 추가
|
||
wiki_claim_gate.py ← MOD. subagent_stop_gate 가 wiki-verdict 마커 시 스키마 검증·차단
|
||
wiki_quorum.py ← NEW CLI. N개 verdict 블록 → per-finding 결정론 tally
|
||
test_wiki_rules.py ← MOD. validate_verdict_block + tally_quorum 테스트
|
||
test_wiki_quorum.py ← NEW. CLI 통합 테스트
|
||
.claude/agents/
|
||
branch-depth-auditor.md / coverage-auditor.md / project-readiness-auditor.md
|
||
wiki-diagram-reviewer.md ← MOD. Output 에 wiki-verdict 블록 추가(표준형)
|
||
wiki-adversarial-reviewer.md ← MOD. per-finding 블록 + default-refute + N=3 quorum 문서
|
||
```
|
||
|
||
### 4.1 `wiki-verdict` 기계 블록
|
||
|
||
**표준 judge** (depth/coverage/readiness/diagram):
|
||
````
|
||
```wiki-verdict
|
||
agent: branch-depth-auditor
|
||
verdict: ready|not-ready|blocked
|
||
blocking: N
|
||
should_fix: M
|
||
advisory: K
|
||
```
|
||
````
|
||
- `diagram-reviewer` 는 점수형이므로 매핑: `verdict: ready`(≥95) / `not-ready`(<95) / `blocked`; `blocking` = HARD-STOP 수.
|
||
|
||
**adversarial-reviewer** (per-finding):
|
||
````
|
||
```wiki-verdict
|
||
agent: wiki-adversarial-reviewer
|
||
finding: 4.1.1 action: KEEP|DOWNGRADE|REJECT
|
||
finding: 4.2.1 action: REJECT
|
||
```
|
||
````
|
||
|
||
### 4.2 `wiki_rules.py` 추가
|
||
|
||
```python
|
||
import re
|
||
|
||
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
|
||
body = m.group(1)
|
||
out = {"agent": None, "kv": {}, "findings": []}
|
||
for line in body.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 is 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)")
|
||
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 는 정수여야 함")
|
||
blocking = None
|
||
# 정합성: verdict↔blocking
|
||
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
|
||
|
||
|
||
def tally_quorum(block_texts, refutations_required=REFUTATIONS_REQUIRED):
|
||
"""N개 adversarial verdict 블록 → per-finding 결정론 판정.
|
||
|
||
refute = DOWNGRADE 또는 REJECT (원 severity 에 대한 반박).
|
||
default-refute: 어떤 pass 가 그 finding 을 *누락*하거나 블록이 malformed →
|
||
그 pass 는 해당 finding 에 대해 abstain(=non-KEEP) 로 집계.
|
||
결정:
|
||
reject ≥ refutations_required → KILL
|
||
(reject+downgrade) ≥ refutations_required → DOWNGRADE
|
||
keep ≥ refutations_required → KEEP
|
||
그 외(정족수 미달) → UNVERIFIED (통과 금지)
|
||
"""
|
||
n = len(block_texts)
|
||
per = {} # fid -> Counter-like
|
||
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)
|
||
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 # default-refute: 누락/malformed = non-KEEP
|
||
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": n, "decision": decision}
|
||
return per
|
||
```
|
||
|
||
### 4.3 SubagentStop 강제 (`wiki_claim_gate.subagent_stop_gate` 확장)
|
||
|
||
기존 `subagent_stop_gate` 의 COMPLETE 키워드 검사 *뒤에* 추가:
|
||
```python
|
||
# 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))
|
||
```
|
||
- 마커 없는 일반 subagent 는 영향 없음(parsed is None → skip). settings.json 무변경(SubagentStop 이미 배선).
|
||
|
||
### 4.4 `wiki_quorum.py` (신규 CLI)
|
||
|
||
```
|
||
사용:
|
||
python3 wiki_quorum.py vote1.md vote2.md vote3.md # 파일 N개
|
||
cat votes.md | python3 wiki_quorum.py --stdin # --- 구분 멀티블록
|
||
출력: per-finding 표(finding | keep-down-reject-abstain | decision) + 요약
|
||
exit: 1 if any KILL or UNVERIFIED, else 0 (controller 가 신호로 사용)
|
||
```
|
||
내부는 `wiki_rules.tally_quorum` 호출 + 표 출력. dispatch 는 controller 가 N=3 병렬로 `wiki-adversarial-reviewer` 를 띄운 뒤 각 출력을 본 CLI 에 투입.
|
||
|
||
### 4.5 agent `.md` 편집 (수기 3-플랫폼 미러)
|
||
|
||
| agent | 변경 | 미러 대상 |
|
||
|---|---|---|
|
||
| branch-depth-auditor | Output 에 표준 wiki-verdict 블록 추가 | `.agents/plugins/wiki-superpowers/agents/` + `.codex/agents/*.toml` |
|
||
| coverage-auditor | 동상 | 동상 |
|
||
| wiki-diagram-reviewer | 점수→verdict 매핑 블록 추가 | 동상 |
|
||
| project-readiness-auditor | 표준 블록 추가 | **Claude 전용 — 미러 없음** |
|
||
| wiki-adversarial-reviewer | per-finding 블록 + **default-refute**(불확실→REJECT 경향, INSUFFICIENT_CONTEXT 는 KEEP 아님) + N=3 quorum 흐름 문서 | 미러 대상 |
|
||
|
||
미러는 SSOT `.md` 본문을 해당 variant 파일에 **본문 복제**(frontmatter 형식만 플랫폼 규칙대로). 생성기 부재 → 수기.
|
||
|
||
## 5. 영향 분석 / 위험
|
||
|
||
| 위험 | 완화 |
|
||
|---|---|
|
||
| SubagentStop 가 비-judge subagent 를 잘못 차단 | `parsed is None`(마커 부재) 시 무조건 통과. judge 만 마커 방출 → opt-in 검증. |
|
||
| 무한 재방출 루프 | 기존 `stop_hook_active` 가드 재사용. |
|
||
| agent 가 verdict 블록을 안 내면 게이트 미발동(조용한 우회) | 의도된 설계(Spec B는 *블록이 있을 때* 검증). 모든 judge `.md` 에 블록을 필수 Output 으로 명시 → 누락은 별도 lint 후보(Spec C). |
|
||
| 3-플랫폼 미러 drift | 미러 대상 4개만(readiness 제외). 변경이 작음(블록 추가). 미러 체크리스트를 plan 에 포함. |
|
||
| quorum tally 가 DOWNGRADE 를 과도 집계 | refute=REJECT+DOWNGRADE 는 *severity 반박*의 보수적 정의. KILL 은 REJECT≥2 로만(엄격). |
|
||
|
||
## 6. 수용 기준 (검증 가능)
|
||
|
||
1. `validate_verdict_block`: 표준 블록 정상 → errors=[]. `verdict=ready blocking=2` → "모순" 오류. `verdict=foo` → 비허용 오류. 마커 부재 → (None, []).
|
||
2. `validate_verdict_block`: adversarial 블록 `finding: x action: NOPE` → 비허용 오류. finding 0개 → 오류.
|
||
3. `tally_quorum`: 3블록 중 finding A가 REJECT×2 → KILL. KEEP×3 → KEEP. REJECT×1+DOWNGRADE×1 → DOWNGRADE. 한 블록만 KEEP, 나머지 누락 → UNVERIFIED(default-refute).
|
||
4. `subagent_stop_gate`: `wiki-verdict` 마커 + 스키마 오류 메시지 → exit 2. 마커 없는 메시지 → exit 0. 정상 블록 → exit 0.
|
||
5. `wiki_quorum.py`: 3 파일 입력 → per-finding 표 + 요약. KILL 포함 시 exit 1, 전부 KEEP 시 exit 0.
|
||
6. `python3 .claude/hooks/test_wiki_rules.py` + `test_wiki_quorum.py` + 기존 3 스위트 전부 green.
|
||
7. 5 judge `.md` 각 Output 에 `wiki-verdict` 블록 예시 존재(grep 확인). 미러 4개 variant 에 동일 블록 반영(grep 확인).
|
||
|
||
## 7. 구현 순서 (writing-plans 에서 단계화)
|
||
|
||
1. `wiki_rules.py` — `parse_verdict_block`/`validate_verdict_block`/`tally_quorum` + 상수. (TDD: test 먼저)
|
||
2. `test_wiki_rules.py` — §6.1~6.3 케이스.
|
||
3. `wiki_quorum.py` + `test_wiki_quorum.py` — §6.5.
|
||
4. `wiki_claim_gate.subagent_stop_gate` 확장 + 회귀 테스트(마커/오류/정상).
|
||
5. `wiki-adversarial-reviewer.md` — per-finding 블록 + default-refute + quorum 문서.
|
||
6. 표준 4 judge `.md` — wiki-verdict 블록 추가.
|
||
7. 수기 3-플랫폼 미러(4개) + grep 검증.
|
||
8. 전체 스위트 + §6 수용 기준 스모크.
|
||
|
||
## 8. 메모
|
||
|
||
- Spec A 와 동일 철학: 기존 메커니즘 강화, 저위험, hook/script SSOT 우선. Workflow/quorum-dispatch 자동화는 Spec D.
|
||
- `wiki_rules.py` 가 Spec A 에 이어 공유 SSOT 로 계속 성장 — verdict 스키마/tally 는 *기계장치*이지 정책 아님(정책은 각 agent `.md` + quorum 임계값 상수).
|