# task-2569 — finish-task.sh / workspace isolation / task md 보존 시스템 fix

**팀**: 개발3팀 (다그다)
**Lv**: 3
**우선순위**: critical
**완료일**: 2026-05-13
**검증 상태**: CONDITIONAL PASS (마아트 독립 검증)

---

## SCQA 요약

**S**: task-2568 Queue Merge Chain Executor 진행 중(2026-05-13 19:25~33), Phase B PR #117 처리 도중 task md/3-docs 소실, stash 20+ 누적, whisper 상태 reset 발생 — 시스템 critical 손상.

**C**: 회장 박제 결과 ROOT CAUSE 4 후보 + 추가 6 영역 식별. 보호 정책이 두 cleanup 스크립트에 분산 하드코딩이며, pre-push hook이 `origin/main..HEAD` 기준만 사용해 PR sub-task worktree에서 scope 오판이 가능한 구조적 결함이 확인됨.

**Q**: 분산 hardcoded protection, untracked task md cleanup 영향, scope guard 오판을 단일 진실 소스 + lock_sha 자동 분기 + intent-to-add 등 최소 변경으로 차단 가능한가?

**A**: `memory/specs/protection-list.json` 단일 config 도입 + `dispatch/__init__.py` 4 위치 `git update-index --add --intent-to-add` 호출 + `pre-push hook`·`pre_push_guard.py`·`task_scope.py`에 `lock_sha..HEAD` 자동 분기 + 5개 파일에 `cleanup-audit.jsonl` 박제. 12 파일 수정 + 4 파일 신규, regression test 16건 PASS, 마아트 CONDITIONAL PASS.

(총 405단어)

---

## 변경 사항 요약

### 신규 파일
- `memory/specs/protection-list.md` (146줄) — 사람 읽기용 보호 경로 + AD-6 정책 명시
- `memory/specs/protection-list.json` (35줄) — 머신 파싱용 단일 protection config
- `tests/regression/test_task_md_preservation_2569.py` (107줄) — MT-T1 (4 tests)
- `tests/regression/test_finish_task_stash_lifecycle_2569.py` (73줄) — MT-T2 (4 tests)
- `tests/regression/test_scope_guard_lock_sha_2569.py` (138줄) — MT-T3 (4 tests)
- `tests/regression/test_cleanup_workspace_protection_2569.py` (135줄) — MT-T4 (4 tests)

### 수정 파일
- `scripts/cleanup-workspace.py` (196 → 282줄, +86) — `load_protection()` + `is_protected()` config 사용 + `_audit_log()` 박제
- `scripts/file_cleanup.py` (685 → 802줄, +117) — `SafetyChecker.__init__` config 로드 + `_audit_log_entry()`
- `scripts/cleanup-stale-tasks.sh` (50 → 63줄, +13) — post-cleanup audit log (tasks_md_count/plans_dirs_count)
- `memory/task-timer.py` (1224 → 1244줄, +20) — `cleanup_stale()` audit log
- `scripts/finish-task.sh` (1129 → 1149줄, +20) — 진입/종료 `_STASH_AUDIT_*` 박제, 5개 초과 시 WARN
- `dispatch/__init__.py` (4660 → 4704줄, +44) — task_file.write_text 후 4 위치 `git update-index --add --intent-to-add`
- `scripts/git-hooks/pre-push` (200 → 219줄, +19) — `LOCK_SHA` 추출 + `WORKTREE_CONTEXT` 감지 + `_DIFF_BASE` 자동 분기
- `scripts/pre_push_guard.py` (465 → 498줄, +33) — `_resolve_diff_base()` helper + `auto/AUTO` 분기
- `scripts/task_scope.py` (207 → 237줄, +30) — `_resolve_diff_base()` + `--base-sha auto` 분기

---

## RC/AD 커버리지 (회장 명시 영역)

