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

26 KiB
Raw Blame History

Spec B — Judge Verdict Schema & Adversarial Quorum Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. Commits excluded per user instruction — no git commit steps; each task ends with a test-green checkpoint.

Goal: Make judge-agent verdicts machine-validated at the SubagentStop boundary and make the adversarial-review kill-decision a deterministic quorum tally — without the Workflow tool (Claude subagents lack tool-layer schema enforcement).

Architecture: Extend the shared wiki_rules.py with validate_verdict_block (P1 schema + verdict↔count consistency) and tally_quorum (≥2 REJECT = KILL, default-refute, abstain≠pass). wiki_claim_gate.subagent_stop_gate validates any subagent output carrying a wiki-verdict marker (block on schema error; non-judge output untouched). New wiki_quorum.py CLI tallies N adversarial-review outputs. Five judge .md files emit the machine block; wiki-adversarial-reviewer also flips to default-refute. Deterministic core is unit-tested; agent prose is mirrored across 3 platforms by hand (the generator is absent).

Tech Stack: Python 3 stdlib (re, json, argparse, unittest, subprocess). Claude Code hooks. Markdown agent specs.

Spec: docs/superpowers/specs/2026-06-06-spec-b-judge-verdict-schema-and-quorum-design.md


File Structure

  • Modify .claude/hooks/wiki_rules.py — add parse_verdict_block, validate_verdict_block, tally_quorum, constants.
  • Modify .claude/hooks/test_wiki_rules.py — add TestVerdictBlock, TestTallyQuorum.
  • Create .claude/hooks/wiki_quorum.py — CLI over tally_quorum.
  • Create .claude/hooks/test_wiki_quorum.py — CLI integration tests.
  • Modify .claude/hooks/wiki_claim_gate.pysubagent_stop_gate calls validate_verdict_block.
  • Modify .claude/hooks/test_wiki_claim_gate.py — add TestSubagentStopVerdict (subprocess).
  • Modify .claude/agents/wiki-adversarial-reviewer.md — per-finding block + default-refute + quorum doc.
  • Modify .claude/agents/{branch-depth-auditor,coverage-auditor,project-readiness-auditor,wiki-diagram-reviewer}.md — standard verdict block.
  • Mirror (manual) the 4 shared judges into .agents/plugins/wiki-superpowers/agents/<name>.md, .codex/agents/<name>.md, .codex/agents/<name>.toml. (project-readiness-auditor is Claude-only — no mirror.)

Task 1: wiki_rules — verdict block parse + validate

Files:

  • Modify: .claude/hooks/wiki_rules.py (append functions + constants)

  • Test: .claude/hooks/test_wiki_rules.py (append TestVerdictBlock)

  • Step 1: Write the failing test

Append to .claude/hooks/test_wiki_rules.py before the if __name__ line:

STD_OK = "리포트...\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: not-ready\nblocking: 2\nshould_fix: 1\nadvisory: 0\n```\n끝"
STD_CONTRADICT = "```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 2\nshould_fix: 0\nadvisory: 0\n```"
STD_BADVERDICT = "```wiki-verdict\nagent: x\nverdict: foo\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"
ADV_OK = "```wiki-verdict\nagent: wiki-adversarial-reviewer\nfinding: 4.1.1 action: KEEP\nfinding: 4.2.1 action: REJECT\n```"
ADV_BADACTION = "```wiki-verdict\nagent: wiki-adversarial-reviewer\nfinding: 4.1.1 action: NOPE\n```"
ADV_EMPTY = "```wiki-verdict\nagent: wiki-adversarial-reviewer\n```"


