# task-2784 — P1 WORKSPACE fallback 제거 (safe repo_dir invariant, ACTIVE=false)

## 레벨
Lv3 (transplant/replacement write 경로 safe repo_dir invariant + 회귀). 코드 작업. **코드+테스트+worktree 커밋+ANU 보고까지. PR 생성 금지(ANU가 별도 승인 후 생성).**

## allowed_resources

```yaml
allowed_resources:
  paths:
    - "utils/replacement_pr_runner.py"
    - "tests/regression/test_replacement_pr_runner_2510.py"
    - "memory/reports/task-2784.md"
  forbidden_paths:
    - "utils/merge_queue_executor.py"
    - "utils/real_merge_hooks.py"
    - "utils/repository_policy_adapter.py"
    - "utils/automation_contracts.py"
    - "scripts/finish-task.sh"
    - "scripts/harness/v36/terminal_state_callback.py"
    - "scripts/gemini_evidence_verify.py"
    - "dispatch.py"
    - ".git/hooks/**"
    - ".github/**"
    - ".claude/**"
    - ".env*"
    - "memory/state/**"
  commands:
    - "git"
    - "pytest"
    - "python3"
  merge_policy: "none"
  ttl_hours: 8
```

## 배경 (단일 소스)
설계 v2: `memory/reports/p1_workspace_fallback_design_packet_v2_260702.md` + 회장/GPT 승인 보완. PR#262(e9e5b34d)는 test 격리+guard만 반영(partial). `cwd = repo_dir or str(WORKSPACE)`(replacement_pr_runner.py 185/231/250/290/316)로 automation/real-merge 경로가 canonical WORKSPACE에 write 가능한 구조위험이 **LIVE 재발**한 바 있음(P1 실증 근거·root-cause confirmed 아님).

## 목표 (한 줄)
replacement write/mutate 경로가 **canonical WORKSPACE에 직접 write 못 하게** safe repo_dir invariant를 도입한다. **코드+테스트까지만. merge_queue_executor.py 무접촉(옵션 b).**

## 핵심 구현 요구 (GPT verbatim)
**1. `_resolve_safe_repo_dir(repo_dir, *, allow_workspace=False)` helper 도입**
- `repo_dir is None` → **`MISSING_REPO_DIR` RuntimeError**(fail-closed).
- `Path(repo_dir).resolve() == WORKSPACE.resolve()` (allow_workspace=False면) → **`WORKSPACE_WRITE_FORBIDDEN`**.
- ★ **git top-level 기준 판정(핵심)**: `git -C <repo_dir> rev-parse --show-toplevel` 결과가 canonical WORKSPACE와 같으면(allow_workspace=False) → **`WORKSPACE_WRITE_FORBIDDEN`**.
  - repo_dir가 **canonical 하위 일반 디렉터리**(예: `/home/jay/workspace/some_subdir`)라 git top-level이 WORKSPACE면 → **fail**.
  - repo_dir가 **별도 git worktree**(예: `/home/jay/workspace/.worktrees/task-xxx`)라 git top-level이 그 worktree root면 → **허용**(경로가 WORKSPACE 하위여도).
  - git rev-parse 실패/비-git dir 처리는 보수적으로(설계 판단; canonical write만 확실히 차단, 정상 tmp sandbox는 허용).
- ★ **`WORKSPACE.resolve() in Path(repo_dir).resolve().parents` 같은 단순 parent 차단 금지**(표준 worktree가 WORKSPACE 하위에 있을 수 있음).
- `allow_workspace=True`는 **CLI 수동 모드에만**(replacement_pr_runner.py CLI `__main__`이 명시 opt-in). **automation/merge_queue/real-merge dry-run 경로는 allow_workspace 절대 금지**.

