# task-2774 — Spawn Safety Governor 구현 (무한 spawn 방지, guard-native)

## 목표 (normal callback wired=true 전 blocker 제거 — 안전장치가 본체)
normal callback / owner-pickup 을 안전하게 켜기 위한 **무한 spawn 방지 게이트**를 구현한다. ★activation/canary/systemd 0 · 코드+테스트만. 설계: `memory/plans/spawn_safety_governor/` (plan/context/checklist 회장 승인본).
ANU read-only 조사 결과: 체인은 이미 구현·검증됨. **미구현 blocker = rate-limit/loop-budget/circuit-breaker + terminal-only hard gate**. dedup/owner-proof/single-flight 는 **기존 구현 재사용**(재작성 0).

## expected_files (정확히 3개)
```text
utils/spawn_safety_governor.py                          # 신규 governor (stdlib only, config 상수)
dispatch/anu_result_pickup_runner.py                    # pickup_once owner-gate 직전 1지점 evaluate_spawn() 차단 호출
tests/regression/test_spawn_safety_governor_2774.py     # 신규 차단 증명 테스트
```
※ pickup_once 기존 체인(owner-proof/dedup/collector/done/terminal) **재작성 절대 금지**. governor 호출 1지점 추가만.

## 구현 지시
### utils/spawn_safety_governor.py (신규, self-contained stdlib)
- 상단 **config 상수**(운영 활성화 X, 값만 정의):
  - `RATE_PER_MIN=1`, `RATE_PER_HOUR=5`
  - `LOOP_PER_TASK=3`, `LOOP_PER_FAMILY=5`
  - `CIRCUIT_WINDOW_MIN=10`, `CIRCUIT_MAX_SPAWN=10`, `CIRCUIT_FAIL_RATE=0.5`
- `evaluate_spawn(candidate, *, counters_path=None, ledger_path=None, now=None, live_lock_present=None) -> Decision`
  - Decision = dict(decision in {ALLOW/BLOCK/QUEUE/TRIP}, reason, task_id, key, ts).
  - 차단 순서(첫 차단에서 멈춤):
    1. **terminal-only HARD GATE**: candidate.terminal_state 가 terminal enum(completed/failed/blocked/crash) 아니면 → BLOCK NON_TERMINAL (spawn 금지, skip 아님).
    2. **dedup**: key=(task_id, head_sha, terminal_state) 가 기존 dedup ledger(callback_4tuple_index.jsonl) 또는 spawn_decisions 에 ALLOW 기록 존재 → BLOCK DEDUP_HIT. (기존 dedup 로직 재사용/연계, 중복 재작성 0)
    3. **single-flight**: live_lock_present(또는 anu_session_alive.lock) True → QUEUE SINGLEFLIGHT_LIVE (spawn 0, inbox 적재 신호).
    4. **rate-limit**: 분당>RATE_PER_MIN or 시간당>RATE_PER_HOUR → QUEUE RATE_EXCEEDED.
    5. **loop-budget**: task_id 누적 spawn≥LOOP_PER_TASK or family≥LOOP_PER_FAMILY → BLOCK LOOP_EXCEEDED. (family = task-N / +M / -rM 동일 base)
    6. **circuit-breaker**: CIRCUIT_WINDOW_MIN 내 spawn≥CIRCUIT_MAX_SPAWN or 실패율≥CIRCUIT_FAIL_RATE → TRIP CIRCUIT_TRIP: spawn_circuit_tripped.marker 생성 + (★flag p0b_driver_enabled **존재 시에만** 'blocked' 쓰기, 없으면 marker만) + 회장보고 stub(실발사 0).
    7. 전부 통과 → ALLOW.
  - ★ **모든 결정(ALLOW/BLOCK/QUEUE/TRIP)을 spawn_decisions.jsonl 에 append**: reason·task_id·key(head_sha+terminal_state)·timestamp 4필드 필수 (silent drop 0).
  - 카운터: spawn_counters.json 원자적 갱신(rate/loop 윈도우). governor 자체 재귀 0.