class TestVerdictBlock(unittest.TestCase):
    def test_no_marker_returns_none(self):
        parsed, errors = wr.validate_verdict_block("그냥 산문, 마커 없음")
        self.assertIsNone(parsed)
        self.assertEqual(errors, [])

    def test_standard_valid(self):
        parsed, errors = wr.validate_verdict_block(STD_OK)
        self.assertEqual(errors, [])
        self.assertEqual(parsed["agent"], "branch-depth-auditor")
        self.assertEqual(parsed["kv"]["verdict"], "not-ready")

    def test_standard_contradiction_flagged(self):
        _, errors = wr.validate_verdict_block(STD_CONTRADICT)
        self.assertTrue(any("blocking" in e for e in errors))

    def test_standard_bad_verdict_flagged(self):
        _, errors = wr.validate_verdict_block(STD_BADVERDICT)
        self.assertTrue(any("verdict" in e for e in errors))

    def test_adversarial_valid(self):
        parsed, errors = wr.validate_verdict_block(ADV_OK)
        self.assertEqual(errors, [])
        self.assertEqual(len(parsed["findings"]), 2)

    def test_adversarial_bad_action_flagged(self):
        _, errors = wr.validate_verdict_block(ADV_BADACTION)
        self.assertTrue(any("action" in e for e in errors))

    def test_adversarial_empty_findings_flagged(self):
        _, errors = wr.validate_verdict_block(ADV_EMPTY)
        self.assertTrue(any("finding" in e for e in errors))
  • Step 2: Run test to verify it fails

Run: python3 .claude/hooks/test_wiki_rules.py -k Verdict Expected: FAIL — AttributeError: module 'wiki_rules' has no attribute 'validate_verdict_block'.

  • Step 3: Implement in wiki_rules.py

Append to .claude/hooks/wiki_rules.py (the import re must be added to the top import block — currently wiki_rules.py imports only json, shlex, sys):

import re  # (add to the existing import block at top of file)

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
    out = {"agent": None, "kv": {}, "findings": []}
    for line in m.group(1).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 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)")
        blocking = None
        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 는 정수여야 함")
        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
  • Step 4: Run test to verify it passes

Run: python3 .claude/hooks/test_wiki_rules.py -k Verdict Expected: PASS (7 tests).

  • Step 5: Checkpoint (no commit)python3 .claude/hooks/test_wiki_rules.py all green.

Task 2: wiki_rules — quorum tally

Files:

  • Modify: .claude/hooks/wiki_rules.py (append tally_quorum)

  • Test: .claude/hooks/test_wiki_rules.py (append TestTallyQuorum)

  • Step 1: Write the failing test

Append:

def _adv(*pairs):
    lines = "\n".join(f"finding: {fid} action: {act}" for fid, act in pairs)
    return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"


class TestTallyQuorum(unittest.TestCase):
    def test_two_rejects_kill(self):
        blocks = [_adv(("A", "REJECT")), _adv(("A", "REJECT")), _adv(("A", "KEEP"))]
        per = wr.tally_quorum(blocks)
        self.assertEqual(per["A"]["decision"], "KILL")

    def test_unanimous_keep(self):
        blocks = [_adv(("A", "KEEP")), _adv(("A", "KEEP")), _adv(("A", "KEEP"))]
        self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "KEEP")

    def test_reject_plus_downgrade_is_downgrade(self):
        blocks = [_adv(("A", "REJECT")), _adv(("A", "DOWNGRADE")), _adv(("A", "KEEP"))]
        self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "DOWNGRADE")

    def test_abstain_not_pass(self):
        # 한 블록만 KEEP, 나머지 둘은 A 를 누락(abstain) → 정족수 미달 → UNVERIFIED
        blocks = [_adv(("A", "KEEP")), _adv(("B", "KEEP")), _adv(("C", "KEEP"))]
        self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "UNVERIFIED")
  • Step 2: Run test to verify it fails

Run: python3 .claude/hooks/test_wiki_rules.py -k Tally Expected: FAIL — AttributeError: ... 'tally_quorum'.

  • Step 3: Implement in wiki_rules.py

Append:

