# task-2642 — CI_WATCH_HANDOFF_RUNNER 신설 완료 보고

- **executor**: dev6-페룬 (Claude Opus 4.7 · 1M context)
- **executor_start_kst**: 2026-05-23 20:11:17 KST
- **executor_end_kst**: 2026-05-23 20:14:58 KST
- **base_sha**: `0e172435` (origin/main · PR #144 task-2641 merge 반영)
- **branch**: `task/task-2642-runner`
- **local commit**: `d4098b042822728d6b09ee235e8d2f8f6a3fff1f`
- **push/PR/merge**: ★ 수행 0 (task md 명시 — BOT App token 부재로 로컬 한정)

## 1. 회장 verbatim 정책 코드화 (2026-05-23 19:38 KST)

> ANU 는 CI/Gemini 를 직접 기다리지 않는다. PR open 이후 대기/감시/자동수렴은 반드시 bot 또는 watcher task 에 위임한다.

본 task = 위 정책을 코드로 박제한 CI_WATCH_HANDOFF_RUNNER 신설.

## 2. 신규 파일 26건 (★ INDEX 1 포함 · 기존 stack 0 수정)

### 2.1 utils/ helper 3

- `utils/ci_watch_handoff_schema.py` — 12 필수 필드 + 5 terminal_states enum validator (`SCHEMA="utils.ci_watch_handoff_schema.v1"`)
- `utils/ci_watch_handoff_audit.py` — JSONL append-only · atomic flock · token sentinel redact (defense in depth). audit 경로: `memory/events/ci-watch-handoff-runner-audit.jsonl`
- `utils/ci_watch_handoff_runner.py` — state machine. PR_OPEN → poll loop → terminal → ANU normal callback 발사. envelope ≤3900 bytes hard limit (task-2612+3 박제).

### 2.2 fixture 18 + INDEX 1

`tests/fixtures/ci_watch_handoff_runner/<6 시나리오>/{evidence,expected,PROVENANCE}`:
1. `merge_ready_clean_gemini_fresh` → `MERGE_READY`
2. `gemini_stale_nudge_posted_re_poll_fresh` → `MERGE_READY` (POSTED→re-poll→FRESH)
3. `gemini_stale_after_nudge_timeout` → `GEMINI_EXTERNAL_TRIGGER_STALE`
4. `ci_failure_auto_remediation_medium_fix` → `MERGE_READY` (medium fix → re-push)
5. `forbidden_path_modification_detected` → `CHAIR_REQUIRED` (Critical7 즉시 escalate)
6. `loop_boundary_three_high_attempts` → `LOOP_BOUNDARY` (same-function HIGH × 3회 attempts)

+ `INDEX.md` (verbatim → 시나리오 매핑 + router state 8종 → terminal 5종 매핑).

### 2.3 regression 4

- `test_ci_watch_handoff_schema.py` — **35 PASS** (12 필드 + 5 enum + nudge ≤1 + severity 화이트리스트)
- `test_ci_watch_handoff_audit.py` — **16 PASS** (event 6종 + terminal_state required + redaction guard)
- `test_ci_watch_handoff_runner.py` — **26 PASS** (state machine 8 분기 · auto_remediation 4 outcome · envelope byte 검증)
- `test_ci_watch_handoff_runner_fixture_parametrized.py` — **6 PASS** (6 시나리오 audit event sequence 단언)

**총 83 PASS · PR #144 baseline 0 회귀 · full new fail 0.**

## 3. 12 필수 필드 + 5 terminal_states 1:1 박제 (회장 verbatim §4/§5)

| field | 검증 |
|---|---|
| pr_number | positive int |
| head_sha | 40-char hex (lower 정규화) |
| branch | non-empty str |
| expected_files | list[non-empty str] |
| forbidden_paths | list[non-empty str] (빈 list 허용) |
| watcher_owner | non-empty str |
| max_watch_minutes | positive int (default 120) |
| poll_interval_seconds | positive int (default 60) |
| gemini_nudge_policy | dict (enabled bool + max_nudges_per_pr_head ≤ 1 hard limit + on_403='report') |
| auto_remediation_policy | dict (enabled bool + allow_severities ⊆ {medium,style,quality,non-critical-high}) |
| callback_on_terminal_state | bool |
| terminal_states | list ⊆ 5 enum |

5 terminal_states: `MERGE_READY` / `CHAIR_REQUIRED` / `GEMINI_EXTERNAL_TRIGGER_STALE` / `CI_FAILED_NON_REMEDIABLE` / `LOOP_BOUNDARY`.

## 4. state machine (spec §4)

```
PR_OPEN → poll loop · max_polls
  ├─ ci.forbidden_path_touched=True → CHAIR_REQUIRED (즉시 · router 호출 skip)
  ├─ ci=PENDING → polling continue
  ├─ ci=FAIL
  │   ├─ same_function_high_repeated + attempts>=3 → LOOP_BOUNDARY (선검사)
  │   ├─ auto_remediation_fn(handoff, ci)
  │   │   ├─ APPLIED → polling continue
  │   │   ├─ FORBIDDEN_HIT → CHAIR_REQUIRED
  │   │   ├─ LOOP_BOUNDARY → LOOP_BOUNDARY
  │   │   └─ NON_REMEDIABLE → CI_FAILED_NON_REMEDIABLE
  └─ ci=PASS → gemini_router_call_fn (PR #144 wrapper)
      ├─ FRESH → MERGE_READY
      ├─ NUDGE_POSTED / NUDGE_DEDUPED → polling continue
      ├─ STALE → GEMINI_EXTERNAL_TRIGGER_STALE
      └─ CHAIR_UI_FALLBACK / PERMISSION_DENIED / FAILED / NOT_GEMINI_TRIGGER → CHAIR_REQUIRED
TERMINAL_REACHED → CALLBACK_FIRED (envelope UTF-8 ≤3900 bytes hard limit)
```

`handoff.terminal_states` subset 외부의 분류는 자동으로 `CHAIR_REQUIRED` 로 escalate (ANCHOR-5).

## 5. ANU normal callback 결과 (★ watcher policy 자체 자기검증)

| 항목 | 값 |
|---|---|
| schema | `utils.callback_envelope_schema.v2` |
| anu_key | `c119085addb0f8b7` (단일 출처 · self-key 차단 검증 통과) |
| canonical_root | `/home/jay/workspace` (envelope 명시) |
| registration_intent | `true` |
| registration_attempted | `true` |
| registration_result_status | `REGISTERED` ★ |
| callback_delivery_status | `DELIVERED` ★ |
| collector_receipt_status | `UNCONFIRMED` ★ |
| cron_schedule_id | `80FFF941` (non-null) ★ |
| registered_at_ts | `2026-05-23T11:14:58Z` |
| envelope UTF-8 bytes (printf '%s' \| wc -c) | **1092** |
| envelope chars (wc -m) | 1076 (참조 · 측정 기준은 wc -c) |
| byte limit | 3900 (task-2612+3 박제) |
| byte_within_limit | ✓ |

**측정 방법**: `printf '%s' "$P" | wc -c` (UTF-8 byte count · **NOT wc -m**). task md §finalize §4 명시 강제 정합.

## 6. 안전 불변식 확인

- ANU key `c119085addb0f8b7` 단일 출처 — registrar `_assert_independent_anu_key` 통과 ✓
- OWNER_GEMINI_TRIGGER_TOKEN 단일 출처 (BOT_GITHUB_TOKEN 사용 0) ✓
- envelope UTF-8 1092 ≤ 3900 hard limit ✓
- live cokacdir / gh CLI 실호출 — regression 0 / finalize 시 cokacdir --cron 1회 (정상 등록) ✓
- merge/push/PR/admin override — 0 ✓
- real auto-merge activation — 0 ✓
- PR #141 pilot 재시도 혼합 — 0 ✓
- foreign dirty 정리 — 0 ✓
- forbidden 15종 + owner_trigger 4종 + owner_gemini_trigger_router 3종 — **무수정 확인** ✓
- BLOCKING_SECRET — 0 ✓

## 7. PR #144 OWNER_GEMINI_TRIGGER_ROUTER stack 재사용 (anu_v2/ 무수정)

runner 는 `gemini_router_call_fn` 을 caller-injected callable 로 받음 → PR #144 router wrapper 를 직접 import 하지 않음 (one-way isolation 정합). router final_state 8종 → terminal_states 5종 매핑은 runner 내부 `_map_router_state` / `_terminal_reason_for_router` 에 박제.

## 8. ANU 8 완료 보고 항목 (회장 verbatim §9 — 정책 spec)

1. CI_WATCH_HANDOFF 생성: ★ 신규 schema/audit/runner 3 helper 작성
2. watcher 주체: handoff.watcher_owner 필수 (audit 매 record 기록)
3. watcher schedule_id: audit 매 record 기록 + result.json `cron_schedule_id=80FFF941`
4. terminal state: 5 enum 박제 (audit EVENT_TERMINAL_REACHED/CALLBACK_FIRED 강제)
5. 자동수렴 내역: audit `auto_remediation_attempts` + EVENT_AUTO_REMEDIATE 기록
6. CI/Gemini/phase3 최종 상태: `ci_status` + `router_final_state` 매 record 기록
7. merge-ready 여부: terminal_state == MERGE_READY 명시
8. ANU callback 수신 여부: 본 보고서 §5 — REGISTERED + DELIVERED + UNCONFIRMED

## 9. frozen anchor 1:1

- ANCHOR-1: ANU 직접 CI/Gemini 대기 금지 정책 (회장 verbatim 2026-05-23 19:38 KST) 코드화
- ANCHOR-2: 12 필수 필드 + 5 terminal_states + Watcher 5 단계 1:1 박제
- ANCHOR-3: PR #144 OWNER_GEMINI_TRIGGER_ROUTER stack 재사용 (무수정) + 기존 owner_trigger 4종 + forbidden 15종 무수정
- ANCHOR-4: state machine: PR_OPEN → poll loop → terminal → ANU normal callback 발사
- ANCHOR-5: 자동수렴 = watcher 책임 / CHAIR_REQUIRED = Critical7+credential+밖+admin override+smoke fail
- ANCHOR-6: OWNER nudge 1회 hard limit · NUDGE_403 = CHAIR_REQUIRED permission
- ANCHOR-7: envelope ≤3900 bytes · live 실호출 0 (regression mock)
- ANCHOR-8: real auto-merge 0 · PR #141 pilot 혼합 0 · ANU 직접 코드 구현 0

## 10. 후속 (회장 결정 영역)

- BOT App token 도입 시 push/PR 절차 활성화 (task md 명시: 본 task 는 로컬 한정)
- watcher cron/bot 실배포 시점에 runner 를 thin wrapper 로 schedule (envelope 5축 + canonical_root 그대로 사용)
- task-2640 결선 active 가정 (validate_spawn_callback_contract 정합) — registrar `_assert_independent_anu_key` 통과로 self-key 차단 확인됨

— 끝.
