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

> 팀: dev1-team (헤르메스 팀장) | 작성일: 2026-06-24 | 검증레벨: normal | ACTIVE=false

## SCQA 요약

- **S (Situation)**: normal callback / owner-pickup 체인(result.json 수거→owner-proof→dedup→collector→done→terminal)은 이미 IMPLEMENTED+WIRED+테스트 다수. 끊김은 코드 결함이 아닌 의도된 FROZEN(activation flag 부재 + systemd inactive).
- **C (Complication)**: wired=true 로 안전하게 전환하기 전 blocker = **rate-limit / loop-budget / circuit-breaker 미구현 + terminal-only hard gate 미강제**. dedup은 같은 head_sha만 막아, 같은 task가 새 commit으로 result.json을 계속 생성하면 무한 spawn 사각지대가 존재.
- **Q (Question)**: 기존 체인 재작성 0으로, 무한 spawn을 결정론적으로 차단하는 단일 게이트를 어떻게 추가하는가?
- **A (Answer)**: 신규 `utils/spawn_safety_governor.py`(6게이트 순수함수 + ledger)를 만들고, `pickup_once`의 owner-proof gate **직전 1지점**에 opt-in 파라미터(`spawn_governor_fn`)로 차단 호출을 결선. ALLOW만 진행, BLOCK/QUEUE/TRIP은 spawn 0. **차단증명 9종 + 회귀 76 PASS + L1 실동작 스모크**로 검증. activation/canary/systemd 0 · ACTIVE=false 유지.

## 변경 파일 (expected_files 정확히 3개)

1. `utils/spawn_safety_governor.py` (신규, **345줄**, stdlib only/self-contained)
2. `dispatch/anu_result_pickup_runner.py` (수정, **+41줄**, 1지점 추가 — 기존 체인 재작성 0)
3. `tests/regression/test_spawn_safety_governor_2774.py` (신규, **659줄**, 차단증명 9종)