def tally_quorum(block_texts, refutations_required=REFUTATIONS_REQUIRED):
    """N개 adversarial verdict 블록 → per-finding 결정론 판정.

    refute = DOWNGRADE 또는 REJECT (원 severity 반박).
    default-refute: 어떤 pass 가 finding 을 누락/malformed → abstain(non-KEEP).
    결정: reject≥req → KILL · (reject+downgrade)≥req → DOWNGRADE ·
          keep≥req → KEEP · 그 외(정족수 미달) → UNVERIFIED(통과 금지).
    """
    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)
    per = {}
    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
        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": len(block_texts), "decision": decision}
    return per
  • Step 4: Run test to verify it passes

Run: python3 .claude/hooks/test_wiki_rules.py -k Tally Expected: PASS (4 tests).

  • Step 5: Checkpoint (no commit)python3 .claude/hooks/test_wiki_rules.py all green.

Task 3: wiki_quorum.py CLI

Files:

  • Create: .claude/hooks/wiki_quorum.py

  • Test: .claude/hooks/test_wiki_quorum.py

  • Step 1: Write the failing test

Create .claude/hooks/test_wiki_quorum.py:

#!/usr/bin/env python3
"""wiki_quorum.py CLI 통합 테스트."""
import subprocess
import tempfile
import unittest
from pathlib import Path

CLI = str(Path(__file__).with_name("wiki_quorum.py"))


def _adv(*pairs):
    lines = "\n".join(f"finding: {fid} action: {act}" for fid, act in pairs)
    return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"


class TestQuorumCLI(unittest.TestCase):
    def _files(self, d, *texts):
        paths = []
        for i, t in enumerate(texts):
            p = Path(d) / f"v{i}.md"
            p.write_text(t)
            paths.append(str(p))
        return paths

    def test_kill_exits_1(self):
        with tempfile.TemporaryDirectory() as d:
            paths = self._files(d, _adv(("A", "REJECT")), _adv(("A", "REJECT")), _adv(("A", "KEEP")))
            r = subprocess.run(["python3", CLI] + paths, capture_output=True, text=True)
            self.assertEqual(r.returncode, 1)
            self.assertIn("KILL", r.stdout)

    def test_all_keep_exits_0(self):
        with tempfile.TemporaryDirectory() as d:
            paths = self._files(d, _adv(("A", "KEEP")), _adv(("A", "KEEP")), _adv(("A", "KEEP")))
            r = subprocess.run(["python3", CLI] + paths, capture_output=True, text=True)
            self.assertEqual(r.returncode, 0)
            self.assertIn("KEEP", r.stdout)


if __name__ == "__main__":
    unittest.main(verbosity=2)
  • Step 2: Run test to verify it fails

Run: python3 .claude/hooks/test_wiki_quorum.py Expected: FAIL — FileNotFoundError (wiki_quorum.py missing) → subprocess returncode nonzero / can't run.

  • Step 3: Implement wiki_quorum.py

Create .claude/hooks/wiki_quorum.py:

#!/usr/bin/env python3
"""wiki_quorum.py — N개 adversarial verdict 블록의 결정론 quorum tally CLI.

사용:
  python3 wiki_quorum.py vote1.md vote2.md vote3.md
  cat votes.md | python3 wiki_quorum.py --stdin     # '---' 구분 멀티블록
exit: 1 if any KILL/UNVERIFIED, else 0.
"""
from __future__ import annotations
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import wiki_rules


