{
  "pass": false,
  "risks": [
    {
      "severity": "critical",
      "description": "설계 문서의 핵심 요구사항인 '진짜 죽음만 판정'이 현재 코드에 반영되어 있지 않습니다. `scripts/session-watchdog.sh:147-193`는 사실상 `PID 없음 + heartbeat 10분 경과`만으로 STALLED를 선언하며, 문서가 요구한 단계 마커(`.codex-gate`, `.qc-done`, `.done.merging`, `.pr-creating`, `.external-running`), events mtime, PR 상태, worktree 상태 교차 검증이 전혀 없습니다. PR 생성/QC/G3/외부 CLI 중인 정상 작업을 계속 오탐지할 수 있습니다."
    },
    {
      "severity": "high",
      "description": "`.escalate` 억제 요구사항이 구현과 충돌합니다. 현재 코드에는 `memory/events/<task>.escalate` 존재 시 알람을 건너뛰는 분기가 없고, 오히려 `scripts/session-watchdog.sh:241-248`는 retry 초과 시 `status=escalated`만 기록합니다. 문서의 재현 사례처럼 `.escalate`가 이미 발급된 작업도 매 사이클 반복 알람될 가능성이 그대로 남아 있습니다."
    },
    {
      "severity": "high",
      "description": "`no-taskfile` false positive의 직접 원인이 현재 코드에 존재합니다. `memory/task-timers.json`의 실제 값은 `memory/tasks/task-2389.md` 같은 상대경로인데, `scripts/session-watchdog.sh:263-339`는 이를 `[[ -f \"$TASK_FILE\" ]]`로 그대로 검사합니다. 시스템 서비스나 다른 cwd에서 실행되면 파일이 실제로 있어도 실패합니다. 같은 분기에서 `stalled-alert-only`를 먼저 push한 뒤 `no-taskfile`를 다시 push하므로 한 task가 2건으로 집계되는 중복 알람도 구조적으로 발생합니다."
    },
    {
      "severity": "high",
      "description": "설계 문서의 범위 제약이 자체적으로 모순됩니다. 버그 #8은 `dispatch.py`의 봇 free 판정까지 강제하라고 요구하지만, 같은 문서의 `변경 금지`와 `forbidden_paths`에는 `dispatch.py`가 포함되어 있습니다. 이 상태로는 문서가 요구한 완료 정의(.done + PR merged + QC PASS)를 dispatch 경로에 강제할 수 없습니다."
    },
    {
      "severity": "medium",
      "description": "heartbeat 차등 정책이 현재 데이터 모델과 연결되지 않았습니다. 코드에는 전역 `STALE_THRESHOLD=600`만 있고(`scripts/session-watchdog.sh:11`), 실제 `task-timers.json`에는 design 작업이 `team_id: \"design\"`, `role: \"design\"`으로 존재합니다. 작업 종류를 어디서 읽을지(frontmatter vs timer metadata) 설계가 명확하지 않으면 30분 정책이 적용되지 않거나 케이스별로 불일치가 생깁니다."
    },
    {
      "severity": "medium",
      "description": "회귀 테스트 계획과 현재 테스트 자산 사이의 단절이 큽니다. 기존 `tests/test_session_watchdog.py`는 셸 스크립트가 아니라 `scripts/session_watchdog.py`를 검증하고 있어, 이번 이슈의 실제 수정 대상인 `scripts/session-watchdog.sh` 경로를 보호하지 못합니다. 또한 `allowed_resources.paths`에는 `memory/plans/tasks/task-XXXX/**`가 적혀 있지만 실제 task 파일은 `memory/tasks/task-2389.md` 등으로 존재해 테스트/검증 경로 정의도 어긋나 있습니다."
    },
    {
      "severity": "low",
      "description": "텔레그램 알림 포맷이 설계 요구와 맞지 않습니다. 현재는 `scripts/session-watchdog.sh:352-363`에서 task 목록만 집계해 보내며, 문서가 요구한 `task_file 존재`, `escalate 상태`, `heartbeat last` 같은 디버깅 정보가 포함되지 않습니다. 운영 중 원인 판별 시간이 길어질 수 있습니다."
    }
  ],
  "suggestions": [
    "`session-watchdog.sh`의 stalled 판정을 단일 함수로 재구성하고, 최종 조건을 `PID 없음 AND 진행 마커 없음 AND events mtime 정지 AND PR/worktree 변화 없음`으로 강제하세요.",
    "`task_file`은 `[[ -f \"$TASK_FILE\" ]]` 전에 절대경로로 정규화하세요. 상대경로면 `\"$WORKSPACE/$TASK_FILE\"`로 보정하고, 비어 있을 때만 fallback 경로를 계산해야 합니다.",
    "`STALLED_LIST`는 배열 대신 task_id keyed map으로 바꿔 한 task당 하나의 verdict만 남기고, `no-taskfile`은 독립 알람이 아니라 `stalled-alert-only`의 reason/sub_cause로 병합하세요.",
    "루프 초반에 `memory/events/<task>.escalate` 존재 && `.escalate.acked` 부재이면 즉시 skip 하도록 넣어 반복 알람을 차단하세요.",
    "작업 종류는 frontmatter와 `task-timers.json` 중 하나를 authoritative source로 먼저 고정하세요. 현재 데이터가 이미 `team_id/role`를 가지므로, 1차 판정은 timer metadata를 쓰고 frontmatter는 fallback으로 두는 편이 구현 리스크가 낮습니다.",
    "설계 문서의 범위를 먼저 정리하세요. `dispatch.py`를 정말 건드릴 수 없다면 버그 #8의 dispatch 강제 요구를 이번 작업 범위에서 제외하거나 후속 작업으로 분리해야 합니다.",
    "신규 회귀 테스트는 Python에서 셸 스크립트를 black-box로 실행하는 방식으로 추가해 `scripts/session-watchdog.sh` 자체를 검증하세요. 최소한 상대경로 task_file, `.escalate` 억제, design 30분, 중복 push 방지, 텔레그램 본문 필드 포함 여부는 직접 커버해야 합니다."
  ],
  "source": "codex_companion",
  "fallback_reason": null,
  "error": null,
  "target_dir": "/home/jay/workspace",
  "target_dir_source": "workspace_root_fallback",
  "task_id": "task-2399",
  "timestamp": "2026-05-02T23:24:07.290426+00:00"
}