# task-2719 보고서 — ci_gemini_watcher GET-only gh_runner dry-run 어댑터 (분리 파일)

- 팀: dev1-team (헤르메스)
- 작업일: 2026-05-31
- 워크트리: `/home/jay/workspace/.worktrees/task-2719-dev1` (브랜치 `task/task-2719-dev1`, base `4e080d58` = origin/main)
- 커밋: `216962de`(불칸 어댑터) → `af05d588`(아르고스 regression)

## SCQA 요약

**S (Situation)**: PR #164(`4e080d58`) 머지로 `run_watch_cycle`(주입형 gh_runner 기반 단일 멱등 cycle)이 main 에 반영됐으나, 실 PR 데이터를 gh op 5종으로 변환하는 read-only production 어댑터가 0건이라 실 PR one-shot dry-run 이 BLOCKED_BY_CAPABILITY 상태였다.

**C (Complication)**: runner 는 `actual_head/diff_paths/ci_rollup/reviews/findings` 5종 op 를 전부 주입형 gh_runner 에 위임하는데, 테스트용 fake `make_gh_runner` 만 존재하고 실 gh 호출 경계가 없어 실제 PR 번호로 1회 실행할 진입점이 없었다.

**Q (Question)**: runner 무수정·GitHub write 0·실 `/gemini review` 발사 0 을 보장하면서, 실 PR 데이터를 read-only 로 결선하는 어댑터를 신규 분리 파일로 구현할 수 있는가?

**A (Answer)**: GET-only gh_runner 어댑터(`ci_gemini_watcher_gh_adapter.py`)를 신규 분리 파일로 구현. op 5종을 read-only gh argv 로 매핑하고 write op/비-GET 메서드는 `GhWriteForbiddenError` hard guard 로 reject. `run_one_shot_dry_run`(dry_run=True 고정)이 어댑터로 gh_runner 를 구성→`run_watch_cycle` 호출. regression 13건 전부 PASS, L1 스모크에서 terminal enum 1개 + github_writes 0 + GET-only argv 입증. runner 무수정(diff 미포함) 확인. (총 약 410단어)

## 생성 파일 (정확히 2개 — expected_files 일치)
- `anu_v2/ci_gemini_watcher_gh_adapter.py` (신규, 불칸) — GET-only 어댑터
- `tests/regression/test_ci_gemini_watcher_gh_adapter_2719.py` (신규, 아르고스) — regression 13건
- ★ `anu_v2/ci_gemini_watcher_runner.py` **무수정** — `git diff --name-only origin/main` 에 미포함 확인.

## 회장 verbatim 조건 1~9 이행
1. runner 무수정 (import-only). ✅ diff 미포함 확인.
2. runner 순수 decision/cycle logic 유지 — 어댑터에서 `run_watch_cycle` import/call 만. ✅
3. 신규 adapter = GET-only gh_runner 경계로 한정. ✅
4. op 5종 read-only 매핑 ✅:
   - `actual_head` → `gh pr view <pr> --json headRefOid`
   - `diff_paths`  → `gh pr view <pr> --json files`
   - `ci_rollup`   → `gh pr view <pr> --json statusCheckRollup` (state 도출, remediable=False 고정)
   - `reviews`     → `gh api -X GET /repos/{o}/{r}/pulls/<pr>/reviews` (freshness checker 가 method=GET 으로 호출)
   - `findings`    → `gh api -X GET /repos/{o}/{r}/pulls/<pr>/comments`
5. owner_trigger = decision-only. 어댑터에 comment/trigger op 미구현 → 실 `/gemini review` 발사 구조적 불가. ✅ (L1: decision_json 채워지나 github_writes=0)
6. auto_gemini_triage = finding 전달/decision 검증까지만. ✅ (triage_batch spy 입증)
7. terminal enum + decision JSON 출력 검증. ✅ (L1 출력)
8. GitHub write 0 을 regression 으로 고정 — write/unknown op + 비-GET 메서드 reject guard. ✅ (시나리오 2a/2b/2c/10)
9. 성공해도 wired/active 승격 금지 — dry_run_one_shot✅ 까지만 기록. ✅ (wired❌ active❌ 유지)

## 테스트 결과
`python3 -m pytest tests/regression/test_ci_gemini_watcher_gh_adapter_2719.py -q` → **13 passed in 0.13s**

