# task-2673 — PR watcher terminal/callback fix implementation 보고서

- 작성: dev8 라
- 작성 시각: 2026-05-26 01:04 KST
- chair_authorization_id: `CHAIR-AUTH-PR-WATCHER-TERMINAL-CALLBACK-FIX-20260526-JJONGS-IMPLEMENT-001`
- 완료 상태: **`PR_WATCHER_TERMINAL_CALLBACK_FIX_IMPLEMENTED`**

---

## 0. TL;DR (회장 보고 형식)

task-2670 RCA packet 의 회장 verbatim 4 후보 (P0 #1 #2 #3 / P1 #4) 적중을 doctrine 으로
박제하고, `utils/pr_watcher_terminal_state_classifier.py` 모듈 1개 + regression 24건 +
spec v2 1편으로 fix 구현 완료. PR #149 코드와 혼합 0 · task-2662~2672 파일 충돌 0 ·
forbidden 위반 0 · 별도 PR 권장 (merge 결정 별도 chair signature).

---

## 1. 수정된 파일 list (★ 보고 필수 #1)

| # | 파일 | 종류 |
|---|---|---|
| 1 | `utils/pr_watcher_terminal_state_classifier.py` | 신설 — classifier + envelope + callback registrar |
| 2 | `tests/pr_watcher_terminal_state_classifier/__init__.py` | 신설 — 패키지 마커 |
| 3 | `tests/pr_watcher_terminal_state_classifier/conftest.py` | 신설 — fixture (poll #12 박제 등) |
| 4 | `tests/pr_watcher_terminal_state_classifier/test_classify_hold_for_chair.py` | 신설 — RS-2670-B (5 tests) |
| 5 | `tests/pr_watcher_terminal_state_classifier/test_classify_loop_boundary.py` | 신설 — RS-2670-A (5 tests) |
| 6 | `tests/pr_watcher_terminal_state_classifier/test_classify_existing_branches.py` | 신설 — Critical7/MERGE_READY/Gemini stale regression (4 tests) |
| 7 | `tests/pr_watcher_terminal_state_classifier/test_callback_registrar.py` | 신설 — RS-2670-C (10 tests) |
| 8 | `memory/specs/system_ci_watch_handoff_policy_spec_260523_v2.md` | 신설 — v1 §5/§6/§11 보강 |
| 9 | `memory/events/task-2673.pr-watcher-fix-implementation-result-260526.json` | 신설 — 결과 박제 |
| 10 | `memory/events/task-2673.done` | 신설 — done marker |
| 11 | `memory/reports/task-2673.md` | 본 문서 |

★ PR #149 코드 (`utils/anu_codex_micro_refinement_loop.py`, `utils/codex_cc_decision_loop.py`,
`tests/anu_codex_micro_refinement_loop/**`) 변경 0.

---

## 2. 6 수정 목표 각각 구현 evidence (★ 보고 필수 #2)

### 목표 1 — fresh unresolved 발생 시 HOLD_FOR_CHAIR 조기 전환

`utils/pr_watcher_terminal_state_classifier.py` `classify()` 내 신규 분기:

```python
if gemini_fresh and unresolved > 0 and mss == "BLOCKED":
    return (
        HOLD_FOR_CHAIR,
        f"fresh_gemini_head_match + unresolved={unresolved} + mss=BLOCKED "
        f"(자동수렴 불가 · 회장 판정 필요)",
    )
```

★ 사고 박제 (dev7 watcher poll #12 silent fall-through) 의 재발 방지 결정적 line.
test: `test_classify_hold_for_chair.py::test_poll_12_returns_hold_for_chair_immediately`.

### 목표 2 — terminal_state 5 enum evaluation 보강

모듈 상수 + decision tree 명확화:

```python
MERGE_READY = "MERGE_READY"
HOLD_FOR_CHAIR = "HOLD_FOR_CHAIR"
GEMINI_EXTERNAL_TRIGGER_STALE = "GEMINI_EXTERNAL_TRIGGER_STALE"
CI_FAILED_NON_REMEDIABLE = "CI_FAILED_NON_REMEDIABLE"
LOOP_BOUNDARY = "LOOP_BOUNDARY"
TERMINAL_STATES = (MERGE_READY, HOLD_FOR_CHAIR, ..., LOOP_BOUNDARY)
```

`classify()` 우선순위: Critical7 → head_drift HOLD → fresh-evidence HOLD →
MERGE_READY → Gemini stale → LOOP_BOUNDARY (residual escalate).
spec v2 §1·§2 doctrine 박제.
tests: `test_classify_existing_branches.py::*` (4 tests).

### 목표 3 — ANU normal callback registrar 호출 보장

`register_terminal_callback(envelope, anu_key=None, chat_id=None, ...)` 신설. invariant:

- `cokacdir --cron <envelope> --at <ABSOLUTE TIMESTAMP> --chat <CHAT> --key <ANU> --once`
- envelope UTF-8 ≤ 3900 bytes
- ANU key fail-closed default (`c119085addb0f8b7`, self-key 0)
- subprocess timeout 30s, retry 0, exception silent

test: `test_callback_registrar.py::test_register_calls_cokacdir_with_required_args`.

### 목표 4 — max_watch_minutes 도달 전 HOLD 조건 보강

fresh-evidence HOLD 분기가 LOOP_BOUNDARY 보다 먼저 발화하므로, max_watch 도달 전에
HOLD 로 즉시 break 됨.
test: `test_classify_loop_boundary.py::test_fresh_evidence_priority_over_loop_boundary`.

### 목표 5 — LOOP_BOUNDARY elapsed-only 우선순위 보정

`classify()` LOOP_BOUNDARY 분기:

```python
if elapsed_watcher_sec >= max_watch_seconds:
    if unresolved > 0 or mss == "BLOCKED":
        return (HOLD_FOR_CHAIR,
                f"loop_boundary_with_residual: unresolved=... mss=...")
    return LOOP_BOUNDARY, ...
```

test: `test_classify_loop_boundary.py::test_loop_boundary_with_unresolved_escalates_to_hold`,
`test_loop_boundary_blocked_only_escalates`.

### 목표 6 — regression 추가

`tests/pr_watcher_terminal_state_classifier/` 신설, 24건 모두 PASS.

---

## 3. regression suite list (★ 보고 필수 #3)

| 파일 | 케이스 수 | 비고 |
|---|---|---|
| `test_classify_hold_for_chair.py` | 5 | RS-2670-B 본체 + B-pos/B-neg/B-edge + head drift |
| `test_classify_loop_boundary.py` | 5 | RS-2670-A 본체 + fresh priority + clean LB + blocked-only + below max_watch |
| `test_classify_existing_branches.py` | 4 | Critical7 / MERGE_READY / Gemini stale / priority |
| `test_callback_registrar.py` | 10 | envelope invariant + subprocess 인자 + absolute timestamp + fail-closed default + skip cases + non-zero exit |
| **합계** | **24** | |

---

## 4. pytest PASS/FAIL count (★ 보고 필수 #4)

```
============================== 24 passed in 0.10s ==============================
```

PASS: 24 · FAIL: 0 · ERROR: 0.

---

## 5. file overlap (★ 보고 필수 #5)

| 영역 | 변경 파일 수 |
|---|---|
| PR #149 코드 (`utils/anu_codex_micro_refinement_loop.py`, `utils/codex_cc_decision_loop.py`, `tests/anu_codex_micro_refinement_loop/**`) | **0** |
| task-2662~2672 events (`memory/events/task-266X*`, `task-267[012]*`) | **0** |
| forbidden_paths (settings.json / hooks / dispatch.py / .github / schemas / cokacdir bin) | **0** |

---

## 6. forbidden_action_count (★ 보고 필수 #6)

| 금지 8 (회장 verbatim) | 위반 |
|---|---|
| 1. PR #149 코드와 혼합 | 0 |
| 2. PR #149 merge | 0 |
| 3. auto-merge | 0 |
| 4. Axis 1/2/3 runtime 변경 | 0 |
| 5. live settings.json 변경 | 0 |
| 6. hooks live 변경 | 0 |
| 7. dispatch.py 변경 | 0 |
| 8. HARNESS_ENFORCED 전체 선언 | 0 |
| **합계** | **0** |

---

## 7. PR open 여부 (★ 보고 필수 #7)

- branch: `task-2673-dev8` (★ worktree `/home/jay/workspace/.worktrees/task-2673-dev8`)
- 별도 PR 발행 (★ PR #149 와 분리, merge_policy = `fix_implementation_separate_pr_no_merge_chair_signature_required`)
- merge 결정은 본 task 가 아니라 별도 chair signature 발급 후 진행.

---

## 8. recommended next action (★ 보고 필수 #8)

1. **PR review + merge 별도 chair signature** (★ 본 task ttl 48h 이내 회장 verbatim 발급 필요)
2. 신규 PR watcher cron 발사 시 본 모듈 import 해서 사용 (★ v2 spec §7 사용 예시 인용)
3. `/home/jay/.cokacdir/workspace/29C74592/watcher.py` 는 사고 박제 보존용 — 수정 0
4. 후속 watcher 발사는 신규 schedule 로만 (PR #149 자체의 watcher 재기동 0)

---

## 9. ANU normal callback

- envelope UTF-8 ≤ 3900 bytes (invariant)
- absolute timestamp now+30s
- ANU key 단일출처 (`c119085addb0f8b7`) · self-key 0
- envelope only (★ raw schedule 0)
- task md verbatim 명령에 따라 finalize 단계에서 cron 1회 발사

---

## 10. anchors (회장 verbatim 정합)

- ANCHOR-1: task-2673 단일 표기 = HOLD_FOR_CHAIR (★ spec v1 §5 CHAIR_REQUIRED 와 watcher.py L113 HOLD_FOR_CHAIR 통합)
- ANCHOR-2: classify() decision tree 우선순위 = Critical7 → head_drift HOLD → fresh-evidence HOLD → MERGE_READY → Gemini stale → LOOP_BOUNDARY (residual escalate)
- ANCHOR-3: fresh-evidence HOLD 분기 = gemini_fresh AND unresolved>0 AND mss==BLOCKED (사고 박제 poll #12 결정적 line)
- ANCHOR-4: callback invariant = UTF-8 ≤3900 / absolute timestamp / ANU key 단일출처 / `--once` / timeout 30s / retry 0 / exception silent
- ANCHOR-5: PR #149 코드 변경 0 · task-2662~2672 파일 충돌 0 · 별도 PR

끝
