---
task_id: task-2399
type: context
scope: task
created: 2026-05-03
updated: 2026-05-03
status: completed
---

# 맥락 노트: task-2399

**task**: task-2399

---

## 결정 근거

### 결정 1: Bug #1 근본 원인 = 상대 경로 + cwd 미지정
- 진단 결과: `task-timers.json`의 `task_file` 필드는 `memory/tasks/task-NNNN.md` (상대 경로)
- 스크립트 line 264, 338에서 `[[ -f "$TASK_FILE" ]]` 검사 시 **cwd가 /home/jay/workspace가 아닌 경우 실패**
- 실측 확인: cwd=/home/jay/.cokacdir/workspace/CA2F1BFC에서 `[[ -f "memory/tasks/task-2389.md" ]]` → MISSING. systemd 기본 cwd=/ 동일.
- **fix**: 스크립트 시작부에 `cd "$WORKSPACE"` 추가 + 안전책으로 line 264 검사 시 absolute fallback (`${WORKSPACE}/${TASK_FILE}`도 검사)
- 대안 (기각): task-timers.json의 모든 task_file을 absolute로 마이그레이션 → forbidden_paths 위반 + 쓰기 측 코드(dispatch.py 등) 수정 필요

### 결정 2: AND 조건 죽음 판정 (회장 핵심 통찰 — 버그 #6)
- (a) PID 없음 + (b) 진행 마커 부재 + (c) events mtime 노후 + (d) PR/worktree 정지
- **모두 충족시만 stalled 판정**. 1개라도 진행 신호 → `alive (long-running)`
- 대안 (기각): heartbeat-only / PID-only — 회장 직접 비판한 false stalled 원인

### 결정 3: 한 task = 한 알람 (버그 #2)
- `add_stalled <task_id> <reason>` 헬퍼 함수로 통합. 함수 내부에서 동일 task 재push 차단.
- no-taskfile은 stalled-alert-only의 sub-cause로 reason 필드에 통합

### 결정 4: design 작업 30분 / code 작업 10분 차등 (버그 #4)
- 1차: task-timers.json `team_id` 필드 검사 (`design` 또는 `team_id == "design"` → 1800초)
- 2차: task 파일 frontmatter (`team:` 필드) 백업 검사
- 명시 부재 시 **보수적 1800초** (false alert 방지 우선)

### 결정 5: long-running 마커 화이트리스트 (버그 #7)
- 5종 마커 in `memory/events/`: `<tid>.codex-gate`, `<tid>.qc-done`, `<tid>.done.merging`, `<tid>.pr-creating`, `<tid>.external-running`
- 어느 하나라도 존재하면 heartbeat 무관 alive 판정
- 신규 마커(pr-creating, external-running)는 watchdog만 인식 (생성 코드는 별도 작업)

### 결정 6: PR/worktree 교차 검증 (버그 #9)
- PID 없음 + 마커 부재 + heartbeat 노후 시 마지막 검증
- `gh pr list --search "<task_id>" --state all --limit 1` (출력 있으면 alive)
- `git -C "$WORKSPACE" worktree list` 결과에 task_id 포함 시 alive
- gh/git 미설치/실패 시 graceful fallback (false alert 방지 우선 = alive 처리)

### 결정 7: escalate 마커 알람 억제 (버그 #3)
- `memory/events/<task_id>.escalate` 존재 → watchdog 사이클에서 skip (알람 X)
- `<task>.escalate.acked` 존재 → 정상 처리 재개
- **자동 acked 금지** (회장 승인 전용)

### 결정 8: 알람 텔레그램 본문 디버깅 (버그 #5)
- 본문 형식: `<tid>(team=<team>, reason=<r>, taskfile=<y/n>, escalate=<y/n>, hb_age=<sec>s, markers=<list>)`

## 3 Step Why 자문 결과

- **1st Why**: 왜 이 설계가 필요한가? → A: 회장이 명시적으로 "와치독은 봇이 죽어있는걸 찾아내는 기능이어야 하는데, 자꾸 false alert로 노이즈를 발생시킨다. 개판칠거면 하지마" 강한 명시. 4건/사이클 false alert 발생 중. 노이즈 0건 + "있는 그대로" 검출이 절실.
- **2nd Why**: 왜 AND 조건 죽음 판정이 최선의 접근인가? → B: heartbeat-only 또는 PID-only 단일 신호는 long-running 단계(PR 생성/Codex 호출/G3 검증)를 죽음으로 오판. 회장 통찰: "엄연히 작업으로 돌고있는데 자꾸 와치독이 버그로 뜨니까 열받는거지". AND 조건은 모든 진행 신호가 동시에 사라져야만 죽음 판정 → false positive 본질적 차단.
- **3rd Why**: 왜 AND 조건이 다른 대안(heartbeat 노후 시간 늘리기, 알람 throttle)보다 나은가? → C: ① 시간 늘리기는 진짜 죽음 검출 지연을 야기 (회장 의도 위반: "봇이 진짜 죽었는가" 검출 기능). ② throttle은 노이즈 감추기일 뿐 근본 원인 무해결. AND 조건만이 "있는 그대로" + "진짜 죽음 검출" 두 목표를 동시 만족.

A-B-C 일관성 확인: ✅ 회장 의도(노이즈 0 + 진짜 죽음 검출)와 설계(AND 조건)가 직선 연결.

## 참조 자료

- 회장 명시: `memory/tasks/task-2399.md` (2026-05-03)
- 기존 watchdog 작업: `memory/meetings/2026-04-12-session-watchdog-v2.md`
- 회장 4 목표 #1: 무오류 (시스템 인프라 직접)

## 주의사항

- forbidden_paths **절대 수정 금지**: dispatch.py, bot_status_resolver.py, done-watcher.py, finish-task.sh, scripts/session_watchdog.py, scripts/orphan-watchdog.py, scripts/bot-status-watchdog.py 등
- 새로운 watchdog 스크립트 도입 금지 (session-watchdog.sh 단일 파일만 수정)
- bash 5.0+ 호환 (set -euo pipefail 유지)
- shellcheck 0 error 필수
- 회장 명시: "개판칠거면 하지마" → 부분 수정 X, 완전 검증 후 deploy