| 영역 | 구현 | 검증 |
|---|---|---|
| RC-1: cleanup-stale/task-timer audit | cleanup-stale-tasks.sh:49-61 + task-timer.py:839-859 | grep 확인 ✅ |
| RC-2: cleanup-workspace/file_cleanup protection | load_protection() + SafetyChecker config 로드 | pytest 5건 PASS ✅ |
| RC-3: finish-task stash audit | _STASH_AUDIT_BEFORE/AFTER + WARN | pytest 4건 PASS ✅ |
| RC-4: dispatch intent-to-add | 4 위치 subprocess.run | grep 12 hits, pytest PASS ✅ |
| AD-1~3: lock_sha..HEAD scope guard | pre-push + pre_push_guard + task_scope | pytest 동작 검증 PASS ✅ |
| AD-4: protection-list.md | 신규 + memory/tasks/, plans/tasks/ 포함 | pytest 확인 ✅ |
| AD-5: lifecycle audit log | 5개 파일에 cleanup-audit.jsonl 박제 | grep 5 hits ✅ |
| AD-6: post-merge stale 정책 | protection-list.md preserved_lifecycle_paths | escalate (watcher 실 수정은 별도) |

---

## L1 스모크테스트 결과 (필수)

- **서버 재시작**: 해당없음 (시스템 스크립트 작업, 서버 영역 없음)
- **API 응답 확인**: 해당없음
- **스크립트 직접 실행**:
  - `python3 scripts/cleanup-workspace.py --report` → returncode 0, 보고서 출력 정상 (8009개 후보 509MB, 보호 경로 제외 확인)
  - `bash -n scripts/cleanup-stale-tasks.sh` → OK
  - `bash -n scripts/finish-task.sh` → OK
  - `bash -n scripts/git-hooks/pre-push` → OK
  - `python3 -m py_compile dispatch/__init__.py scripts/pre_push_guard.py scripts/task_scope.py scripts/cleanup-workspace.py scripts/file_cleanup.py memory/task-timer.py` → 전부 OK
- **regression test**: `pytest tests/regression/test_*_2569.py -v` → **16 passed in 1.01s**
- **스크린샷**: 해당없음 (UI 없음)

---

## 마아트 독립 검증 결과

**판정**: CONDITIONAL PASS

