# normal callback registration enforcement implementation spec — 260523

회장 결정(2026-05-23): task-2634 dev6 NORMAL_CALLBACK_NOT_REGISTERED 사고 hardening 의 **구현 spec**. 기반 triage spec=`system_normal_callback_registration_enforcement_spec_260523.md`(read-only/사고분석). 본 spec=expected_files/fixture/enum/fail-closed 구체화. **real auto-merge executor 활성화 미승인 유지 · callback hardening 만 진행.**

기반: `feedback_normal_callback_not_registered_variant_260523.md` + `feedback_callback_self_key_helper_not_wired_260521.md` + `feedback_callback_must_spawn_independent_anu_not_executor_self_260518.md` + `feedback_dispatch_must_register_fallback_safetynet_260520.md` + `feedback_executor_completion_callback_mandatory_260518.md`.

---

## 1. 목표 (회장 verbatim)

1. executor 완료 시 ANU key normal callback registrar 가 반드시 실행되도록 코드/검증으로 강제
2. sendfile/report 전송은 callback 대체 아님을 분리
3. result.json 에 `normal_callback_registration_status` 필수화
4. status enum 정의 (5값)
5. `NOT_REGISTERED` / `SENDFILE_ONLY` 는 완료 성공으로 보지 않도록 fail-closed
6. fallback cancel-on-success 의 success-signal source 명확화
7. fixture: REGISTERED / NOT_REGISTERED / SENDFILE_ONLY / REGISTER_FAILED
8. 회장 수동 전달 없이 ANU collector spawn 검증 regression 또는 dry-run fixture

---

## 2. normal_callback_registration_status enum (정본)

```
REGISTERED                       — registrar 호출 성공 + cron schedule_id 회수 완료 (ANU key 발사 정상)
NOT_REGISTERED                   — registrar 호출 0 (envelope 작성만 함 / sendfile 만 함 등 task-2634 패턴)
REGISTER_FAILED                  — registrar 호출 시도했으나 cokacdir --cron 실패 (network/CLI 오류)
SENDFILE_ONLY                    — envelope sendfile 만 수행, cron 등록 의도적 누락 (회장 복붙 의존 패턴 박제)
SKIPPED_WITH_EXPLICIT_REASON     — 회장 명시 사전 승인된 callback 면제 (debug/dry-run 모드, 사유 필수)
```

**fail-closed 동작**:
- `REGISTERED` / `SKIPPED_WITH_EXPLICIT_REASON` → 완료 성공
- `NOT_REGISTERED` / `SENDFILE_ONLY` / `REGISTER_FAILED` → **완료 실패 처리** (executor exit code !=0, finalize hook reject, fallback 미cancel)
- 미명시 (필드 부재) → 자동 `NOT_REGISTERED` 로 간주

---

## 3. helper 코드 결선 위치 (회장 verbatim "executor 완료 경로")

### 3.1 신규 helper 모듈
- **`utils/anu_callback_registrar.py`** — 순수함수
  - `build_callback_envelope(task_id, result, anu_key, collector_role="ANU") -> envelope_dict`
  - `register_normal_callback(envelope, delay_seconds=10) -> {status, schedule_id?, registered_at_ts, error?}`
  - 내부: `cokacdir --cron "..." --at "T+10s" --chat 6937032012 --key <ANU>` subprocess 호출
  - ANU key hardcoded fail-closed (`c119085addb0f8b7`) — self-key 차단 정적 가드
  - envelope UTF-8 byte 측정 (`wc -c` 등가) + ≤3900 hard limit + ≤2800~3200 warn

### 3.2 schema 모듈
- **`utils/callback_envelope_schema.py`** — registration_status enum + validator
  - `NormalCallbackRegistrationStatus` enum (5값 위 §2)
  - `validate_envelope(envelope) -> (ok, errors)` — registration_status 필드 존재/enum 일치 단언
  - `is_success_status(status) -> bool` — fail-closed 분기 함수

### 3.3 dispatch lifecycle hook
- **`dispatch/finalize_hooks.py`** — executor 완료 lifecycle 결선
  - `finalize_with_callback_registration(task_id, result, anu_key) -> finalize_result`
  - 호출 순서: envelope build → registrar 호출 → registration_status 회수 → result.json 갱신 → fail-closed 분기
  - sendfile 은 별도 함수 `send_envelope_to_chat(envelope, chat_id)` — **callback 대체 아님 (보조)**
- **`dispatch/__init__.py`** — 신규 모듈 import 1줄 추가 (`from dispatch.finalize_hooks import finalize_with_callback_registration`) — 결선 enforcement

