# Stash Lifecycle Spec

**task**: task-2571
**status**: active
**created**: 2026-05-14
**author**: 비슈누 (개발4팀장)
**related**:
- task-2569+2 (finish-task.sh `_STASH_AUDIT_BEFORE/AFTER` 박제 + 5개 초과 WARN)
- task-2570 (stash_audit.py read-only 진단 도구 + `memory/specs/stash-origin-audit.md`)
- `feedback_stash_accumulation_doctrine_260513.md`
- `feedback_local_operational_patch_doctrine_260514.md`

---

## 1. 목적

`scripts/finish-task.sh` 종료 직후 git stash 누적 상태를 **분류별 명시적 lifecycle**로 처리한다.

- **무엇을**: 각 stash의 source 분류를 기준으로 auto-pop / explicit drop / quarantine / preserve 결정
- **왜**: 60개+ 누적 stash 사건 (회장 박제 2026-05-13) 재발 방지 + 출처 불명 stash의 무분별 cleanup 방지
- **어떻게**: `stash_audit.py` JSON 출력 기반 자동 분류 + dry-run 기본값 + explicit approval gate
- **read-only 원칙**: 파괴적 동작 (drop / pop / clear) 은 **default dry-run**, `FINISH_TASK_STASH_APPROVE=1` 환경변수로만 실행

---

## 2. 분류 × Lifecycle 매트릭스

`stash_audit.py` (task-2570 spec §3, §5) 의 6 카테고리에 대한 lifecycle 정책:

| source | lifecycle 정책 | 정상 동작 | dry-run 동작 | 비고 |
|--------|---------------|----------|-------------|------|
| `pre-task` | **auto-pop** | `git stash pop stash@{N}` | `[DRY] would pop stash@{N}` | post-task 자동 복원 (정상 path) |
| `finish-task` | **검증 후 auto-pop** | PR 머지 완료 검증 PASS 시 `pop`, 아니면 preserve | `[DRY] would pop stash@{N} if merge verified` | task-2569 RC-3 quarantine |
| `other-files` | **explicit drop only** | `FINISH_TASK_STASH_APPROVE=1` + 명시 인덱스만 `drop` | `[DRY] would drop stash@{N}` | 회장 사전 승인 필요 |
| `wip` | **explicit preserve** | NO-OP (보존) | `[DRY] preserved (wip — user intent)` | 사용자 의도 작업 |
| `quarantine` | **explicit preserve** | NO-OP (보존) + 박제 | `[DRY] preserved (quarantine — explicit)` | 격리 보존 박제 |
| `unknown` | **quarantine + cleanup 금지** | NO-OP + audit log | `[DRY] preserved (unknown — manual review)` | 후속 review task 필요 |

### 2.1 결정 흐름

```
stash_audit.py --json
  ↓
entries 순회
  ↓
case e.source of:
    pre-task     → if APPROVED: pop      else: dry-run-pop
    finish-task  → if APPROVED and PR_VERIFIED: pop else: preserve+log
    other-files  → if APPROVED and IDX_LISTED: drop else: dry-run-drop
    wip          → preserve (always)
    quarantine   → preserve+log (always)
    unknown      → preserve+quarantine_log (always — cleanup 금지)
```

### 2.2 metadata 미존재 (legacy stash)

- `stash_audit.py` 의 패턴 매칭 (`stash-origin-audit.md` §5) 실패 시 → `source=unknown`
- legacy stash 는 **자동으로 unknown 분기 → quarantine** 으로 분류
- 메시지 수정/재작성 일절 금지 (read-only 원칙)

---

## 3. Approval Gate

### 3.1 환경변수 / CLI 플래그

| 변수 | 기본값 | 효과 |
|------|--------|------|
| `FINISH_TASK_STASH_APPROVE` | `0` (또는 미설정) | 모든 파괴적 동작 dry-run |
| `FINISH_TASK_STASH_APPROVE=1` | — | 분류별 lifecycle 정책에 따라 실제 실행 (단, unknown/wip/quarantine 은 여전히 preserve) |
| `FINISH_TASK_STASH_INDICES` | (미설정) | other-files drop 대상 인덱스 명시 (`"3,7,12"` 형식). 미명시 시 other-files 전체도 dry-run. |
| `FINISH_TASK_STASH_DEBUG` | `0` | `1` 설정 시 분류/결정 디테일을 stderr 출력 |