**2. fallback 5곳 모두 helper 통과**
- `transplant_expected_files` · `commit_local` · `push_branch` · `precheck_local_replacement_diff` · `assert_clean_working_tree` — 각 함수의 `cwd = repo_dir or str(WORKSPACE)`를 `cwd = _resolve_safe_repo_dir(repo_dir, allow_workspace=<CLI만 True>)`로 교체.
- `_run` 범용 helper(66)는 **이번 범위 제외**(호출자가 cwd 명시).

**3. 보존 (회귀 0 — 순서 불변)**
- rc0+empty stdout = 정상 empty blob **허용** 유지.
- guard 순서: **GIT_SHOW_FAILED → TARGET_OUTSIDE_REPO_ROOT → symlink unlink → write** 그대로.
- symlink guard / GIT_SHOW_FAILED / TARGET_OUTSIDE_REPO_ROOT 유지.

## expected_files (2파일)
- `utils/replacement_pr_runner.py`
- `tests/regression/test_replacement_pr_runner_2510.py`
- ★ **merge_queue_executor.py 수정이 필요해지는 순간 STOP_REPORT**(옵션 a로의 확장 금지·재승인 필요).

## 테스트 요구 (신규 회귀)
1. `repo_dir=None` → `MISSING_REPO_DIR` fail + canonical 무접촉.
2. `repo_dir=canonical WORKSPACE` → `WORKSPACE_WRITE_FORBIDDEN` fail + canonical 무접촉.
3. **`repo_dir=canonical 하위 일반 디렉터리`(git top-level=WORKSPACE)** → `WORKSPACE_WRITE_FORBIDDEN` fail.
4. **`repo_dir=별도 tmp git worktree/clone`(git top-level=그 root)** → 정상 동작(transplant/commit/push).
5. **merge_queue W2 회귀**: `ReplacementPRRunner(repo_dir 없음)`.execute → invariant **raise** → (merge_queue W2의) `except` fallthrough 보존 시뮬레이션(execute가 RuntimeError raise 확인 = W2 except 경로 보존). ※merge_queue_executor.py 무접촉이므로 replacement 측에서 "repo_dir 없으면 raise"만 검증.
6. 정상 empty-file PR(rc0+empty, tmp repo_dir) → 허용 유지.
7. 기존 T17/T17b/T18/T19/T20 회귀 0.
8. fresh worktree: canonical 2파일 size/sha 전후 불변.

## 검증
- `pytest tests/regression/test_replacement_pr_runner_2510.py` 전체 PASS(개수 명시).
- diff **2파일**뿐(merge_queue·forbidden 무접촉) 확인.
- canonical 2파일 size/sha 불변.

## ★ 종료 검증 함정
- 봇 자신 finish-task는 canonical(이미 PR#262 fix 반영) QC를 돌리지만, **이 P1 수정본은 아직 미merge**라 finish-task 종단으로 P1 효과 주장 금지. 수정본 효과는 **worktree isolated pytest + size/sha 불변**으로만. 완료해도 **구현 후보(ACTIVE=false)**.

## 금지
merge_queue_executor/real_merge_hooks 접촉 · 신규 PR 생성 · merge · ci.yml/ruleset · callback/finish-task 축 · real-merge 활성화 · allow_workspace를 automation 경로에 사용.

## 산출물 & 보고 (PR 없음 — ANU 보고까지만)
1. worktree 커밋(브랜치). **PR 생성 금지**(ANU가 독립검증 후 별도 승인 시 생성).
2. ANU 보고: diff(2파일)/pytest 결과/canonical size/sha 불변/safe repo_dir invariant(git top-level 판정) 구현 설명/merge_queue 무접촉 확인.
3. 봇 종료 전 ANU callback(envelope만·UTF-8 ≤3900B).

## 검증 루프
자체 리뷰 1회 → pytest 통과 → 2파일 size/sha 불변 → forbidden 무접촉 → 함정 준수.

## goal_assertions (auto-generated)
- `pytest tests/regression/test_replacement_pr_runner_2510.py`