def main():
    args = sys.argv[1:]
    if "--stdin" in args:
        blob = sys.stdin.read()
        blocks = [b for b in blob.split("\n---\n") if "wiki-verdict" in b]
    else:
        blocks = []
        for a in args:
            try:
                blocks.append(Path(a).read_text(encoding="utf-8"))
            except Exception as e:
                print(f"파일 읽기 실패: {a}{e}", file=sys.stderr)
    if not blocks:
        print("verdict 블록 입력 없음", file=sys.stderr)
        sys.exit(2)
    per = wiki_rules.tally_quorum(blocks)
    print(f"== Quorum tally: N={len(blocks)} votes, {len(per)} findings ==")
    print("| finding | keep | down | reject | abstain | decision |")
    print("|---|---|---|---|---|---|")
    bad = 0
    for fid in sorted(per):
        r = per[fid]
        if r["decision"] in ("KILL", "UNVERIFIED"):
            bad += 1
        print(f"| {fid} | {r['keep']} | {r['downgrade']} | {r['reject']} | {r['abstain']} | {r['decision']} |")
    print(f"\nKILL/UNVERIFIED: {bad} / {len(per)}")
    sys.exit(1 if bad else 0)


if __name__ == "__main__":
    main()
  • Step 4: Run test to verify it passes

Run: python3 .claude/hooks/test_wiki_quorum.py Expected: PASS (2 tests).

  • Step 5: Checkpoint (no commit) — green.

Task 4: claim_gate.subagent_stop_gate verdict enforcement

Files:

  • Modify: .claude/hooks/wiki_claim_gate.py (subagent_stop_gate)

  • Test: .claude/hooks/test_wiki_claim_gate.py (append TestSubagentStopVerdict, subprocess-based)

  • Step 1: Write the failing test

Append to .claude/hooks/test_wiki_claim_gate.py:

import json as _json
import subprocess as _sp

_GATE = str(Path(__file__).with_name("wiki_claim_gate.py"))


def _run_stop(message):
    ev = {"hook_event_name": "SubagentStop", "last_assistant_message": message}
    return _sp.run(["python3", _GATE], input=_json.dumps(ev), capture_output=True, text=True)


class TestSubagentStopVerdict(unittest.TestCase):
    def test_malformed_verdict_blocks(self):
        msg = "리뷰 끝.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"
        r = _run_stop(msg)
        self.assertEqual(r.returncode, 2)

    def test_valid_verdict_allows(self):
        msg = "리뷰 끝.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"
        r = _run_stop(msg)
        self.assertEqual(r.returncode, 0)

    def test_no_marker_allows(self):
        r = _run_stop("그냥 일반 subagent 출력, 마커 없음")
        self.assertEqual(r.returncode, 0)
  • Step 2: Run test to verify it fails

Run: python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopVerdict Expected: FAIL — test_malformed_verdict_blocks gets returncode 0 (gate not yet checking verdict).

  • Step 3: Implement — extend subagent_stop_gate

In .claude/hooks/wiki_claim_gate.py, replace the subagent_stop_gate body's final emit_allow() with a verdict check before it:

def subagent_stop_gate(event: dict) -> None:
    message = event.get("last_assistant_message") or ""
    if not isinstance(message, str):
        emit_allow()
    if re.search(r"Verdict:\s*COMPLETE|\bDONE\b|완료", message):
        missing = []
        for term in ("Claim ID", "Decision Evidence Map", "UNSUPPORTED_DECISION"):
            if term not in message:
                missing.append(term)
        if missing and not event.get("stop_hook_active"):
            emit_block(
                "Subagent output claims completion but does not report claim-traceability checks: "
                + ", ".join(missing)
            )
    # 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))
    emit_allow()

(wiki_rules is already imported in wiki_claim_gate.py from Spec A.)

  • Step 4: Run test to verify it passes

Run: python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopVerdict Expected: PASS (3 tests).

  • Step 5: Checkpoint (no commit)python3 .claude/hooks/test_wiki_claim_gate.py all green (old 11 + 3).

Task 5: wiki-adversarial-reviewer.md — block + default-refute + quorum doc

Files:

  • Modify: .claude/agents/wiki-adversarial-reviewer.md

  • Step 1: Add the machine verdict block to the Output section

In the ## Output markdown template (after the ## Aggregate metrics block, before the closing ```), add — as part of the report the agent must emit:

## Machine verdict (필수 — SubagentStop 가 검증)