- **A. RC/AD 커버리지**: 모든 영역 PASS
- **B. 금지 위반**: 없음 (PR #117/#118/#119 추가 commit 없음, forbidden_paths 수정 없음)
- **C. 회복력**: PASS (try/except 격리, fallback 하드코딩, audit 실패 차단 없음)
- **D. 테스트 적정성**: PASS — 단, 일부 정적 문자열 검증 위주(예: intent-to-add 카운트). 실제 동작은 grep+직접 코드 read로 보완 확인됨.
- **E. 회귀 위험**: 낮음

---

## 발견 이슈 및 해결

### 자체 해결 (2건)
1. **cleanup-workspace.py:48 Pyright 타입 에러** — `_PROTECTION_CACHE: dict | None` 글로벌 변수 narrowing 불가 → local 변수 `loaded: dict | None`로 분리 후 마지막에 cache 할당, return 시점에는 narrow된 local 반환. py_compile + 동적 로드 검증 OK.
2. **regression test 4종 Pyright spec/loader Optional 에러** — `importlib.util.spec_from_file_location()`이 `ModuleSpec | None` 반환 → `assert spec is not None and spec.loader is not None` 추가 + `setattr(mod, "_PROTECTION_CACHE", None)` 사용 (동적 속성 할당). 16건 모두 재실행 PASS.

### 범위 외 미해결 (3건 — 회장 escalate)

1. **CONCERN-E (마아트 지적): lock 파일에 `lock_sha` 키 기록 누락** — 본 task에서 pre-push/pre_push_guard/task_scope에 `lock_sha` 자동 분기 인프라는 구축했으나, `.tasks/locks/<task-id>.lock` 파일 **생성** 코드에 `lock_sha` 기록 추가는 별도 task 필요. 현재 grep 결과 `lock_sha`는 본 task 신규 코드에만 존재. 범위 외 사유: lock 파일 생성 책임은 `taskctl_start`/`worktree_manager.py` 영역이며 본 task allowed_paths 밖. **escalate**.

2. **RC-3 stash 실 누적 원인 (Codex 권고)** — `grep stash scripts/finish-task.sh` 결과 0건 — `finish-task.sh`는 stash를 생성/조작하지 않음. 따라서 stash 20+ 누적의 실 출처는 외부 (worktree_manager.py 등 의심). 본 task는 audit log 박제로 다음 발생 시 추적 가능 상태로 만들었으나 원인 제거는 별도 task 필요. **escalate**.

3. **AD-6 watcher 실 수정** — `scripts/done-watcher.sh` 및 `utils/merge_queue_executor.py`의 escalation marker 동작 변경은 다른 task의 acked/escalated 흐름에 직접 영향. 본 task에서는 `protection-list.md`에 `preserved_lifecycle_paths` 정책 명시까지만 진행. 30분 → 60분 등 정책 수치 조정은 회장 합의 필요. **escalate**.

### 관찰 사항
- `memory/reports/task-2568.md`에 unstaged 변경 4줄 (`+## 세션 통계`) — 자동 hook 추가분으로 추정. task-2568은 forbidden_path이므로 본 task에서 commit 금지. finish-task.sh stash 격리에 위임.

---

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 호출 횟수 | 비고 |
|---|---|---|---|---|
| 다그다 | 팀장 | Opus 4.7 (1M) | — | 설계/위임/통합/검토 |
| 루-A | 백엔드 (protection+audit) | sonnet | 1회 (46 tool uses) | MT-1~4 |
| 루-B | 백엔드 (lifecycle+scope) | sonnet | 1회 (67 tool uses) | MT-5~7 |
| 모리건 | 테스터 | sonnet | 1회 (30 tool uses) | MT-T1~T4 |
| 마아트 | 독립 검증 | sonnet | 1회 (72 tool uses) | 횡단조직 — cross-start/end 로깅 |
| 브리짓 | 프론트엔드 | — | 0회 | **시스템 작업, 페르소나 고정 — 활성화 안 함** |
| 아네 | UX/UI | — | 0회 | **시스템 작업, 페르소나 고정 — 활성화 안 함** |

haiku 미사용 (모든 작업이 시스템 critical, sonnet 이상 필요).

## Codex 사전 검증 (Lv.3+ 필수)

- `scripts/codex_gate_check.py --task-id task-2569` 실행: PASS
- 결과 파일: `memory/events/task-2569.codex-gate`
- 핵심 통찰 반영:
  - RC-1/RC-3 audit-first로 좁힘 (Codex medium 위험 평가 채택)
  - 단일 protection config로 통합 (Codex 권고 채택)
  - lock_sha..HEAD 한 세트 설계 (Codex 권고 채택)
  - dispatch attempt 파일 분리는 본 task 제외 (Codex 권고 부분 채택, intent-to-add로 대체)

## 3 Step Why (Lv.3+ 자문)

- **1st Why**: task-2568 시스템 손상 재발 방지 + 회장 박제 RC-1~4/AD-1~6 처리.
- **2nd Why**: 중앙 config + lock_sha guard + intent-to-add가 최소 변경으로 최대 보호.
- **3rd Why**: 대안 1~4 (각 스크립트 hardcode, immutable fs, pre-commit 강제, attempt 분리) 모두 회복력/운영 부담 측면에서 열위 — 채택 근거 `context-notes.md` 결정 1~5에 박제.

A-B-C 일관성: ✅

## 산출물 파일

- `memory/specs/protection-list.md`
- `memory/specs/protection-list.json`
- `scripts/cleanup-workspace.py` (수정)
- `scripts/file_cleanup.py` (수정)
- `scripts/cleanup-stale-tasks.sh` (수정)
- `memory/task-timer.py` (수정)
- `scripts/finish-task.sh` (수정)
- `dispatch/__init__.py` (수정)
- `scripts/git-hooks/pre-push` (수정)
- `scripts/pre_push_guard.py` (수정)
- `scripts/task_scope.py` (수정)
- `tests/regression/test_task_md_preservation_2569.py`
- `tests/regression/test_finish_task_stash_lifecycle_2569.py`
- `tests/regression/test_scope_guard_lock_sha_2569.py`
- `tests/regression/test_cleanup_workspace_protection_2569.py`
- `memory/plans/tasks/task-2569/plan.md`
- `memory/plans/tasks/task-2569/context-notes.md`
- `memory/plans/tasks/task-2569/checklist.md`
- `memory/reports/task-2569.md`

## 토큰 사용량 (Token Usage)

(finish-task.sh의 토큰 추적기에서 자동 집계)
