15 KiB
Spec C — Funnel Stats & No-Silent-Truncation Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans. Steps use checkbox (
- [ ]) syntax. Commits excluded per user instruction — nogit commitsteps; each task ends with a test-green checkpoint.
Goal: Make coverage-bounding outputs report a balanced funnel (found = processed + dropped) with a mandatory dropped_reason, enforced at SubagentStop for the 4 key judge/research agents — so silent truncation becomes visible (audit gap G4).
Architecture: Extend the shared wiki_rules.py with validate_stats_block (funnel-balance + dropped-reason). wiki_claim_gate.subagent_stop_gate validates any subagent output carrying a wiki-stats marker (reuses the Spec B SubagentStop path). Add a no-silent-truncation contract to rules/reporting-standards.md; add a ## Stats wiki-stats block to 4 agents + a ## Stats funnel to /ingest. Deterministic core unit-tested; agent prose mirrored 3-platform by hand.
Tech Stack: Python 3 stdlib (re, json, unittest, subprocess). Claude Code hooks. Markdown agent/command specs.
Spec: docs/superpowers/specs/2026-06-06-spec-c-funnel-stats-no-silent-truncation-design.md
File Structure
- Modify
.claude/hooks/wiki_rules.py—parse_stats_block,validate_stats_block,STATS_FENCE_RE. - Modify
.claude/hooks/test_wiki_rules.py—TestStatsBlock. - Modify
.claude/hooks/wiki_claim_gate.py—subagent_stop_gatevalidateswiki-stats. - Modify
.claude/hooks/test_wiki_claim_gate.py—TestSubagentStopStats. - Modify
rules/reporting-standards.md— "No silent truncation" 절. - Modify
.claude/agents/{coverage-auditor,branch-depth-auditor,wiki-decision-researcher,wiki-research-lane}.md—## Statsblock. - Modify
.claude/commands/ingest.md—## Statsfunnel 계약. - Mirror (manual) the 4 agents →
.agents/plugins/wiki-superpowers/agents/<name>.md(G3) +.codex/agents/<name>.md/.toml.
Task 1: wiki_rules — stats block parse + validate
Files:
-
Modify:
.claude/hooks/wiki_rules.py -
Test:
.claude/hooks/test_wiki_rules.py(appendTestStatsBlock) -
Step 1: Write the failing test
Append to test_wiki_rules.py before if __name__:
def _stats(found, processed, dropped, reason=None):
body = f"agent: coverage-auditor\nfound: {found}\nprocessed: {processed}\ndropped: {dropped}"
if reason is not None:
body += f"\ndropped_reason: {reason}"
return f"```wiki-stats\n{body}\n```"
class TestStatsBlock(unittest.TestCase):
def test_no_marker_returns_none(self):
parsed, errors = wr.validate_stats_block("산문, 마커 없음")
self.assertIsNone(parsed)
self.assertEqual(errors, [])
def test_balanced_ok(self):
parsed, errors = wr.validate_stats_block(_stats(12, 10, 2, "2 out-of-scope"))
self.assertEqual(errors, [])
self.assertEqual(parsed["agent"], "coverage-auditor")
def test_imbalance_flagged(self):
_, errors = wr.validate_stats_block(_stats(12, 10, 0))
self.assertTrue(any("불균형" in e for e in errors))
def test_dropped_without_reason_flagged(self):
_, errors = wr.validate_stats_block(_stats(12, 10, 2))
self.assertTrue(any("dropped_reason" in e for e in errors))
def test_non_integer_flagged(self):
block = "```wiki-stats\nagent: x\nfound: many\nprocessed: 1\ndropped: 0\n```"
_, errors = wr.validate_stats_block(block)
self.assertTrue(any("정수" in e for e in errors))
- Step 2: Run test to verify it fails
Run: python3 .claude/hooks/test_wiki_rules.py -k Stats
Expected: FAIL — AttributeError: ... 'validate_stats_block'.
- Step 3: Implement in
wiki_rules.py
Append (after the tally_quorum function):
STATS_FENCE_RE = re.compile(r"```wiki-stats\s*\n(.*?)\n```", re.S)
def parse_stats_block(text):
m = STATS_FENCE_RE.search(text or "")
if not m:
return None
out = {"agent": None, "kv": {}}
for line in m.group(1).splitlines():
line = line.strip()
if not line:
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_stats_block(text):
"""(parsed, errors). parsed None → 마커 없음(통과). errors → SubagentStop 차단.
funnel 균형(found=processed+dropped) + dropped>0 시 dropped_reason 필수 (no-silent-truncation)."""
parsed = parse_stats_block(text)
if parsed is None:
return None, []
errors = []
if not parsed["agent"]:
errors.append("wiki-stats 블록에 `agent:` 누락")
nums = {}
for k in ("found", "processed", "dropped"):
try:
nums[k] = int(parsed["kv"].get(k, ""))
except ValueError:
errors.append(f"wiki-stats `{k}` 는 정수여야 함 (funnel 필수 필드)")
if len(nums) == 3:
if nums["found"] != nums["processed"] + nums["dropped"]:
errors.append(
f"funnel 불균형: found({nums['found']}) ≠ processed({nums['processed']}) "
f"+ dropped({nums['dropped']}) — 조용한 누락 의심"
)
if nums["dropped"] > 0 and not parsed["kv"].get("dropped_reason", "").strip():
errors.append("dropped>0 인데 `dropped_reason` 누락 (no-silent-truncation 위반)")
return parsed, errors
- Step 4: Run test to verify it passes
Run: python3 .claude/hooks/test_wiki_rules.py -k Stats
Expected: PASS (5 tests).
- Step 5: Checkpoint (no commit) —
python3 .claude/hooks/test_wiki_rules.pyall green.
Task 2: claim_gate.subagent_stop_gate — wiki-stats enforcement
Files:
-
Modify:
.claude/hooks/wiki_claim_gate.py(subagent_stop_gate) -
Test:
.claude/hooks/test_wiki_claim_gate.py(appendTestSubagentStopStats) -
Step 1: Write the failing test
Append (the _run_stop helper already exists from Spec B):
class TestSubagentStopStats(unittest.TestCase):
def test_imbalanced_stats_blocks(self):
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 0\n```"
self.assertEqual(_run_stop(msg).returncode, 2)
def test_balanced_stats_allows(self):
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 10\nprocessed: 10\ndropped: 0\n```"
self.assertEqual(_run_stop(msg).returncode, 0)
def test_dropped_without_reason_blocks(self):
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 2\n```"
self.assertEqual(_run_stop(msg).returncode, 2)
- Step 2: Run test to verify it fails
Run: python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopStats
Expected: FAIL — test_imbalanced_stats_blocks returns 0 (gate not checking stats yet).
- Step 3: Implement — extend
subagent_stop_gate
In .claude/hooks/wiki_claim_gate.py, add a stats check right after the verdict-block check (before the final emit_allow()):
# judge verdict 검사(위) 다음 — wiki-stats 마커가 있으면 funnel 검증(없으면 통과).
sparsed, serr = wiki_rules.validate_stats_block(message)
if sparsed is not None and serr and not event.get("stop_hook_active"):
emit_block("wiki-stats 블록 오류:\n- " + "\n- ".join(serr))
emit_allow()
(Replace the existing trailing emit_allow() with the block above so the stats check precedes it.)
- Step 4: Run test to verify it passes
Run: python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopStats
Expected: PASS (3 tests).
- Step 5: Checkpoint (no commit) —
python3 .claude/hooks/test_wiki_claim_gate.pyall green.
Task 3: rules/reporting-standards.md — No silent truncation 계약
Files:
-
Modify:
rules/reporting-standards.md -
Step 1: Append the contract section
Add at the end of rules/reporting-standards.md:
## No silent truncation (funnel 계약)
출력이 캡/슬라이스/top-N/skip 으로 coverage 를 bound 하면 **드롭한 수 + 이유**를 반드시 보고한다. funnel 은 균형해야 한다:
found = processed + dropped
- `found` = 식별한 총 항목. `processed` = 실제 판정한 수(결과 무관 — covered/missing/verified/promoted 모두 포함). `dropped` = 판정하지 않고 의도 제외(이유 필수).
- **agent 출력**은 `wiki-stats` 블록으로 보고한다(SubagentStop 이 균형·dropped_reason 검증).
- **command 출력**은 `## Stats` 절로 보고한다.
- 침묵 누락은 "전부 다뤘다" 는 거짓 신호다 — 제3의 보고되지 않은 버킷을 두지 않는다.
- Step 2: Verify
Run: grep -c "No silent truncation" rules/reporting-standards.md
Expected: ≥1.
- Step 3: Checkpoint (no commit).
Task 4: 4 agents — ## Stats block (Claude)
Files:
-
Modify:
.claude/agents/coverage-auditor.md,branch-depth-auditor.md,wiki-decision-researcher.md,wiki-research-lane.md -
Step 1: Add the block to each agent's Output section
In each agent's Output section (for coverage-auditor / branch-depth-auditor, place it directly after the ## Machine verdict block added in Spec B; for decision-researcher / research-lane place it at the end of the Output template), add — with the agent's own name and an example funnel:
## Stats (funnel — SubagentStop 가 균형·dropped_reason 검증)
리포트 끝에 기계 파싱용 funnel 을 **반드시** 방출한다. `found = processed + dropped` 균형 필수, `dropped>0` 면 `dropped_reason` 필수:
```wiki-stats
agent: <이 에이전트 name>
found: 12
processed: 10
dropped: 2
dropped_reason: 2 out-of-scope (사유)
```
Per-agent funnel 의미:
-
coverage-auditor: found=governing 관심사 수, processed=covered+delegated+missing, dropped=범위 밖(이유). -
branch-depth-auditor: found=점검한 claim/결정 수, processed=판정 완료, dropped=범위 밖(이유). -
wiki-decision-researcher: found=식별 후보 수, processed=archive 한 수, dropped=bound(N) 초과 제외(이유). -
wiki-research-lane: found=슬라이스 파일 수, processed=정독+추출, dropped=무관/제외(이유). -
Step 2: Verify all four
Run: for a in coverage-auditor branch-depth-auditor wiki-decision-researcher wiki-research-lane; do echo -n "$a: "; grep -c "wiki-stats" .claude/agents/$a.md; done
Expected: each ≥1.
- Step 3: Checkpoint (no commit).
Task 5: /ingest — ## Stats funnel 계약
Files:
-
Modify:
.claude/commands/ingest.md -
Step 1: Append the funnel contract
Add a new section at the end of .claude/commands/ingest.md:
## 출력: Stats funnel (no-silent-truncation)
작업 종료 시 `## Stats` 절을 보고한다 (`rules/reporting-standards.md` No silent truncation 계약):
Stats
found: <식별한 promotable 항목 수> processed: <canonical 로 promote 한 수> dropped: <추출 안 한 수> dropped_reason: <항목별 제외 사유 (raw 보존 / 잡담 / abandoned / 등)>
`found = processed + dropped` 균형 필수. daily/branch 특수처리에서 "추출 안 함" 으로 raw 에 남긴 항목도 `dropped` 에 카운트하고 사유를 적는다 — 무엇을 안 옮겼는지 보이게.
- Step 2: Verify
Run: grep -c "Stats funnel" .claude/commands/ingest.md
Expected: ≥1.
- Step 3: Checkpoint (no commit).
Task 6: 3-platform manual mirror (4 agents)
Files (mirror the ## Stats block from Task 4):
-
.agents/plugins/wiki-superpowers/agents/{coverage-auditor,branch-depth-auditor,wiki-decision-researcher,wiki-research-lane}.md— integrate into the G3 Output Schema ({{ }}placeholder style, framed "형식 외 응답 금지"). -
.codex/agents/{...}.mdand.codex/agents/{...}.toml— same block as Claude (plain body, insidedeveloper_instructionsfor.toml). -
Step 1: Mirror
For each of the 4 agents, copy the ## Stats block into the variant files. Antigravity uses {{ }} placeholders (e.g. found: {{N}}); Codex uses the same concrete-example body as Claude. (Note: wiki-decision-researcher / wiki-research-lane — confirm they have Antigravity/Codex variants; mirror only those that exist.)
- Step 2: Grep-verify parity
Run:
for a in coverage-auditor branch-depth-auditor wiki-decision-researcher wiki-research-lane; do
echo "== $a =="
grep -l "wiki-stats" .claude/agents/$a.md .agents/plugins/wiki-superpowers/agents/$a.md .codex/agents/$a.md .codex/agents/$a.toml 2>/dev/null
done
Expected: every existing variant lists for each agent.
- Step 3: Checkpoint (no commit).
Task 7: Full regression + acceptance smoke (spec §6)
Files: none (verification)
- Step 1: 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.2 — live SubagentStop stats gate
Run (printf; no > redirect to avoid the bash-gate):
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py 2>/dev/null; echo "imbalance exit=$?"
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-stats\nagent: coverage-auditor\nfound: 10\nprocessed: 10\ndropped: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py 2>/dev/null; echo "balanced exit=$?"
Expected: imbalance exit=2, balanced exit=0.
- Step 3: Acceptance §6.3-6.5 — grep contracts
Run:
grep -c "No silent truncation" rules/reporting-standards.md
for a in coverage-auditor branch-depth-auditor wiki-decision-researcher wiki-research-lane; do echo -n "$a: "; grep -c wiki-stats .claude/agents/$a.md; done
grep -c "Stats funnel" .claude/commands/ingest.md
Expected: reporting-standards ≥1; each agent ≥1; ingest ≥1.
- Step 4: Final checkpoint (no commit) — report results; leave changes in working tree.
Self-Review (completed by plan author)
- Spec coverage: §4.2 validate_stats_block → Task 1. §4.3 SubagentStop → Task 2. §4.4 reporting-standards → Task 3. §4.5 agents → Tasks 4+6. §4.6 ingest → Task 5. §6 acceptance 1-6 → Task 1/2 tests + Task 7 smokes. No gap.
- Placeholder scan: deterministic-core tasks carry full code; agent/command tasks give exact block text + per-agent mapping + exact mirror file list. No "TBD"/"similar to".
- Type/name consistency:
parse_stats_block/validate_stats_blocknames match Tasks 1-2 and tests; thewiki-statsfence + funnel fields (found/processed/dropped/dropped_reason) identical across spec, impl, tests, agent blocks, smokes;found = processed + droppedinvariant consistent everywhere.