# task-2387 — Phase β-2: session-watchdog.sh verdict 통합 + archived/escalated 영구 박제

- 작업 ID: task-2387
- 팀: dev3-team (다그다 팀장 / 루 백엔드 / 모리건 테스터)
- 작업 레벨: Lv.1 (운영 메타 인프라 보강)
- 완료일: 2026-05-03

## SCQA

### Situation
회장 마스터플랜 v1 목표 2 ("사람이 쓸 때 문제없이"). Phase β(task-2384)에서 `whisper-compile.py`/`auto_merge.py`에 verdict 신호등 단일 진실 소스 통합 완료. 그러나 `session-watchdog.sh`는 별도 흐름으로 `task-timers.json`의 `status="running"`만 직접 참조하여 verdict와 단절.

### Complication
- 회장이 task-2359/2360을 status=completed로 sync해도 어떤 retry 흐름이 다시 status=running으로 덮어쓰면서 watchdog stalled 알림 무한 루프 발생.
- `archived`/`escalated` 같은 영구 종료 status가 코드 레벨에서 보호되지 않아, dispatch retry/start 경로가 status를 운영자 의도와 무관하게 갱신.

### Question
session-watchdog.sh와 dispatch.py의 어떤 지점을 surgical 하게 수정하면 회장 stalled 알림을 영구 종료할 수 있는가?

### Answer
3개 fix를 surgical 적용:
1. **Fix 1** — session-watchdog.sh line 59 jq 쿼리에 `archived/escalated` 명시 제외 필터 추가.
2. **Fix 2** — dispatch.py 상단(`_patch_timer_metadata` 다음)에 `_set_task_status(task_id, new_status) -> bool` 가드 함수 신규 추가. 모든 task-timer start subprocess 호출 직전에 가드 호출하여 archived/escalated 상태이면 dispatch 차단.
3. **Fix 3** — session-watchdog.sh `RETRY_COUNT >= MAX_RETRY` 분기에 jq 박제 블록 추가하여 status="escalated"를 task-timers.json에 영구 기록.

## 작업 내용

### 수정 파일
- `scripts/session-watchdog.sh`
  - Fix 1 (line 59): `select(.value.status != "archived" and .value.status != "escalated")` 필터 추가
  - Fix 3 (line 241-249): escalation 분기에 `flock` + `jq '.tasks[$tid].status = "escalated"'` 박제 블록 추가
- `dispatch.py`
  - 신규 `_set_task_status` 함수 (line 762, `_patch_timer_metadata` 직후)
  - 가드 호출 2곳:
    - line 2551 (`_dispatch_composite` 함수 내 task-timer start 직전)
    - line 3265 (`dispatch` 함수 내 task-timer start 직전)
  - 차단 시 `{"status": "error", "message": "...archived/escalated 상태 — 영구 박제로 dispatch 차단"}` 즉시 반환

### 신규 파일
- `tests/dev3/test_phase_beta2_watchdog.py` (98줄, 4 test cases)