### 3.2 Approval 검증 순서

```
1. FINISH_TASK_STASH_APPROVE != "1"  →  전체 dry-run + 정상 종료
2. FINISH_TASK_STASH_APPROVE == "1"  →
   2.1 분류별 정책 적용
   2.2 each destructive action 직전 audit log 박제 (memory/events/stash-lifecycle-action.<ts>.json)
   2.3 실패 시 stop (다음 stash 건드리지 않음)
```

> §3.2.2.3 보강: destructive `git stash pop`/`drop` 실패 시 해당 decision 은 `action="failed"` 로 기록되며, 추가로 `stderr` (trim) 와 `exit_code` 필드가 포함된다. 루프는 즉시 `break` 하고 audit log payload 에 `fail_stop: true` 가 기록된 뒤 스크립트는 `sys.exit(1)` 로 종료한다 (non-zero exit).

### 3.3 finish-task 분기 추가 검증 (PR 머지 검증)

`source=finish-task` 의 auto-pop 은 다음 조건 모두 만족 시에만 실행:
- `FINISH_TASK_STASH_APPROVE=1`
- 본 task 의 PR 이 `.done` 마커로 머지 완료 표기됨 (`$EVENTS_DIR/$TASK_ID.done` 존재)
- stash 의 `task_id` 가 본 task `$TASK_ID` 와 일치 (다른 task 의 finish-task quarantine 은 건드리지 않음)

위 조건 미충족 시 preserve + audit log 박제.

---

## 4. Audit Log 포맷

### 4.1 위치

```
memory/events/stash-lifecycle-action.<ISO_TIMESTAMP_UTC>.json
```

예: `memory/events/stash-lifecycle-action.2026-05-14T11-55-42Z.json`

### 4.2 포맷 (JSON)

```json
{
  "timestamp_utc": "2026-05-14T11:55:42Z",
  "task_id": "task-2571",
  "approval_mode": "dry-run" | "approved",
  "stash_count_before": 12,
  "stash_count_after": 11,
  "decisions": [
    {
      "index": 0,
      "source": "pre-task",
      "task_id": "task-2571",
      "reason": "pre-task 사전 dirty 격리",
      "policy": "auto-pop",
      "action": "popped" | "dry-run-pop" | "preserved" | "dropped" | "dry-run-drop" | "skipped" | "failed",
      "exit_code": 0
    }
  ],
  "skipped_unknown_count": 35,
  "notes": "unknown 35건 preserved — manual review required"
}
```

### 4.3 timezone 정책