### 3.4 fallback success-signal source 정의
- ANU fallback cron 의 cancel-on-success signal = **`normal_callback_registration_status == REGISTERED` + collector durable-success 마커 생성**
- 단순 envelope sendfile 또는 result.json 존재 만으로는 cancel 안 함 (false cancel 차단)
- `utils/anu_callback_fallback.py`(또는 `dispatch/fallback_cancel_logic.py`) — success-signal 판정 함수

---

## 4. fixture (frozen, live 의존 0)

### 4.1 신규 fixture (4 시나리오 최소)
`tests/fixtures/normal_callback_registration/<scenario>/{evidence.json, expected.json, PROVENANCE.md}`

| 시나리오 | input registration_status | input delivery_method | expected fail_closed | expected fallback_cancel |
|---|---|---|---|---|
| `registered_normal` | REGISTERED | anu_cron_callback | false (성공) | true (cancel) |
| `not_registered_envelope_only` | NOT_REGISTERED | (없음) | true (실패) | false (no-cancel) |
| `sendfile_only_no_cron` | SENDFILE_ONLY | sendfile_only | true (실패) | false (no-cancel) |
| `register_failed_cli_error` | REGISTER_FAILED | anu_cron_callback (시도) | true (실패) | false (no-cancel) |

권장 추가 (5번째): `skipped_explicit_reason_dryrun` → registration_status=`SKIPPED_WITH_EXPLICIT_REASON` + reason 필드 명시 → fail_closed=false / fallback_cancel=true

### 4.2 evidence schema
- `task_id` · `executor_name` · `result_path` · `report_path`
- `attempted_callback_registration` (bool)
- `registration_status` (enum 위 §2)
- `cron_schedule_id` (optional · REGISTERED 시 필수)
- `delivery_method` enum (`anu_cron_callback` / `sendfile_only` / `both` / `none`)
- `error_message` (optional · REGISTER_FAILED 시 필수)
- `explicit_skip_reason` (optional · SKIPPED 시 필수)

### 4.3 expected schema
- `finalize_result` (`success` / `fail`)
- `fallback_cancel_signal` (true/false)
- `collector_spawn_expected` (bool)
- `is_callback_complete` (bool)

---

## 5. regression 구현

`tests/regression/test_anu_callback_registrar.py`:
- registrar 호출 시 ANU key 정확 단언 (`c119085addb0f8b7` 사용, self-key 차단)
- envelope UTF-8 byte 측정 ≤3900 단언
- cron 등록 시도 subprocess 호출 시그니처 일관

`tests/regression/test_callback_registration_enforcement.py`:
- §4.1 4 fixture 각 finalize_result 단언
- `NOT_REGISTERED` / `SENDFILE_ONLY` / `REGISTER_FAILED` 시 fail-closed 단언
- `REGISTERED` 시 success + fallback cancel 단언

`tests/regression/test_callback_vs_sendfile_separation.py`:
- sendfile_only 만으로는 callback 충족 안 됨 단언
- envelope sendfile 함수 호출 ≠ cron 등록 함수 호출 (별개 함수)
- 두 함수 시그니처/책임 분리 단언

`tests/regression/test_collector_spawn_dry_run.py`(또는 fixture 확장):
- REGISTERED status + cron_schedule_id 입력 → collector spawn expected=true 단언
- NOT_REGISTERED 입력 → collector spawn expected=false 단언
- subprocess 실호출 0 (frozen fixture 기반 dry-run)

---

## 6. expected_files (task-2635 범위)

### 6.1 신규
- `utils/anu_callback_registrar.py`
- `utils/callback_envelope_schema.py`
- `utils/anu_callback_fallback.py`(또는 `dispatch/fallback_cancel_logic.py`)
- `dispatch/finalize_hooks.py`
- `tests/fixtures/normal_callback_registration/<5 시나리오>/{evidence.json,expected.json,PROVENANCE.md}` (15 files)
- `tests/regression/test_anu_callback_registrar.py`
- `tests/regression/test_callback_registration_enforcement.py`
- `tests/regression/test_callback_vs_sendfile_separation.py`
- `tests/regression/test_collector_spawn_dry_run.py`
- (선택) `tests/fixtures/normal_callback_registration/INDEX.md`

### 6.2 수정 (최소 결선)
- `dispatch/__init__.py` — import 1줄 추가 (`from dispatch.finalize_hooks import finalize_with_callback_registration`)
- (필요 시) `dispatch/<기존 finalize 진입점>` — finalize hook 호출 1줄 추가

