---
task_id: task-2510
type: plan
scope: task
created: 2026-05-08
updated: 2026-05-08
status: completed
---

# 계획서: task-2510 — replacement_pr_runner

**task**: task-2510 — contaminated PR 자동 replacement 생성 (5 모듈 #2)
**목표**: effective diff가 expected_files를 초과하거나 base 누적 commit이 섞인 오염 PR을 자동 감지하고, origin/main 기준 clean branch에서 expected_files만 이식하여 replacement PR을 자동 생성. 원 PR은 close/delete 없이 보존. 실패 시 Critical 보고.
**승인**: 회장 2026-05-08 직접 발행 (5 모듈 #2)
**근거**: `memory/tasks/task-2510.md`, task-2509+2 freeze (main `9cd28bf1`)

---

## 목표

`utils/replacement_pr_runner.py` + `tests/regression/test_replacement_pr_runner_2510.py` 2 파일.

측정 기준:
- expected_files = 정확히 2 파일
- 회귀 12건 PASS (회장 §1~12)
- CLI dry-run: `python3 utils/replacement_pr_runner.py --pr <N> --dry-run` → `ReplacementResult` JSON
- PR #54 / task-2506 / task-2507 fixture 재현 PASS
- forbidden path 0
- automation_contracts.py 기반 ReplacementResult 사용
- Critical 7종 외 회장 보고 0
- contaminated branch 재활용 / cherry-pick / rebase / force / admin override 0

## 범위

### 포함
- contaminated PR 감지 로직 (effective diff vs expected_files, forbidden, base 누적 commit, 다른 task 파일)
- origin/main 기준 clean replacement branch 생성 (`task/<task_id>-replacement-<ts>`)
- expected_files만 git show 기반으로 이식 (cherry-pick 금지)
- 원 PR 보존 + `[REPLACED]` 코멘트
- replacement PR 생성 + ReplacementResult 작성
- Critical escalation 분기 (FORBIDDEN_PATH_INTRUSION / REPLACEMENT_PR_AUTO_CREATION_FAILED_FOR_CONTAMINATED_DIFF / REPLACEMENT_PR_FAILED)
- CLI entrypoint (--pr, --dry-run)
- 회귀 테스트 12건 (PR #54 / task-2506 / task-2507 fixture 포함)

### 제외 (회장 명시 금지)
- dispatch.py wiring 금지
- merge_queue_executor 대규모 수정 금지 (helper import만)
- auto_gemini_triage 구현 금지 (task-2511)
- post_merge_smoke_runner 구현 금지 (task-2512)
- critical_escalation_reporter 구현 금지 (task-2513)
- 원 PR close/delete 금지
- 자동 cherry-pick 구현 금지 (회장 amendment)
- contaminated branch 재활용 금지

## 위임 계획

- Phase 1 (병렬): **루(Lugh, 백엔드)** — `utils/replacement_pr_runner.py` 본체 작성
- Phase 1 (병렬): **모리건(Morrigan, 테스터)** — `tests/regression/test_replacement_pr_runner_2510.py` 12 케이스 작성
- Phase 2: 팀장(다그다) — pytest/import smoke + L1 + 보고서 + Codex 재검증

## 검증 기준

- pytest 12건 PASS: `cd .worktrees/task-2510-dev3 && python3 -m pytest tests/regression/test_replacement_pr_runner_2510.py -v`
- import smoke: `python3 -c "from utils.replacement_pr_runner import ReplacementPRRunner, main"`
- automation_contracts import 가능성: `python3 -c "from utils.automation_contracts import ReplacementResult, CriticalEscalationType, EscalationPacket"`
- effective diff = 정확히 2 파일 (worktree git diff origin/main...HEAD --name-only)
- forbidden path 0
- CLI dry-run 동작: `python3 utils/replacement_pr_runner.py --pr 54 --dry-run`

## 설계 노트

### 핵심 알고리즘
1. PR metadata 수집 (`gh pr view --json`)
2. effective diff 산출 (`git diff origin/main...PR_HEAD --name-only` 또는 `gh pr diff --name-only`)
3. expected_files vs effective diff 비교 (compare_effective_diff)
4. clean → no-op (success=True, replacement_pr=None)
5. forbidden path 발견 → Critical FORBIDDEN_PATH_INTRUSION + ReplacementResult(success=False)
6. contaminated → 신규 branch `task/<task_id>-replacement-<ts>` from origin/main
7. expected_files 각 파일을 contaminated PR HEAD에서 `git show <head>:<path>` 로 추출 → clean branch에 write → commit
8. push → `gh pr create` → 원 PR `[REPLACED]` 코멘트
9. replacement PR effective diff 재검증 → expected_files와 정확 일치 안 하면 Critical REPLACEMENT_PR_FAILED
10. push/PR open 실패 → Critical REPLACEMENT_PR_AUTO_CREATION_FAILED_FOR_CONTAMINATED_DIFF

### 의존
- helpers는 `utils.merge_queue_executor`에서 import: `compare_effective_diff`, `detect_forbidden_paths`, `assert_no_forbidden_git_flags`, `_normalize_file_list`, `DEFAULT_FORBIDDEN_PATTERNS`
- 데이터 클래스/enum: `utils.automation_contracts`에서 import: `ReplacementResult`, `CriticalEscalationType`, `EscalationPacket`

### Subprocess runner 패턴
- merge_queue_executor와 동일하게 `RunnerType = Callable[..., subprocess.CompletedProcess]`
- 테스트에서 fake runner inject 가능
- 모든 `gh`/`git` 호출은 runner 통과
- `assert_no_forbidden_git_flags`로 force/rebase/admin/cherry-pick 정적 차단

### Fixture 재현 (회귀 §8, §9)
- PR #54: effective diff 78건 (task-2487+1/2503/2485+1/2488/2489/2493 + POC + scripts) — fake runner로 시뮬레이션
- task-2506: 117건 base 누적
- task-2507: 위와 유사 — fake runner inject로 재현
