ADR 0060 — Outcome telemetry as governance measurement surface
ADR 0060 — Outcome telemetry as governance measurement surface
- Status: Accepted
- Date: 2026-05-19
- Authors: Hyunsoo Kim
- Related: ADR 0007 (issue-first + branch convention), retired aggregate policy (aggregate report surface), CLAUDE.md “거버넌스 ROI” + Q2-2026 self-review 5축 #3
- Issue: #1039
- Augments: governance critique 2026-05-19 (plan file
parsed-rolling-liskov-governance.md) 메타 발견 + 약점 #1·#4·#7·#8 동시 해소
Context
거버넌스 표면 (.githooks/, scripts/claude-hooks/ 8 hook, CI workflows, ship surfaces, PR template) 이 3+ 개월간 unbounded 하게 grow. 그러나 자기 효과성을 평가하지 못한다:
.claude/.hook-fires.log58줄 직접 확인: 57줄aware|load-bearing|..., 1줄ok|memory-lines|..., 0줄blocked|*. 강제력 있는 hook (bash-guard, adr-template, plan-slug-race, memory-lines BLOCK) 의 block 이벤트가 telemetry 자체에 부재.scripts/claude-hooks/pretooluse-bash-guard.sh:131-133은 이미 5-fieldblocked|gh-pr-create-stacked|...포맷을 emit 하지만, 2024-06-06 deploy 이후 fire 한 적 0번 → “훅이 사고를 막았다” 검증 불가.scripts/_self_review.py가 emit 하는 5축 #3 raw signal 은 fire count + memory line 같은 input metric. outcome metric (실제 block 횟수, bypass 횟수, false-positive 횟수, friction latency) 미수집.- 귀결: Q2-2026 self-review 5축 #3 △→✓ 회복은 ADR 0038 / PR #449 / PR #177 추가 라는 input 으로 받음. 기존 거버넌스가 사고를 막았다는 outcome 데이터 부재. ADR 0041/0042/0043 가 만들어낸 Decision Theatre 와 동일 패턴이 거버넌스 평가 자체에 적용됨.
scripts/_governance.py:175-204 의 ADR Verification lint 가 ADR 약속에 verifies-key 마커를 강제하지만, 거버넌스 평가에는 같은 회로 부재 — 본 ADR 이 그 회로.
Decision
.claude/.hook-fires.log5-field+ 표준 포맷 확립 —pretooluse-bash-guard.sh:131이 이미 사용하는 포맷을 canonical 로 promote:<ts>|<outcome>|<hook>|<category>|<path>[|<extra>]outcome∈ {aware, blocked, bypassed, false_positive, false_negative, nudged, pipeline_start, pipeline_end, ok}hook∈ {bash-guard, loadbearing, memory-lines, adr-template, plan-slug-race, delegation-gate, stop-ship} (SSoT:scripts/claude-hooks/README.md)category: hook-internal sub-classification (예: memory-lines 의 aware vs block sub-level)path: 영향 받은 파일/브랜치 (옵션)extra: 자유 메타데이터 (옵션, e.g.on=other-branch)
scripts/_governance.py에 emit_hook_fire() Python 헬퍼 +--emit-fireCLI subcommand — bash + Python hook 양쪽이 호출. 표준 포맷 한 곳에서 보장.def emit_hook_fire(outcome: str, hook: str, category: str = "", path: str = "", extra: str = "") -> None: ...python3 scripts/_governance.py --emit-fire \ --outcome blocked --hook bash-guard --category gh-pr-create-stacked \ --path "$current_branch" --extra "on=$stacked_on" 2>/dev/null || trueoutcome가KNOWN_OUTCOMES미포함이면 raise (typo guard).- 5 hook 의 emit 코드 통일 (PR4 본체):
pretooluse-loadbearing.sh: 3-field →--outcome aware --hook loadbearing --category file-edit --path <p>pretooluse-memory-lines.sh: 4-field →--outcome ok|aware|blocked --hook memory-lines --category <line-count-bin>+--path <p>userpromptsubmit-delegation-gate.sh: 4-field →--outcome nudged --hook delegation-gate --category <keyword-bin>pretooluse-adr-template.sh: 신규 emit →--outcome blocked --hook adr-template --category missing-verification --path <p>plan-slug-race.sh: 신규 emit →--outcome blocked --hook plan-slug-race --category cross-worktree-window --path <p>
pretooluse-bash-guard.sh,stop-ship.sh는 변경 최소:- bash-guard: 이미 5-field 포맷 emit → CLI 헬퍼 사용으로 통일만 (동일 라인 출력).
- stop-ship:
.ship-history.log별도 telemetry 유지 — pipeline outcome 은 hook gate outcome 과 의미 달라 별도 표면 합리. 통합은 별도 ADR 후보.
- Legacy entries grandfathered — 기존
.hook-fires.log58줄의 v1-3field / v1-4field 형식은 그대로 두고, 신규 entries 만 5-field.scripts/analyze_hook_outcomes.py(PR #1038) 가 세 포맷 모두 parse.
Why these specific choices
| 결정 | 근거 |
|---|---|
| outcome enum 9개 | aware/blocked/bypassed/false_*/nudged/pipeline_*/ok — 거버넌스 4-pillar (catch / nudge / measure / pipeline) 의 union. 9개가 최소 covering set |
_governance.py 단일 헬퍼 |
bash hook 7개 + Python _self_review.py 가 동일 포맷 보장. drift 위험 제거. bash-guard 의 기존 5-field 직접 printf 도 같은 헬퍼로 점진 migrate |
| Legacy grandfathered | 58 줄의 v1 entries 를 v2 로 rewrite 시 history 손실. parse 호환만 보장 |
| pipeline outcome 별도 | stop-ship 의 pipeline_start/pipeline_end 가 hook gate 와 의미 달라 — .ship-history.log 분리 유지 |
| outcome 미정의 typo → raise | 새 hook 작성자가 outcome 자유 입력 시 silent drift. CLI 가 enum 검증 |
Consequences
이득:
- 거버넌스 비판 보고서 (2026-05-19) 메타 발견 직접 해소 —
blockedoutcome 이 telemetry 에 entry. “X 훅이 Y 사고를 막는다” 주장 검증 가능. - 약점 #1 효과측정 unlock: required check 가 켜진 후 admin force-merge 시도가 발생하면 측정 가능 (별도 surface 필요 — 이 ADR scope 외).
- 약점 #4 효과측정 unlock: hook 별 enforcement label (PR #1036) ↔ 실제 outcome 분포 사이의 일관성을 90일 후 자동 검증 가능.
- 약점 #7 효과측정 unlock: memory-lines AWARE/BLOCK threshold (20/30) 가 실제 데이터로 정당화 또는 조정 가능 (analyze script PR #1038 의 threshold_recommendation 함수).
- 약점 #8 해소 경로: 90일 후 fire 0회 hook 식별 → surface 30% 축소 결정 (사용자 영역).
- 다음 분기 self-review 5축 #3 채점이 input metric (“ADR/PR 추가”) 에서 outcome metric (“blocked 이벤트 - false_positive 이벤트”) 으로 전환 가능 (사용자 메모 수정 필요 — 본 PR scope 외).
비용 + 잠재 위험:
- 8 hook + helper 모두 수정 → 회귀 위험. 회귀 테스트 (
tests/test_hook_telemetry.py) 가 각 hook 의 emit contract lock-in. - emit_hook_fire() 가 매 hook fire 마다 python3 fork → ~30ms 추가. hook 자체가 비동기 (PreToolUse) 라 사용자 perception 영향 미미.
- Decision Theatre 재발 가능성 — 이 ADR 도 “측정 표면 추가” 라는 input metric. 자기 일관성 보장 메커니즘: 본 ADR 의 verifies-key 마커가 90일 후
.hook-fires.log의 실제blocked|*entries 로 backed 되는지 자동 lint (_governance.py --lint-adr-consequences). 자동 lint 실패 시 ADR 0060 자체가 invalidated.
Invariance check
- ADR 0001 (naive baseline byte-identical): hook telemetry 는 production code path 0 변경 → 합성 baseline 영향 없음.
- ADR 0003 (answer dict schema_version=2): hook 표면이지 answer contract 아님.
- ADR 0005 (public fixture smoke / private real 분리):
.claude/.hook-fires.log가 이미.claude/*gitignore (issue #495) — 동일 boundary. - ADR 0007 (issue-first + branch convention): 본 ADR 은 issue #1039 + branch
feat/issue-1039-outcome-telemetry자체가 ADR 0007 준수.
Alternatives considered
- 별도 telemetry DB (SQLite / event log service): 1-day 작업이 1-week 로 확대. fire-log = append-only text 가 사용자 환경 (단일 머신) 에 충분. 데이터 수집 layer 와 분석 layer 분리는 PR #1038 의
analyze_hook_outcomes.py가 이미 담당. - 포맷 빅뱅 (legacy entries 모두 rewrite): 58줄 history 손실 + 미래 신규 hook 추가 시 다시 위험. v2 만 새로 emit, v1 parse 호환 (PR #1038) 으로 transition 부담 분산.
- stop-ship 의 .ship-history.log 도 합치기: pipeline outcome 의미가 gate outcome 과 달라 —
pipeline_start와blocked를 같은 분석 함수가 처리하면 hook ROI 측정에 noise. 별도 표면 유지 합리. - outcome 분류를 5개로 축소: aware/blocked/ok 만 두면 bypass / false-positive / nudge 의 fine-grained ROI 측정 불가. 9 enum 의 최소 covering set 선택.
Verification
90일 후 자기-검증 (ADR 0060 본문이 약속한 outcome 측정이 실제로 일어났는지):
python3 scripts/analyze_hook_outcomes.py --window 90d의blocked카운트가 ≥ 1 이면 약속 충족.- 0 이면 거버넌스 surface 가 fire 안 한 것 — surface 30% 축소 결정 트리거.