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

13 KiB
Raw Blame History

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
harness
claude-code
hooks
agents
design
automation
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-reviewerINSUFFICIENT_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. 수용 기준 (검증 가능)

  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.pyparse_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 임계값 상수).