# CI_WATCH_HANDOFF_RUNNER spec (task-2642 즉시 후속) — 260523

회장 결정 (2026-05-23 19:38 KST · CI_WATCH_HANDOFF 정책 도입 직후): **CI_WATCH_HANDOFF_RUNNER task 발행 — ANU 가 직접 대기하지 않고, PR open 이후 watcher bot/cron 이 terminal state 까지 책임지게 하는 것.**

기반: `system_ci_watch_handoff_policy_spec_260523.md` (정책 단일소스) + `system_owner_gemini_trigger_router_spec_260523.md` (PR #144 OWNER nudge stack) + `audit_self_collector_enforcement_gap_260523.md` (SELF_COLLECTOR 차단 doctrine 정합).

---

## 1. 본 task 범위

### 1.1 단일 batch (Track 구분 없음 — runner 본체)
- 신규 모듈: `utils/ci_watch_handoff_runner.py` (CI/Gemini polling + auto-remediation + terminal classification + ANU callback 발사)
- 신규 helper 1: `utils/ci_watch_handoff_schema.py` (CI_WATCH_HANDOFF JSON v1 schema + 12 필수 필드 + 5 terminal_states validator)
- 신규 helper 2: `utils/ci_watch_handoff_audit.py` (JSONL audit + watcher 주체 / schedule_id / terminal state 기록)
- 신규 fixture: 6 시나리오 × 3 (evidence + expected + PROVENANCE) = 18 file
- 신규 regression: 4 (schema / runner state machine / audit / fixture parametrized)
- (선택) INDEX 1

총 ~27 file.

### 1.2 기존 stack 무수정 (회장 verbatim)
- `anu_v2/owner_gemini_trigger_router.py` (PR #144 stack 재사용만)
- `anu_v2/owner_trigger_only.py` 등 4종 (PR #98 stack 무수정)
- forbidden 15종 (task-2640 merge 후 stack 그대로)

---

## 2. 회장 verbatim 12 필수 필드 1:1 매핑

| field | 타입 | 검증 |
|---|---|---|
| `pr_number` | int>0 | 필수 · GitHub PR API 호출 가능 여부 |
| `head_sha` | str | 40-char hex · stale 판정 기준점 |
| `branch` | str | non-empty |
| `expected_files` | list[str] | 자동수렴 scope · path glob 형식 |
| `forbidden_paths` | list[str] | 수정 시 즉시 CHAIR_REQUIRED |
| `watcher_owner` | str | dev bot name 또는 `"cron-watcher"` enum |
| `max_watch_minutes` | int>0 | hard timeout. default=120 |
| `poll_interval_seconds` | int>0 | CI polling 간격. default=60 |
| `gemini_nudge_policy` | dict | 1회 hard limit · `{enabled: bool, max_nudges_per_pr_head: 1, on_403: "report"}` |
| `auto_remediation_policy` | dict | `{enabled: bool, allow_severities: ["medium","style","quality","non-critical-high"]}` |
| `callback_on_terminal_state` | bool | default=true · terminal 도달 시 ANU normal callback 발사 |
| `terminal_states` | list[str] | enum 5종 sub-set |

---

## 3. terminal_states 분류 로직 (회장 verbatim 5 enum)

### 3.1 `MERGE_READY`
- CI 11/11 SUCCESS (또는 모든 required checks PASS)
- Gemini fresh review evidence (commit_id == current head_sha)
- 0 unresolved review threads
- mergeStateStatus == "CLEAN"
- mergeable == "MERGEABLE"

### 3.2 `CHAIR_REQUIRED`
- Critical7 hit 발견 (forbidden path / scope expansion / replacement_pr fail / smoke fail / dep cycle / admin override / BLOCKING_SECRET)
- credential/permission expansion 감지 (`feedback_credential_scan_3tier_doctrine_260522.md` 기준)
- expected_files 외부 수정 감지
- admin override 필요 시점
- post-merge smoke fail

### 3.3 `GEMINI_EXTERNAL_TRIGGER_STALE`
- OWNER nudge 1회 hard limit 후 (`gemini_nudge_policy.max_nudges_per_pr_head=1` 소진)
- 그 후 timeout (poll_interval 기준) 까지 fresh review 미도착
- watcher fail-closed → ANU callback `GEMINI_EXTERNAL_TRIGGER_STALE`

### 3.4 `CI_FAILED_NON_REMEDIABLE`
- 자동수렴 불가능한 CI failure
- test regression (자동수렴 attempt 후에도 fail)
- forbidden path 수정 시도 감지
- credential/permission 추가 감지

### 3.5 `LOOP_BOUNDARY`
- 자동수렴 attempt N회 (default=3) 후 동일 함수/file-boundary HIGH 반복
- `feedback_auto_remediation_loop_boundary_review_260519.md` 정합

---

## 4. Watcher state machine

```
PR_OPEN (handoff 수령)
    ↓
[poll loop · interval=poll_interval_seconds · timeout=max_watch_minutes]
    ↓
CHECK_CI_STATUS
    ├─ CI 미완료 → 계속 polling
    ├─ CI FAILURE (자동수렴 가능) → AUTO_REMEDIATE → push → re-poll
    ├─ CI FAILURE (자동수렴 불가) → CI_FAILED_NON_REMEDIABLE
    └─ CI PASS → CHECK_GEMINI_FRESHNESS
                     ├─ Gemini fresh + 0 unresolved → MERGE_READY
                     ├─ Gemini stale + nudge_remaining>0 → OWNER_NUDGE → re-poll
                     ├─ Gemini stale + nudge_remaining==0 (POSTED 후) → GEMINI_EXTERNAL_TRIGGER_STALE
                     ├─ NUDGE_403 → CHAIR_REQUIRED (permission)
                     └─ Critical7 detected → CHAIR_REQUIRED
                     └─ loop count >= N → LOOP_BOUNDARY
    ↓
CALLBACK_FIRE (ANU normal callback)
    ↓
WATCHER_EXIT (cancel-on-success)
```

### 4.1 OWNER_GEMINI_TRIGGER_ROUTER 통합
- PR #144 `anu_v2/owner_gemini_trigger_router.py` 호출만 (재사용)
- watcher 가 stale 감지 시 `router.route_for_pr(pr_number, current_head_sha, owner, repo)` 호출
- result enum 5종 (FRESH / NUDGE_POSTED / NUDGE_DEDUPED / STALE / CHAIR_UI_FALLBACK_REQUIRED) → watcher state 로 매핑

### 4.2 자동수렴 entrypoint
- expected_files 내부 medium/style/quality/non-critical HIGH → `_apply_auto_remediation(finding)` 호출
- regression 재실행 → push → thread resolve → re-poll
- 자동수렴 attempt 횟수 audit JSONL 기록

### 4.3 ANU normal callback 발사
- terminal state 도달 시 `cokacdir --cron` 호출 (envelope ≤3900 bytes)
- envelope 5축 + canonical_root + terminal_state + watcher_owner + schedule_id 명시
- collector_role=ANU · owner_key=c119085addb0f8b7

---

## 5. 신규 fixture 6 시나리오

`tests/fixtures/ci_watch_handoff_runner/<scenario>/{evidence.json,expected.json,PROVENANCE.md}` (6×3 = 18 file):

1. `merge_ready_clean_gemini_fresh` → MERGE_READY (CI 11/11 + Gemini fresh + CLEAN)
2. `gemini_stale_nudge_posted_re_poll_fresh` → MERGE_READY (1차 nudge POSTED → fresh review 도착)
3. `gemini_stale_after_nudge_timeout` → GEMINI_EXTERNAL_TRIGGER_STALE (nudge POSTED 후 timeout)
4. `ci_failure_auto_remediation_medium_fix` → MERGE_READY (medium fix → re-push → CI PASS)
5. `forbidden_path_modification_detected` → CHAIR_REQUIRED (Critical7)
6. `loop_boundary_three_high_attempts` → LOOP_BOUNDARY (자동수렴 3회 후 동일 함수 HIGH 반복)

---

## 6. 신규 regression 4

- `tests/regression/test_ci_watch_handoff_schema.py` (12 필드 + 5 enum validator)
- `tests/regression/test_ci_watch_handoff_runner.py` (state machine + terminal classification)
- `tests/regression/test_ci_watch_handoff_audit.py` (JSONL append + watcher 주체 + schedule_id 기록)
- `tests/regression/test_ci_watch_handoff_runner_fixture_parametrized.py` (6 fixture parametrized)

---

## 7. 안전 불변식

- ANU key `c119085addb0f8b7` 단일 출처 유지
- OWNER_GEMINI_TRIGGER_TOKEN 단일 출처 (BOT_GITHUB_TOKEN 사용 0)
- envelope UTF-8 ≤3900 bytes 유지
- callback prompt ≤2800 bytes 권장 (cokacdir 한계)
- live cokacdir / gh CLI 실호출 0 (regression mock)
- merge/push/PR/admin override 호출 0 (watcher 만 push/thread resolve)
- real auto-merge activation 0
- PR #141 pilot 재시도 혼합 0
- foreign dirty 정리 0
- production service task 혼합 0
- ANU 직접 코드 구현 0 (dev bot 위임)
- expected_files 외부 수정 0
- BLOCKING_SECRET 0
- forbidden 15종 + 기존 owner_trigger 4종 + PR #144 owner_gemini_trigger_router 3종 무수정

---

## 8. 자동수렴 정책

- Gemini medium/style/quality + expected_files 내부 → 자동수렴
- 동일 함수 HIGH 반복 시 LOOP_BOUNDARY → 회장 보고
- 회장 보고 트리거: Critical7 / credential expansion / expected_files 밖 / admin override / post-merge smoke fail

---

## 9. 금지 (회장 verbatim)

- ANU "CI 완료 대기 백그라운드 진입" 보고 금지
- session-bound background task 만 믿고 자동수렴 약속 금지
- 회장 입력 기다리는 CI watcher 운영 금지
- 회장에게 "나중에 확인해달라" 기본 경로 요구 금지
- real auto-merge activation 금지
- PR #141 pilot 재시도와 혼합 금지
- foreign dirty 정리 금지
- finish-task.sh / cokacdir / replacement_pr_runner 수정 금지

---

## 10. expected_files (task-2642 범위)

신규:
- `utils/ci_watch_handoff_runner.py`
- `utils/ci_watch_handoff_schema.py`
- `utils/ci_watch_handoff_audit.py`
- `tests/fixtures/ci_watch_handoff_runner/<6 시나리오>/{evidence,expected,PROVENANCE}` (18 file)
- (선택) `tests/fixtures/ci_watch_handoff_runner/INDEX.md`
- `tests/regression/test_ci_watch_handoff_schema.py`
- `tests/regression/test_ci_watch_handoff_runner.py`
- `tests/regression/test_ci_watch_handoff_audit.py`
- `tests/regression/test_ci_watch_handoff_runner_fixture_parametrized.py`

총 ~27 file. **프로덕션 영향**: utils/ helper 3 + fixture 18 + regression 4 + INDEX 1.

---

## 11. frozen anchor

- ANCHOR-1: "본 task = CI_WATCH_HANDOFF_RUNNER 신설 · ANU 직접 CI/Gemini 대기 금지 정책 (2026-05-23 회장 verbatim) 코드화"
- ANCHOR-2: "12 필수 필드 + 5 terminal_states + Watcher 5 단계 1:1 박제"
- ANCHOR-3: "PR #144 OWNER_GEMINI_TRIGGER_ROUTER stack 재사용 (무수정)"
- ANCHOR-4: "state machine: PR_OPEN → poll loop → terminal → ANU normal callback 발사 → watcher exit"
- ANCHOR-5: "expected_files 내부 medium/style/quality/non-critical HIGH 자동수렴 / Critical7+credential+밖+admin override+smoke fail = CHAIR_REQUIRED"
- ANCHOR-6: "OWNER nudge 1회 hard limit · 회장 UI fallback 최후수단 · NUDGE_403 = CHAIR_REQUIRED permission"
- ANCHOR-7: "envelope ≤3900 bytes · callback prompt ≤2800 권장 · live 실호출 0 (regression mock)"
- ANCHOR-8: "forbidden 15종 + owner_trigger 4종 + owner_gemini_trigger_router 3종 무수정 · real auto-merge 0"
