# BASE_SOURCE_ISOLATION 설계 — worktree base source 격리·고정 (task-2729+9)

작성: 다그다(dev3 팀장) · 2026-06-06 · base = fresh origin/main e386d4cf

## 1. 문제 (확정 root cause, 재현으로 입증)

dispatch/봇 worktree 생성이 **stale 로컬 `main`(14ff8339)** 에서 분기하여 #181 hardening 없는
driver 위에 작업 → merge 충돌·stale base·EXTERNAL_DIRTY_BLOCKER 유발.

- 로컬 `main` = 14ff8339 (stale) ≠ `origin/main` = e386d4cf (fresh).
- 미스터리(audit): fallback(`git worktree add -b <b> <p>`, base 미지정)은 canonical 현재
  HEAD(audit 시점 75fdf540, task-2716)를 줘야 하는데 실제 base 는 14ff8339(로컬 main)였다.

### 1.1 재현으로 특정된 정확한 buggy 라인
isolated temp git repo 재현(`/tmp/repro-2729p9/`)으로 **`scripts/worktree_manager.py`
`cmd_create()` 의 fallback `git worktree add -b <branch> <wt_path>` (base 인자 없음)** 가
원인임을 입증했다.

- `git worktree add -b <b> <p>` 에 commit 인자가 없으면 git 은 **그 repo 의 현재 HEAD**
  에서 분기한다.
- task-2729+8 생성 시점에 canonical repo 의 HEAD 가 로컬 `main`(14ff8339)에 있었고,
  `git rev-parse --verify origin/main` 이 실패(fetch 실패/추적 ref 미존재)하여 fallback 에
  진입 → HEAD(=로컬 main 14ff8339)에서 silent 분기. audit 가 가정한 "HEAD=75fdf540" 은
  **HEAD 가 시점에 따라 바뀐다는 점**을 놓친 것이며, fallback 은 항상 *그 순간의 HEAD* 를
  쓴다. 미스터리 해소.
- 신호: `base_fallback=True` + `base_sha=null` + marker `enforced=false`.
- 정상 경로(origin/main 해결 시 explicit SHA base, line 383)는 올바르게 origin/main 을 사용.

→ buggy 라인은 **expected_files(`scripts/worktree_manager.py`) 안**. HOLD 불필요.

## 2. 경로 분리 (origin/main vs canonical HEAD/로컬 main)

| 경로 | base source | 정상/버그 |
|---|---|---|
| `worktree_manager.cmd_create` origin/main 해결 성공 (line 383) | `origin/main` SHA (explicit) | 정상 |
| `worktree_manager.cmd_create` fallback (base 미지정) | **repo 현재 HEAD = 로컬 main** | **버그(silent stale)** |
| `taskctl.py:2241` (`worktree add <p> -b <b> origin/main`) | `origin/main` literal | 정상 (봇 미사용) |
| `pre-push` scope_check `origin/main..HEAD` (two-dot) | — | stale base 시 과다 차단·메시지 모호 |

봇 경로(DIRECT-WORKFLOW.md:174 → `worktree_manager.py create`)는 base_ref override 인자가
없어 항상 default `origin/main` + `enforce_origin_base=True`. 따라서 literal `"main"`(로컬 ref)
사용 경로는 없으며, 유일한 stale 진입점은 fallback 이다.

## 3. 채택 설계 (구현)

### B1. base source 고정 — fail-closed (nuanced)
origin/main 미해결 시:
- **origin remote 존재 + origin/main 미해결** → **fail-closed**. worktree 미생성,
  `status="failed"`, HOLD 마커 `<task_id>.worktree-base-failed.json`(reason
  `STALE_BASE_ORIGIN_UNRESOLVED`) 기록. → stale 로컬 main silent 분기 **차단**.
- **origin remote 없음(로컬 전용 repo)** → 현재 HEAD 를 **explicit SHA 로 resolve**
  하여 base 지정(`base_source="local_head_no_origin"`). silent HEAD 분기가 아니라
  명시적 SHA 기록 → 기존 로컬 전용 repo(task-2377 류) **회귀 방지**.