- `_task_family(task_id)`: task-N / +M / -rM → base "task-N".

### dispatch/anu_result_pickup_runner.py (1지점)
- pickup_once 의 **owner-proof gate 호출 직전**에 `governor.evaluate_spawn(candidate)` 추가. ALLOW 만 owner-proof 진행. BLOCK/QUEUE/TRIP → spawn 0 + 반환(기존 체인 무손상). import 실패/governor 오류는 안전하게 처리(단 차단형이므로 오류 시 보수적 BLOCK 권장 — fail-closed).

### tests/regression/test_spawn_safety_governor_2774.py (차단 증명 — 회장 명시)
1. **terminal 아닌 result → spawn 0** (NON_TERMINAL BLOCK).
2. **같은 result 재투입 → spawn 0** (DEDUP_HIT).
3. **같은 task_id 다른 sha 반복 → loop-budget 초과 후 spawn 0** (LOOP_EXCEEDED, LOOP_PER_TASK+1).
4. **rate 초과 → spawn 0 + queued/blocked 기록** (RATE_EXCEEDED).
5. **circuit trip → spawn 0 + BLOCKED marker** (flag 없을 때 marker만, flag 있을 때 OFF).
6. **single-flight live → spawn 0 또는 inbox 적재** (SINGLEFLIGHT_LIVE).
7. **ALLOW 케이스 → 기존 체인 안 깨고 1회 통과**.
8. **ENFORCED**: 각 BLOCK/QUEUE/TRIP 시 실제 spawn 함수 호출 0 (mock spy로 우회 불가 증명).
9. **모든 BLOCK → spawn_decisions.jsonl reason 기록** 확인.

## 금지
```text
기존 owner-pickup 체인 재작성 · dedup/owner-proof/single-flight 중복 재작성 ·
activation flag(p0b_driver_enabled) 생성/enable · systemd/cron/canary 실행 · matrix 값 true 변경 ·
expected_files 3개 밖 수정 · dispatch.py(wrapper)/finish-task.sh 수정 · 운영 활성화 · 자동 merge 권한 확대 ·
문서만으로 VERIFIED/WIRED 승격 보고.
```

## STOP 조건
```text
기존 체인 재작성 필요 · pickup_once 시그니처 대폭 변경 필요 · activation/systemd 축 번짐 ·
governor 가 새 무한 재귀/spawn 원천 됨 · 차단 증명 테스트 미작성 · self-contained 깨짐(외부 의존).
```

## 검증 (완료 전 필수)
```text
pytest tests/regression/test_spawn_safety_governor_2774.py -q (9종 PASS)
+ utils/spawn_safety_governor.py py_compile PASS
+ 차단 증명: 각 BLOCK 케이스에서 spawn 호출 0 (mock spy) 실증
+ ALLOW 1케이스 기존 체인 무손상 실증
+ 기존 pickup 회귀(test_anu_result_pickup_runner_2720) 무손상 PASS
```

## 보고 (ANU callback envelope + report)
```text
변경 파일(expected 3개) · governor 6게이트 + config 상수 · pickup_once 1지점 diff · 차단 증명 9종 결과 ·
ENFORCED proof(spawn 호출 0) · 기존 체인/회귀 무손상 · spawn_decisions.jsonl 4필드 · activation/canary/systemd 0 · ACTIVE=false.
```

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "utils/spawn_safety_governor.py"
    - "dispatch/anu_result_pickup_runner.py"
    - "tests/regression/test_spawn_safety_governor_2774.py"
  forbidden_paths:
    - "dispatch.py"
    - "scripts/finish-task.sh"
    - "memory/state/automation_capability_matrix.json"
    - "memory/state/p0b_driver_enabled"
    - ".claude/**"
    - ".env*"
  commands:
    - "git"
    - "pytest"
    - "python3"
    - "gh"
  merge_policy: "manual"
  ttl_hours: 12
```