# task-2988 — InsuRo 백엔드 배포 원자화: 워킹트리 직접 참조 제거 + release 고정

- **레벨**: Lv.3 · **팀**: dev7-team (이참나) · **작성**: 2026-08-20
- **상태**: 구현·검증 완료 / **라이브 전환은 ANU 승인 대기** (임의 전환하지 않음)
- **브랜치**: `task/task-2988-dev7` · **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2988-dev7`

---

## S — 상황 (Situation)

라이브 `insuro-api.service` 가 git **워킹트리를 직접 참조**해 구동된다
(`WorkingDirectory=/home/jay/projects/InsuRo/server`, `Restart=always`, `RestartSec=5`).
2026-08-20, 디스크 HEAD 는 `74679f2`(미검증 3,015줄 포함)인데 구동 프로세스는 `49c17a9` 인 상태가
16시간 넘게 지속됐다. **크래시가 곧 배포 버튼**인 상태였다. ANU 가 디스크를 `49c17a9` 로
detached 고정해 임시 차단해 둔 것이 현재 상태다.

## C — 갈등 (Complication)

결함은 배포 계층에 셋이 겹쳐 있다. (1) 실행 아티팩트가 불변 산출물이 아니라 가변 폴더다.
(2) `git pull` 과 재기동이 원자적으로 묶여 있지 않다. (3) `Restart=always` 가 크래시를
비통제 배포 트리거로 만든다. 동시에 서비스는 **가동 중이며 죽으면 안 된다** —
즉 "사고를 막는 변경" 자체가 새로운 사고가 되어선 안 된다.

## Q — 과제 (Question)

워킹트리 참조를 끊고 배포를 원자화하되, **작업 중 무중단을 유지**하고,
되돌리기 어려운 조작(유닛 교체·symlink 전환)은 백업과 롤백 절차를 갖춘 뒤 승인받아 수행한다.

## A — 답 (Answer)

`git archive <sha>` 로 **객체DB에서 추출한 불변 release** 를 만들고 `current` symlink 를
`rename(2)` 로 **원자 교체**한다. 서비스는 `current` 만 바라본다.

```
$RELEASES_ROOT/
  shared/.env               # 비밀값 단일 소스 (release 마다 복사하지 않음)
  releases/<sha>/           # 불변·읽기전용 (server/ config/ extension/ + .env symlink + RELEASE_ENV)
  current  -> releases/<sha>
  previous -> releases/<sha>
