# task-2559 plan — 봇 프로세스 source-of-truth 박제

## 본질 1줄
봇 프로세스 활성 여부 = 신호등 단일 source-of-truth. spec §9 2-Layer 모델(팀장 봇 + subagent child) 1:1 박제.

## 구현 단위

### 1. `dashboard/bot_process_collector.py` (신규 — Layer 1 + Layer 2)
- `BotProcessCollector` 클래스
- `get_active_lead_bots(now=None) -> List[LeadBotProcess]` (Layer 1)
  - psutil로 `claude` 프로세스 스캔
  - `/proc/<pid>/cwd` readlink → cwd 패턴 매칭
  - cron 봇: `/home/jay/.cokacdir/workspace/<schedule_id>/` 경로 → schedule_history 로그에서 chat_id 검증, bot_verifier 추출 → display_name 매핑
  - dispatch 봇: `/home/jay/workspace/.worktrees/task-XXX-devN/` 경로 → task_id + dev_id 직접 파싱
- `get_subagent_children(lead_pid) -> List[SubagentProcess]` (Layer 2)
  - psutil.Process.children(recursive=False) 중 claude 프로세스
  - cmdline에서 system_prompt 또는 task_id trace 추출
- 매핑 헬퍼:
  - `_parse_schedule_log(schedule_id) -> Dict[chat_id, bot_verifier, prompt_excerpt]`
  - `_resolve_bot_identity(cwd, schedule_id, bot_verifier) -> Optional[bot_key + display_name + dev_id]`
- 격리: chat_id 6937032012 (회장)만 인정. 다른 chat의 schedule_history는 무시.
- 보안: /proc 접근은 jay user 본인 프로세스만 (psutil 자체 권한 모델 의존).

### 2. `dashboard/data_loader.py` 수정 — hybrid mode
- `get_member_status()` 확장:
  - primary signal = `bot_process_collector.get_active_lead_bots()` 결과
  - 보조 = 기존 `running_tasks` (task-timers.json)
  - 봇 프로세스 죽었으면 → task-timers.json running이라도 stale, idle 처리
  - 봇 프로세스 살아있고 Layer 2 child 있으면 → 해당 팀원 working
- `_enrich_bot_activity()` 확장:
  - 활성 봇 프로세스 list를 has_running_tasks 신호로 결합
  - borrowed_tasks 추적 유지 (§3 호환)

### 3. `dashboard/routes_get.py` 또는 `server.py` 확장
- 기존 `/api/member-status`, `/api/bot-activity` 엔드포인트 그대로 유지 (스펙 §9.5)
- 추가 endpoint 불필요 (응답 payload만 새 source 반영)

### 4. 테스트 — `dashboard/tests/test_bot_process_collector.py`
- `BotProcessCollector` 단위 테스트
- 8 시나리오 fixture-driven (psutil monkeypatch + 파일 시스템 mock):
  1. cron 봇 활성 (workspace/<schedule_id> cwd, child 0)
  2. dispatch.py 봇 활성 (.worktrees/task-XXX-devN cwd)
  3. subagent 1개 활성 (Layer 1 alive + Layer 2 child 1개)
  4. subagent 다수 병렬 (Layer 1 alive + Layer 2 child 다수)
  5. subagent 종료 후 (Layer 1 alive + Layer 2 child 0) → 팀원 대기 복귀
  6. 팀장 봇 종료 (Layer 1 dead) → 전원 유휴
  7. borrowed_tasks (dev1 봇이 marketing 차용 시)
  8. .done 마커 archive (실시간 신호 X)

### 5. 테스트 — `dashboard/tests/test_traffic_light_layer_signals.py`
- 4 전이 규칙(spec 2.1~2.4) 1:1 PASS 어셀션
- task-timers.json hybrid mode: stale running entry + dead process → idle 박제

### 6. Fixture 5종
- `bot_process_cron_active.json` — Layer 1 cron 봇 살아있음
- `bot_process_dispatch_worktree_active.json` — Layer 1 dispatch 봇 살아있음
- `bot_process_subagent_parallel.json` — Layer 2 children 다수
- `bot_process_lead_terminated.json` — Layer 1 dead, task-timers stale
- `bot_process_borrowed_task.json` — borrowed task scenario

## forbidden — 손대지 말 것
- traffic-light-spec.md (§9 박제 완료)
- scripts/ci.sh, dispatch/, team_prompts.py, .github/workflows/, .env*
- PR #98~#111 branch
- anu_v2/ (task-2558 영역)

## 검증 단계
- pytest dashboard/tests/ → 5 신규 + 기존 PASS
- static check: pyflakes / py_compile
- effective diff == expected_files dispatch_decision.json authoritative
- PR 생성 → CI 11 checks SUCCESS → BOT squash merge → smoke + reconcile
- 라이브 검증: cron 봇 1개 발사 → 대시보드 작업중 표시 → 종료 후 유휴 전환

## ESCALATED 조건
- Layer 1/2 추출 실패 → BOT_PROCESS_SOURCE_GAP
- spec §9 박제 drift → IMPLEMENTATION_SPEC_DRIFT
- forbidden path hit → FORBIDDEN_PATH_HIT
- CI fail → CI_FAILURE
- admin override 시도 → ADMIN_OVERRIDE_ATTEMPT
