# task-1968: Gemini PR 리뷰 피드백이 자동 적용/강제되지 않는 원인 심층 분석

## SCQA

**S**: worktree_manager.py의 `finish --action pr` 플로우에 Gemini PR 리뷰 자동화가 구현되어 있다. PR 생성 → Gemini 리뷰 대기(5분) → 코멘트 파싱 → HIGH 0건이면 자동 머지, HIGH 발견 시 차단하는 로직이 존재한다.

**C**: task-1964에서 InsuRo PR #1에 대해 Gemini가 security-critical 1건 + HIGH 4건 + MEDIUM 3건을 지적했으나, 팀장(헤르메스)이 "Gemini PR 리뷰: PASS (High 0건, Low/Medium 8건)"으로 보고하고 PR을 그대로 머지했다. 시스템이 이를 차단하지 못했다.

**Q**: 왜 시스템이 Gemini의 CRITICAL/HIGH 리뷰를 자동 감지하여 머지를 차단하지 못했는가?

**A**: 5가지 근본 원인을 발견. (1) 파싱 자체는 정상이나 `collect_mode=True`가 기본값이라 자동 수정이 비활성, (2) 팀장이 `worktree_manager.py finish`를 사용하지 않고 직접 `gh pr merge`를 실행한 것으로 추정, (3) QC/G3 파이프라인에 Gemini 리뷰 교차검증 체크 부재, (4) 팀장의 severity 재분류를 검증하는 메커니즘 없음, (5) 타임라인상 Gemini 리뷰(11:44:28) → 머지(11:44:45) 17초 간격으로 자동 수정 루프가 실행될 시간 자체가 없었음.

---

## 1. 근본 원인 분석 (Root Cause Analysis)

### 1차 원인: 파싱 로직은 정상이나 실제 실행 경로에서 우회됨

`_parse_gemini_comments()` (worktree_manager.py:423-472)의 severity 감지 로직:
```python
if (
    "severity: high" in body_text.lower()
    or "severity: critical" in body_text.lower()
    or "🔴" in body_text
    or "HIGH" in body_text      # ← 이것이 매칭됨
    or "CRITICAL" in body_text  # ← 이것이 매칭됨
):
    severity = "high"
```

실제 Gemini 코멘트 트레이싱:
- `![security-critical](...) ![critical](...)` → body에 "critical" 문자열 포함 → **파서: "high"로 정상 감지**
- `![high](...)` → body에 "high" 문자열 포함하지 않음 (URL 내 "high-priority"만 존재)
  - **BUT**: "HIGH"는 대소문자 구분 없이 body_text에 "high" 포함 여부로 체크하지 않고, body_text 원문에 대문자 "HIGH"가 있는지 체크
  - Gemini 코멘트의 alt text는 `![high]`이고 URL에는 `high-priority`가 있으나, 본문 텍스트에 대문자 "HIGH"는 없음
  - **결론: img alt text 내의 소문자 "high"는 감지하지만, `"HIGH" in body_text`도 매칭됨 (마크다운 `![high]`에 소문자 "high" 포함)**

**파싱 정확도 검증**: `"HIGH" in body_text` → `body_text`에 `"![high]"` 포함 → `"HIGH" in body_text`는 **False** (대소문자 구분). 그러나 `"severity: high" in body_text.lower()` → body 전체를 lower()한 뒤 검색이므로 "high"가 있으면 매칭.

- 실제 body에 "high"가 소문자로 존재 (`![high]`) → `body_text.lower()`에 "high" 포함 → `"severity: high"` 검색 시 **실패** (정확한 문자열은 "severity: high"인데 body에는 "severity: high"이 아닌 `![high]`만 있음)
- `"HIGH" in body_text` → body에 대문자 "HIGH" 없음 → **실패**
- `"🔴" in body_text` → 없음 → **실패**

**핵심 파싱 결함 발견**: Gemini의 HIGH severity 코멘트는 `![high](https://www.gstatic.com/codereviewagent/high-priority.svg)` 형태의 마크다운 이미지로 표현된다. 파서는 `"severity: high"` 문자열이나 대문자 `"HIGH"`, `"🔴"` 이모지만 감지한다. **Gemini의 실제 HIGH 포맷(`![high](...)`)과 파서의 기대 포맷이 불일치하여 HIGH가 low로 분류된다.**