- 모든 timestamp 는 **timezone-aware UTC** (`datetime.now(timezone.utc).isoformat()`)
- naive `datetime.now()` 사용 금지 (Gemini Medium #2 carry-over 정합)

---

## 5. Magic Number 상수화

다음 magic number 들은 named constant 로 정의한다 (Gemini Medium #3 carry-over):

| 상수 | 값 | 위치 | 의미 |
|------|----|----|------|
| `STASH_WARN_THRESHOLD` | `5` | finish-task.sh | 5개 초과 시 WARN 로그 |
| `STASH_QUARANTINE_HARD_LIMIT` | `100` | finish-task.sh | 100개 초과 시 추가 진입 차단 (안전핀) |
| `STASH_AUDIT_TIMEOUT_SEC` | `30` | finish-task.sh | stash_audit.py 호출 timeout |

---

## 6. TASK_ID 안전 전달 (Gemini Medium #1 carry-over)

`finish-task.sh` 내 `python3 -c "...$TASK_ID..."` 직접 shell interpolation 패턴은 **`python3 - "$TASK_ID" <<'PYEOF'` heredoc + `sys.argv` 패턴**으로 교체한다.

### Before (취약 — shell injection risk)
```bash
python3 -c "obj=json.load(sys.stdin); print(...'$TASK_ID'...)"
```

### After (안전)
```bash
python3 - "$TASK_ID" <<'PYEOF'
import sys
task_id = sys.argv[1]
# ... task_id 사용
PYEOF
```

이 정합화는 본 task 에서 신규/수정되는 python 인라인 블록 전체에 적용한다.

---

## 7. Guard #7 LOCAL_OPERATIONAL_PATCH 정합화 (Gemini Medium #4 carry-over)

### 7.1 현행 동작

`scripts/start_task_guard.py` 검증 #7:
- 메인 workspace HEAD == origin/main 이면 PASS
- 그 외 모두 FAIL (DIVERGED / AHEAD / BEHIND 구분 없음)

### 7.2 문제

LOCAL_OPERATIONAL_PATCH doctrine (회장 박제 2026-05-14):
- 메인 workspace 로컬 main 이 origin/main 보다 ahead 인 상태가 운영상 발생 가능
- 단, worktree 작업은 항상 `origin/main` 기반 fresh worktree 에서 수행
- 현재 guard #7 은 이 경우에도 FAIL → 정상 worktree 작업까지 차단 (false-positive)

### 7.3 refinement (BYPASS 아닌 판정 기준 정합화)

guard #7 의 결과를 다음 3-state 로 분기한다:

| 메인 HEAD vs origin/main | 결과 |
|--------------------------|------|
| `HEAD == origin/main` | **PASS** (현행과 동일) |
| `HEAD` 가 `origin/main` 의 ancestor (behind) | **FAIL** (이전과 동일 — fetch 권장) |
| `origin/main` 이 `HEAD` 의 ancestor (ahead-only, divergent 아님) | **PASS + WARN** (LOCAL_OPERATIONAL_PATCH 허용) |
| 그 외 (diverged) | **FAIL** (BYPASS 금지 doctrine 유지) |

### 7.4 BYPASS 금지

- `start_task_guard.py` 에 `--bypass` 플래그 추가 금지
- 환경변수 `GUARD_BYPASS=1` 등 무조건 통과 메커니즘 도입 금지
- 위 3-state 분기는 git history 기반 사실 판정 (`git merge-base --is-ancestor`)이며, doctrine 에 의한 정합화이지 BYPASS 가 아님

---

## 8. 60개 stash 실제 cleanup 정책 (본 task 범위 외)

- 본 task 는 **spec + 분기 박제 + dry-run** 만 수행
- task-2570 분류 결과 60-stash 의 **실제 cleanup 실행** 은 별도 task / 회장 사전 승인 필요
- 특히 unknown 35건은 **drop 절대 금지** (manual review 후 처리)

---

## 9. 호환성 / 회귀 시나리오

| 시나리오 | 기대 동작 |
|---------|----------|
| `FINISH_TASK_STASH_APPROVE` 미설정 (기존 동작) | dry-run only, stash 상태 변경 없음 |
| metadata 미존재 legacy stash | `source=unknown` → preserve+log |
| `pre-task` stash 만 존재 (정상 path) | `APPROVE=1` 시 auto-pop, 미설정 시 dry-run |
| `finish-task` quarantine stash 가 다른 task 의 것 | task_id 불일치로 preserve |
| 60개 stash 누적 | WARN + audit log, 실제 cleanup 은 explicit indices 지정만 |

---

## 10. 구현 참조

- 진단 도구 (read-only): `scripts/stash_audit.py` (task-2570 — 본 task 에서 수정 금지)
- 진단 spec: `memory/specs/stash-origin-audit.md`
- lifecycle 박제: `scripts/finish-task.sh` (본 task 에서 수정)
- guard #7 정합화: `scripts/start_task_guard.py` (본 task 에서 수정)
- audit log: `memory/events/stash-lifecycle-action.<ts>.json`
- regression: `tests/regression/test_stash_lifecycle_*.py`

---

## 11. 관련 doctrine

### D-1. Read-only Default
파괴적 동작은 explicit approval 없이 절대 실행하지 않는다.

### D-2. Unknown Preserve
출처 미상 stash 는 자동 cleanup 금지. quarantine + manual review.

### D-3. LOCAL_OPERATIONAL_PATCH
local main 이 origin/main 보다 ahead-only 인 상태는 정상 운영 상태로 인정. divergent 는 여전히 차단.

### D-4. Audit-First
모든 lifecycle action 은 audit log 박제 후 수행. log 작성 실패 시 action 중단.
