# task-2774+2 보고서 — governor PR#250 remediation round 1 (fail-open→fail-closed)

: 상태 `TASK2774_GOVERNOR_PR250_REMEDIATION_ROUND1_APPROVED_ACTIVE_FALSE`

## Situation
PR #250 (`task/task-2774-dev1`, head `cb910567`)의 spawn safety governor는 카운터/ledger I/O·파싱
오류 시 **빈 dict 무시 또는 silent pass(fail-open)** 로 차단 게이트가 우회될 수 있었다. Gemini가
unresolved 10건(HIGH 4 + medium 6)을 지적했다.

## Complication
fail-open 은 안전장치의 본질을 깨뜨린다 — `_load_counters` 오류 → 빈 카운터로 rate/loop 게이트 무력화,
`_save_counters` 실패 → count 미증가로 무한 spawn 개방, ledger 비대화 시 전량 메모리 적재(O(N)).
단, 파일 부재(첫 실행)는 정상이므로 **(a)부재=정상 / (b)손상·IO오류=fail-closed** 를 반드시 분리해야
기존 8/8 ALLOW 회귀가 깨지지 않는다.

## Question
1 remediation round 안에서 fail-open 4곳을 fail-closed 로 전환하고, ledger 를 bounded read 로 바꾸며,
medium 6도 같은 round에 처리하되 **차단 시맨틱 불변 + 기존 9종 회귀 보존**이 가능한가?

## Answer (수정 commit)
- **remediation commit**: `64d4ec1f` (base `cb910567` 위에 추가, hook 우회/--no-verify 없음, pre-commit/pre-push guard PASS)
- **변경 파일 = 정확히 2개 (expected_files, 2파일 밖 수정 0)**:
  - `utils/spawn_safety_governor.py`
  - `tests/regression/test_spawn_safety_governor_2774.py`

### HIGH 4 해소 (Gemini)
- **HIGH-3 `_load_counters` (L110)** — FileNotFoundError만 빈 dict(정상 첫 실행), 손상/파싱/권한·IO 오류 및
  비-dict는 예외 전파 → 호출부에서 fail-closed `COUNTERS_UNREADABLE → BLOCK`.
- **HIGH-4 `_save_counters` (L130)** — silent pass 제거, 저장 실패 시 예외 전파. ALLOW 경로는 **저장 우선**으로
  재배치하여 저장 실패 시 `COUNTERS_SAVE_FAILED → BLOCK` (count 미증가 + ALLOW 미기록 → dedup 오염/무한 spawn 차단).
- **HIGH-1 `_load_jsonl_lines` (L161)** — 전량 list 적재 제거. `_iter_jsonl_lines` **스트리밍 제너레이터**로 전환
  (파일부재=빈 제너레이터, 권한/IO 오류=전파).
- **HIGH-2 dedup 검사 (L248)** — 스트리밍 순회 + **일치 시 즉시 return(조기탈출)**. ledger/decisions open IO 오류는
  `LEDGER_UNREADABLE → BLOCK` 보수 처리.

### medium 6 해소
- `_get_event_epoch` 헬퍼 추가 — 이벤트 epoch **1회 파싱/캐시**. events_1min/1hour/window 3곳(L266/L288)에서 사용.
- ALLOW 이벤트 append 시 `epoch` 필드 선저장(L324).
- 테스트 `__import__("datetime").timedelta` 2곳(L188/L201) → 상단 import 된 `timedelta` 직접 사용.

### fail-open → fail-closed 전환 증거 (신규 reason enum)
`REASON_COUNTERS_UNREADABLE` / `REASON_COUNTERS_SAVE_FAILED` / `REASON_LEDGER_UNREADABLE`
(모두 BLOCK 결정 + `spawn_decisions.jsonl` 에 reason·task_id·key·ts 기록, silent drop 0).

## 테스트 결과
- `pytest tests/regression/test_spawn_safety_governor_2774.py` → **12 passed** (기존 9종 회귀 전부 PASS + 신규 3종)
  - 신규 1: `test_counters_unreadable_fails_closed` (깨진 JSON + 비-dict → COUNTERS_UNREADABLE)
  - 신규 2: `test_counters_save_failure_fails_closed` (mock 저장 실패 → COUNTERS_SAVE_FAILED, ALLOW 미기록)
  - 신규 3: `test_ledger_bounded_read_and_unreadable_fails_closed` (대용량 ledger 조기탈출 consumed≤2 + 디렉토리 IO오류 → LEDGER_UNREADABLE)
- `py_compile` 양 파일 PASS.
- 기존 ALLOW 첫 실행 경로(파일 부재) 보존 확인 — 회귀 무손상.

## L1 스모크테스트 결과 (필수)
- **서버 재시작**: 해당없음 (governor는 순수 stdlib 모듈, 데몬 없음)
- **API 응답 확인**: 해당없음 (HTTP API 아님). 대신 모듈 직접 실행으로 실동작 검증:
  - 별도 python 프로세스에서 `evaluate_spawn` 호출 → 손상 counter 파일 → `BLOCK / COUNTERS_UNREADABLE` 반환 확인
  - 파일 부재(첫 실행) → `ALLOW` 반환 확인 (fail-closed 전환이 정상 ALLOW 경로를 깨지 않음 실증)
  - 결과: `corrupt counters -> BLOCK COUNTERS_UNREADABLE`, `first-run (no files) -> ALLOW ALLOW` → **L1 SMOKE PASS**