```wiki-verdict
agent: wiki-adversarial-reviewer
finding: 4.1.1 action: KEEP
finding: 4.2.1 action: DOWNGRADE
```
(모든 Falsification Summary 행의 Finding ID 를 `finding: <id> action: KEEP|DOWNGRADE|REJECT` 로 1:1 반영.)
  • Step 2: Flip to default-refute

In the ## Severity Adjustment and Shortcut Trap sections, change the uncertainty default. Replace the current INSUFFICIENT_CONTEXT guidance so that uncertainty leans toward REJECT/DOWNGRADE, not KEEP:

  • Add to ## Severity Adjustment:

    Default-refute (deep-research 정렬): 세 검사 중 하나라도 확신이 안 서면 KEEP 이 아니라 최소 DOWNGRADE. INSUFFICIENT_CONTEXT 는 "판단 보류 후 KEEP" 이 아니라 "근거 부족 → 그 finding 의 원 severity 를 신뢰할 수 없음 → DOWNGRADE 권고" 로 처리한다. KEEP 은 세 검사가 적극적으로 통과할 때만.

  • Step 3: Document the N=3 quorum flow

Add a new section ## Quorum (opt-in N=3):

고위험 검증 시 controller 가 이 에이전트를 **독립적으로 N=3 병렬 dispatch** 하고, 각 출력의 `wiki-verdict` 블록을 `wiki_quorum.py` 에 투입한다:

```
python3 .claude/hooks/wiki_quorum.py vote1.md vote2.md vote3.md
```

`wiki_quorum.py` 가 per-finding 결정(KILL/DOWNGRADE/KEEP/UNVERIFIED)을 **결정론적으로** 계산한다(≥2 REJECT=KILL, abstain≠pass). controller 는 임계값을 못 바꾼다. 기본은 N=1(단일 패스).
  • Step 4: Verify block present

Run: grep -c "wiki-verdict" .claude/agents/wiki-adversarial-reviewer.md Expected: ≥1.

  • Step 5: Checkpoint (no commit).

Task 6: Standard 4 judges — verdict block

Files:

  • Modify: .claude/agents/branch-depth-auditor.md, coverage-auditor.md, project-readiness-auditor.md, wiki-diagram-reviewer.md

  • Step 1: Add the block to each judge's Output section

In each agent's output template (## 출력 / ## Output), directly under the human Verdict: line, add the machine block. Use the agent's own name and map its verdict:

```wiki-verdict
agent: <이 에이전트 name>
verdict: ready|not-ready|blocked
blocking: <N>
should_fix: <M>
advisory: <K>
```

Per-agent mapping note to include inline:

  • branch-depth-auditor: verdict=ready ⟺ Blocking 0; else not-ready.

  • coverage-auditor: verdict=ready ⟺ missing(Blocking) 0; else not-ready. (Covered→ready, Not-covered→not-ready.)

  • project-readiness-auditor: verdict=ready ⟺ Blocking 0 (Ready); else not-ready.

  • wiki-diagram-reviewer: verdict=ready ⟺ 점수 ≥95 (PASS); <95not-ready; BLOCKED→blocked. blocking = HARD-STOP 수.

  • Step 2: Verify all four have the block

Run: for a in branch-depth-auditor coverage-auditor project-readiness-auditor wiki-diagram-reviewer; do echo -n "$a: "; grep -c "wiki-verdict" .claude/agents/$a.md; done Expected: each prints ≥1.

  • Step 3: Checkpoint (no commit).

Task 7: 3-platform manual mirror (4 shared judges)

Files (mirror the SAME block/edits made in Tasks 5-6):

  • .agents/plugins/wiki-superpowers/agents/{branch-depth-auditor,coverage-auditor,wiki-adversarial-reviewer,wiki-diagram-reviewer}.md

  • .codex/agents/{branch-depth-auditor,coverage-auditor,wiki-adversarial-reviewer,wiki-diagram-reviewer}.md

  • .codex/agents/{...}.toml (the verdict block goes inside the developer_instructions string)

  • NOT project-readiness-auditor — Claude-only, no variants exist.

  • Step 1: Mirror the body changes

