# 보고서: task-2729+10 (r2) — START_TASK_GUARD worktree-root hardening + Gemini HIGH remediation (Option A replacement)

- 팀: dev2-team (오딘) | 레벨: Lv.3 | 판정: **MERGE_APPROVAL_CANDIDATE_INFRA_HARDENING_ACTIVE_FALSE**
- 작성일: 2026-06-07
- base: fresh `origin/main` (8c5af051) | 신규 브랜치: `task/task-2729+10-r2-dev2` (PR #185 same-branch push 금지 → Option A replacement)

## S - Situation (상황)
PR #185(head 8bed1fee, branch `task/task-2729+10-dev6`) 는 G1(start_task_guard worktree-root #7) + G2(finish-task EXTERNAL_DIRTY 면제) 핵심 로직이 self-validated 되었으나, Gemini fresh **HIGH** 적발로 merge blocker 가 되어 CLOSED(SUPERSEDED). same-PR post-Gemini push 금지 doctrine 에 따라 fresh origin/main base 새 브랜치로 수정(Option A replacement)한다.

## C - Complication (문제)
Gemini HIGH (`finish-task.sh:881`): `nullglob` 상태에서 `ls -d "$WORKSPACE/.worktrees/${TASK_ID}-"*` 가 빈 인자로 확장되면 `ls -d` 가 현재 디렉토리 `.` 을 나열하며 return 0 → 매칭 worktree 가 없는데도 `_WT_ISOLATED=1` 로 오설정 → canonical 의 무관 dirty 가 잘못 EXTERNAL_DIRTY 면제됨. **fail-closed 위반**. (MEDIUM: 회귀 테스트의 bash 스니펫이 옛 `ls -d` 방식으로 실제 로직과 불일치.)

## Q - Question (질문)
PR #185 의 5파일을 base 로 유지하면서, HIGH(nullglob false-exempt)를 fail-closed 로 교정하고 회귀 테스트를 실제 로직과 동기화하려면?

## A - Answer (해결: Option A replacement, nullglob-safe 루프)

### 변경 파일 (정확히 5 — EXPECTED FILES 준수)
1. `scripts/start_task_guard.py` — **G1** worktree-root #7 (PR #185 그대로 유지, self-validated).
2. `scripts/finish-task.sh` — **G2 + HIGH fix**: worktree 탐지를 `ls -d` + nullglob 의존 제거 → **nullglob-safe for 루프**(`for _wt in ...; do [ -e "$_wt" ] && _WT_ISOLATED=1 && break; done`, 기본 0). 매칭 worktree 없으면 nullglob ON/OFF 무관하게 **반드시 `_WT_ISOLATED=0`**.
3. `tests/regression/test_start_task_guard_worktree_root_2729p10.py` — **MEDIUM 동기화**(스니펫을 루프 방식으로 교체) + **worktree-없음+nullglob 회귀 케이스 추가**(`test_g2_nullglob_no_worktree_blocks`).
4. `memory/reports/task-2729+10.md` — 본 보고서.
5. `memory/plans/p0b-pickup/canonical_branch_assumption_design_260606.md` — 설계 문서(r2 HIGH remediation 반영).

### HIGH fix 정확 diff (finish-task.sh)
```diff
-        _WT_ISOLATED=0
-        if ls -d "$WORKSPACE/.worktrees/${TASK_ID}-"* >/dev/null 2>&1; then
-            _WT_ISOLATED=1
-        fi
+        # task-2729+10 r2 (HIGH fix): nullglob-safe 루프. ls -d + glob 빈확장 의존 제거.
+        # nullglob ON: 매칭 없으면 루프 미실행 → 0 유지. nullglob OFF: 리터럴 glob → [ -e ] 실패 → 0 유지.
+        # 어느 경우든 매칭 worktree 없으면 반드시 _WT_ISOLATED=0 (fail-closed).
+        _WT_ISOLATED=0
+        for _wt in "$WORKSPACE/.worktrees/${TASK_ID}-"*; do
+            [ -e "$_wt" ] && _WT_ISOLATED=1 && break
+        done
```

## 테스트 결과
신규 회귀 (worktree `python3 -m pytest tests/regression/test_start_task_guard_worktree_root_2729p10.py`): **6 passed** (#185 5건 + r2 신규 nullglob 회귀 1건).
- TC1(필수검증 2): canonical parked+dirty + fresh worktree → #7 PASS + lock 생성.
- TC2(필수검증 4): stale base(behind) worktree → #7 FAIL (방어 유지).
- TC3(무회귀): canonical=main → #7 PASS.
- TC4: classify_blocker 무관 dirty → EXTERNAL_DIRTY_BLOCKER.
- TC5: G2 면제 결정 — 격리+EXTERNAL→EXEMPT / 비격리→BLOCK / OWN_DIRTY→BLOCK (스니펫 루프 동기화).
- **TC6(r2 신규, HIGH 회귀)**: `shopt -s nullglob` ON + 매칭 worktree 없음 + EXTERNAL_DIRTY_BLOCKER → **BLOCK**(_WT_ISOLATED=0). 옛 `ls -d` 였다면 `.` 나열로 잘못 EXEMPT 되었을 케이스.

## L1 스모크테스트 결과 (필수 기록)
- **서버 재시작**: 해당없음 (서버/HTTP API 무관 — git 가드 셸 스크립트 작업).
- **API 응답 확인**: 해당없음. 대신 **실제 셸 실행 검증**(OLD vs NEW 대조) 수행 ↓.
- **실 셸 실동작 (HIGH 재현·교정 대조, nullglob ON, 매칭 worktree 없음)**:
  - OLD(`ls -d`): `_WT_ISOLATED=1` (취약 — 무관 dirty 잘못 면제).
  - NEW(for 루프): `_WT_ISOLATED=0` (교정 — fail-closed BLOCK). ✅
  - NEW + 매칭 worktree 존재: `_WT_ISOLATED=1` (정상 면제 경로 보존). ✅
  - NEW + nullglob OFF + 매칭 없음: `_WT_ISOLATED=0`. ✅
  - `bash -n scripts/finish-task.sh` 문법 PASS.
- **스크린샷**: 해당없음 (UI 무관 백엔드 셸 가드).

## 발견 이슈 및 해결
- **HIGH(nullglob false-exempt)**: `ls -d <glob>* >/dev/null 2>&1` 는 nullglob ON + 빈 매칭 시 `.` 을 나열·return 0 으로 false-positive. → for 루프 + `[ -e ]` 로 교체하여 nullglob 의존 자체를 제거(L1 대조로 실증).
- **MEDIUM(테스트 drift)**: 회귀 스니펫(line ~356)이 옛 `ls -d` 를 사용 → 실제 finish-task 로직(루프)과 동기화. 추가로 nullglob 회귀 케이스를 명시 추가하여 HIGH 재발 방지.
- **검증 안전성**: `finish-task.sh` 전체 실행은 `WORKSPACE` 하드코딩으로 canonical 부작용 위험 → 면제 결정 로직(line ~964 조건)을 충실 재현한 격리 bash 스니펫 + `classify_blocker` 단위 검증으로 안전 대체(전체 실행 금지).

## 모델 사용 기록
- 오딘(팀장, **Opus**): 설계·분배·검토·통합·커밋·L1·보고서·플랜. (직접 코딩 없음 — 위임)
- 토르(BE, **sonnet**): finish-task.sh HIGH fix(nullglob-safe 루프). 셸 보안 수정 — haiku 부적합, sonnet 사용.
- 헤임달(테스터, **sonnet**): 회귀 스니펫 동기화 + nullglob 회귀 케이스 추가. 테스트 분석 — sonnet 사용.
- haiku 미사용.

## 머지 판단
- **머지 필요**: Yes (회장 승인 후)
- **브랜치**: task/task-2729+10-r2-dev2
- **워크트리 경로**: /home/jay/.cokacdir/workspace/507C85CF/wt-2729p10-r2
- **머지 의견**: HIGH(nullglob false-exempt)를 fail-closed 로 외과적 교정. G1/G2 핵심 로직은 #185 self-validated 유지. 회귀 6 PASS + L1 OLD/NEW 대조 실증 + invariant(raw key0·ACTIVE=false·systemctl0·spawn0) 충족. canonical 무손상. Lv.3 → G3 PR(Gemini fresh gate) 경유. **merge·production activation 은 별도 회장 승인 전 금지.**

## canonical 무손상 확인
canonical `/home/jay/workspace` 는 `task/task-2716-pr-diff-hygiene-guard-dev4` 에 그대로 유지. reset/clean/stash/checkout -f/branch 전환 **0회**, task-2716 branch 수정 0, live memory artifacts(task-2716*.json) 무손상. 모든 검증은 격리 worktree/temp repo 기준.

## 필수 검증 (회장 verbatim 11)
1. worktree 없음 + canonical dirty → 면제 금지(_WT_ISOLATED=0): ✅ (TC6 + L1 NEW nullglob)
2. worktree 있음 + unrelated canonical dirty → 면제 허용: ✅ (TC5-A + L1 NEW 매칭 존재)
3. worktree OWN_DIRTY → fail: ✅ (TC5-C)
4. stale/behind/diverged worktree → fail: ✅ (TC2)
5. forbidden/expected_files 초과 → fail: ✅ (classify_blocker OWN_DIRTY_FAIL → TC5-C)
6. canonical task-2716 branch + live memory artifacts 무손상: ✅
7. regression PASS: ✅ (6 passed)
8. raw key 0: ✅ (diff scan)
9. ACTIVE=false: ✅ (ACTIVE=true 0)
10. systemctl enable 0: ✅
11. real spawn 0: ✅

## 셀프 QC (8항목)
1. 요구 충족(HIGH fix/MEDIUM sync/G1·G2 유지): ✅  2. EXPECTED FILES 정확히 5: ✅  3. 회귀 PASS(6): ✅
4. Edit 후 grep 검증: ✅  5. L1 실동작(셸 OLD/NEW 대조): ✅  6. canonical 무손상: ✅
7. 보고서 SCQA+L1+모델기록: ✅  8. 금지행위(canonical 파괴·task-2716·real ANU·merge·active) 0: ✅
