# task-2942 — dispatch spawn 검증 결선 (false-OK 감지)

- **팀**: 개발1팀 (헤르메스)
- **일자**: 2026-08-13
- **지시서**: `memory/tasks/dispatch-fix-spawn-verification-260813.md` (sha256 `5611cac6…6664c58` 검증 일치)
- **merge_policy**: `local_only` — **머지는 ANU 소관, 본 봇은 로컬 커밋/검증까지만**
- **범위**: 1차 = **감지 + 표면화만**. ⚠️ **자동 재전송(봇별 key fallback)은 미포함** (무한루프/중복 위험 → 2차 별도)

---

## 1. 근본원인 (재확인)

`dispatch()` 는 `cokacdir --cron` **등록 성공만으로** `status="dispatched"` 를 반환하고 봇의 실제 spawn 을 검증하지 않았다.
- `_verify_bot_spawn` (`terminal_state_classifier.py` §5.2.1) 은 구현돼 있었으나 **dispatch 실행 경로 호출부 0건**(미결선). `dispatch.py` 에서 `# noqa: F401` 로 import 만 되고 있었음.
- 봇이 "나 떴다"를 알리는 **spawn-confirmed marker 생성 메커니즘 자체가 부재**.
- 결과: cron silent-drop 시 봇 미spawn인데 dispatch 는 `ok` = **false-OK**.

---

## 2. 변경 파일

| 파일 | 변경 | 내용 |
|---|---|---|
| `dispatch/spawn_verification.py` | **신규 371줄** | 검증 wiring + marker writer + 봇 시작 CLI (stdlib 전용) |
| `dispatch/__init__.py` | +42 | `_verify_spawn_or_annotate` 헬퍼 + 성공 반환 경로 **3곳** 결선 |
| `prompts/team_prompts.py` | +19 | 프롬프트 최상단 spawn-confirmed marker 생성 지시 주입 |
| `scripts/harness/v36/terminal_state_classifier.py` | +25/-5 | `timeout_sec` optional 인자(하위호환) + **marker 최소 1회 확인 버그 수정** |
| `tests/regression/test_dispatch_spawn_verification_2942.py` | **신규 377줄** | 신규 테스트 27건 |

### 결선 위치 (`dispatch/__init__.py`)
- `1642` — `_verify_spawn_or_annotate()` 정의 (비차단 wrapper)
- `3021` — 복합업무(composite) 성공 반환 직전
- `4085` — **정상 dispatch** 성공 반환 직전 (cron 등록 직후)
- `4150` — fallback 키 재전송 성공 반환 직전

### 판정 → 처리 매핑
| `_verify_bot_spawn` 반환 | dispatch result 처리 |
|---|---|
| `SPAWNED` | status 유지 (**무손상**), `spawn_verification` 만 기록 |
| `DISPATCH_FALSE_OK` | `status="DISPATCH_FALSE_OK"` + `spawn_verification_reason="spawn_false_ok"` + **handoff marker disk 박제**(`INFRA_DEFECT`) |
| `TIMEOUT_BOT_ALIVE_BUT_NO_MARKER` | 경고 필드만 기록, **status 는 ok 유지** (봇 생존) |

---

## 3. 무손상(비차단) 확인

아래 **모든** 경우에 원래 `result` 를 그대로 반환한다 (테스트로 고정):
- `_verify_bot_spawn` import 실패(`None`) → 검증 스킵
- 검증 중 예외 발생 → 예외 흡수, 기존 ok 유지
- `result` 가 dict 가 아님 → 원본 그대로
- kill-switch `DISPATCH_SPAWN_VERIFY_ENABLED=0` → 검증 스킵
- handoff 박제 실패 → status 표면화는 유지, 예외 전파 없음
- 구버전 classifier(`timeout_sec` 미지원) → `TypeError` 흡수 후 env 기본값으로 재시도

---

## 4. 검증 결과

### 신규 테스트
```
tests/regression/test_dispatch_spawn_verification_2942.py  →  27 passed
```

### 기존 회귀 (지시서 명시 2종)
```
test_progress_watcher_gate_2729.py + test_ci_watcher_lifecycle_2729p0a.py  →  53 passed
```

### 광역 회귀 스윕
```
tests/harness tests/dispatch tests/regression  →  18 failed, 2962 passed, 11 skipped
```
**18건은 전부 pre-existing** — 독립 검증함(추정 아님):
1. 변경분 3파일을 `git checkout` 으로 되돌리고 신규 2파일을 이동 제거 → **동일 서브셋 재실행 결과 `36 failed / 108 passed`**
2. 변경분 복원 후 **동일 서브셋 재실행 → `36 failed / 108 passed` (완전 동일)**
→ 내 변경으로 인한 신규 실패 **0건**. (실패군: `finish-task.sh` 문자열 감사, `anu_pickup_p0b`, `stash_origin_audit`, `worktree_isolation` 등 본 변경과 무관 영역)

`tests/regression/test_base_source_isolation_2729p9.py` 는 collection 단계 에러(해당 테스트가 `wt-2729p9` 워크트리 전용) → pre-existing, `--ignore` 처리.