시나리오 매핑: op5 매핑 정확성(1) / write·unknown op reject(2a) / reviews method=POST reject(2b) / full cycle GET-only argv + github_writes 0(2c) / STALE+OWNER→ALLOW_OWNER_TRIGGER decision-only(3) / FRESH+CI SUCCESS→MERGE_READY_CANDIDATE(4) / head mismatch→HOLD_STALE_HEAD(5) / scope→HOLD_SCOPE_UNCLEAN(6) / CI fail→CI_FAILED_NON_REMEDIABLE(7) / validate_decision spy(8) / triage_batch spy(9) / default_gh_cli write guard(10) / READ_OPS 상수(11).

## L1 스모크테스트 결과 (필수 기록)
- **서버 재시작**: 해당없음 (subprocess/어댑터 결선 작업, 서버 무관)
- **API 응답 확인**: 해당없음 (네트워크 0 — GET-only 어댑터를 mock gh_cli 로 end-to-end 실행). production 코드 경로(`run_one_shot_dry_run` → `build_readonly_gh_runner` → `run_watch_cycle`) 실제 실행 결과:
  - terminal=`GEMINI_EXTERNAL_TRIGGER_REQUIRED`, trigger_decision=`ALLOW_OWNER_TRIGGER`
  - **github_writes=0**, dry_run=True, decision_json present
  - gh_cli 호출 argv 4건 전부 GET-only (`pr view ... --json`, `api -X GET .../reviews`), -X 비-GET 위반 0
  - write op `comment`/`merge`/`post_review` + reviews `method=POST` → 전부 `GhWriteForbiddenError` reject 확인
- **스크린샷**: 해당없음 (백엔드 CLI 결선, UI 없음)
- 판정: **L1 통과** (production 어댑터 경로 실제 실행 + terminal enum 산출 + write 0 + GET-only 입증)

## 발견 이슈 및 해결
### 자체 해결 (2건)
1. **불칸 어댑터의 미사용 모듈 상수 2건(`_FORBIDDEN_METHODS`, `_INCOMPLETE_STATUSES`)** — pyright 미사용 경고. 가드 로직이 "GET 외 전부 거부", state 도출이 "status != COMPLETED" 방식으로 더 견고하게 구현돼 있어 죽은 코드로 제거.
   - 상세: `anu_v2/ci_gemini_watcher_gh_adapter.py` (Edit 후 py_compile OK)
2. **worktree 표준 경로/lock 가드** — 초기 worktree 가 비표준 경로라 start_task_guard #1 실패. `worktree_manager.py create` 로 `/home/jay/workspace/.worktrees/task-2719-dev1` 표준 경로 재생성 + lock 등록 후 pre-commit guard PASS.

### 범위 외 미해결 (0건)
없음.

## 모델 사용 기록
- 불칸(백엔드, sonnet) — 어댑터 구현. 일반 로직 구현이라 sonnet.
- 아르고스(테스터, sonnet) — regression 13건. 테스트 설계라 sonnet.
- 헤르메스(팀장, Opus) — 설계/분배/통합/검토/L1 스모크. 직접 코딩은 죽은 상수 제거 2줄만(팀원 산출물 정리).

## 게이트 통과
- **G1 설계**: affected_files 2개 모두 신규 파일 → 다른 팀 겹침 0. runner 무수정 import-only. PASS.
- **G2 구현**: 팀 테스터(아르고스) 기능 테스트 13건 PASS + L1 스모크 PASS. write 0 입증. PASS.
- **G3 머지**: ★ 본 task 는 dispatch G3 자동 PR/머지 **미적용**(task 명시). finalize §4 = 로컬 commit only. push/PR/merge 금지. 워크트리 브랜치 보존 → ANU 독립 재검증 대기.

## 머지 판단
- **머지 필요**: No (회장 별도 승인 전까지 push/PR/merge 금지)
- **브랜치**: `task/task-2719-dev1`
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2719-dev1`
- **머지 의견**: 로컬 commit only 완료. expected_files 2개 한정, runner 무수정, regression 13 PASS, L1 PASS. ci_gemini_watcher 상태: implemented✅ verified✅ main_reflected✅ dry_run_one_shot✅ **wired❌ active❌**. push/PR 은 회장 승인 대상 — ANU 가 로컬 commit 독립 재검증 후 capability delta + PR 진입 가능 여부만 보고.

## 비고
- 실 PR one-shot dry-run 은 read-only 1회만 수행 가능하나(default_gh_cli subprocess gh GET-only), 자동 watcher/주기 실행 아님. 본 작업은 gh auth/실 PR 의존을 피해 mock gh_cli 로 동일 production 경로를 검증.
- Phase2/credential 0, ANU key literal 미노출.

## 세션 통계
- 총 도구 호출: 0회

