# task-2549+1 보고서 — scripts/ci.sh -type f + _run_find_pruned 재작성 (Option 3 clean replacement)

**레벨**: Lv.2 (normal, code-changing replacement)
**트랙**: A (control-plane, replacement PR — PR #98 보존)
**팀**: dev2 오딘
**선행**: task-2549 / PR #98 OPEN ESCALATED (OWNER_DECISION_REQUIRED)
**worktree**: `.worktrees/task-2549plus1-dev2` (origin/main `ed8c1250` 분기)

---

## 본질

PR #98 head `93eac627` 기준 fresh Gemini 권고 2건 unresolved:
1. `scripts/ci.sh` `-type f` 추가 권고 — **code-changing (production code)**
2. `_run_find_pruned` 가 ci.sh 동작을 재정의 → **test decoupling 결함**

회장 §명시 2026-05-11 Option 3 승인: same-PR push 금지 / PR #98 close/reopen 거부 →
**origin/main 기준 clean replacement PR** 로 베이스 fix (worktree exclude / -prune 가속)
유지 + Gemini 권고 2건 반영.

PR #98 (head 93eac627) 은 OPEN 상태로 절대 보존. 본 task 에서 commit/push/close/reopen 0.

---

## expected_files (3개)

| # | path | 상태 | 변경 요지 |
|---|------|------|----------|
| 1 | `scripts/ci.sh` | modified | find 1단계에 `-prune` + `-type f` 동시 적용 (디렉토리명 *.py 매칭 방지, vendor 6종 가지치기 가속 유지) |
| 2 | `tests/regression/test_ci_sh_worktree_exclude_2549.py` | added | `_run_find_pruned` 가 ci.sh 의 find 블록을 정규식 추출 후 bash 실행 — ci.sh 변경에 자동 반영 (재정의 X) + `-type f` 회귀 catch 박제 추가 |
| 3 | `memory/reports/task-2549+1.md` | added | 본 보고서 |

`git diff origin/main --stat` (보고서 add 직전):
```
 scripts/ci.sh                                       |  5 ++++-
 tests/regression/test_ci_sh_worktree_exclude_2549.py | 351 ++++++++++++++++++
```

---

## scripts/ci.sh fix — `-type f` 추가

**Before (main)**:
```bash
find "$WORKSPACE" -name "*.py" -print0 2>/dev/null
```

**After (task-2549+1)**:
```bash
find "$WORKSPACE" \
    \( -path "*/.worktrees" -o -path "*/.venv" -o -path "*/venv" \
       -o -path "*/.codegraph-venv" -o -path "*/node_modules" -o -path "*/.git" \) \
    -prune -o -type f -name "*.py" -print0 2>/dev/null
```

변경 포인트:
- **vendor 6종 `-prune`**: `.worktrees`, `.venv`, `venv`, `.codegraph-venv`, `node_modules`, `.git` 가지치기 (베이스 fix 유지)
- **`-type f`** (Gemini 권고): 디렉토리 이름이 `*.py` 매칭되어 py_compile 에 디렉토리가 전달되는 회귀 차단

회귀 catch 실증 (테스트 fixture):
```
fake.py/ (디렉토리) + real.py (파일)
- without -type f: ['fake.py', 'real.py']   ← 디렉토리 누설
- with -type f:    ['real.py']               ← 파일만 (fix)
```

---

## test_ci_sh_worktree_exclude_2549.py fix — 재정의 X 강제

**기존 문제**: `_run_find_pruned` 가 find 인자를 헬퍼 내부에 하드코딩 → ci.sh
변경 시 테스트가 자동 반영되지 않아 False Positive 위험.

**해결 (회장 §명시 C 채택)**: `_read_find_block(ci_sh_path)` 기반 정적 추출 + bash 실행.

```python
_FIND_BLOCK_RE = re.compile(
    r'done\s*<\s*<\(\s*(?P<find_cmd>find\s+"\$WORKSPACE".*?-print0[^)]*?)\)',
    re.DOTALL,
)

def _extract_find_command(ci_sh_path = CI_SH) -> str:
    content = ci_sh_path.read_text()
    return _FIND_BLOCK_RE.search(content).group("find_cmd").strip()

def _run_find_pruned(workspace: pathlib.Path) -> list[str]:
    cmd_template = _extract_find_command()
    env = os.environ.copy()
    env["WORKSPACE"] = str(workspace)
    result = subprocess.run(
        ["bash", "-c", cmd_template],
        capture_output=True, check=True, env=env,
    )
    return [p.decode("utf-8") for p in result.stdout.split(b"\0") if p]
```

**자동 반영 박제 테스트 (`test_ci_sh_change_is_auto_reflected`)**:
- 가짜 ci.sh 작성 (`-prune` 없음) → 헬퍼가 그 가짜 find 명령을 그대로 실행 → `.worktrees/leak/should_leak.py` 누설을 catch
- 즉, ci.sh 가 변경되면 헬퍼 행동도 자동으로 그 변경을 따라간다 (decoupling 0)

---

## 17단계 실행 결과

| § | 단계 | 결과 |
|---|------|------|
| 1 | BOT_TOKEN refresh | `ghs_Kc8uiJ4T...` (exp `2026-05-11T08:27:41Z`) |
| 2 | worktree (origin/main `ed8c1250`) | `.worktrees/task-2549plus1-dev2` 생성 |
| 3 | 시작 diff 0 | `nothing to commit, working tree clean` |
| 4 | PR #98 `93eac627` 참조 | scripts/ci.sh + test 베이스 fix 확인 (prune 6종 + -print0 유지) |
| 5 | ci.sh `-type f` 추가 | ✅ + `-prune` 가속 유지 |
| 6 | test `_run_find_pruned` 재작성 | ✅ ci.sh 의 find 블록 자동 추출 + bash 실행 |
| 7 | ci.sh 변경 자동 반영 검증 | ✅ `test_ci_sh_change_is_auto_reflected` PASS |
| 8 | 615 regression PASS | ✅ 충돌 0 (19 new + 596 existing) |
| 9 | effective diff == 3 | ✅ scripts/ci.sh + test + report |
| 10 | forbidden path 0 | ✅ (anu_v2/.github/POC/타task marker 변경 0) |
| 11~17 | PR 생성/CI/Gemini/merge | 본 보고서 작성 후 진행 |

---

## 완료 기준 10항목 매핑

| # | 기준 | 달성 |
|---|------|------|
| 1 | task-2549+1 PR merged | (post-PR) |
| 2 | mergedBy = `app/jeon-jonghyuk-taskctl-bot` | (post-merge) |
| 3 | effective diff == 3 files | ✅ ci.sh + test + report |
| 4 | forbidden path 0 | ✅ |
| 5 | CI all SUCCESS | (post-PR) |
| 6 | Gemini unresolved 0 | (post-PR) |
| 7 | smoke + reconcile evidence | (post-merge) |
| 8 | PR #98 `93eac627` unchanged | ✅ 본 task 에서 PR #98 commit/push/close/reopen 0 |
| 9 | ci.sh `-type f` 적용 + 회귀 catch | ✅ `test_ci_sh_uses_type_f_filter` + `test_directory_named_dot_py_is_not_collected` |
| 10 | test ci.sh 실제 호출 + 재정의 X | ✅ `test_find_block_extracted_from_ci_sh` + `test_ci_sh_change_is_auto_reflected` |

---

## 18 금지 준수

1. ❌ PR #98 추가 commit/push — **N/A** (별도 PR)
2. ❌ PR #98 close/reopen — **0회**
3. ❌ force push / rebase / empty commit — **0회**
4. ❌ bot `/gemini review` 댓글 — **0회** (task-2552 사전조사 0/5)
5. ❌ owner PAT — **사용 0**
6. ❌ GH_TOKEN fallback — **사용 0**
7. ❌ expected_files amendment 로 PR #98 살리기 — **시도 0**
8. ❌ md/report 만으로 PASS — **N/A** (ci.sh + test 실제 변경)
9. ❌ 자동 task-2549+2 발행 — **시도 0**
10. ❌ source code 외 변경 — **scripts/ci.sh 1 file 외 production code 0**
11. ❌ POC files — **변경 0**
12. ❌ .github/workflows/ — **변경 0**
13. ❌ anu_v2/** — **변경 0**
14. ❌ 타 task marker — **변경 0**
15. ❌ existing marker 삭제 (task-2549.escalated 보존) — **삭제 0**
16. ❌ Gemini 후 same-PR push — **N/A** (sequential gate)
17. ❌ long polling / self-register — **0회**
18. ❌ self-policy replacement chain — **본 task 에서 또 code-changing Gemini 발견 시 ESCALATED + task-2549+2 자동 발행 0 (OWNER_DECISION_REQUIRED 멈춤)**

---

## chain 정책 §18 박제

본 task (task-2549+1) 에서 Gemini fresh review 가 또 code-changing 권고를 내면:
- **즉시 ESCALATED `SELF_POLICY_REPLACEMENT_CHAIN_LIMIT_HIT`**
- **task-2549+2 자동 발행 절대 금지**
- **OWNER_DECISION_REQUIRED** (회장 결정 대기)

reasoning: 회장 §명시 "replacement chain limit 1" — 1회 replacement 후에도 미해결이면
자동 chain 금지. 시스템 자동화 → 회장 수동 결정으로 escalation.