- security-critical 코멘트: `![security-critical](...)` → `"CRITICAL" in body_text` 검색 → body에 "critical" 소문자만 있고 대문자 "CRITICAL"은 없음 → **실패**. `"severity: critical" in body_text.lower()` → body에 "severity: critical" 문자열 없음 → **실패**.

**결론**: _parse_gemini_comments()는 Gemini의 실제 코멘트 포맷을 올바르게 파싱하지 못한다. 모든 HIGH/CRITICAL이 "low"로 분류된다.

### 2차 원인: collect_mode=True 기본값으로 자동 수정 비활성

`_auto_fix_high_comments()` (worktree_manager.py:475-524):
```python
if collect_mode:
    return {"prompts": prompts, "executed": False, "diff_stat": ""}
```
- `collect_mode=True`(기본값)이면 HIGH를 감지해도 프롬프트만 생성하고 실제 수정/push를 하지 않음
- 주석에 "Week 2 이후 활성화"라고 되어 있으나 현재까지 활성화되지 않은 상태

### 3차 원인: 팀장의 수동 머지 경로 존재

DIRECT-WORKFLOW.md의 Step 4.7에서:
```
4. 각 코멘트 판정:
   - 수용(Accept): High severity 실제 버그/보안 → 코드 수정 후 재push
   - 기각(Dismiss): High severity but 의도적 설계 → PR에 기각 사유 코멘트
   - 보류(Defer): 판단 불가 → 보고서에 기록, 아누 판단 요청
   - Medium/Low → 참고만 (PASS에 영향 없음)
5. PASS 기준: 미수정 High 0건 확인 → merge
```
**팀장에게 severity 판단 재량권이 있다.** 팀장이 "High 0건"으로 판정하면 시스템이 이를 교차검증하지 않는다.

### 4차 원인: QC/G3 파이프라인에 Gemini 교차검증 부재

- `g3_independent_verifier.py`: Gemini 관련 코드 **0줄**. 보고서 내 파일 존재 여부만 검증.
- `qc_verify.py`: Gemini 관련 코드 **0줄**. 16개 verifier 모듈 중 Gemini 리뷰 검증 모듈 없음.
- `QC-RULES.md`: "Gemini" 언급 **0건**. Gemini 리뷰 관련 QC 규칙 없음.

### 5차 원인 (근본): 타임라인 상 자동 수정 루프 실행 불가

PR #1 실제 타임라인:
- Gemini 리뷰 제출: `2026-04-19T11:44:28Z`
- PR 머지: `2026-04-19T11:44:45Z`
- **간격: 17초**

worktree_manager.py의 자동 수정 루프는 30초 간격으로 폴링하므로 (`time.sleep(30)`), 17초 간격에서는 Gemini 리뷰를 감지하기 전에 이미 머지가 완료된 것으로 추정된다.

**가능한 시나리오**: 팀장이 `worktree_manager.py finish --action pr`을 사용했더라도, Gemini 리뷰 대기 중 타임아웃이 발생하기 전에 별도 경로(수동 `gh pr merge` 또는 GitHub UI)로 머지했을 가능성.

---

## 2. 현재 시스템의 Gemini 리뷰 처리 흐름도

```
[팀장] worktree finish --action pr
  │
  ├─ 1. PR 생성 (gh pr create)
  │
  ├─ 2. Gemini 리뷰 대기 (30초 간격 폴링, 최대 300초)
  │     └─ 타임아웃 시 → gemini_verdict="TIMEOUT" → 머지 진행 ← ★ 허점 A
  │
  ├─ 3. _parse_gemini_comments() 실행
  │     ├─ Gemini 유저 코멘트 필터링 (OK)
  │     ├─ severity 감지:
  │     │   ├─ "severity: high" in body.lower() → Gemini 포맷 불일치 → 실패 ← ★ 허점 B
  │     │   ├─ "CRITICAL" in body → 대소문자 불일치 → 실패 ← ★ 허점 B
  │     │   ├─ "HIGH" in body → 대소문자 불일치 → 실패 ← ★ 허점 B
  │     │   └─ 기본값 "low" 반환 ← ★ false negative
  │     └─ 결과: 모든 코멘트가 severity="low"로 파싱됨
  │
  ├─ 4. high_severity_count = 0 (파싱 실패로 인한 거짓 0)
  │
  ├─ 5. gemini_verdict = "PASS" (거짓 PASS) ← ★ 허점 C
  │
  └─ 6. 자동 머지 실행 (gh pr merge --merge --delete-branch)
       └─ HIGH 이슈가 있음에도 머지됨 ← ★ 최종 실패 지점
```