- **스크린샷**: 해당없음 (CLI 모듈)

## 발견 이슈 및 해결
- Pyright 경고(test 파일, 동적 monkeypatch `GOV._save_counters`/`_iter_jsonl_lines` 속성 할당) 4건 — importlib
  동적 로드 모듈에 대한 타입체커 한계로 **런타임 무관**. pytest 12/12 PASS로 기능 정상 확인. 기능 결함 아님.
- `pickup_runner`는 `evaluate_spawn` public API만 주입받아 사용(내부 헬퍼 import 없음) → `_load_jsonl_lines` rename
  안전. pickup 통합 테스트(`test_enforced_spawn_zero_on_block`) PASS로 호출부 무손상 확인.

## CI / Gemini 상태
- push 후 diff 목록 1회 + PR 상태 1회 확인 (CI/Gemini polling 루프 미수행 — ANU 독립검증 담당).
- 원격 `task/task-2774-dev1` → `64d4ec1f` 갱신 완료. PR #250 자동 업데이트됨.
- PR mergeStateStatus=BLOCKED (Gemini unresolved thread 재검토 대기 — 코드 fix push로 해소 예정, 임의 resolve 안 함).

## 머지 판단
- **머지 필요**: Yes (단, 자동 merge 금지 — ANU가 Gemini unresolved 0 독립검증 후 결정)
- **브랜치**: task/task-2774-dev1
- **워크트리 경로**: /home/jay/workspace/.worktrees/task-2774-dev1
- **merge-ready 여부**: 코드/테스트 기준 ready (12/12 PASS, 2파일 밖 수정 0, fail-open 잔존 0).
  Gemini HIGH 4 + medium 6은 코드 fix push로 대응 완료 → 재검토 시 unresolved 0 기대.
- **머지 의견**: fail-closed 전환은 차단 시맨틱을 강화(약화 없음), 첫 실행 ALLOW 경로 보존. 충돌 없음(MERGEABLE).

## ⚠ finish-task.sh scope-gate 결과 — base-resolution 거짓양성 (ANU 판정 필요)
finish-task.sh 실행 시 scope-guard가 `dispatch/anu_result_pickup_runner.py: forbidden_paths 위반`으로
머지를 차단하고 `.escalate` + `failure-envelope.json`(SCOPE_GUARD_FAIL)을 생성했다. **이는 base-resolution
거짓양성이다 — 단서 교차로 확정**:
- scope-guard 의 base = `merge-base(origin/main, HEAD)` = `177792e5` (= cb910567 의 부모, PR 분기점).
  따라서 검증 범위 `177792e5..HEAD` = **PR #250 누적 전체**(base commit cb910567 포함) = 3파일.
- 그러나 **이번 remediation round 델타** `cb910567..HEAD` = 정확히 **2파일**(expected_files):
  `utils/spawn_safety_governor.py`, `tests/regression/test_spawn_safety_governor_2774.py`.
- `git diff cb910567..HEAD -- dispatch/anu_result_pickup_runner.py` = **빈 출력** → pickup_runner 이번 round
  **미수정(byte-identical)** 확정. pickup_runner 변경은 base PR(원래 task-2774, PR #250 승인 구현 = governor
  호출 1지점)에서 온 것이며, 이번 round 가 만든 것이 아니다.
- finish-task.sh 의 scope-base 는 stacked-PR remediation 을 가정하지 않는다(`worktree-base.json` fallback 은
  merge-base 가 빈 경우에만 동작 → merge-base 비어있지 않아 미동작). 구조적으로 누적 PR diff 를 검증하므로
  base PR 의 pickup_runner 를 항상 플래그한다.

**판정 요청(ANU)**: 이번 round 의 scope 평가 base 는 main 이 아니라 `cb910567`(PR head before round)이어야 한다.
round 델타 = 2 expected_files, pickup_runner 미수정. guard 우회/수동 `.done` 생성 안 함(금지 준수). ANU 독립검증으로
round 델타(cb910567..HEAD = 2파일)와 fail-closed 차단 증명(12/12)을 재현 후 closeout/merge 판단 바람.

## 모델 사용 기록
- 토르(백엔드, governor 10 Edit) → **sonnet**
- 헤임달(테스터, 신규 3종 + medium 2) → **sonnet**
- 팀장(오딘) → 설계/통합/검증/commit (직접 코딩 안 함). haiku 미사용.

## STOP 조건 점검
2파일 밖 수정 0 · 기존 체인 재작성 0 · 기존 8/8 회귀 보존 · 첫 실행 ALLOW 경로 무손상 ·
activation/systemd/canary 축 미접촉 · 자동 merge 미시도 → STOP 해당 없음.