### 변경 금지 파일 (준수)
- scripts/whisper-compile.py, scripts/auto_merge.py, scripts/bot_status_resolver.py
- scripts/done-watcher.py, scripts/finish-task.sh
- scripts/cleanup_stale_task_counter.py, scripts/worktree_manager.py
- teams/shared/**, CLAUDE.md, memory/capabilities/**, memory/audit/**
- memory/task-timers.json (데이터 파일)

## 테스트 결과

### 단위 테스트 (4/4 PASS)
```
tests/dev3/test_phase_beta2_watchdog.py::test_archived_task_excluded_from_watchdog PASSED
tests/dev3/test_phase_beta2_watchdog.py::test_escalated_task_excluded_from_watchdog PASSED
tests/dev3/test_phase_beta2_watchdog.py::test_retry_max_triggers_escalated_status PASSED
tests/dev3/test_phase_beta2_watchdog.py::test_set_task_status_blocks_archived_and_escalated PASSED
============================== 4 passed in 0.10s ===============================
```

### Syntax 검증
- `bash -n /home/jay/workspace/scripts/session-watchdog.sh` → OK
- `python3 -m py_compile /home/jay/workspace/dispatch.py` → OK

### grep 검증 (Edit 반영 확인)
- `grep -n 'archived\|escalated' scripts/session-watchdog.sh` → 4건 (0건 아님 ✓)
- `grep -n '_set_task_status\|영구 박제' dispatch.py` → 6건 (0건 아님 ✓)

## L1 스모크테스트 결과

- **서버 재시작**: 해당없음 (운영 메타 인프라, 서버 무관)
- **API 응답 확인**: 해당없음 (HTTP API 아님)
- **스크립트 실행**: 가짜 task-timers.json 시뮬레이션으로 watchdog jq 쿼리 + 박제 명령 실제 실행
- **스크린샷**: 해당없음 (백엔드/스크립트 작업)

### L1 시나리오 1 — Watchdog 필터 (Fix 1)
가짜 4 task (running/archived/escalated/anu-direct) 입력 → Phase β-2 jq 쿼리 실행:
```
==== Watchdog jq 결과 (Phase β-2 후) ====
task-2387-fake-running
========================================
L1 PASS: archived/escalated/anu-direct 모두 정상 제외됨
```

### L1 시나리오 2 — escalated 박제 (Fix 3)
`jq '.tasks[$tid].status = "escalated"'` 명령 실제 실행 → status 변환 확인:
```
==== 박제 후 status: escalated ====
L1 PASS: status=escalated 박제 성공
```

### L1 시나리오 3 — `_set_task_status` 가드 (Fix 2)
실 환경(memory/task-timers.json)에서 dispatch 모듈 import 후 함수 호출:
```
task-2387 (running): True       (✓ running은 박제 대상 아님 → 변경 가능)
task-nonexistent:    True       (✓ 없는 task는 차단하지 않음)
```
4번째 단위 테스트가 archived/escalated → False 반환을 별도 검증함.

### L1 결론
3 시나리오 전부 PASS. 회장 stalled 알림 영구 종료 시나리오 검증됨.

## 회귀 0 검증

- Phase β (task-2384) 산출물 무영향: whisper-compile.py / auto_merge.py 미수정.
- task-2381 (verifier fix) 산출물 무영향: 변경 금지 파일 준수.
- bot_status_resolver.py (task-2375) 무수정.
- task-timers.json 데이터 미변경.

## 발견 이슈 및 해결

### 이슈 1: Pyright import 진단 경고
- 증상: `dispatch.py`에서 `utils.env_loader`, `prompts.team_prompts` 등 11건 import 해석 실패 진단; 테스트 파일에서 `import dispatch` 1건 진단.
- 원인: Pyright가 워크스페이스 외부 CWD(`/home/jay/.cokacdir/workspace/B7E92569`)에서 실행되어 sys.path 미반영. 런타임에서는 정상 동작 (pytest 4 PASS, dispatch 정상 실행 확인).
- 해결: 본 작업 도입 사항 아님 — dispatch.py의 기존 import 패턴이며, 테스트 파일의 `sys.path.insert` 후 import는 의도된 패턴.
- 영향: 런타임 0건. Pyright 정적 분석에서만 노이즈.

## 머지 판단

- **머지 필요**: No (Lv.1 — worktree 미사용, main 직접 작업)
- **브랜치**: main (직접 커밋)
- **커밋**:
  - `b93d8ca9` [task-2387] Lugh: session-watchdog verdict 통합 + dispatch _set_task_status 가드
  - `edd938b0` [task-2387] Morrigan: Phase β-2 watchdog 회귀 테스트 4건 추가
- **머지 의견**: Lv.1 surgical 작업. session-watchdog.sh +9줄 / dispatch.py +1 함수+2 가드 호출 / 신규 테스트 4건. 모든 영향이 retry/escalation 경로에 한정. 기존 정상 흐름(running task → watchdog 처리)에 회귀 없음.

## 모델 사용 기록

- 다그다(팀장, Opus): 설계/분배/검토/통합/L1 스모크테스트
- 루(백엔드, Sonnet): session-watchdog.sh + dispatch.py 코드 변경
- 모리건(테스터, Sonnet): 회귀 테스트 4건 작성
- haiku 미사용 (백엔드 인프라 코드 + 테스트는 sonnet 적합)

## 비고

- 4+3 시나리오 매트릭스 완성:
  - 단위: archived 제외 / escalated 제외 / retry MAX 박제 / 가드 함수 동작
  - 통합 L1: jq 필터 / jq 박제 / 가드 함수 실 환경 호출
- 향후 회장이 status를 archived로 sync하면 영구 박제되어 어떤 retry/dispatch도 status를 running으로 갱신할 수 없음. watchdog stalled 알림 영구 종료.

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


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


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