**병렬 경로 (팀장 수동 머지)**:
```
[팀장] Gemini 코멘트 직접 읽기
  │
  ├─ 자의적으로 severity 재분류 (CRITICAL/HIGH → "기존 코드 개선 제안")
  │   └─ 시스템 교차검증 없음 ← ★ 허점 D
  │
  ├─ 보고서에 "High 0건" 기록
  │   └─ qc_verify.py: Gemini 검증 체크 없음 ← ★ 허점 E
  │
  └─ gh pr merge 수동 실행 → 머지 완료
```

---

## 3. 허점 목록 (exploit 가능한 경로)

### 허점 A: Gemini 타임아웃 시 무조건 머지
- **위치**: worktree_manager.py:911, 918
- **내용**: `gemini_found=False` → `gemini_verdict="TIMEOUT"` → 그래도 `high_severity_count == 0`이므로 머지 진행
- **악용**: Gemini 서비스가 느리거나 장애일 때 모든 PR이 리뷰 없이 머지됨

### 허점 B: 파서의 severity 감지 패턴이 Gemini 실제 포맷과 불일치
- **위치**: worktree_manager.py:449-455
- **내용**: Gemini는 `![high](URL)` / `![critical](URL)` 마크다운 이미지 alt text로 severity 표시. 파서는 `"severity: high"` 문자열 또는 대문자 `"HIGH"`만 감지.
- **결과**: 모든 HIGH/CRITICAL이 "low"로 분류 → false negative 100%
- **심각도**: 이 허점 하나만으로 Gemini 리뷰 전체가 무력화됨

### 허점 C: 파싱 실패 시 기본값이 "low"
- **위치**: worktree_manager.py:448 (`severity = "low"`)
- **내용**: severity 감지 조건 전부 실패 시 기본값 "low" → High 카운트에 포함 안 됨
- **영향**: 파싱 실패 = 자동 PASS

### 허점 D: 팀장의 severity 재분류를 검증하는 메커니즘 없음
- **위치**: DIRECT-WORKFLOW.md Step 4.7 항목 4
- **내용**: "기각(Dismiss): High severity but 의도적 설계 → PR에 기각 사유 코멘트"라는 규정이 있으나, 기각 사유 코멘트 존재 여부를 시스템이 검증하지 않음
- **악용**: 팀장이 모든 HIGH를 "의도적 설계"로 기각하고 사유 코멘트 없이 머지 가능

### 허점 E: QC/G3에 Gemini 교차검증 체크 부재
- **위치**: qc_verify.py, g3_independent_verifier.py
- **내용**: 16개 QC verifier 모듈 중 Gemini 리뷰 결과를 교차검증하는 모듈 없음
- **영향**: 팀장이 "High 0건"이라고 보고하면 시스템이 그대로 수용

### 허점 F: collect_mode=True 기본값으로 자동 수정 비활성
- **위치**: worktree_manager.py:646, 882
- **내용**: HIGH 감지해도 프롬프트만 생성하고 실제 수정/push 안 함
- **영향**: 자동 수정 루프가 사실상 비활성 상태

---

## 4. 실제 사례 분석: task-1964에서 "High 0건" 판정 경위

### Gemini 실제 코멘트 (InsuRo PR #1)

| # | 파일 | Gemini severity | 파서 감지 결과 | 내용 요약 |
|---|------|----------------|---------------|----------|
| 1 | server/main.py:519 | security-critical + critical | **low** (파싱 실패) | API 키가 request body에 노출 |
| 2 | server/anu_provider.py:107 | high | **low** (파싱 실패) | subprocess.run이 async endpoint에서 블로킹 |
| 3 | server/main.py:67 | high | **low** (파싱 실패) | 인메모리 저장소 다중 워커 미지원 |
| 4 | server/main.py:753 | high | **low** (파싱 실패) | 플랜 필터링 누락으로 잘못된 잔여석 |
| 5 | server/pipeline.py:24 | high | **low** (파싱 실패) | 파이프라인 상태 인메모리 관리 |
| 6 | server/main.py:535 | medium | **low** (파싱 실패) | AsyncClient 매번 생성 비효율 |
| 7 | server/main.py:712 | medium | **low** (파싱 실패) | Python 집계 대신 DB 집계 권장 |
| 8 | server/main.py:1196 | medium | **low** (파싱 실패) | N+1 쿼리 문제 |

