# OWNER_GEMINI_TRIGGER_ROUTER spec (task-2641) — 260523

회장 결정(2026-05-23 task-2640 merge 직후): Gemini fresh evidence 가 필요한 경우 회장 UI 입력을 요구하기 전에 ANU 가 OWNER 권한으로 PR-backed issue comment `/gemini review` 를 자동 발사하도록 라우팅한다. **OWNER_GEMINI_TRIGGER_UI_FALLBACK_MISROUTE 재발 방지 + task-2640 사고 (회장 UI nudge → PR Review 빈 body 경로로 잘못 안내) 박제.**

기반: `audit_self_collector_enforcement_gap_260523.md` + `system_owner_trigger_only_capability_spec_260511.md` (이미 머지된 OWNER_TRIGGER_ONLY_CAPABILITY 13 원칙) + task-2640 PR #143 사고 (회장 review COMMENTED empty body 가 Gemini 트리거 0).

---

## 1. 본 task 범위

### 1.1 단일 batch (Track 구분 없음)
- 신규 모듈: `anu_v2/owner_gemini_trigger_router.py` (1 파일)
- 신규 helper: `anu_v2/gemini_evidence_freshness_checker.py` (PR HEAD SHA vs Gemini review commit_id 정합)
- 신규 audit log: `anu_v2/owner_gemini_trigger_router_audit.py` (jsonl · raw token redaction)
- 신규 fixture: 6 시나리오 (PR Review 오인식 / issue comment success / 403 permission / nudge 한도 / fresh review 도착 / stale 분류)
- 신규 regression: 4 (router 발사 / freshness checker / audit redaction / fixture parametrized)

### 1.2 기존 `anu_v2/owner_trigger_only.py` 와의 관계
- **owner_trigger_only**: 단일 capability (POST_GEMINI_REVIEW_TRIGGER_COMMENT) · /repos/{owner}/{repo}/issues/{pr}/comments · dedupe atomic
- **owner_gemini_trigger_router** (신규): **상위 계층 router**. 책임 분담:
  - PR Review comment vs PR Conversation issue comment 명시 구분 (회장 verbatim §1, §2, §3)
  - GEMINI_EVIDENCE_FRESH 판정 (current HEAD SHA vs Gemini review commit_id)
  - nudge 한도 enforce (PR/head 당 1회 hard limit) — `owner_trigger_only.validate_decision` 와 정합
  - 403 발생 시 `X-Accepted-GitHub-Permissions` header 기록
  - fresh review 미도착 timeout → GEMINI_EXTERNAL_TRIGGER_STALE 분류
  - **owner_trigger_only.invoke_from_scheduler** 호출만 사용 (token 직접 노출 0)

---

## 2. 회장 verbatim 12 필수 구현 (1:1 매핑)

### 2.1 PR Review comment ≠ Gemini trigger (회장 §1, §2)
- `is_gemini_trigger_comment(comment)` helper: 
  - comment.kind == "issue_comment" (NOT "review_comment")
  - comment.body.strip() == "/gemini review"
  - comment.author 가 owner allowlist 에 포함
- PR Review (kind="review_comment" or "review" with empty/non-trigger body) 는 nudge 성공 인정 0
- task-2640 회장 review id=4350214991 (state=COMMENTED · body 빈 문자열) 오인식 박제 → fixture 1

### 2.2 PR-backed issue comment body="/gemini review"만 trigger 인정 (회장 §3)
- 정확히 `/gemini review` (앞뒤 whitespace 허용 · 다른 텍스트 0)
- `gh api repos/{owner}/{repo}/issues/{pr_number}/comments` POST endpoint 만
- `owner_trigger_only.COMMENT_BODY = "/gemini review"` 단일 출처 재사용

### 2.3 ANU OWNER issue comment 발사 = 1차 nudge (회장 §4, §11)
- router 가 stale 감지 → 자동 OWNER nudge 발사 (회장 UI 요청 0)
- 발사 실패 (403 / 5xx / token 부재) 시에만 회장 UI 입력 요청 (최후 수단)
- 회장 UI 입력은 **fallback 경로** 만 (1차 경로 금지)

