# task-2959 · done-watcher 성공 완료 알림 + 데몬 상시 가동

- **팀**: dev3-team (다그다)
- **작업일**: 2026-08-15
- **PR**: #270 (https://github.com/Jeon-Jonghyuk/dev_workspace/pull/270) — **OPEN · 머지 ANU 독립검증 대기**
- **커밋**: `bc6b87da` (원격, origin/main tip `4786030` 기준) / 워크트리 `e350b3f5`
- **변경 규모**: +880 / −0 (6파일, 순수 추가)

---

## S — 상황

봇 task 완료 시 회장·ANU가 실시간 알림을 받지 못해 매번 수동 폴링해야 했다(task-2952·2953·2955 반복). 원본 지시서는 원인을 ①done-watcher 데몬이 2026-06-18부터 미가동 ②성공 완료 경로 자체가 부재, 두 가지로 지목했다.

## C — 문제

조사 결과 **전제 ①이 부정확**했고, 진짜 원인은 따로 있었다.

`journalctl` 실측: `done-watcher.service`(shell 판 `done-watcher.sh`, oneshot + 30초 timer)는 **오늘 12:35:29까지 30초마다 정상 가동 중**이었다. 죽어 있던 것은 python 판 `done-watcher.py --daemon` 쪽이다.

그런데 shell 판도 알림을 못 보내고 있었다. `.env.keys`가 **79줄 중 58줄이 `export VAR=value` 형식**인데 systemd `EnvironmentFile=`은 이 형식을 파싱하지 못한다. journal에 `Ignoring invalid environment assignment 'export ...'`가 전량 기록되고, 결국 `COKACDIR_CHAT_ID: unbound variable`로 알림 단계가 깨졌다.

즉 **근본 원인은 `export` 접두 하나**이며, 같은 함정이 `activity-watcher.service`도 크래시 루프에 빠뜨려 놓았다(`Environment=`가 실제로 빈 값임을 확인).

## Q — 핵심 질문

기존 FAIL 알림·idle 전환을 건드리지 않고 성공 완료 알림을 붙이되, **과거 백로그를 소급 발송하지 않고** 데몬을 재부팅까지 살려둘 수 있는가?

## A — 해결

`scripts/done-watcher.py`에 성공 알림 경로를 **순수 추가**(기존 3개 함수 무변경)하고, systemd user service로 상시 가동했다.

### 안전장치 5중

| 장치 | 목적 | 실환경 실측 |
|---|---|---|
| 워터마크 부트스트랩 | 과거 백로그 소급 발송 차단 | `백로그 202건 스킵`, 알림 **0건** |
| `.done.notified` O_EXCL 마커 | 중복 알림 방지(원자적) | 다중 주기 후 전송 **1회** |
| 실패건 제외 | FAIL을 완료로 오보하지 않음 | pytest 4케이스 |
| 주기당 상한 5건 | 만일의 폭주 차단 | pytest(7건 → 5+2) |
| 킬스위치 `DONE_WATCHER_SUCCESS_NOTIFY=0` | 즉시 무력화 | pytest |

**워터마크가 이 작업의 핵심이다.** 가드 없이 기동했다면 `.done.notified`가 없는 과거 `.done` **202건**(최고령 2026-05-05)이 회장 챗에 한꺼번에 쏟아졌다.

---

## 팀장 실증 검토에서 적발한 결함 3건

구현 자체는 테스트를 통과했으나, **실제 리포트 7건에 돌려보니 결함이 나왔다.**

1. **★ PR 번호 오탐 (심각 — 허위정보 발송)**
   `task-2953` 메시지가 `PR#213`으로 렌더됐다. **실제 PR은 #214**다.
   원인: 해당 리포트 13행의 `선행 task-2952(PR #213 MERGED)` — **선행 task의 PR 번호**를 느슨한 `#(\d{2,4})`가 포착했다. task-2953 자신의 PR은 head 40줄에 아예 없었다.
   조치: `PR\s*#?\s*(\d+)`/`pull/(\d+)`로 한정 + **같은 줄에 다른 task-ID가 있으면 배제** + 후보가 2개 이상이면 **생략**(틀린 번호보다 없는 편이 낫다).
2. `PR 0건`(0건 수행했다는 서술)이 `PR#0`으로 포착 → `(?!\s*건)` 추가 *(루 자체 발견)*
3. `PR#212~#216` 범위 표기가 후보를 오염 → 범위 표기 줄 배제 *(루 자체 발견)*