**파싱 실패 원인**: Gemini 코멘트에 `![high](https://www.gstatic.com/codereviewagent/high-priority.svg)` 형태의 마크다운 이미지 태그가 있으나, `_parse_gemini_comments()`는 이 포맷을 인식하지 못함:
- `"severity: high" in body_text.lower()` → "severity: high" 문자열은 body에 없음 → **실패**
- `"HIGH" in body_text` → body에 대문자 "HIGH" 없음 (소문자 "high"만 img alt에 존재) → **실패**
- `"CRITICAL" in body_text` → body에 대문자 "CRITICAL" 없음 → **실패**
- `"🔴" in body_text` → 이모지 없음 → **실패**

### 팀장 보고서의 "High 0건" 판정

task-1964 보고서(task-1964.md:33):
> **Gemini PR 리뷰**: PASS (High 0건, Low/Medium 8건 — 모두 기존 코드 개선 제안)

두 가지 가능성:
1. **worktree_manager.py의 파싱 결과를 그대로 인용**: 파서가 모두 "low"로 분류했으므로 "High 0건"이 시스템 출력과 일치
2. **팀장이 직접 판단하여 재분류**: CRITICAL/HIGH를 "기존 코드 개선 제안"으로 재분류

어느 경우든 **시스템이 교차검증하지 않았기 때문에** 잘못된 판정이 최종 결과로 확정됨.

### 타임라인 증거

- PR 머지 시각: `2026-04-19T11:44:45Z`
- Gemini 리뷰 제출 시각: `2026-04-19T11:44:28Z`
- **간격: 17초** → 자동화 루프(30초 폴링)가 Gemini 리뷰를 감지하기도 전에 머지 실행

---

## 5. 개선 방안 제안

### 단기 (즉시 적용 가능 — 규칙/설정 변경)

**S1. 파서 패턴 업데이트** (worktree_manager.py:449-455)
- Gemini의 실제 severity 포맷 추가 감지:
  - `![security-critical]` → high
  - `![critical]` → high
  - `![high]` 또는 `high-priority` → high
  - `![medium]` 또는 `medium-priority` → medium
- 정규식으로 `!\[(security-critical|critical|high)\]` 패턴 매칭 권장

**S2. DIRECT-WORKFLOW에 Gemini 기각 사유 필수화 규칙 강화**
- "기각(Dismiss) 시 PR 코멘트에 기각 사유를 반드시 남겨야 하며, 보고서에 기각한 코멘트 목록과 사유를 명시" 조항 추가
- 기각 사유 없는 HIGH 기각은 QC FAIL 처리

**S3. collect_mode=False로 전환 검토**
- `_auto_fix_high_comments()`와 `_classify_medium_comments()`의 collect_mode 기본값을 False로 변경하여 자동 수정 루프 활성화
- 혹은 worktree_manager.py finish 호출 시 `--collect-mode false` 옵션 사용 권장

### 중기 (코드 수정 필요)

**M1. Gemini 리뷰 교차검증 QC verifier 신설**
- `verifiers/gemini_review_check.py` 신설
- 보고서의 "Gemini PR 리뷰" 섹션 파싱 → 실제 GitHub PR 코멘트의 severity와 교차검증
- 보고서에 "High 0건"이라고 되어 있는데 실제 Gemini 코멘트에 HIGH가 있으면 QC FAIL

**M2. Gemini 타임아웃 시 머지 차단**
- 현재: 타임아웃 → 머지 진행
- 개선: 타임아웃 → `merge_status = "pending"` → 팀장에게 수동 확인 요청
- Gemini 장애 대비 우회는 아누(개발실장) 승인 시에만 허용

**M3. finish 함수에 머지 잠금**
- `finish --action pr` 실행 중에는 해당 PR의 수동 머지를 방지
- GitHub branch protection rule 활용: required_reviews 설정으로 Gemini 리뷰 완료 전 머지 차단

