# task-2774 Spawn Safety Governor — 차단증명 + canonical 정상화 closeout 2026-06-24

**상태명**: `SPAWN_GOVERNOR_BLOCKPROOF_PASS_CANONICAL_NORMALIZED_PR_PENDING_ACTIVE_FALSE`
**성격**: 독립검증 박제 + canonical 정상화(main 복귀) + 산출물 보존. activation/canary/systemd 0 · merge 0 · ACTIVE=false.

## 1. 산출물 보존 (이중 백업 — 회장 지시 "보존 확인 전 branch 전환 금지" 충족)
- 산출물 commit: **`cb910567`** (dev2-odin, 2026-06-24 10:18:40 KST), branch `task/task-2774-dev1`.
- diff = parent `177792e5`(=main) 대비 **정확히 expected_files 3개** (+1045/-0, scope 위반 0):
  - `utils/spawn_safety_governor.py` (신규 345줄) — sha256 `0dc77874…f348bdd`
  - `tests/regression/test_spawn_safety_governor_2774.py` (신규 659줄) — sha256 `77c8eb86…0f5f7d85`
  - `dispatch/anu_result_pickup_runner.py` (+41줄, governor 호출 1지점 opt-in) — sha256 `22d4e235…ad6ba5aa`
- patch 별도 보존: `memory/handoffs/spawn_safety_governor_2774/task-2774-governor-cb910567.patch` (52518 bytes, 3파일만).
- branch ref `task/task-2774-dev1 @ cb910567` 무손상 — main 복귀 후에도 commit 보존 확인.

## 2. 차단 증명 — ANU 독립검증 8/8 PASS (격리 재검증 = 최종 근거)
무한 spawn 방지장치가 **실제로 spawn을 막는지** PASS/FAIL 중심 검증. spawn 함수 mock spy = 호출 0 증명.
1. **NON_TERMINAL** (terminal 아닌 result) → BLOCK, spawn 0. PASS
2. **DEDUP_HIT** (같은 result 재투입) → BLOCK, spawn 0. PASS
3. **LOOP_EXCEEDED** (같은 task 새 head_sha 반복, LOOP_PER_TASK=3 초과) → BLOCK, spawn 0. PASS
4. **RATE_EXCEEDED** (분당 1/시간당 5 초과) → QUEUE, spawn 0 + 기록. PASS
5. **CIRCUIT_TRIP** (fail_rate≥0.5) → TRIP + marker 생성. flag 부재=marker만 / flag 존재='blocked'(OFF). spawn 0. PASS
6. **SINGLEFLIGHT_LIVE** (live lock) → QUEUE, spawn 0. PASS
7. **ALLOW** (전부 통과) → 기존 체인 1회 통과(무손상). PASS
8. **LEDGER silent-drop 0** — 모든 결정(ALLOW/BLOCK/QUEUE/TRIP) spawn_decisions.jsonl 에 reason·task_id·key·ts 4필드 기록. PASS
- pytest `test_spawn_safety_governor_2774.py` 9 passed. py_compile PASS.

## 3. ★ 1차 검증 false negative 명시 (회장 지시)
- **1차 독립검증은 FALSE NEGATIVE**였음: "silent drop / loop-budget broken" 오판.
- 원인 = ANU가 `decisions_path` 파라미터를 누락(ledger_path 와 별개 인자). 차단 기록이 canonical default 로 가고 ANU는 빈 ledger 를 읽음 → 잘못된 FAIL 결론.
- 수정 = governor 코드를 직접 읽어(line 168-344) `decisions_path` 가 별도 인자(line 175)임을 확인 → 모든 path 격리 재검증 → 전 게이트 정상.
- ★ **격리 재검증 결과(8/8)만 최종 근거로 채택**. 1차 false negative 는 검증 오류로 기록(코드 결함 아님).
- 교훈 = ANU 자신의 인지검증 규칙("단정 말고 코드 직접 확인") 적용으로 자기 오판 포착.

## 4. WIRED 범위 정직 표기 (과장 0)
- governor pickup_once 결선 = **opt-in only**: `spawn_governor_fn` 파라미터(default None → 미평가). owner-proof gate 직전 fail-closed BLOCK 지점만 추가.
- 5단계: **IMPLEMENTED ✓ / VERIFIED ✓(8/8 격리 proof) / WIRED 부분(opt-in 주입, 상시 호출 아님) / ENFORCED 부분(주입 시 우회 불가 차단 증명) / ACTIVE ✗**.
- ★ 문서/테스트만으로 WIRED·ENFORCED·ACTIVE 승격 선언 안 함. 상시 결선·activation 은 별도 승인 단계.

## 5. canonical 정상화 (비정상 task-branch checkout 해소 — 회장 즉시 blocker)
- 정상화 전: canonical workspace 가 `task/task-2774-dev1`(cb910567) checkout + dirty 240 비정상 상태.
- 조치: 산출물 보존(§1) 확인 후 **안전 checkout main** (강제 reset/rebase 0 · 삭제/clean 0). dirty 240 전량 그대로 이월(force 0).
- 정상화 후: HEAD = **main @ `177792e5`** (== origin/main, up to date). dirty 240 보존. branch `task/task-2774-dev1 @ cb910567` 무손상.
- dirty 240 성격 = baseline 오염(44 `*.done.acked/.notified` 삭제 + 7 M: config/constants(Odin_v2 운영변경)·matrix·replacement_pr_runner(별도 follow-up)·specs·gemini_rate_tracker). **산출물과 무관, 일괄 정리 금지(회장 지시), 건드리지 않음.**

## 6. PR 경로 — 현재 blocker + 선택지 (회장 "PR 없으면 blocker와 선택지" 보고)
- 기존 PR 없음. origin 에 branch 미push (cb910567 로컬 전용).
- blocker: PR 생성 = branch push + gh pr create 필요. ANU 직접 push 는 "ANU never direct push" 하드룰 + PR#245 선례(ANU 수동 push 가드 차단→guard-native 재위임)와 충돌 소지.
- ★ 따라서 **push 전 STOP, 회장 결정 대기**. 선택지:
  - **A** — ANU가 bot-authored clean commit(cb910567, dev2-odin, 정확히 3파일, 8/8 검증)을 push + `gh pr create`(merge 아님·리뷰 게이트 진입만). 가장 경량.
  - **B** — guard-native 재위임(dev봇이 worktree에서 push+PR). 가드 정합 ↑, 단 commit이 canonical 브랜치에 있어 재현 부담.
  - **C** — 현 상태 HOLD.
- merge 는 어느 경우든 CI/Gemini/final gate 전까지 금지(회장 명시).

## 7. 불변식 유지
- ACTIVE=false · P0-B FROZEN · activation flag(p0b_driver_enabled) 생성 0 · systemd/cron/canary 0 · matrix 값 true 변경 0 · 자동 merge 0.
- 기존 owner-pickup 체인 재작성 0(governor 1지점 opt-in 추가만). dirty baseline 무손상.
- 별도 follow-up(미착수): hook `_integration` import 결함 / replacement_pr_runner.py re-truncate.