> 설계 판단: 순수 "모든 미해결 = 무조건 fail" 은 origin 없는 정상 로컬 repo 까지 차단하여
> 회귀를 유발한다. 위험 조건(=origin 이 있는데 origin/main 만 미해결, 즉 fetch 실패/
> 추적 ref stale)에서만 fail-closed 하고, origin 자체가 없으면 explicit-HEAD 로 안전
> 분기한다. 이는 회장 선호("worktree 생성은 항상 explicit base SHA 기록")와 일치하며
> "silent stale local main" 만 정확히 봉쇄한다.

### B2. base marker 강제 기록
성공 marker 에 5축 추가: `base_source` · `base_sha` · `merge_base` · `origin_main_sha`
· `canonical_head_sha`. 사후 stale base 추적 가능.

### B3. pre-push merge-base 기반 stale/오탐 구분
- `merge_base(origin/main, HEAD) == origin/main` → HEAD 가 fresh origin/main 후손 →
  **three-dot** diff(`origin/main...HEAD`)로 정확한 scope 검사(two-dot 오탐 제거).
- `merge_base != origin/main` → **STALE_BASE 명시 차단**(exit 1) + 재dispatch 권고.
- origin/main 미해결 → graceful two-dot fallback(기존 동작 유지).

## 4. 대안 비교 — 별도 main-tracking worktree vs dispatch base override

### OPT-A. dispatch base override (= 채택, B1)
`cmd_create` 가 항상 origin/main SHA 를 explicit base 로 강제하고, 미해결 시 fail-closed.
- 장점: 변경 최소(단일 함수), 봇/taskctl 양 경로 일관, 마커로 추적성 확보, 회귀 영향 적음.
- 단점: fetch 가 매 create 마다 필요(이미 존재), 네트워크 단절 시 fail-closed 로
  task 가 막힘(단, 이는 stale base 방치보다 안전 — fail-closed 가 정답).

### OPT-B. 별도 clean main-tracking worktree
canonical 와 분리된 `main` 전용 clean worktree 를 상시 유지하고 거기서 분기.
- 장점: canonical dirty 상태와 물리적 격리, 로컬 main ref 오염 무관.
- 단점: 운영 복잡도↑(상시 worktree 유지·동기화 cron 필요), 디스크 2배,
  동기화 누락 시 그 worktree 도 stale 될 수 있어 결국 origin/main fetch 강제는 동일 필요.
  → 근본 해결은 OPT-A 의 "explicit origin/main SHA 강제" 와 동일하고 부대비용만 큼.

### OPT-C. 로컬 main ref 자체를 origin/main 으로 reset
- 기각: canonical workspace 상태 변경(금지). 로컬 main 을 reset 하면 다른 worktree/작업에
  부작용. 회장 금지 #1(reset/clean/checkout) 위반.

**결론**: OPT-A(B1+B2) 채택. OPT-B 는 향후 멀티봇 부하가 커지면 보조 수단으로 재검토 가능
하나 현재는 과설계. OPT-C 는 금지.

## 5. self-referential 처리
본 task 봇의 worktree 도 동일 버그 영향권이나, worktree_manager/pre-push 가
14ff8339↔e386d4cf IDENTICAL 이라 충돌 0. 따라서 산출물은 **fresh origin/main(e386d4cf)
기반 isolated worktree** (`task/task-2729+9-dev3`)에 explicit SHA 로 생성·보존했고,
canonical `/home/jay/workspace`(task-2716, 58 modified·1418 untracked·HEAD 75fdf540)는
브랜치전환/reset/clean/stash 없이 **무손상** 유지. 완료는 수동 .done 금지 →
ANU normal callback(독립 ANU key) 등록으로 ANU manual pickup 회수.

## 6. expected_files / forbidden / regression
- expected(5): `scripts/worktree_manager.py`, `scripts/git-hooks/pre-push`,
  `tests/regression/test_base_source_isolation_2729p9.py`,
  `memory/reports/task-2729+9.md`, 본 문서.
- forbidden 준수: dispatch.py/taskctl.py/start_task_guard.py/finish-task.sh/.github/hooks
  미변경, canonical 상태 미변경, task-2716/2729+8 미변경.
- regression: 5 케이스(origin base 강제 / fail-closed / 로컬전용 explicit / marker 5축 /
  pre-push merge-base 분기) 전부 PASS.