For each of the 4 shared judges, copy the verdict-block addition (and for adversarial, the default-refute + quorum sections) from the .claude/agents/<name>.md into the three variant files. Keep each platform's frontmatter/format; only the body content is mirrored.

  • Step 2: Grep-verify parity

Run:

for a in branch-depth-auditor coverage-auditor wiki-adversarial-reviewer wiki-diagram-reviewer; do
  echo "== $a =="
  grep -l "wiki-verdict" .claude/agents/$a.md .agents/plugins/wiki-superpowers/agents/$a.md .codex/agents/$a.md .codex/agents/$a.toml
done

Expected: all 4 files listed for each judge (16 total).

  • Step 3: Checkpoint (no commit).

Task 8: Full regression + acceptance smoke (spec §6)

Files: none (verification)

  • Step 1: Run all hook test suites

Run:

for t in test_wiki_rules test_wiki_claim_gate test_wiki_structure_lint test_wiki_quorum; do
  out=$(python3 .claude/hooks/$t.py 2>&1 | tail -1); echo "$t -> $out"
done

Expected: all OK.

  • Step 2: Acceptance §6.4 — SubagentStop live (malformed blocks, valid passes)

Run (printf to avoid JSON mangling; no > redirect to avoid the claim_gate bash-gate):

printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 4\nshould_fix: 0\nadvisory: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py; echo "malformed exit=$?"
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py; echo "valid exit=$?"

Expected: malformed exit=2, valid exit=0.

  • Step 3: Acceptance §6.5 — wiki_quorum CLI live

Run (build 3 vote files in /tmp via a /tmp helper to avoid the bash-gate, then tally):

python3 - <<'PY'
from pathlib import Path
d = Path("/tmp/qsmoke"); d.mkdir(exist_ok=True)
def adv(fid_acts):
    lines = "\n".join(f"finding: {f} action: {a}" for f,a in fid_acts)
    return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"
(d/"v0.md").write_text(adv([("A","REJECT")]))
(d/"v1.md").write_text(adv([("A","REJECT")]))
(d/"v2.md").write_text(adv([("A","KEEP")]))
print("wrote", d)
PY
python3 .claude/hooks/wiki_quorum.py /tmp/qsmoke/v0.md /tmp/qsmoke/v1.md /tmp/qsmoke/v2.md; echo "quorum exit=$?"
rm -rf /tmp/qsmoke

Expected: table with finding A … KILL, quorum exit=1.

  • Step 4: Acceptance §6.7 — grep parity

Run the Task 7 Step 2 grep + the Task 6 Step 2 grep. Expected: every judge file carries wiki-verdict.

  • Step 5: Final checkpoint (no commit) — report all results. Leave changes in working tree.

Self-Review (completed by plan author)

  • Spec coverage: §4.1 block format → Tasks 5/6. §4.2 validate+tally → Tasks 1/2. §4.3 SubagentStop → Task 4. §4.4 wiki_quorum.py → Task 3. §4.5 agent edits + mirror → Tasks 5/6/7. §6 acceptance 1-7 → Tasks 1-4 tests + Task 8 smokes. No requirement unmapped.
  • Placeholder scan: agent-edit tasks give the exact block text + exact mapping per agent + exact mirror file list; no "TBD"/"similar to". Deterministic-core tasks carry full runnable code.
  • Type/name consistency: parse_verdict_block/validate_verdict_block/tally_quorum names match across Tasks 1-4 and the CLI; the wiki-verdict fence string is identical in tests, impl, agent blocks, and smokes; REFUTATIONS_REQUIRED=2 matches the tally tests (REJECT×2 = KILL).