**M4. g3_independent_verifier에 Gemini 검증 추가**
- PR 번호가 보고서에 있으면, 실제 PR 코멘트를 조회하여 severity 교차검증

### 장기 (아키텍처 변경)

**L1. GitHub Branch Protection + Required Status Check**
- Gemini 리뷰를 GitHub Required Status Check으로 등록
- HIGH/CRITICAL 미해결 시 GitHub 레벨에서 머지 차단 (시스템 우회 불가)

**L2. Gemini 리뷰 결과 영속화**
- PR 리뷰 결과를 `memory/events/{task_id}.gemini-review.json`에 저장
- 파싱 결과, 원문, severity, 팀장 판정, 기각 사유를 모두 기록
- QC/G3에서 이 파일을 읽어 교차검증

**L3. 3단계 방어 체계 구축**
1. **자동 방어**: worktree_manager.py 파서 + 자동 수정 루프 (허점 B, F 해소)
2. **QC 방어**: gemini_review_check verifier (허점 E 해소)
3. **인프라 방어**: GitHub Branch Protection (허점 A, D 해소)

---

## 6. 검증 시나리오 트레이싱 결과

### 시나리오 1: _parse_gemini_comments() 파싱 정확도

실제 Gemini 코멘트 `![high](https://www.gstatic.com/codereviewagent/high-priority.svg)` 입력 시:
- `body_text = "![high](https://www.gstatic.com/codereviewagent/high-priority.svg)\n\nsubprocess.run은 동기식..."`
- `"severity: high" in body_text.lower()` → `"severity: high"` 문자열 없음 → **False**
- `"severity: critical" in body_text.lower()` → **False**
- `"🔴" in body_text` → **False**
- `"HIGH" in body_text` → body에 대문자 "HIGH" 없음 → **False**
- `"CRITICAL" in body_text` → **False**
- **결과: severity = "low" (false negative)**

### 시나리오 2: finish() HIGH 감지 시 차단 여부

코드 흐름 (worktree_manager.py:865-933):
```
parsed = _parse_gemini_comments(...)  → 모든 코멘트 severity="low"
high_comments = [c for c in parsed if c["severity"] == "high"]  → 빈 리스트
high_severity_count = 0  → 거짓 0
gemini_verdict = "PASS"  → 거짓 PASS
high_severity_count == 0 → True → gh pr merge 실행 → 머지됨
```
**차단 실패**: 파싱 단계에서 이미 실패했으므로 차단 로직에 도달하지 못함.

### 시나리오 3: task-1964 "High 0건" 판정 지점 특정

**정확한 실패 지점**: worktree_manager.py:449-455의 severity 감지 조건문.
Gemini의 `![high](...)` / `![critical](...)` 마크다운 이미지 포맷이 파서의 어떤 패턴과도 매칭되지 않아 기본값 "low"로 분류.

팀장이 시스템 출력("High 0건")을 보고서에 그대로 옮겼거나, 직접 판단하여 동일한 결론에 도달한 것으로 추정. 어느 경우든 시스템 교차검증이 없어 오판이 최종 결과로 확정.

---

## 요약

| 분류 | 허점 | 심각도 | 현재 상태 |
|------|------|--------|----------|
| 파싱 | Gemini 실제 포맷과 파서 패턴 불일치 (허점 B) | CRITICAL | 모든 HIGH/CRITICAL이 low로 분류 |
| 파싱 | 기본값 "low"로 false negative (허점 C) | HIGH | 파싱 실패 = 자동 PASS |
| 자동화 | collect_mode=True로 자동 수정 비활성 (허점 F) | HIGH | 프롬프트만 생성, 실행 안 함 |
| 자동화 | 타임아웃 시 리뷰 없이 머지 (허점 A) | HIGH | Gemini 장애 시 전부 통과 |
| 거버넌스 | 팀장 severity 재분류 무검증 (허점 D) | HIGH | 기각 사유 없이 우회 가능 |
| 거버넌스 | QC/G3에 Gemini 교차검증 없음 (허점 E) | CRITICAL | 보고서 거짓말 방지 불가 |

**가장 시급한 조치**: 허점 B(파서 패턴 수정)와 허점 E(QC Gemini 교차검증 verifier 신설). 이 두 가지만 해결해도 task-1964와 같은 사례의 재발을 방지할 수 있다.
