# task-2729+1 P0-A 보고서 — CI_WATCHER lifecycle state machine / terminal callback contract 보강

- 작업 ID: task-2729+1 (P0-A)
- 팀: dev6-team (페룬/스바로그/벨레스)
- 일자: 2026-06-05
- base: fresh origin/main `d925b873` (PR#173 머지본)
- worktree: `/home/jay/workspace/.worktrees/task-2729+1-dev6`
- branch: `task/task-2729+1-dev6`
- 검증 레벨: normal | 게이트: G1/G2/G3 (팀장 자체 통과)

---

## S (Situation)
task-2729(PR#173, d925b873)에서 progress watcher 골격 — terminal callback contract,
`WATCHER_TERMINAL_CALLBACK_NOT_WIRED`, quiet-window, 5 terminal_states,
classifier(`utils/pr_watcher_terminal_state_classifier.py`)는 IMPLEMENTED+VERIFIED 상태였다.
단, WIRED=partial_record_only 로 lifecycle 측면(watcher 세션 소멸 감지)이 비어 있었다.

## C (Complication)
watcher 세션이 사라졌는데(heartbeat/last_poll stale) PR 이 여전히 pending(non-terminal)이면
이를 식별할 분류 contract 가 없었다. 또한 terminal 도달 시 normal callback 발사의
"필수성(MANDATORY)"이 코드 계약으로 명시되지 않아 미발사가 silent 하게 통과할 여지가 있었다.

## Q (Question)
OS 감지/재기동(P0-B) 없이, **classification/판정 contract 수준**에서
(1) CI_WATCHER_SESSION_LIFETIME_GAP 진단 분류, (2) terminal callback 필수화,
(3) lifecycle staleness 판정을 어떻게 회귀 무손상으로 보강하는가?

## A (Answer)
ACTIVE=false · record-only 원칙 하에 순수 판정 함수 + record-only 호출부만 추가했다.
OS 감지/watcher 재기동/auto-register/live polling 은 일절 구현하지 않았다(P0-B 영역).

---

## 구현 내역 (effective diff = expected_files 4개)

### 1. `utils/pr_watcher_terminal_state_classifier.py` (추가만)
- `CI_WATCHER_SESSION_LIFETIME_GAP` 상수 신규 — **diagnostic, non-terminal-merge**.
  ★ `TERMINAL_STATES`(5 enum)에 절대 미포함 → merge 판단 무손상.
- `LIFETIME_GAP_DIAGNOSTIC_STATES` 튜플 (TERMINAL_STATES 와 분리).
- `WATCHER_HEARTBEAT_STALE_THRESHOLD_SEC = 3600` (회장 §명시 60분 staleness 정합;
  `utils/schedule_id_freshness.py` SCHEDULE_FRESHNESS_THRESHOLD_MIN=60 convention).
- `classify_lifetime_gap(*, watcher_stale, pr_terminal_state="")` 순수 판정 함수:
  watcher_stale=True + PR pending(비-terminal) → CI_WATCHER_SESSION_LIFETIME_GAP.
  PR 이 TERMINAL_STATES 면 gap 아님. (OS 감지/재기동은 P0-B.)
- 기존 5 terminal_states/chair-auth 주석/classify/envelope/registrar 일체 무변경.

### 2. `scripts/ci_watch_handoff_runner.py` (추가만, 기존 보존)
- `WatcherState` 에 `last_poll_ts`/`heartbeat_ts: Optional[float]=None` 필드 추가.
  ★ `as_tracked_dict()`/`all_tracked()` 무변경 → 6-state(WATCHER_TRACKED_STATES) dict 무오염.
- `is_watcher_stale(*, last_poll_ts, now_ts, stale_threshold_sec=3600)` 순수 staleness 판정.
  (last_poll_ts None → stale=True.)
- `record_poll_heartbeat(state, now_ts)` — heartbeat 기록 helper(record-only).
- `classify_watcher_lifetime_gap(*, state, now_ts, pr_terminal_state="", ...)` record-only 호출부 —
  staleness 판정 후 classifier.classify_lifetime_gap 호출, `active=False` 반환.
- `fire_terminal_callback` 필수화 계약 강화: 반환 dict 에 `callback_mandatory=True`,
  `not_wired=(not fired)`, `skipped_reason` 추가. 기존 키(fired/terminal_state/callback_status) 보존.
- 기존 run_once/quiet-window(is_quiet_window_settled)/fallback/Phase2 함수 일체 무변경.

### 3. `tests/regression/test_ci_watcher_lifecycle_2729p0a.py` (신규, 30 PASS)
- (a) watcher stale + PR pending → CI_WATCHER_SESSION_LIFETIME_GAP (TERMINAL 미포함 단언)
- (b) terminal 도달 → callback 필수(callback_mandatory), 미발사 시 NOT_WIRED(not_wired=True)
- (c) quiet-window settled/not-settled 판정 + 상수 120
- (d) fallback = dead-man safety-net only — fallback_prune set 이 normal_callback 미트리거,
  gate fallback_only→DISPATCH_INCOMPLETE, `FALLBACK_ROLE_SINGLE_PURPOSE ==
  "RECOVERY_ONLY_NO_FINAL_REPORT_TRIGGER"` doctrine 단언
- (e) 기존 6-state/terminal 회귀 무손상 — as_tracked_dict 키 == WATCHER_TRACKED_STATES,
  TERMINAL_STATES 길이 5, run_once head-drift → HOLD_FOR_CHAIR

### 4. `memory/state/automation_capability_matrix.json` (progress_watcher delta)
- WIRED: `partial_record_only` → `lifecycle_gap_classified_candidate`
- ACTIVE: **false 유지** (production 전환 별도 회장 승인)
- IMPLEMENTED/VERIFIED/WIRED/ACTIVE 4축 분리 기록(`capability_axes`) + P0-A evidence
- Phase1 기록은 `prev_phase1` 로 보존.

---

## 테스트 결과
- py_compile: **PASS** (classifier/runner/test 3파일)
- 신규 회귀 `test_ci_watcher_lifecycle_2729p0a.py`: **30 passed**
- 기존 회귀 무손상: `test_progress_watcher_gate_2729.py`(23) +
  `test_pr_convergence_pipeline_2729.py`(19) + `tests/pr_watcher_terminal_state_classifier/`(전체)
  포함 통합 실행 **96 passed / 0 failed**
- TERMINAL_STATES 여전히 5개, CI_WATCHER_SESSION_LIFETIME_GAP 미포함 확인.

## L1 스모크테스트 결과 (실동작 확인)
- **서버 재시작**: 해당없음 (순수함수/분류 라이브러리 — 서버/API/프론트 없음)
- **API 응답 확인**: 해당없음 (네트워크 호출 없는 record-only 라이브러리)
- **실제 프로세스 실행(subprocess형 L1)**:
  - `python3 scripts/ci_watch_handoff_runner.py --dry-run` → **exit 0** (import-safe, 네트워크 0)
  - `python3 scripts/ci_watch_handoff_runner.py --once --task-id 2729 --pr 99 --expected-head abc123` → **exit 0**
  - 실제 런타임 함수 호출(네트워크/cokacdir/gh 0):
    - (a) stale+pending → `CI_WATCHER_SESSION_LIFETIME_GAP` (detected=True, active=False)
    - (a) heartbeat 기록 직후 → `""` (gap 없음)
    - (b) fake runner rc=0 → `WATCHER_TERMINAL_CALLBACK_WIRED` (mandatory=True, not_wired=False)
    - (b) fake runner rc=1 → `WATCHER_TERMINAL_CALLBACK_NOT_WIRED` (not_wired=True)
- **스크린샷**: 해당없음 (CLI/라이브러리)
- 결론: L1 PASS — 실제 런타임에서 신규 분류·콜백 필수화·staleness 판정 동작 확인.

---

## 게이트 / doctrine 준수 검증
- **G1 설계**: affected_files = expected_files 4개, 타 팀 파일 겹침 0.
- **G2 구현**: 팀 테스터(벨레스) 기능 회귀 30 신규 + 42 기존 PASS.
- **G3 머지**: PR 생성 → owner Gemini 리뷰 → review-settle. **merge 는 회장 승인 전 금지(merge_policy: none)** → MERGE_APPROVAL_CANDIDATE.
- effective diff = expected_files(4) 내 / forbidden_paths 수정 **0** / ANU key raw 노출 **0** / `"ACTIVE": true` 전환 **0**.
- ACTIVE=false 유지, record-only, live OS 기동/auto-register **0** (P0-B 영역 미침범).
- P1/P2 구현 미혼입, unrelated cleanup 0.

## 발견 이슈 및 해결
- (환경) 메인 워크스페이스(/home/jay/workspace)가 타 팀 브랜치(task-2716)에 체크아웃 →
  fresh origin/main(d925b873) 기준 신규 worktree 생성으로 격리 해결.
- (환경) pre-commit guard 검증 #7(메인 워크스페이스 main 아님)은 본 작업 무관 환경 전제 →
  worktree 검증 1~6 통과 + lock 정식 스키마 생성으로 정상 커밋(bypass 미사용).
- `CI_WATCHER_SESSION_LIFETIME_GAP` 미사용 import lint → `lifetime_gap_detected` boolean 으로 의미있게 사용 처리.

## 머지 판단
- **머지 필요**: Yes (단, 회장 승인 전 merge 금지 — MERGE_APPROVAL_CANDIDATE)
- **브랜치**: task/task-2729+1-dev6
- **워크트리 경로**: /home/jay/workspace/.worktrees/task-2729+1-dev6
- **머지 의견**: 96 회귀 PASS, diff 4파일(expected 내), forbidden/ANU key/ACTIVE-true 0.
  ACTIVE=false record-only 로 안전. Gemini fresh HIGH/CRITICAL 0 확인 후 회장 승인 시 머지 가능.

## 모델 사용 기록
- 페룬(팀장, Opus): 설계/분배/검토/통합/capability matrix/보고서.
- 스바로그(백엔드, Sonnet): classifier + runner 구현.
- 벨레스(테스터, Sonnet): 회귀 테스트 30 신규.
- haiku 미사용 (계약 정밀도 요구 작업으로 Sonnet 이상 적용).

## P0-A 완료 조건 체크
1. 신규+기존 regression PASS — ✅ (30 + 66 무손상)
2. py_compile PASS — ✅
3. effective diff = expected_files(≤4) 내 / ACTIVE=false / forbidden 0 / ANU key raw 0 — ✅
4. CI_WATCHER_SESSION_LIFETIME_GAP · WATCHER_TERMINAL_CALLBACK_NOT_WIRED 둘 다 fixture 실증 — ✅

→ P0-A PASS. (P0-B = OS-level staleness 감지/watcher 재기동/auto-register, 별도 Phase.)

---

## 완료 상태 / ANU callback / 환경 블로커 (필수 기록)

### ANU normal callback (mandatory contract — 충족)
- **등록 완료**: cron id `E74F8E6E`, 발사 예정 `2026-06-05 14:34:39`(absolute, --once).
- **collector**: 독립 ANU key `c119085addb0f8b7` (executor self-key `1e41a2324a3ccdd0` 아님).
- enforcement 검증: `owner_is_independent_anu=true`, `collector_role=ANU`, "executor self-collector structurally impossible" → **SELF_COLLECTOR_FORBIDDEN / SENDFILE_ONLY / NOT_REGISTERED 모두 회피**.
- 등록 경로: finish-task GIT-GATE 차단으로 notify 단계 미도달 → `callback_preregistration.py launch`(ANU owner-key)로 ANU-owned argv 검증(PASS) 후 정식 cron 등록.
- ★ 봇 자가 callback 회수검증 미수행(SELF_COLLECTOR_VERIFICATION_FORBIDDEN 준수).

### .done 보류 사유 (환경 블로커 — task 책임 아님)
finish-task.sh `task-2729+1 dev6`(project_path 미전달, merge 스킵) 실행 시 **GIT-GATE EXTERNAL_DIRTY_BLOCKER** 로 차단:
- 메인 워크스페이스(/home/jay/workspace, 현재 타 작업 `task-2716` 브랜치 체크아웃)에 **무관 dirty 2건**:
  - `utils/replacement_pr_runner.py` ← ★ 본 task의 **forbidden_path** (수정 절대 불가)
  - `tests/regression/test_replacement_pr_runner_2510.py`
- 둘 다 본 task 소산 아님(dirty_registry 분류 = `EXTERNAL_DIRTY_BLOCKER`). 시스템 remediation 권고: "origin/main sync 또는 무관 dirty 정리 — **task 재실행 불필요**".
- 공유 워크스페이스 stash/정리는 타 팀(task-2716) 활성 작업 파괴 위험 + forbidden_path 침범 → **봇이 정리하지 않음**(아누/회장 환경 조치 영역).
- QC gate 자체는 **PASS**(file_check/data_integrity/critical_gap/spec_compliance/duplicate/git_evidence/l1_smoketest 전부 PASS, 유일 WARN=claude_md 비차단). scope-guard 도 worktree 정식 검증 시 **PASS(4 files in scope)**.

### ANU 후속 조치 (callback E74F8E6E 로 전달)
1. 메인 워크스페이스 무관 dirty 정리 또는 origin sync 후 `finish-task.sh task-2729+1 dev6` 재실행 → `.done` 생성.
2. 회장 머지 승인 시 PR #174 머지(merge_policy: none 준수, 봇 자동머지 0).
3. P0-A PASS → 회장 재확인 없이 P0-B 자동 연속 판단.

### 산출물 상태
- PR: #174 OPEN (MERGE_APPROVAL_CANDIDATE) ✅
- 보고서: `memory/reports/task-2729+1-p0a.md` (+ `task-2729+1.md`) ✅
- `memory/events/task-2729+1-p0a.done`: **보류**(환경 블로커 — 수동 .done 위조 금지 doctrine 준수, finish-task.sh 만이 유일 완료 경로).
- ANU callback cron: `E74F8E6E` 등록 ✅