### L1 스모크테스트
- **서버 재시작**: 해당없음 (런타임 서버 없는 메타인프라 모듈)
- **봇 시작 CLI 실동작**: `python3 dispatch/spawn_verification.py --task-id task-2942 --bot-id dev1-team --events-dir /tmp/…` → exit 0, 마커 생성 확인
  ```json
  {"task_id":"task-2942","bot_id":"dev1-team","pid":799141,"ts":"20260813T005942"}
  ```
- **결선 실동작(실제 `dispatch` 모듈 경유)**:
  - marker 없음 → `status=DISPATCH_FALSE_OK`, handoff marker `task-T1.failure-handoff-marker.json` 박제됨
  - marker 있음 → `status=dispatched` (무손상), `spawn_verification=SPAWNED`
- **프롬프트 주입**: `build_prompt('dev1-team', …)` 실호출 → spawn 블록이 전체 100줄 중 **8번째 줄**(CRITICAL 헤더 직후, 첫 실행명령)에 위치. 블록 크기 **< 400 bytes**
- **스크린샷**: 해당없음 (CLI/라이브러리)

---

## 5. ★ 스모크 중 발견·수정한 실제 결함 (지시서 범위 외 · 보고 필요)

`_verify_bot_spawn` 의 루프가 `while elapsed < timeout_sec:` 로 **선판정** 구조여서, `timeout_sec=0` 이면 marker glob 을 **단 한 번도 돌지 않고** 즉시 `DISPATCH_FALSE_OK` 를 반환했다. 즉 **이미 마커가 있는(=봇이 빠르게 뜬) 경우까지 false-OK 로 오판**하는, 본 태스크가 잡으려는 바로 그 오판 유형이다.

→ do-while 구조로 수정(**marker 최소 1회 확인 보장**). `timeout_sec>=1` 동작은 완전 동일. 회귀 테스트 2건 추가(`test_marker_checked_at_least_once_even_with_zero_timeout`, `test_zero_wait_end_to_end_does_not_false_flag`).
※ 이 결함은 실제 통합 스모크를 돌리지 않았으면 테스트만으로는 드러나지 않았을 것임.

---

## 6. ★★ 회장/ANU 판단 필요 — 타이밍 리스크 (가장 중요)

**지시서의 "기본 15초" 를 그대로 쓰면 정상 dispatch 대부분이 false-OK 로 오판될 위험이 있다.**

근거(코드 사실):
- `dispatch/__init__.py:2906, 3882` → `_dispatch_delay = 10`. dispatch 는 cron 을 **현재+10초** 에 등록한다.
- 즉 dispatch 반환 시점 기준 **처음 10초는 cron 발사조차 일어나지 않은 구간** = 봇이 존재할 수 **없는** 구간.
- 마커는 그 이후 봇 세션 부팅 → 프롬프트 로드 → 첫 명령 실행까지 가야 생성된다.

→ 조치: 총 대기 = **cron 발사 offset(10s) + 마커 예산(15s) = 기본 25초**. 마커 예산 15초는 지시서대로 보존(`FAILURE_CALLBACK_2712_SPAWN_TIMEOUT_SEC`), 총 대기는 `DISPATCH_SPAWN_VERIFY_WAIT_SEC` 로 직접 override 가능.

**남은 미확정(추측하지 않음)**: 실제 봇 세션 부팅~첫 명령까지의 지연을 **실측하지 못했다**(실측하려면 실제 봇을 spawn 해야 하므로 본 태스크 범위 밖). 25초로도 부족하면 정상 dispatch 가 `DISPATCH_FALSE_OK` 로 표시된다.

**권고 (회장/ANU 결정 사항)**
1. **다음 실제 dispatch 1건에서 마커 생성까지의 실지연을 측정** → 그 값 + 여유로 `DISPATCH_SPAWN_VERIFY_WAIT_SEC` 확정. (측정 전까지는 `DISPATCH_SPAWN_VERIFY_ENABLED=0` 으로 무력화 가능 — 코드 변경 불필요)
2. dispatch 가 매 호출 **25초 블로킹**된다는 점(배치 dispatch 시 누적). 이게 과하면 2차에서 **비동기/지연 검증**(dispatch 는 즉시 반환하고 별도 워커가 나중에 마커를 확인)으로 전환하는 것이 구조적으로 옳다고 판단됨 — 1차 범위를 넘으므로 제안만 함.
3. 기존 미완료 봇들은 프롬프트에 마커 지시가 없던 상태로 떠 있으므로, **이미 진행 중인 task 에 대해서는 false-OK 판정이 부정확**할 수 있음.

---

## 7. 완료 조건 체크

- [x] `_verify_bot_spawn` dispatch 결선 (호출부 0건 → **3곳**)
- [x] 봇 시작 시 spawn-confirmed marker 생성 (프롬프트 주입 + CLI)
- [x] 신규 테스트 27건 PASS
- [x] 기존 회귀 53건 PASS · 광역 스윕 신규 실패 0건 (독립 대조 검증)
- [x] 무손상(import 실패·예외·비-dict·kill-switch) 확인
- [x] 자동 재전송 미포함 명시
- [ ] **머지 — ANU 소관** (`merge_policy: local_only`)
