13 KiB
title, source_type, status, confidence, tags, last_reviewed
| title | source_type | status | confidence | tags | last_reviewed | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Spec B — judge 출력 verdict 스키마 강제 + adversarial quorum (비-Workflow) | llm-generated | draft | medium |
|
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 추가
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 키워드 검사 뒤에 추가:
# 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. 수용 기준 (검증 가능)
validate_verdict_block: 표준 블록 정상 → errors=[].verdict=ready blocking=2→ "모순" 오류.verdict=foo→ 비허용 오류. 마커 부재 → (None, []).validate_verdict_block: adversarial 블록finding: x action: NOPE→ 비허용 오류. finding 0개 → 오류.tally_quorum: 3블록 중 finding A가 REJECT×2 → KILL. KEEP×3 → KEEP. REJECT×1+DOWNGRADE×1 → DOWNGRADE. 한 블록만 KEEP, 나머지 누락 → UNVERIFIED(default-refute).subagent_stop_gate:wiki-verdict마커 + 스키마 오류 메시지 → exit 2. 마커 없는 메시지 → exit 0. 정상 블록 → exit 0.wiki_quorum.py: 3 파일 입력 → per-finding 표 + 요약. KILL 포함 시 exit 1, 전부 KEEP 시 exit 0.python3 .claude/hooks/test_wiki_rules.py+test_wiki_quorum.py+ 기존 3 스위트 전부 green.- 5 judge
.md각 Output 에wiki-verdict블록 예시 존재(grep 확인). 미러 4개 variant 에 동일 블록 반영(grep 확인).
7. 구현 순서 (writing-plans 에서 단계화)
wiki_rules.py—parse_verdict_block/validate_verdict_block/tally_quorum+ 상수. (TDD: test 먼저)test_wiki_rules.py— §6.1~6.3 케이스.wiki_quorum.py+test_wiki_quorum.py— §6.5.wiki_claim_gate.subagent_stop_gate확장 + 회귀 테스트(마커/오류/정상).wiki-adversarial-reviewer.md— per-finding 블록 + default-refute + quorum 문서.- 표준 4 judge
.md— wiki-verdict 블록 추가. - 수기 3-플랫폼 미러(4개) + grep 검증.
- 전체 스위트 + §6 수용 기준 스모크.
8. 메모
- Spec A 와 동일 철학: 기존 메커니즘 강화, 저위험, hook/script SSOT 우선. Workflow/quorum-dispatch 자동화는 Spec D.
wiki_rules.py가 Spec A 에 이어 공유 SSOT 로 계속 성장 — verdict 스키마/tally 는 기계장치이지 정책 아님(정책은 각 agent.md+ quorum 임계값 상수).