### 2.4 OWNER_GEMINI_TRIGGER_TOKEN / owner_trigger_only capability 사용 (회장 §5)
- `anu_v2/owner_trigger_only.TOKEN_ENV_NAME = "OWNER_GEMINI_TRIGGER_TOKEN"` 그대로 사용
- 다른 token fallback 0 (BOT_GITHUB_TOKEN 사용 금지)
- token boundary §11 정적 가드 유지

### 2.5 raw token 출력 금지 (회장 §6)
- audit log 에 `token_hash_prefix` (12 char hex) 만 기록
- exception traceback 에서 token redaction
- `_redact_diagnostics` 재사용 (`owner_trigger_only._redact_diagnostics`)

### 2.6 gh api / gh pr comment 중 안전한 경로 사용 (회장 §7)
- 본 router 는 `owner_trigger_only.invoke_from_scheduler` 호출 → 내부 http_post 사용
- `gh pr comment` CLI 직접 호출 금지 (외부 명령어 의존성 + redaction 우회 위험)
- `gh api repos/{owner}/{repo}/issues/{pr}/comments` REST POST 직접 호출 (owner_trigger_only 가 이미 단일 endpoint 가드)

### 2.7 403 발생 시 header 기록 (회장 §8)
- 신규 `_record_403_headers(response, audit)` helper
- 기록 대상 header:
  - `X-Accepted-GitHub-Permissions` (GitHub 권한 부족 진단)
  - `X-Accepted-OAuth-Scopes`
  - `X-OAuth-Scopes`
  - `X-RateLimit-Remaining`
- token / Authorization / api_key 값은 redact (raw 0)
- audit jsonl `permission_header_diagnostics` 필드 추가

### 2.8 nudge 횟수 제한 (회장 §9)
- PR/head 당 **1회 hard limit** (owner_trigger_only.validate_decision 의 `nudge_count_for_pr_head=0` 정합)
- transaction dedupe (audit JSONL + fcntl flock) 재사용
- 2회째 시도 = RESULT_DEDUPED (실제 POST 0)
- 24시간 cooldown (같은 PR + 다른 head) — 별도 추가 옵션 (회장 결정 시)

### 2.9 fresh review 미도착 → GEMINI_EXTERNAL_TRIGGER_STALE 분류 (회장 §10)
- 신규 enum `GEMINI_EXTERNAL_TRIGGER_STALE`
- 분류 조건:
  - OWNER nudge POSTED 성공
  - timeout (기본 600s · cron polling 시 별도 조정 가능)
  - 그 동안 PR current HEAD 에 대한 Gemini fresh review commit_id 0
- 분류 결과: audit JSONL state=stale + return 값으로 caller fail-closed
- 별도 보고 (회장 verbatim 회장 보고 트리거 포함 검토)

### 2.10 회장 UI 입력 요청 = 최후 수단 (회장 §11)
- router state machine:
  - state=FRESH → 통과 (action 0)
  - state=STALE + nudge_remaining > 0 → OWNER nudge 발사
  - state=STALE + nudge_remaining == 0 (POSTED 후 timeout) → GEMINI_EXTERNAL_TRIGGER_STALE
  - state=NUDGE_FAILED (403 / token 부재) → **CHAIR_UI_FALLBACK_REQUIRED** (최후 수단)
- CHAIR_UI_FALLBACK_REQUIRED 분류 → ANU 가 회장 UI 입력 요청 (PR Conversation 탭 + `/gemini review` 정확 안내)
- task-2640 사고 박제: router 가 1차 OWNER nudge 시도 0 + 바로 회장 UI 요청 → MISROUTE

### 2.11 OWNER_GEMINI_TRIGGER_UI_FALLBACK_MISROUTE 재발 방지 fixture (회장 §12)
- 6 fixture × 3 (evidence + expected + PROVENANCE):
  - `pr_review_empty_body_misroute_block` → MISROUTE 차단 + return NOT_GEMINI_TRIGGER
  - `issue_comment_exact_body_trigger_success` → POSTED + Gemini fresh review 도착 시뮬
  - `permission_403_diagnostics_record` → 403 + X-Accepted-GitHub-Permissions 기록 + CHAIR_UI_FALLBACK_REQUIRED
  - `nudge_limit_exceeded_dedupe` → 2회째 시도 DEDUPED + 실제 POST 0
  - `fresh_review_arrives_within_timeout` → state=FRESH 회수
  - `stale_after_nudge_timeout_classify` → GEMINI_EXTERNAL_TRIGGER_STALE