## governor 6게이트 + config 상수

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, ledger_path, now, live_lock_present, decisions_path, circuit_marker_path, p0b_flag_path) -> Decision(dict)` — 차단 순서(첫 차단에서 멈춤):

1. **terminal-only HARD GATE**: terminal_state ∉ {completed,failed,blocked,crash} → BLOCK / NON_TERMINAL
2. **dedup**: (task_id, head_sha, terminal_state) 기존 ledger(callback_4tuple_index.jsonl) 또는 spawn_decisions ALLOW 기록 존재 → BLOCK / DEDUP_HIT
3. **single-flight**: live_lock(anu_session_alive.lock) 존재 → QUEUE / SINGLEFLIGHT_LIVE
4. **rate-limit**: 분당>1 또는 시간당>5 → QUEUE / RATE_EXCEEDED
5. **loop-budget**: task_id≥3 또는 family≥5 → BLOCK / LOOP_EXCEEDED (family = task-N/+M/-rM 동일 base)
6. **circuit-breaker**: 10분 윈도우 spawn≥10 또는 실패율≥0.5 → TRIP / CIRCUIT_TRIP (marker 생성 + p0b_flag **존재 시에만** 'blocked' 쓰기, 없으면 marker만)
7. 전부 통과 → ALLOW

- 모든 결정(ALLOW/BLOCK/QUEUE/TRIP) → `spawn_decisions.jsonl` append **4필드 필수**(reason·task_id·key=head_sha+terminal_state·ts), silent drop 0.
- 카운터(spawn_counters.json) ALLOW 시에만 원자적(tempfile+os.replace) 증가. governor 자체 재귀/spawn 0.

## pickup_once 1지점 diff 요약

- verdict 상수 추가: `PICKUP_SPAWN_BLOCKED = "SPAWN_GOVERNOR_BLOCKED"` (+`__all__` 등록).
- 시그니처에 opt-in 파라미터 `spawn_governor_fn: Optional[Callable[..., dict]] = None` 추가.
- owner-proof gate(`if owner_gate_fn is not None:`) **직전**에 governor 블록 삽입: `spawn_governor_fn` 주입 시에만 candidate{task_id, head_sha, terminal_state} 구성→evaluate_spawn 호출→`decision != "ALLOW"` 면 callback_launch_fn 미호출·owner gate 미호출·argv 미구성으로 `PICKUP_SPAWN_BLOCKED` 반환. 호출/오류는 fail-closed BLOCK(보수적).
- **기본 None → 기존 체인/회귀 완전 무손상** (회귀 테스트들은 이 파라미터 미전달).

## 차단증명 9종 결과 (test_spawn_safety_governor_2774.py)

`9 passed in 0.18s` — 전종 PASS:
1. NON_TERMINAL → spawn 0
2. 같은 result 재투입 → DEDUP_HIT spawn 0
3. 같은 task_id 다른 sha 반복 → LOOP_EXCEEDED(LOOP_PER_TASK+1) spawn 0
4. rate 초과 → RATE_EXCEEDED queued
5. circuit trip → marker 생성(flag 없을 때 marker만, flag 있을 때 파일 'blocked' 기록)
6. single-flight live → SINGLEFLIGHT_LIVE queue
7. ALLOW → 통과
8. **ENFORCED**: pickup_once에 실제 GOV.evaluate_spawn 주입 → BLOCK 시 `verdict==PICKUP_SPAWN_BLOCKED` AND `callback_launch_fn 호출 0회`(mock spy) — 우회 불가 증명. ALLOW sub-case는 기존 체인 진행(무손상).
9. 모든 BLOCK/QUEUE/TRIP → spawn_decisions.jsonl reason/task_id/key/ts 4필드 기록 확인.

## L1 스모크테스트 결과 (실동작 — pytest와 별개)

- **서버 재시작**: 해당없음 (라이브러리/subprocess 게이트 작업, 서버 무관)
- **API 응답 확인**: 해당없음. 대신 **실 end-to-end 실행** 수행:
  - 실제 result.json(non-terminal) 생성 → 실제 `pickup_once` + 실제 `GOV.evaluate_spawn` 호출 → `verdict=SPAWN_GOVERNOR_BLOCKED`, `callback_launch_fn 호출 0회`(실 spawn 0) 관측.
  - `spawn_decisions.jsonl` 실파일 기록 확인: `{decision:BLOCK, reason:NON_TERMINAL, task_id:task-L1, key:"cafe|needs_relay", ts:2026-06-24T01:16:59Z}` — 4필드 전부 존재.
  - ALLOW 케이스(completed) → `verdict=CLOSEOUT_DONE`(기존 체인 진행, 무손상) 관측.
  - 결과: **L1_SMOKE_PASS** ✅
- **스크린샷**: 해당없음 (UI 없음)

## 회귀/체인 무손상 검증

- owner_gate_fn 주입 회귀 전체 **76 passed**: test_anu_result_pickup_runner_2720(12) · test_p0b_event_strategy_2760 · test_p0b_event_strategy_wiring_2760 · test_provenance_callback_bridge_2756 · test_p0b_callback_launch_signature_2763 · test_p0b_owner_gate_signature_2762.
- `py_compile` 3파일 ALL_COMPILE_OK.

## 발견 이슈 및 해결

1. **다수 회귀가 owner_gate_fn 주입**(2741/2760/2756/2763): governor를 owner_gate_fn 경로에 무조건 삽입하면 회귀 파손. → **해결**: opt-in 파라미터 `spawn_governor_fn`(기본 None) 설계로 명시 주입 시에만 동작 → 회귀 완전 무손상 + 신규 테스트는 실 함수 주입으로 WIRED/ENFORCED 증명.
2. **test_os_owner_proof_gate_2741 collection 에러**(ModuleNotFoundError: dispatch.anu_executor_provenance): 인지검증 차원에서 내 변경을 git stash 후 재현 → **동일 에러 발생** = task-2774와 무관한 사전 존재 테스트 인프라(_load 순서) 이슈로 확정. (모듈 파일 자체는 존재)
3. **automation_capability_matrix.json 수정 표시**: 02:55(내 작업 10:16보다 7시간 전) 타임스탬프의 사전 미커밋 변경(v2_13_non_matching_thin_gate/PR240) — 내 작업과 무관, 스테이징 제외(forbidden path 미수정 준수).
4. **커밋 가드**: main 직접 커밋 금지 + task lock 부재 → 정식 절차로 브랜치 `task/task-2774-dev1` 생성 + `taskctl_start.record_lock_sha`로 lock 생성 후 커밋.

## 금지/제약 준수 확인

- 기존 owner-pickup 체인/dedup/owner-proof/single-flight 재작성 **0** (governor 호출 1지점 추가만).
- activation flag(p0b_driver_enabled) 생성/enable **0** · systemd/cron/canary 실행 **0** · matrix 값 true 변경 **0**.
- forbidden_paths(dispatch.py, finish-task.sh, matrix, p0b_driver_enabled, .claude, .env*) 수정 **0**.
- governor 새 spawn/재귀 원천 **0** · self-contained(stdlib only) 유지.
- **ACTIVE=false** · 운영 활성화 0 (문서만으로 VERIFIED/WIRED 승격 아님 — 각 단계 proof 첨부).

## 5단계 승격 상태

- IMPLEMENTED ✅ (governor.py 존재) · VERIFIED ✅ (9종+L1 proof) · WIRED ✅ (pickup_once가 실 evaluate_spawn 호출, mock 아님) · ENFORCED ✅ (BLOCK 시 callback_launch_fn 호출 0 — spy 증명) · **ACTIVE ✗ (의도 — flag/systemd 미활성, 본 작업 범위 외)**.

## 모델 사용 기록

- 불칸(백엔드, **sonnet**): governor.py + pickup 통합. (코딩 — sonnet 기본값)
- 아르고스(테스터, **sonnet**): 차단증명 9종 테스트. (코딩/테스트 — sonnet 기본값)
- haiku 미사용. 헤르메스(Opus): 설계/통합검증/회귀증명/정리만 직접 수행(팀원 코딩 위임 준수).

## 머지 판단

- **머지 필요**: Yes (3파일 신규/수정, Lv.2 normal)
- **브랜치**: `task/task-2774-dev1` (커밋 cb910567)
- **워크트리 경로**: 해당없음 (system task, project_id 없음 → 메인 workspace repo 브랜치)
- **머지 의견**: 차단증명 9종 + 회귀 76 PASS + L1 실동작 PASS. 기존 체인 무손상(opt-in 설계). 충돌 위험 낮음(신규 2파일 + 기존 파일 additive +41줄). G3 게이트(PR/Gemini 리뷰) 후 머지 권장. ACTIVE=false 유지로 운영 영향 0.

## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회