```

`git archive` 를 쓴 이유가 핵심이다. 워킹트리를 복사하면 "그 폴더에 그때 있던 것"이 굳는데,
그게 바로 이번 사고의 본질이다. 객체DB 추출은 release 내용 == 그 커밋임을 **구조적으로** 보장한다.
(실측: 추출된 `server/main.py` 블롭 sha `332086fb…` = 커밋의 블롭 sha, `extension/manifest.json`
블롭 sha `bbc2bed8…` 일치)

### P1~P4 달성
| 목표 | 달성 방법 | 근거 |
|---|---|---|
| P1 워킹트리 미참조 | `WorkingDirectory=$RELEASES_ROOT/current/server` | 유닛 템플릿 · V1 실측 |
| P2 배포 원자화 | 추출·검증 전부 성공 후 **마지막에만** symlink 교체 | V2 실측 (실패 시 미전환) |
| P3 크래시 안전 | 재시작이 `current`(승인된 release) 만 로드 | **V4 실측** |
| P4 안전장치 | 헬스체크 실패 시 자동 롤백 · release 3개 보관 | V3 · V5 실측 |

---

## 수정 파일별 검증 상태

| 파일 | 줄수 | 역할 | 검증 방법 | status |
|---|---|---|---|---|
| /home/jay/projects/InsuRo/.worktrees/task-2988-dev7/ops/deploy.conf | 107 | 단일 설정 소스(하드코딩 제거) | grep ARCHIVE_PATHS/HEALTH_REQUIRE_DB 확인 + 실배포 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2988-dev7/scripts/deploy/insuro-deploy.sh | 749 | 원자적 배포·헬스체크·자동롤백·무결성 | bash -n + shellcheck + 격리 실배포 6종 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2988-dev7/scripts/deploy/insuro-rollback.sh | 227 | 원자적 롤백 | bash -n + 실제 롤백 실행 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2988-dev7/ops/insuro-api.service.new | 89 | 유닛 교체안 템플릿(미설치) | 라이브 유닛과 diff 주석 대조 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2988-dev7/docs/deploy/README.md | 403 | 런북(배포·롤백·전환·함정) | 절차 항목 존재 확인 | verified |

합계 **1,575줄 신규**. 애플리케이션 소스(`src/**`, `server/**`) 변경 **0줄**.

---

## 검증 5종 — 전부 스테이징 실측

라이브(8001)를 건드리지 않기 위해 **포트 8002 스테이징 인스턴스**(`Restart=always`·`RestartSec=5` 동일)에서 수행했다.

| # | 검증 | 결과 | 실측 증거 |
|---|---|---|---|
| V1 | 지정 sha 배포 → sha 일치 | PASS | `sha=1c4e4dde…`, `sha_source=env`, `ready=true` |
| V2 | 중간 실패 주입 → 미전환 | PASS | `--fail-at=verify`, `readlink -f current` 전후 동일, orphan tmp 없음 |
| V3 | 롤백 실습 | PASS | `6de797b` → 롤백 → `/api/status.sha` = `1c4e4dd` 실측 |
| V4 | **크래시 시뮬레이션** | PASS | 아래 별도 서술 |
| V4-b | 새 release 존재·current 불변 시 크래시 | PASS | `d8ee3e6` 존재하나 재기동 후에도 `74679f2` 보고 |
| V5 | 헬스체크 실패 → 자동 롤백 | PASS | symlink 실제 스왑 후 자동 복귀, sha `74679f2` 실측 복귀 |
| V6 | 읽기전용 잠금 + 무결성 | PASS | 쓰기/삭제 전부 Permission denied, `--verify-integrity` 통과 |

### V4 — 사고 재현 시험 (가장 중요)
사고 당시 구도는 "워킹트리 sha ≠ 구동 sha, 크래시 시 워킹트리로 수렴"이었다.
이 구도는 **이미 존재**하므로 워킹트리를 건드릴 필요가 없었다 —
워킹트리는 `49c17a9` 로 고정된 채, 스테이징에 **다른 sha `74679f2e`** 를 배포했다.

- `kill -9 <스테이징 MainPID 3634916>` → 새 PID `3636109`, `NRestarts` 0→1 (실제 크래시-재시작 발생 확인)
- 재기동 후 `/api/status.sha` = **`74679f2e`** — 워킹트리 sha `49c17a9` 로 **수렴하지 않음**

즉 **동일 조건에서 사고가 재현되지 않는다.** 이것이 P3 의 실증이다.

### 라이브 무중단 증거
| 시점 | MainPID | NRestarts | uptime_sec | sha |
|---|---|---|---|---|
| 검증 전 | 1085 | 0 | 61,159 | 49c17a9 |
| 검증 중(크래시 시험 직후) | 1085 | 0 | — | 49c17a9 |
| 검증 종료 시점 | 1085 | 0 | 62,221 | 49c17a9 |
| 보고서 작성 중(19:01:25) | **3838584** | 0 | 재기동 | **deeaf84** ← 타팀 재기동, 본 태스크 무관 |

`ActiveEnterTimestamp` 는 `2026-08-20 01:27:53 KST` 로 전 구간 불변. 재시작 0회.

---

## L1 스모크테스트

- **서버 재시작**: 성공 — 단 **스테이징 인스턴스(포트 8002)** 에 한함.
  라이브(8001)는 무중단 요구에 따라 **의도적으로 재시작하지 않음**(전환은 ANU 승인 후 1회).
- **API 응답 확인**: 실제 curl 실행. 스테이징 `/api/status` 원문:
  ```json
  {"status":"ok","sha":"1c4e4dde762bdfabad09fcb70bb2719ea9702451","sha_short":"1c4e4dd",
   "sha_source":"env","ready":true,"db":"unconfigured","service_role_key_present":false,
   "build_time":"2026-08-20T09:28:22Z","uptime_sec":7}
  ```
  라이브 `/api/status` (무중단 확인용): `sha=49c17a9`, `db=ok`, `ready=true`, `uptime_sec=62221`
- **스크린샷**: 해당없음 (백엔드 배포 인프라 작업, UI 변경 0건)

`db":"unconfigured"` 는 정상이다 — 스테이징은 프로덕션 쓰기를 막으려 자격증명을 의도적으로 비웠다(아래 참조).

---

## 발견 이슈 및 해결

### 이슈 1 (HIGH) — `extension/` 페이로드 누락 → 실사용 엔드포인트 장애 유발
`RELEASE_ENV` 가 `INSURO_EXTENSION_PATH=<release>/extension` 을 주입하는데 `ARCHIVE_PATHS` 는
`"server config"` 뿐이라 그 경로가 존재하지 않았다. `/api/insuro/composite-design/extension-version`
은 **최근 24시간 라이브 로그에 70건** 찍히는 실사용 엔드포인트다.
그대로 전환했다면 **release 구조가 없던 장애를 새로 만들었다.**
→ 해결: `ARCHIVE_PATHS="server config extension"` + verify 단계에 **`*_PATH` 키 실존 검사** 추가
(특정 변수명 하드코딩 없이 네이밍 패턴 전체를 커버해 동종 재발을 구조적으로 차단).
재검증: 고의로 `extension` 을 빼면 배포가 거부되고 `current` 는 불변.

### 이슈 2 (MEDIUM) — 드릴이 스스로를 배신함
`--fail-at=healthcheck` 드릴에서 자동 롤백은 **실제로 성공**하는데 스크립트는
"자동 롤백도 실패"로 오보했다. `FAIL_AT` 이 전역이라 `attempt_rollback()` 내부의 확인용
`healthcheck()` 에도 주입이 재발동한 탓이다. 드릴의 목적이 "롤백이 되는지" 확인인데
결과가 항상 실패로 나오면 **진짜 롤백 실패와 구별할 수 없다.**
→ 해결: 주입을 **one-shot** 으로 변경(발동 즉시 소거). 이제 "롤백 성공" 을 정확히 보고한다.

### 이슈 3 (설계 결함, 격리 프로브로 발견) — symlink 만으로는 부족하다
팀장이 InsuRo 와 무관한 일회용 유닛으로 systemd symlink 동작을 선실증한 결과,
**release 디렉토리 내용물을 변조하면 크래시 재시작이 변조본을 그대로 집었다.**
symlink 는 "어느 디렉토리"만 고정할 뿐 "그 안의 내용"은 고정하지 않는다 —
이번 사고와 **구조가 동일한 사고가 release 계층에서 재발** 가능하다는 뜻이다.
→ 해결: release 확정 직후 `chmod -R a-w` 읽기전용 잠금 + `RELEASE_META.json` 에
`manifest_sha256` 기록 + `--verify-integrity` 변조 탐지. 정리 로직은 `u+w` 해제 후 삭제하도록 보정.
(런타임 write 경로가 전부 `/tmp` 임이 사전 확인되어 읽기전용이 안전함)

### 이슈 4 (MEDIUM) — 헬스체크가 DB 단절을 통과시킴
성공 조건이 `db != "down" && db != "error"` 라는 **부정 목록**이라 `db="unconfigured"`
(자격증명 유실)도 배포 성공으로 판정됐다.
→ 해결: `db == "ok"` **화이트리스트** + `HEALTH_REQUIRE_DB` 설정 분리(라이브 1 / 스테이징 0).
`sha` 일치와 `ready` 는 두 모드 모두 항상 필수.

### 이슈 5 (자체 발견·해결) — 무결성 해시가 항상 깨짐
verify 단계의 `py_compile` 이 `__pycache__/*.pyc` 를 생성해 저장 해시와 실제 콘텐츠가 늘 어긋났다.
→ `PYTHONDONTWRITEBYTECODE=1` + `__pycache__` 청소 + 해시 계산 시점 이동으로 해결.

### 이슈 6 (사전 차단) — 스테이징이 프로덕션에 쓸 뻔함
`server/main.py:265` startup 훅이 `trend_keywords` 상태에 따라 **프로덕션 Supabase 쓰기 +
네이버 API 실호출** 스레드를 띄운다. 검증 전 감사에서 잡아, 스테이징 `.env` 에서
`INSURO_NEW_SERVICE_ROLE_KEY`·`POSTHOG_API_KEY` 를 비워 fail-closed 시켰다.
비밀값 유출 검사 결과 스테이징 env 에 실제 자격증명 **0건**.

---

## 미해결 / 판단 필요

1. **라이브 전환 미실행** — 태스크 지시대로 ANU 승인 전에는 전환하지 않았다. 절차는 런북 §3에 있다.
2. **`RELEASES_ROOT` 경로 확정 필요** — 기본값 `/home/jay/insuro-releases`. ANU 승인 시 확정.
3. **디스크 여유 2.5G (루트 100%)** — release 1개 5.9M, 3개 보관 시 약 18M 로 부담은 없으나
   루트 자체가 포화 상태다. `.worktrees` 4.8G 가 주원인이며 정리는 회장 승인 사항이라 범위 밖.
4. **`insuro-trend-collector.service`(현재 masked)** 가 여전히 낡은 워킹트리 경로를 가리킨다.
   재활성화하는 사람이 release 구조를 모른 채 쓸 수 있어 런북에 기록해 두었다.

---

## ★★★ CRITICAL — 작업 중 동일 사고가 재발했다 (ANU 즉시 판단 요청)

PR 생성 절차 중 라이브 워킹트리가 **ANU 의 고정에서 이탈**한 것을 발견했다.

```
git -C /home/jay/projects/InsuRo reflog --date=iso
deeaf84 HEAD@{2026-08-20 18:50:38}: checkout: moving from 49c17a9... to deeaf84
49c17a9 HEAD@{2026-08-20 17:49:51}: checkout: moving from main to 49c17a9   ← ANU 임시 차단
```

| 항목 | 값 |
|---|---|
| 디스크 HEAD | deeaf84 (task-2986 머지, 부모 74679f2 = 미검증 워크벤치 3,015줄 포함) |
| 구동 프로세스 | 49c17a9 → **19:01:25 재기동으로 deeaf84 로 교체됨** |
| 재시작 정책 | Restart=always · RestartSec=5 |

### ★ 그리고 실제로 배포가 일어났다 (19:01:25)
보고서 작성 중 라이브 서비스가 **재기동되어 `deeaf84` 로 교체**됐다.

```
19:01:25 systemd[920]: Stopping insuro-api.service ...
19:01:25 systemd[920]: Stopped insuro-api.service.
19:01:29 systemd[920]: Started insuro-api.service.
19:01:32 uvicorn[3838584]: Application startup complete.
```
- 이전: MainPID **1085** · sha **49c17a9** · uptime 17시간
- 현재: MainPID **3838584** · sha **deeaf84** · ActiveEnterTimestamp 2026-08-20 19:01:29

크래시가 아니라 **의도적인 stop→start** 다(systemd 로그가 "Stopping"으로 시작).
직후 로그에 `POST /api/v1/silson/calculator/estimate` 호출이 연속으로 찍히는 것으로 보아,
**task-2986(실손계산기) 측이 자기 변경을 활성화하려고 재기동**한 것으로 보인다.
본 태스크는 `insuro-api` 에 대해 systemctl 을 **한 번도 실행하지 않았다**(전 명령이 기록에 남아 있음).

### 현재 서비스는 정상이다 (과장하지 않음)
- `db` 는 재기동 직후 `timeout` 이었으나 **`ok` 로 회복**됐다(콜드스타트 첫 ping 이슈, DB 에러 로그 0건)
- `ready=true`, `/api/status` 200 정상 응답
즉 **이번엔 결과적으로 사고가 나지 않았다.** 문제는 결과가 아니라 **과정**이다 —
누구도 승인하지 않은 시점에, 워킹트리에 있던 것이 그대로 라이브가 되는 경로가 실제로 작동했다.
`deeaf84` 의 조상 `74679f2` 에 포함된 **프로덕션 미검증 워크벤치 3,015줄**도 함께 라이브가 됐다.

### 원인 규명 (교차 확인 — 본 태스크가 유발한 것이 아님)
- checkout 시각 18:50:38 vs 본 태스크 `taskctl` 실행 18:59:54 — **9분 선행**
- `scripts/taskctl.py` 에 `checkout` 호출 **0건**. `cmd_commit()` 은 HEAD 를 **읽기만** 한다
- `deeaf846` 은 task-2986 의 머지 커밋(taskctl-bot, 18:46:51 작성)
→ 동시 진행 중이던 **다른 태스크의 정상 종결 플로우**가 라이브 워킹트리를 checkout 한 것으로 보인다

### 이 사건이 증명하는 것
"사람이 조심하면 된다"로는 막을 수 없음이 **실증**됐다. ANU 가 명시적으로 고정한 트리조차
1시간 만에 **다른 태스크의 정상 절차**에 의해 풀렸다. 비난할 대상이 없다는 것이 핵심이다 —
절차를 지켜도 뚫리는 구조라면 구조를 바꿔야 한다.

### 팀장 조치 및 판단
워킹트리를 **임의로 되돌리지 않았다**. 이유: (1) 다른 팀(task-2986)이 방금 사용한 트리라
되돌리면 그쪽 작업을 침범한다 (2) 라이브 서빙 디렉토리라 임의 조작 자체가 위험하다
(3) 본 태스크 지시가 "전환은 ANU 승인 후"다.

**권고**: 본 태스크의 전환(cutover)을 **승인해 주시기 바란다.** 근거 —
- 전환 후 서비스가 로드하는 것은 `current` → **현재 구동 중인 49c17a9 로 만든 release** 다.
  즉 **지금 도는 코드와 동일한 코드로 재기동**하는 것이라 코드 변화가 0이다.
- 전환이 끝나면 워킹트리가 무엇으로 바뀌든 라이브와 무관해진다 — 이 위험이 **구조적으로 소멸**한다.
- 실패 시 백업 유닛 파일 1개 복사로 즉시 원복된다(워킹트리는 그대로 남아 있음).
- 미승인 시 대안: 워킹트리를 `49c17a9` 로 재고정(임시방편, 또 풀릴 수 있음).


---

## 롤백 절차 (요약 — 상세는 런북 §2·§3-1)

**전환 실패 시 원상복구** (워킹트리는 이번 작업에서 전혀 건드리지 않았으므로 항상 되살아난다):
```bash
cp ~/.config/systemd/user/insuro-api.service.bak-<ts> ~/.config/systemd/user/insuro-api.service
systemctl --user daemon-reload && systemctl --user restart insuro-api.service
curl -s http://127.0.0.1:8001/api/status
```
백업 위치: `/home/jay/workspace/memory/backups/task-2988/insuro-api.service.orig` (+ 전환 시 타임스탬프 백업 별도 생성)

**release 롤백**: `insuro-rollback.sh` (직전 release 또는 특정 sha 지정, `--list` 로 목록 확인)

---

## 머지 판단
- **머지 필요**: Yes (배포 스크립트·문서만. 머지해도 **라이브 동작은 변하지 않는다** — 유닛 파일 교체 전까지 스크립트는 비활성)
- **브랜치**: `task/task-2988-dev7`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2988-dev7`
- **머지 의견**: 애플리케이션 소스 변경 0줄, 신규 파일만 추가. 검증 5종 실측 PASS.
  머지 자체는 무해하며, **위험한 단계는 머지가 아니라 유닛 파일 전환**이고 그것은 승인 대기 중이다.

## PR
- **PR #238** — https://github.com/Jeon-Jonghyuk/InsuRo/pull/238
- state OPEN · mergeable MERGEABLE · **+1,575 / −0** · 5 files
- 원격 head `42bc228b76df4ccef08db502d24c586a2a599ffc` = 로컬 head **일치**
- **CI 11/11 PASS** (pytest `ci` 6m26s pass · `e2e-test` pass · guard/qc-check/merge-safety 등 전부 pass)
- 변경 파일 전부 allowed_resources 범위 내(`scripts/deploy/**`, `ops/**`, `docs/deploy/**`).
  forbidden_paths(`src/**`, `server/**`, `.github/**`) 침범 **0건**

## QC 게이트 결과
| 항목 | 결과 | 비고 |
|---|---|---|
| three_docs_check | PASS | YAML 필수 필드 보정 후 |
| l1_smoketest_check | PASS | |
| spec_compliance / critical_gap / duplicate_check / data_integrity / planned_check | PASS | |
| scope_check | WARN | audit-trail 파싱 이슈 + 예상 파일수 차이(FAIL 불가 항목) |
| claude_md_check | WARN | 기존 상태 |
| file_check | FAIL | `.done` 미생성 — finish-task.sh 실행 시 해소 |
| git_evidence | FAIL | workspace 리포로 fallback 조회. 실제 커밋은 InsuRo 워크트리에 2건 존재(`e87b0bf`, `0172a05`, `42bc228`) |
| tdd_check | FAIL | **ANU 판단 요청** — 산출물이 shell 배포 스크립트라 pytest 대응물이 없다. 대신 스테이징 실서비스 검증 6종으로 대체했다. 비코드 해치 자가부여는 self-bypass 이므로 하지 않았다 |


## trip-wire 5종 (실측)
| 항목 | 실측치 | 근거 |
|---|---|---|
| Critical7 | 0 | shellcheck(warning) 신규 0건 + bash -n 통과. 애플리케이션 소스 0줄 변경(diff 확장자 conf/md/new/sh, Python 0건) |
| PII net-new | 0 | PR diff 1,605줄의 추가(+) 라인에 JWT/sk-/주민번호/supabase URL 패턴 스캔 → 0건. 스테이징 env 도 실제 자격증명 0건 |
| 회귀 실패 | 0 | PR #238 CI 11/11 pass (pytest `ci` job 6m26s) |
| forbidden_paths 침범 | 0 | `git diff --name-only` 에 `src/` `server/` `.github/` 매치 0건 |
| nonce | task-2988 | 발사된 task_id 와 일치 |

result.json: `/home/jay/workspace/memory/events/task-2988.result.json`


## 종결 상태 — .done 미생성 (ANU 판단 대기)

`finish-task.sh` 를 worktree 경로로 1회 foreground 실행했다 (`FINALIZE_ONLY=1`, `G4_GATE_ENABLED=0`).
결과: **QC Gate FAIL (재시도 1/3) → .done 미생성**. 단일 차단 사유는 `tdd_check` 다.
(1차 실행에서 FAIL 이던 `file_check`·`git_evidence`·`three_docs_check` 는 이번 실행에서 전부 해소됨)

### tdd_check 차단 사유와 ANU override 요청
```
변경 파일 총 5개: 테스트 0개, 구현 4개 → 테스트 파일 없이 구현 파일만 변경됨 → FAIL
IMPL: ops/deploy.conf, ops/insuro-api.service.new,
      scripts/deploy/insuro-deploy.sh, scripts/deploy/insuro-rollback.sh
```
산출물이 **bash 스크립트·systemd 유닛 템플릿·설정 파일**이라 pytest 대응물이 존재하지 않는다.
Python 코드 변경 0줄이다. `tdd_check` 는 파이썬 테스트 파일 존재 여부로 판정하므로
shell 산출물에 대해 **구조적으로 통과 불가**하다.

**자가해소하지 않았다.** 비코드 해치(`## 레벨: 코드 수정 없음`) 자가부여는 self-bypass 이고
부여권한은 ANU 에 있으며, 본 태스크는 실제로 파일을 만들었으므로 그 해치가 사실과도 맞지 않는다.
근거를 박제해 두었다: `/home/jay/workspace/memory/events/task-2988.tdd-override-request.json`

**보상 검증**: 스테이징 실서비스 6종 실측 + `bash -n` + shellcheck 신규 0건 + CI 11/11 pass.

### task-timer end 를 호출하지 않은 이유 (의도적)
`memory/task-timer.py end` 는 내부적으로 **`.done` 이벤트 파일을 생성**한다
(`task-timer.py:278` → `_write_event_file`). 지금 호출하면 방금 **QC 게이트가 막은 `.done` 을
우회 생성**하게 되어, 게이트 FAIL 상태를 완료로 위장하는 결과가 된다.

- 호출하지 않을 때의 비용: 대시보드상 dev7 이 "작업중"으로 남는 고스트 태스크(운영상 불편)
- 호출할 때의 비용: 실패한 게이트를 우회한 완료 선언 (CRITICAL 규칙 위반, 하위 완료 처리까지 연쇄)

후자가 명백히 더 나쁘므로 **타이머를 켜둔 채 ANU 판단을 기다린다.**
override 승인 시 `finish-task.sh` 재실행으로 `.done` 생성과 타이머 종료가 정상 경로로 함께 처리된다.

### ANU 에 요청하는 판단 2건
1. **[CRITICAL] 비승인 배포가 실제로 발생** (19:01:25, deeaf84 라이브화) — cutover 승인 여부 (위 CRITICAL 절 참조)
2. **[게이트] tdd_check override** — shell 산출물에 대한 구조적 오탐 인정 여부


## 모델 사용 기록
| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| 쿠쿨칸 | 백엔드(조사·구현·하드닝·결함수정) | sonnet | 코딩 작업 |
| 카마소츠 | 테스트/QA(안전성 감사·검증 5종) | sonnet | 분석·검증이라 haiku 미사용 |
| 이참나(팀장) | 설계·검토·통합·독립 교차검증 | opus | 직접 코딩 없음 |

haiku 사용 0건 — 전부 분석/인프라 작업이라 지침상 sonnet 이상 필요.

## 세션 통계
- 총 도구 호출: 0회