---

## 3. 신규 helper / 검증자

### 3.1 `anu_v2/gemini_evidence_freshness_checker.py`
- `check_gemini_evidence_fresh(pr_number, current_head_sha, github_api) -> FreshnessResult`
- 책임:
  - PR head SHA 실측 (`gh api repos/{owner}/{repo}/pulls/{pr}`)
  - Gemini review commit_id 회수 (gemini-code-assist[bot] author 필터)
  - SHA 일치 검증
- 분류: FRESH / STALE / NO_REVIEW
- task-2640 사고 박제: HEAD 변경 후 stale 자동 감지

### 3.2 `anu_v2/owner_gemini_trigger_router.py`
- `class OwnerGeminiTriggerRouter`:
  - `__init__(token_provider, http_post, audit, freshness_checker, trigger_runner)`
  - `route_for_pr(pr_number, current_head_sha, owner, repo) -> RouterResult`
- 책임 (state machine):
  1. check freshness
  2. STALE → nudge 발사 (owner_trigger_only.invoke_from_scheduler)
  3. nudge 결과 분류 (POSTED / DEDUPED / FAILED / NUDGE_FAILED_403)
  4. 403 → header diagnostics 기록 → CHAIR_UI_FALLBACK_REQUIRED
  5. POSTED → polling (timeout 까지) → FRESH 도착 시 통과 / timeout 시 STALE
- return RouterResult enum: FRESH / NUDGE_POSTED / NUDGE_DEDUPED / GEMINI_EXTERNAL_TRIGGER_STALE / CHAIR_UI_FALLBACK_REQUIRED / NUDGE_PERMISSION_DENIED

### 3.3 `anu_v2/owner_gemini_trigger_router_audit.py`
- audit JSONL schema v1:
  - `schema`: "anu_v2.owner_gemini_trigger_router_audit.v1"
  - `ts_utc`, `pr_number`, `current_head_sha`
  - `freshness_state`, `gemini_commit_id_observed`
  - `nudge_attempted`, `nudge_result`, `permission_header_diagnostics` (null or dict)
  - `token_hash_prefix` (12 hex, raw 0)
  - `final_state`
- redaction:
  - token value never logged (hash prefix만)
  - response body 에서 Authorization / api_key 패턴 redact

---

## 4. 회장 verbatim 자동수렴 / 금지 / 병렬 (verbatim)

### 4.1 자동수렴
- expected_files 내부 Gemini medium/style/quality/non-critical HIGH → 자동수렴
- Critical7 / credential expansion / expected_files 밖 / admin override / post-merge smoke fail → 회장 보고

### 4.2 금지
- 회장 UI 입력 1차 경로 안내 금지 (CHAIR_UI_FALLBACK_REQUIRED 만 fallback)
- PR Review comment nudge 성공 오인 금지
- 무한 nudge 금지 (1회 hard limit)
- raw token 출력 금지 (token_hash_prefix 만)
- real auto-merge activation 금지
- PR #141 pilot 재시도와 혼합 금지
- foreign dirty 정리 금지
- finish-task.sh 수정 금지
- cokacdir 본체 수정 금지
- replacement_pr_runner 수정 금지

### 4.3 병렬 허용 (회장 §명시)
- Track A: 본 task-2641 OWNER_GEMINI_TRIGGER_ROUTER 구현
- Track B: PR #141 B1 pilot 재시도 준비안 read-only 업데이트 (실행/chair_authorization 발급 0 · task-2641 merge 후 별도 승인까지 금지)
- Track C: CI_WATCHER_SESSION_LIFETIME_GAP backlog 정리만 (코드 구현 0)

---

## 5. 안전 불변식

