---
task_id: task-2700
type: plan
scope: task
created: 2026-05-27
updated: 2026-05-27
status: completed
---

# 계획서: task-2700 — LOCAL_MAIN_DIVERGENCE & EXTERNAL_DIRTY_BLOCKER 예방

**task**: task-2700
**목표**: local main ↔ origin/main divergence + 외부 dirty 로 인한 stale-base worktree / finish-task GIT-GATE 차단 / callback 미발사를 **fail-closed** 로 예방
**승인**: 회장 verbatim 2026-05-27 · `CHAIR-AUTH-TASK-2700-LOCAL-MAIN-DIVERGENCE-PREVENTION-20260527-JJONGS-IMPLEMENT-001`
**근거**: `memory/events/task-2699-callback-missing-main-dirty-deepcheck-plan-260527.json` (task-2699 stale base 사고 박제)

---

## 목표 (측정 가능)

task-2699 재발 방지를 위해 5개 산출물을 제공한다:

1. **divergence 측정 + dispatch HOLD 게이트** (fail-closed): local main ≠ origin/main 이면 coding/security/callback/finish-task task dispatch 차단
2. **worktree origin/main SHA 강제 생성** + base SHA marker 기록 + spawn 시 base SHA 검증
3. **dirty registry JSONL**: dirty 파일별 mtime/status/diff summary/추정 owner task 기록 (dispatch 전·finish 전)
4. **EXTERNAL_DIRTY_BLOCKER vs own dirty FAIL 분리 진단**: own dirty 는 계속 FAIL, unrelated dirty 는 환경 블로커 분류
5. **callback 미발사 원인 구분**: NORMAL_CALLBACK_MISSING vs FINISH_TASK_GIT_GATE_BLOCKED + finish profile 4종 연결

검증: `tests/regression/test_local_main_divergence_prevention_2700.py` 8 시나리오 + task-2699 fixture 전부 PASS.

## 범위

### 포함
- **신규 모듈** `utils/divergence_guard.py` — divergence 측정(`git rev-list --left-right --count origin/main...HEAD`) + HOLD 판정(fail-closed) + CLI/import
- **신규 모듈** `utils/dirty_registry.py` — dirty 수집/JSONL 기록 + owner 추정 + EXTERNAL_DIRTY_BLOCKER/own FAIL 분류 + own·unrelated 분리
- **신규 모듈** `utils/callback_cause_classifier.py` — callback 미발사 원인 구분 + finish profile(read_only_watcher/diagnosis/callback_only/closeout_marker_only) 연결
- **확장** `scripts/worktree_manager.py` — `cmd_create` origin/main SHA base 강제 + base SHA marker, `verify_spawn_base` 신규
- **확장** `scripts/finish-task.sh` — GIT-GATE merge-base 검증 + dirty 분리 진단 마커(코어 보존·additive)
- **테스트** `tests/regression/test_local_main_divergence_prevention_2700.py`

### 제외 (다음 페이즈 이후)
- dispatch.py / dispatch/__init__.py **핵심 로직 변경** (회장 doctrine — pre-flight 모듈 + import 연결로 우회 설계). 핵심 변경 필요 시 HOLD_FOR_CHAIR.
- live settings.json / hooks live / Axis runtime / .github 변경
- own dirty FAIL 완화 (요구 11 위반 금지)
- divergence HOLD bypass flag (fail-closed 불변)

## 위임 계획

- Phase 1 (모듈): **스바로그(백엔드)** — utils 3종 모듈 (요구 1,2,6,7,8,9,10,11,12,14)
- Phase 2 (인프라 확장): **스바로그(백엔드)** — worktree_manager + finish-task.sh (요구 3,4,5,8,10,11,15)
- Phase 3 (테스트): **벨레스(테스터)** — 8 시나리오 + task-2699 fixture (요구 13,14)
- 라다(프론트)/모코시(UX): 비활성 (순수 인프라 작업 — 보고서에 사유 명시)

## 검증 기준 (★ "이게 되면 성공")

- divergence test: ahead/behind ≠ 0 → coding dispatch HOLD: `pytest -k divergence_hold` → PASS
- clean test: main == origin/main → 통과: `pytest -k clean_pass` → PASS
- base SHA test: worktree base ≠ origin/main → spawn FAIL: `pytest -k spawn_base` → PASS
- EXTERNAL_DIRTY_BLOCKER test: expected ∩ dirty = 0 + unrelated dirty → 블로커 분류(task fail 아님): `pytest -k external_dirty` → PASS
- own dirty FAIL test: expected ∩ dirty ≠ 0 → FAIL 유지: `pytest -k own_dirty_fail` → PASS
- callback 구분 test: NORMAL vs GIT_GATE_BLOCKED 정확 분류: `pytest -k callback_cause` → PASS
- task-2699 fixture: ahead/behind + 다수 dirty 재현 → EXTERNAL_DIRTY_BLOCKER + HOLD 둘 다: `pytest -k task_2699_fixture` → PASS
- registry JSONL: dirty 파일별 mtime/owner 기록: `pytest -k registry_jsonl` → PASS
- 전체: `python3 -m pytest tests/regression/test_local_main_divergence_prevention_2700.py -q` → all PASS
