# task-716.1: 고스트 태스크 방지 시스템

## 배경
팀 세션이 크래시/타임아웃되면 `task-timer.py end`가 호출되지 않아 고스트 태스크가 발생함.
- task-700.1: 25시간째 running 상태로 방치 (2팀 세션 죽음)
- task-707.1: 다른 태스크로 대체됐는데 원래 건이 안 닫힘
- whisper가 이걸 감지 못해서 "작업중"으로 잘못 보고 → 팀 놀게 됨

## 수정 사항 (2건)

### 수정 1: whisper-compile.py에 고스트 감지 추가
**파일**: `/home/jay/workspace/scripts/whisper-compile.py`

#### 1-1. 상수 추가 (IDLE_THRESHOLD_HOURS 근처, line 35 부근)
```python
GHOST_THRESHOLD_HOURS = 4  # running 상태가 이 시간 이상이면 고스트 의심
```

#### 1-2. 고스트 감지 함수 추가 (detect_idle_teams 함수 아래)
```python
def detect_ghost_tasks(task_timers: dict[str, Any]) -> list[dict[str, Any]]:
    """running 상태가 GHOST_THRESHOLD_HOURS 이상인 태스크 탐지."""
    now = datetime.now(timezone.utc)
    threshold = timedelta(hours=GHOST_THRESHOLD_HOURS)
    ghosts: list[dict[str, Any]] = []

    for task_id, task in task_timers.items():
        if task.get("status") != "running":
            continue
        start_str = task.get("start_time", "")
        if not start_str:
            continue
        try:
            start = _parse_dt(start_str)
            elapsed = now - start
            if elapsed >= threshold:
                hours = round(elapsed.total_seconds() / 3600, 1)
                team_id = task.get("team_id", "unknown")
                bot_key = team_id.replace("-team", "")
                team_name = TEAM_NAME_MAP.get(bot_key, bot_key)
                ghosts.append({
                    "task_id": task_id,
                    "team_name": team_name,
                    "hours": hours,
                    "desc": _short_desc(task.get("description", "")),
                })
        except Exception:
            pass

    return ghosts
```

#### 1-3. compile_briefing()에 [고스트경고] 섹션 추가
`[유휴경고]` 섹션 바로 아래에 추가 (line 385 부근):
```python
    # ------------------------------------------------------------------
    # [고스트경고] 섹션 (있을 때만)
    # ------------------------------------------------------------------
    ghost_tasks = detect_ghost_tasks(task_timers)
    if ghost_tasks:
        ghost_parts = []
        for gt in ghost_tasks:
            ghost_parts.append(
                f"⚠️ {gt['task_id']}({gt['team_name']}) {gt['hours']}h째 running — 고스트? `task-timer.py end {gt['task_id']}`로 정리 필요"
            )
        lines.insert(-1, "[고스트경고] " + " / ".join(ghost_parts))
```
주의: `lines.append("</whisper-briefing>")` 보다 앞에 삽입해야 함.
`lines.insert(-1, ...)` 사용하면 마지막 `</whisper-briefing>` 앞에 들어감.

#### 1-4. [팀] 섹션에서 고스트 태스크 표시 변경
line 291~293 부근, running_tasks를 표시할 때 고스트 여부도 같이 표시:
```python
    for bot_id in all_bot_ids:
        info = bots[bot_id]
        team_name = TEAM_NAME_MAP.get(bot_id, bot_id)
        status = info.get("status", "unknown")

        running_tasks = running_by_team.get(bot_id, [])
        if running_tasks:
            now = datetime.now(timezone.utc)
            ghost_threshold = timedelta(hours=GHOST_THRESHOLD_HOURS)
            task_strs = []
            has_only_ghosts = True
            for t in running_tasks[:2]:
                desc = _short_desc(t.get('description', ''))
                start_str = t.get('start_time', '')
                is_ghost = False
                if start_str:
                    try:
                        start = _parse_dt(start_str)
                        if (now - start) >= ghost_threshold:
                            is_ghost = True
                    except Exception:
                        pass
                if is_ghost:
                    task_strs.append(f"{t['task_id']} {desc} ⚠️고스트?")
                else:
                    task_strs.append(f"{t['task_id']} {desc}")
                    has_only_ghosts = False
            if has_only_ghosts:
                # 고스트만 있으면 실질적으로 유휴
                team_parts.append(f"{team_name}:{' / '.join(task_strs)} (실질유휴)")
            else:
                team_parts.append(f"{team_name}:{' / '.join(task_strs)} 작업중")
        elif status == "idle" or (status == "processing" and not running_tasks):
            ...  # 기존 코드 유지
```

### 수정 2: 팀 CLAUDE.md 모순 해소 + timer end 강화
**파일들**:
- `/home/jay/workspace/teams/dev1/CLAUDE.md`
- `/home/jay/workspace/teams/dev2/CLAUDE.md`

모든 팀 CLAUDE.md에서 작업 규칙 부분을 아래로 교체:

**변경 전** (dev1 예시):
```
## 작업 규칙
1. dispatch.py가 task-timer를 자동 처리하므로 task-timer.py를 직접 호출하지 마세요
2. 모든 코드는 `/home/jay/workspace/` 하위에 작성
3. 코드 작성 후 반드시 테스트 실행
4. 작업 완료 시 반드시 task-timer 종료: `python3 /home/jay/workspace/memory/task-timer.py end <task_id>`
```

**변경 후**:
```
## 작업 규칙
1. `task-timer.py start`는 dispatch.py가 자동 처리 → 직접 start 호출 금지
2. ★ `task-timer.py end`는 **반드시 팀장이 직접 호출** → 보고서 저장 직후, cokacdir 전송 직전에 실행
   - `python3 /home/jay/workspace/memory/task-timer.py end <task_id>`
   - 이걸 안 하면 고스트 태스크가 됨 (팀이 놀고 있는데 "작업중"으로 표시)
3. 모든 코드는 `/home/jay/workspace/` 하위에 작성
4. 코드 작성 후 반드시 테스트 실행
```

핵심: "start는 자동, **end는 수동 필수**"를 명확히 분리. 모순 제거.

## 검증 방법
1. whisper-compile.py 변경 후 직접 실행 테스트:
   ```bash
   python3 /home/jay/workspace/scripts/whisper-compile.py
   ```
   - 현재 running 태스크 중 4시간 이상 된 것이 있으면 [고스트경고] 섹션이 출력되는지 확인

2. dev1/CLAUDE.md, dev2/CLAUDE.md 모순이 해소되었는지 확인

## 주의사항
- whisper-compile.py는 **항상 exit code 0** 유지 (에러 시에도)
- 기존 함수/로직 구조 변경 최소화
- CLAUDE.md는 해당 규칙 부분만 교체, 다른 내용 건드리지 말 것
- dev3/CLAUDE.md는 GLM 워크플로우 기반이므로 별도 구조 → 이번엔 건드리지 않음