총 25~27 files. **프로덕션 코드 영향**:
- `dispatch/__init__.py` 1줄 import (read-only safety)
- 기존 finalize 진입점 1줄 hook 호출 (있는 경우)
- replacement_pr_runner / finish-task.sh / merge_ready_classifier / merge_ready_dryrun_executor 전부 **무수정**

---

## 7. 안전 불변식

- ANU key `c119085addb0f8b7` 하드코딩 (registrar 내부 단일 출처) — self-key 변종 차단
- envelope UTF-8 ≤3900 bytes hard limit · 위반 시 `REGISTER_FAILED`
- `NOT_REGISTERED` / `SENDFILE_ONLY` / `REGISTER_FAILED` 시 fail-closed (executor exit !=0 + fallback no-cancel)
- live cokacdir CLI 호출은 helper subprocess wrapper 단일 경로 (regression 은 dry-run · subprocess 실호출 0)
- merge/push/PR/cron/admin override 호출 0 (regression 차원)
- replacement_pr_runner.py / finish-task.sh / merge_ready_classifier / merge_ready_dryrun_executor 무수정
- expected_files 외부 수정 0

---

## 8. 자동수렴 원칙

- Gemini medium/style/quality + expected_files 내부 + Critical7 0 + credential expansion 0 → 자동수렴
- expected_files 내부 non-critical HIGH 도 자동수렴, 단 동일 함수 HIGH 반복 시 LOOP_BOUNDARY → 회장 보고
- 회장 보고 트리거: Critical7 / credential expansion / expected_files 밖 수정 / admin override / replacement_pr fail / post-merge smoke fail

---

## 9. 금지 (회장 verbatim)

- real auto-merge executor 구현 금지
- auto-merge 활성화 금지
- NL intake 코드 구현 금지
- foreign dirty 정리 금지
- replacement_pr_runner.py 수정 금지
- PR #137 산출물과 섞기 금지
- production service task 와 혼합 금지
- callback 재발사 실험 남발 금지
- merge_ready_classifier · merge_ready_dryrun_executor 본체 수정 금지

---

## 10. finalize 프로토콜 (★ BOT App token 부재 — 로컬 한정)

1. base = 최신 origin/main 5ffa87ae (PR #137 shadow validation 머지분) clean worktree
2. 신규 helper 4 + fixture 15 + regression 4 + dispatch/__init__.py 1줄 import 수정 전부 PASS + full new-fail 0
3. **로컬 commit만** (push/PR/merge 금지). finish-task.sh project_path 없이 로컬 종결
4. ANU normal completion callback — **본 task 자체로 신규 registrar 사용 검증**: helper 모듈 구현 후 본 task 종료 callback 도 새 registrar 로 발사 (자기 hardening 실증)
5. registrar 미완성 시 fallback: 기존 패턴(envelope 명시 + cokacdir --cron --key c119085addb0f8b7 직접 호출) — collector_role=ANU
6. callback envelope UTF-8 ≤3900 bytes · envelope 만: task_id=task-2635 · 로컬 commit SHA · result_path · report_path · 25~27 file 검증 요약 · regression 요약 · sha256 · `normal_callback_registration_status=REGISTERED` 필드 포함 (본 task 의 자기 검증)
7. executor 시작/종료 ts·로컬 commit SHA 명기

이후 ANU: 봇 로컬 commit fresh main 재적층 → OWNER push → PR open → Gemini 자동수렴 → 회장 보고 → 회장 merge 승인.

---

## 11. frozen anchor (D-SPEC-EXACTNESS)

- ANCHOR-1: "executor 완료 시 ANU key normal callback registrar 실제 코드 결선 강제 — prompt doctrine 만으로는 NORMAL_CALLBACK_NOT_REGISTERED 재발"
- ANCHOR-2: "registration_status enum 5값 (REGISTERED/NOT_REGISTERED/REGISTER_FAILED/SENDFILE_ONLY/SKIPPED_WITH_EXPLICIT_REASON) · NOT_REGISTERED/SENDFILE_ONLY/REGISTER_FAILED fail-closed"
- ANCHOR-3: "sendfile/report 전송 ≠ callback — 함수/스키마 분리 (test_callback_vs_sendfile_separation 단언)"
- ANCHOR-4: "fallback cancel-on-success = REGISTERED status + collector durable-success 마커 (false cancel 차단)"
- ANCHOR-5: "real auto-merge executor 미승인 — 본 task = callback hardening 까지만"
- ANCHOR-6: "replacement_pr_runner/finish-task.sh/merge_ready_classifier/merge_ready_dryrun_executor 무수정 · dispatch/__init__.py 는 import 1줄 추가만 허용"