- ANU key `c119085addb0f8b7` 단일 출처 유지 (변경 0)
- OWNER_GEMINI_TRIGGER_TOKEN 단일 출처 (BOT_GITHUB_TOKEN 사용 0)
- envelope UTF-8 ≤3900 bytes 유지
- live cokacdir CLI 실호출 0 (regression mock)
- merge/push/PR/admin override 호출 0
- 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종 무수정 (task_2640 merge 직후 stack 그대로 유지)
- 신규 4 종 무수정 추가: `anu_v2/owner_trigger_only.py` · `anu_v2/owner_trigger_decision.py` · `anu_v2/owner_trigger_audit.py` · `anu_v2/owner_trigger_pat.py` (기존 OWNER_TRIGGER_ONLY_CAPABILITY stack 보호 — 본 task 는 상위 layer 신규만)

---

## 6. expected_files (task-2641 범위)

신규:
- `anu_v2/owner_gemini_trigger_router.py`
- `anu_v2/gemini_evidence_freshness_checker.py`
- `anu_v2/owner_gemini_trigger_router_audit.py`
- `tests/fixtures/owner_gemini_trigger_router/<6 시나리오>/{evidence,expected,PROVENANCE}` (18 files)
- (선택) `tests/fixtures/owner_gemini_trigger_router/INDEX.md`
- `tests/regression/test_owner_gemini_trigger_router.py`
- `tests/regression/test_gemini_evidence_freshness_checker.py`
- `tests/regression/test_owner_gemini_trigger_router_audit.py`
- `tests/regression/test_owner_gemini_trigger_router_fixture_parametrized.py`

총 ~28 files. **프로덕션 영향**: anu_v2/ 상위 layer 신규 helper 3 + fixture 18 + regression 4 + (선택) INDEX 1.

---

## 7. finalize 프로토콜 (★ BOT App token 부재 — 로컬 한정)

1. base = origin/main 033dcf8a clean
2. 신규 helper 3 + fixture 18 + regression 4 PASS · 기존 1440+ 유지 (task-2640 merge 후 baseline)
3. **로컬 commit만** (push/PR/merge 금지)
4. ANU normal callback — 본 task 자체가 OWNER_TRIGGER router 이므로 자기검증 강제:
   - 신규 `validate_spawn_callback_contract` 호출로 self-check (task-2640 결선 활용)
   - envelope 5축 + canonical_root 명시
   - REGISTERED + schedule_id non-null + DELIVERED + UNCONFIRMED
5. envelope UTF-8 ≤3900 bytes
6. executor 시작/종료 ts + 로컬 commit SHA 명기

---

## 8. frozen anchor

- ANCHOR-1: "본 task = OWNER_GEMINI_TRIGGER_UI_FALLBACK_MISROUTE 재발 방지 router 신설 · 회장 UI 1차 경로 안내 금지"
- ANCHOR-2: "회장 verbatim 12 필수 구현 1:1 박제 (PR Review vs issue comment 구분 + body 정확 일치 + OWNER nudge 1차 + token 단일 출처 + redaction + endpoint 단일 + 403 header 기록 + nudge 1회 hard limit + STALE 분류 + UI fallback 최후수단 + 재발 방지 fixture)"
- ANCHOR-3: "기존 owner_trigger_only capability stack (PR #98 merge 후 유지) 재사용 · 신규 4종 무수정 (owner_trigger_only/decision/audit/pat) · 본 task 는 상위 layer router 신규만"
- ANCHOR-4: "Gemini evidence freshness = PR current HEAD SHA == Gemini review commit_id · STALE 자동 감지 → 1차 OWNER nudge → fail 시 CHAIR_UI_FALLBACK_REQUIRED"
- ANCHOR-5: "nudge 1회 hard limit per PR/head · dedupe atomic (audit JSONL + fcntl flock) · validate_decision nudge_count_for_pr_head=0 정합"
- ANCHOR-6: "403 발생 시 X-Accepted-GitHub-Permissions / X-Accepted-OAuth-Scopes / X-OAuth-Scopes / X-RateLimit-Remaining 기록 · raw token redact"
- ANCHOR-7: "병렬 Track B (PR #141 B1 pilot 재시도 준비안 read-only) + Track C (CI_WATCHER backlog 정리) 허용 · Track B pilot 실행 / chair_authorization 발급 task-2641 merge 후 별도 승인까지 금지"
- ANCHOR-8: "real auto-merge activation 0 · ACTIVATION_FLAG_DEFAULT=False · admin override 0 · PR #141 pilot 재시도 혼합 0 · finish-task.sh / cokacdir / replacement_pr_runner 무수정"
