Files
llm-wiki/docs/superpowers/specs/2026-06-06-spec-b-judge-verdict-schema-and-quorum-design.md

264 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 임계값 상수).