수정 후 재실증: task-2953은 PR 생략, task-2958/2957/2955는 실제 PR(#219/#218/#216) 정상 유지.

## 운영 중 발견·차단한 사고 2건

1. **ANU 후속 폴백 118건 일괄 발사 위험** — 데몬을 그냥 켜면 `fire_anu_followup_fallback()`이 5분 이상 묵은 `.done` 마다 `extract_followup.py send`를 쏜다. 대상이 **118건**이었다. 데몬이 죽어 있던 동안 어차피 미동작했으므로 `DISABLE_ANU_FOLLOWUP=1`로 현상 유지. **백필 여부는 ANU/회장 판단 필요**(아래 결정 요청).
2. **★ 기존 서비스 덮어쓰기 (팀장 실수 → 원복 완료)** — 처음에 신규 유닛을 `done-watcher.service` 이름으로 작성했는데, **같은 이름의 살아있는 유닛이 이미 있었다**(shell 판 구동). 사전 확인 없이 덮어쓴 것은 명백한 절차 위반이다. 발견 즉시 원 정의를 복원하고, 신규 데몬은 **`done-watcher-success.service`** 로 분리했다. 데이터 손상 여부 확인: 멈춘 `.done.processing` **0건**.

---

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|---|---|---|---|
| scripts/done-watcher.py | 성공 완료 알림 경로 순수 추가(+248), 기존 3함수 무변경 | grep "process_success_done_files" OK | 완료 |
| scripts/done-watcher.py | 워터마크 부트스트랩으로 백로그 202건 소급 발송 차단 | grep "_load_or_bootstrap_watermark" OK | 완료 |
| scripts/done-watcher-daemon.sh | systemd 래퍼 신규 — .env.keys source(EnvironmentFile 함정 회피) | grep "ANU_BOT_TOKEN" OK | 완료 |
| scripts/tests/test_done_watcher_success_notify.py | 신규 테스트 16건(부트스트랩·중복·상한·PR오탐 회귀) | grep "MAX_NOTIFY_PER_CYCLE" OK | 완료 |

---

## L1 스모크테스트

- **서버(데몬) 재시작**: 성공 — `done-watcher-success.service` `active (running)`, PID 128012
- **API 응답 확인**: 실제 Telegram 왕복 성공
  - 12:39:41 더미 `memory/events/task-TEST2959.done` 생성
  - 12:39:43 `[SUCCESS-NOTIFY] task-TEST2959.done: Telegram 알림 전송 시도`
  - 12:39:44 `[SUCCESS-NOTIFY] ... 알림 전송 완료 → task-TEST2959.done.notified` (Telegram API 200)
  - `.done` 원본 보존 확인, 마커 내용 `{"task_id":"task-TEST2959","notified_at":"2026-08-15T12:39:44...","notifier":"done-watcher"}`
  - 다중 주기 경과 후 **전송 시도 1회** = 중복 없음
  - 테스트 마커 2개 정리 완료(잔여 0)
- **스크린샷**: 해당없음(백엔드 데몬 · CLI/로그로 검증). 회장 챗 수신 화면은 회장님 단말에서 확인 가능
- **크래시 복구**: `kill -9` → PID 128012 → 133196 자동 재기동 (`Restart=always`)
- **재부팅 생존**: `is-enabled=enabled` + `loginctl Linger=yes`

---

## 검증 결과

- **pytest 79/79 PASS** (신규 16 + 기존 회귀 63: `test_done_watcher.py`·`test_done_protocol.py`·`test_notify_completion.py`)
- **diff 순수 추가** — 삭제 줄 0. `process_done_files`(idle 전환)·`process_failed_files`(FAIL 알림)·`fire_anu_followup_fallback` **무변경**
- **red-team critical 6건 = 전부 오탐** — 전량 `re.compile` 패턴. 이미 머지된 `validate-output-structure.py`(2건)·`safe_cron_dispatch.py`(2건)·`gemini_evidence_verify.py`(7건)에서 동일 재현 확인 → **net-new 0**
- **code-validator Execution Test 실패 = 사전 존재** — 미수정 원본 파일에서도 동일 재현(검증기가 `done-watcher.py test`로 호출, argparse가 거부). 본 작업 무관

### trip-wire 5종
Critical7 net-new **0** · PII net-new **0** · 회귀 실패 **0**(79/79) · forbidden_paths 침범 **0** · nonce=task-2959 일치

---

## 미완 / 결정 요청

1. **PR #270 머지** — `phase3-merge-gate` failure. 이 게이트는 `gemini_review_gate.py` 별칭이며 Gemini Code Assist sunset 상태다(기존 워크스페이스 문서화 사항). **머지 판정은 ANU 독립검증 소관**이라 팀장 단독 머지하지 않았다.
2. **ANU 후속 폴백 118건** — 재활성 시 과거분 일괄 발사. 선택지: (a) 현행 비활성 유지 (b) `.anu-notified` 마커 백필 후 재활성. **(b)는 "ANU에 통지했다"는 허위 상태를 디스크에 쓰는 것이라 권장하지 않는다.**
3. **`export` 접두 함정 별건 2개** — 같은 근본 원인으로 여전히 깨져 있음. 본 task 범위 밖이라 손대지 않았다.
   - `activity-watcher.service` 크래시 루프 (성공 완료 알림 로직을 이미 갖고 있으나 기동 실패)
   - `done-watcher.sh` 의 `COKACDIR_CHAT_ID: unbound variable` (에스컬레이션 분기)
   → 별도 task 권장. 단, **본 task의 신규 경로와 이중 알림이 나지 않도록** 반드시 `.done.notified` 마커 공유 여부를 함께 검토해야 한다.
4. **라이브 배포 방식** — 데몬이 즉시 동작해야 해서 `scripts/done-watcher.py`·`done-watcher-daemon.sh`를 워크스페이스 작업트리에 직접 배치했다(PR 내용과 sha256 일치 확인). 머지 후 정합.

---

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| 루(Lugh) | 백엔드 구현 · PR 오탐 수정 · gh api 발행 | sonnet | 코딩 작업 |
| 모리건(Morrigan) | pytest 16건 작성 | sonnet | 테스트 |
| 아네(Aine) | 3문서 작성 | sonnet | 문서(haiku 금지 대상) |
| 다그다(팀장) | 설계·검토·실증 대조·운영 조치 | opus | 직접 코딩 없음 |

haiku 미사용.

---

## 산출물

- `scripts/done-watcher.py` (+248)
- `scripts/done-watcher-daemon.sh` (신규, 100755)
- `scripts/tests/test_done_watcher_success_notify.py` (신규 +401, 16 테스트)
- `memory/plans/tasks/task-2959/{plan,context-notes,checklist}.md`
- `~/.config/systemd/user/done-watcher-success.service` (리포 외부)

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

