# task-2729+8 보고서 — P0-B real wake 결선 + Gemini HIGH/SECURITY remediation (Option A replacement, r2)

- 팀: dev5-team (팀장 마르둑) / 백엔드 엔키, 테스터 닌기르수
- 레벨: Lv.3 (real wake 결선 + 보안 remediation. production activation/real spawn 아님)
- 작성일: 2026-06-06 (KST) / 서버시각 15:14
- base: fresh origin/main `c6aacb5e` (#181 hardening + #182 base isolation 포함) — stale(14ff8339) 아님
- branch: `task/task-2729+8-dev5` (Option A replacement 신규 branch, PR #183 same-branch push 금지 준수)

## S (Situation)
PR #183(head `13f1a8fc`) = SUPERSEDED. fresh base 위에서 real wake launcher 결선은 완료했으나 Gemini가
fresh **HIGH + SECURITY-HIGH + MEDIUM** 을 적발해 merge blocker 발생. same-PR push 금지 doctrine 에 따라
**Option A replacement**(새 branch, fresh origin/main base)로 수정 재구현.

## C (Complication)
Gemini 적발 3건이 모두 `dispatch/anu_pickup_wake_launcher.py` 에 집중:
1. **HIGH (`_iso`)**: timezone-aware clock 입력을 UTC 변환 없이 `Z` suffix 부착 → 비-UTC tz(예: KST)에서 잘못된 시각 기록.
2. **SECURITY-HIGH (`anu_key_verifier`)**: `if callable(verifier):` 구조라 verifier 가 None 아닌데 callable 이 아니면
   검증을 **통째로 건너뛰고 진행** → fail-open 보안 구멍.
3. **MEDIUM (`_dedupe_launched`)**: `open()` 을 try 안에서 호출 후 별도 `with fh:` → try-block hygiene 불량.

## Q (Question)
fresh HIGH/CRITICAL 0 + 13 gate 전부 PASS 로 `MERGE_APPROVAL_CANDIDATE_REAL_WAKE_READINESS_ACTIVE_FALSE`
판정을 받으려면, real spawn 0 / raw key 0 / canonical 무손상 / ACTIVE=false 를 유지하면서 위 3건을 어떻게 고치는가?

## A (Answer) — 적용한 수정
대상 파일 `dispatch/anu_pickup_wake_launcher.py`:

1. **HIGH `_iso` UTC 정규화**: `dt.tzinfo is not None → dt.astimezone(timezone.utc)` 후 포맷.
   naive datetime(`tzinfo is None`)은 **명시적 UTC 간주 정책**을 docstring+코드에 고정(기본 clock `_now()` 는 항상 tz-aware UTC).
2. **SECURITY-HIGH `anu_key_verifier` fail-closed**: `if anu_key_verifier is not None:` 로 분기 변경 후,
   **`not callable(...)` 이면 즉시 `FAIL_CLOSED_NON_ANU_KEY`** 반환(검증 우회 차단). verifier 결과 false/예외도 동일 fail-closed 보존.
   타입 주석을 `Optional[object]` 로 완화하여 런타임 callable 검증이 unreachable 로 오판되지 않게 함(pyright 0/0/0).
3. **MEDIUM `_dedupe_launched` hygiene**: 단일 `with open(...)` 으로 일원화. `FileNotFoundError`(→False) 를
   `OSError`(→`_LedgerError` fail-closed)보다 먼저 except. 동작 의미 완전 보존.

driver(`anu_pickup_driver.py`)는 W2 launcher_fn 결선(default None) 상태 그대로 유지 — base(c6aacb5e)→#183 diff = launcher_fn 결선만(20+/3-) 검증 완료.

## 생성/수정 파일 (정확히 5 — EXPECTED FILES 일치)
1. `dispatch/anu_pickup_wake_launcher.py` — HIGH/SECURITY/MEDIUM 수정 (352줄)
2. `dispatch/anu_pickup_driver.py` — W2 launcher_fn 결선(default None, base 대비 +20/-3)
3. `tests/regression/test_anu_pickup_real_wake_wiring_2729p8.py` — C16/C17/C18 추가 (569줄)
4. `memory/reports/task-2729+8.md` — 본 보고서
5. `memory/plans/p0b-pickup/real_wake_wiring_design_260606.md` — base c6aacb5e 갱신 + §6 r2 remediation

forbidden_paths(runner/callback_enforcement/systemd/result.json/state/utils/dispatch.py/.github/hooks) **0건 터치**.

## 테스트 결과
- pytest 36 passed (신규+회귀 2729p8 25건 + baseline 2729p7 11건, 무영향 확인).
- 신규 테스트:
  - `test_c16_iso_timezone_aware_converted_to_utc` — KST(+9) → `2026-06-06T00:00:00Z` 검증.
  - `test_c17_iso_naive_datetime_utc_policy` — naive → UTC 간주(`2026-06-06T12:34:56Z`).
  - `test_c18_non_callable_verifier_fail_closed` (4 parametrize: str/int/list/object) → `FAIL_CLOSED_NON_ANU_KEY` + runner 0.
- pyright: `0 errors, 0 warnings, 0 informations` (launcher) — 248줄 unreachable 경고 해소.

## L1 스모크테스트 결과 (필수)
- **서버 재시작**: 해당없음 (subprocess/라이브러리 모듈 작업 — 기동 서버 없음).
- **API 응답 확인**: 해당없음 (HTTP 엔드포인트 아님). 대신 **실제 모듈 실 호출**로 L1 충족:
  - L1-A (HIGH 실동작): KST clock 주입 → `launch_wake` 결과 `ts == "2026-06-06T00:00:00Z"` (UTC 변환 확인).
  - L1-B (SECURITY 실동작): `anu_key_verifier="NOT_CALLABLE"` → `FAIL_CLOSED_NON_ANU_KEY`, subprocess_runner 호출 0회.
  - L1-C (raw key 0): dry_run 기본 production ledger 미생성, isolated audit 에 fake key 미포함.
- **subprocess sabotage 검증 (verbatim #8)**: `subprocess.run/Popen/call/check_call/check_output/os.system` 전부
  raise 로 sabotage 한 상태에서 **25 passed** — 어떤 테스트도 실 실행 경로 미진입 입증.
- **스크린샷**: 해당없음 (프론트엔드 아님).

## 회장 verbatim 13 gate 결과
1. diff 5파일 유지 ✅  2. raw key 0 ✅  3. ACTIVE=false(activation flag 무변동) ✅  4. systemd enable 0 ✅
5. activation_epoch 0 ✅  6. canonical result.json 무변동(worktree 격리, 미터치) ✅  7. real spawn 0 ✅
8. subprocess sabotage 상태 PASS ✅  9. launcher_fn=None 기본 decision-only 보존(B11) ✅
10. WAKE_BUILT+injected launcher 에서만 호출(B12) ✅  11. legacy/MAX defer/terminal/ledger 경로 launcher 0(B13/B14) ✅
12. CI GREEN(pytest 36 passed) ✅  13. fresh Gemini HIGH/CRITICAL 0 → **PR Gemini 리뷰 대기**(G3에서 확정).

## 발견 이슈 및 해결
- **pyright unreachable(248줄)**: SECURITY 수정의 런타임 `if not callable(...)` 이, 파라미터 타입 `Optional[Callable]`
  narrowing 때문에 unreachable 로 오판됨. → 파라미터 타입을 `Optional[object]`(기대 Callable 주석)로 완화하여 해결.
  pyright 재실행 0/0/0 확인. (보안 체크 코드는 그대로 유지 — 우회 금지.)
- **start_task_guard lock missing(pre-commit 차단)**: 스케줄 재실행이라 task lock 부재 → 완료판정 노트의
  **ANU recovery(lock 복구) 허용** 범위 내에서 `<worktree>/.tasks/locks/task-2729+8.lock` 복구(task_id 일치) → pre-commit PASS.

## 모델 사용 기록
- 엔키(백엔드, launcher 코드+설계문서 수정): **sonnet** — 보안 로직 정밀 수정으로 haiku 부적합.
- 닌기르수(테스터, C16/C17/C18 추가): **sonnet** — 테스트 정밀도 요구로 haiku 부적합.
- 팀장 마르둑(Opus): 설계/리뷰/통합/검증만 수행, 직접 코딩 0 (DIRECT-WORKFLOW 원칙 준수).

## 머지 판단
- **머지 필요**: Yes (단, merge·production activation 은 별도 회장 승인 전 금지 — task doctrine).
- **브랜치**: `task/task-2729+8-dev5`
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2729p8-dev5`
- **머지 의견**: 13 gate 중 12건 자체 PASS, #13(fresh Gemini)만 PR 리뷰 대기. real spawn 0 / raw key 0 /
  ACTIVE=false / canonical 무손상 전부 충족. Gemini High 0건 시 **MERGE_APPROVAL_CANDIDATE_REAL_WAKE_READINESS_ACTIVE_FALSE**.

## doctrine 준수
직접 코딩 금지(팀원 위임) / same-PR post-Gemini push 금지(새 branch) / fail-closed 우선 / raw key 0 /
isolated temp root 검증 / merge·activation 회장 승인 전 금지.
