--- 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: action: ` 행 ≥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 임계값 상수).