# task-2728 보고서 — idempotent callback fallback lifecycle (registry 기반 prune + 6 cause enum + canonical_root + result.json contract)

- 작업 ID: task-2728
- 팀: dev6-team (팀장 페룬)
- base: fresh origin/main `6a44d712`
- 브랜치: `task/task-2728-dev6`
- 워크트리: `/home/jay/workspace/.worktrees/task-2728-dev6`
- 일시: 2026-06-03 18:13~18:30 (KST)

## S (Situation)
ANU가 매 round fallback safety-net cron을 raw `cokacdir --cron`으로 직접 등록하지만, normal callback 도착 후에도 **실제 cron cancel이 코드로 결선되지 않아**(텍스트 doctrine만 존재) stale fallback이 실행됨. 실증 2건:
1. task-2726+1 fallback이 16:46 normal callback 도착 이후 17:50에 stale 실행.
2. task-2726+2 fallback `A86DB611`이 callback(head 80416faa 확정) 후에도 잔존 → ANU 수동 prune.

## C (Complication)
기존 `utils/completion_callback_fallback_cancel.py`(554줄)는 `cancel_fallback_on_success`를 제공하나, `evaluate_safe_remove`가 **dispatch-fired marker의 `callback_policy_a.fallback_callback_cron_id`를 단일 권위로만** 사용한다. ANU가 raw `cokacdir --cron`으로 등록한 fallback의 **실제 cron id(예: A86DB611)는 어떤 durable registry/marker에도 기록되지 않아** marker 예측 id(`task_id::fallback`)와 불일치 → C1 검증 실패 → `SKIPPED_UNTRUSTED` → 영구 미prune. 또한 6 granular cause enum이 없어 정상 자동삭제(schedule_not_found)와 실패(cancel_failed)를 구분 못 함.

## Q (Question)
normal callback/result 수집이 성립했을 때, ANU가 raw로 등록한 fallback까지 포함해 동일 `task_id·round·head`의 pending fallback을 **누락 없이, idempotent하게** prune하려면 어떤 결선이 필요한가?

## A (Answer) — 구현
audit-first(read-only) → 구현 → regression 순으로 진행. 근본원인을 `cancel_not_wired`로 확정하고, durable registry + idempotent prune + 2계층 self-check + 6 cause enum + canonical_root 우선 + result.json contract를 구현.

### Phase 0 — audit (read-only)
- 현 cancel 경로 map + A86DB611/+1 미prune 원인 확정.
- **근본원인 = `cancel_not_wired`(cause (a))**: ANU 수동 fallback이 durable registry에 미기록 → collector가 prune 대상 cron_id 미발견. 보조로 cause (b)(collector entrypoint가 origin/main에 부재) 성립.
- 기록: `memory/events/task-2728.audit-260603.json` (wiring gap-1~gap-5).

### Phase 1 — 구현
1. **`utils/fallback_schedule_registry.py`** (신규): durable JSONL registry(`memory/state/fallback_schedule_registry.jsonl`, canonical_root 우선). `register_fallback / read_records / pending_for / mark_pruned`. append-only last-wins.
2. **`utils/completion_callback_fallback_cancel.py`** (수정):
   - ANU key 리터럴 → `from utils.callback_envelope_schema import ANU_CALLBACK_KEY as ANU_KEY` **sealed import 교체**(파일 내 리터럴 0건).
   - `PruneCause` 6 enum: `cancel_not_wired / cancel_failed / schedule_not_found / stale_round / stale_head / normal_callback_already_collected`.
   - `prune_fallbacks_for_key`: registry 기반 `task_id·round·head` keyed idempotent prune(빈 pending → `[]` 재실행 안전).
   - `detect_unwired_fallback`: dispatch fired인데 registry 미기록 → `cancel_not_wired` 코드화.
   - `fallback_self_check`: fallback cron 실행 시점 2계층 방어(stale_round/stale_head/normal_callback_already_collected → no-op).
   - 기존 `cancel_fallback_on_success` 등 §9-R 함수/클래스 **시그니처 무변경**.
3. **`utils/normal_completion_callback_collector_entrypoint.py`** (신규): `collect_and_prune` — canonical_root/worktree_path 주입 우선(autoset 추측 0), result.json contract(`RESULT_JSON_MISSING_RECOVERED_BY_COLLECTOR` + follow_up), registry 기반 prune 결선 + unwired 감지.
4. **`memory/state/automation_capability_matrix.json`** (신규): `callback_fallback_prune` 4축 — IMPLEMENTED=true / VERIFIED=true / WIRED=true / **ACTIVE=false**(실 운영 collector live prune 관측 전까지 ACTIVE 금지).

### Phase 2 — regression
`tests/regression/test_callback_fallback_prune_2728.py` (신규, 18 tests):
- fixture-A: +1 normal callback 후 fallback prune → CANCELLED, 이후 pending [] (stale fire 차단).
- fixture-B: +2 A86DB611 잔존 → collect_and_prune CANCELLED + idempotent 재실행 [].
- fixture-C: self-check no-op 3종 + recovery proceed(6 cause 정확 기록).
- fixture-D: already_gone → `schedule_not_found`(≠cancel_failed), failed → `cancel_failed`+PENDING 유지.
- fixture-E: canonical_root/worktree_path 주입 시 그 root 사용(autoset 0).
- fixture-F: result.json 부재 → `RESULT_JSON_MISSING_RECOVERED_BY_COLLECTOR`+follow_up.
- fixture-G: registry 미기록 fallback → `cancel_not_wired` 감지(근본원인 회귀).
- fixture-9R: 기존 `evaluate_safe_remove`/`evaluate_durable_evidence` 무손상.

## 생성/수정 파일 목록
- (신규) `utils/fallback_schedule_registry.py`
- (수정) `utils/completion_callback_fallback_cancel.py`
- (신규) `utils/normal_completion_callback_collector_entrypoint.py`
- (신규) `tests/regression/test_callback_fallback_prune_2728.py`
- (신규) `memory/state/automation_capability_matrix.json` (gitignore — memory 산출물)
- (신규) `memory/events/task-2728.audit-260603.json` (gitignore — memory 산출물)
- `git diff --name-only origin/main` = 추적 4파일 (위 코드 3 + 테스트 1), ≤5 충족.

## 테스트 결과
- `python3 -m pytest tests/regression/test_callback_fallback_prune_2728.py -q` → **18 passed**.
- goal_assertion #1 (regression) PASS / #2 (6 cause enum 전부 존재) PASS / #3 (ANU key 리터럴 0) PASS.
- 기존 callback 회귀(`test_callback_runtime_enforcement_2626.py` 등) 무손상. **단, `test_regression_8.py::test_finish_task_sh_uses_absolute_at_command_substitution` 1건 FAIL은 origin/main 사전 실패**(finish-task.sh 정적 패턴 검사 — 미접촉 forbidden 파일, 본 작업과 무관).
- forbidden 0 (finish-task.sh / critical_gap.py / terminal_state_callback.py / deploy/systemd / .github 미접촉).
- canonical_root `/home/jay/workspace` 하드코딩 의존 0 (CANONICAL_ROOT_DEFAULT import만).

## L1 스모크테스트 결과 (필수)
- 서버 재시작: 해당없음 (서버/API/프론트 무관 — 모듈/subprocess 정제 작업).
- API 응답 확인: 해당없음.
- 스크린샷: 해당없음.
- **subprocess/정제 L1 (실제 실행 + 결과 파일 확인)**: 실제 on-disk registry 파일(`<root>/memory/state/fallback_schedule_registry.jsonl`)에 fixture-B 사건(A86DB611, round=2, head 80416faa) 등록 → `collect_and_prune` 실행 → JSONL에 PENDING + PRUNED(cause=`pruned_on_normal_callback_collected`) tombstone 2줄 영속 확인 → idempotent 재실행 시 `prune_outcomes=[]` (stale fire 차단). canonical_root_source=`canonical_root_injected` 확인. **L1_SMOKE_PASS** (실 cron/subprocess 호출 0 — fake remover).

## 발견 이슈 및 해결
- (해결) origin/main에 `normal_completion_callback_collector_entrypoint.py`/`automation_capability_matrix.json` 부재(PR #171/task-2726 소관) → fresh origin/main 기준 self-contained 신규 생성으로 PR #171 혼입 방지.
- (해결) `completion_callback_fallback_cancel.py`의 ANU key 하드코딩 → sealed import 교체(goal_assertion #3 충족).
- (해결) 백엔드 산출물 unused import(os/json/fallback_schedule_registry) 정리 + `normal_callback_collected` 반환 dict 결선.
- (범위 외) `test_regression_8.py` finish-task.sh 정적 검사 1건 FAIL은 origin/main 사전 결함 — finish-task.sh가 forbidden이라 본 task에서 수정 불가(#5는 PR #171 머지 후 별도 재평가 명시).

## 모델 사용 기록
- 페룬(팀장, Opus): audit/설계/검토/통합/L1 스모크 (직접 코딩 최소화).
- 스바로그(백엔드, Sonnet): Phase 1 구현 4파일.
- 라다/모코시: 미소환 (프론트/디자인 작업 없음).
- 벨레스(테스터, Sonnet): Phase 2 regression 18 tests.
- haiku 미사용 (로직/테스트는 sonnet 이상 규칙 준수).

## 머지 판단
- **머지 필요**: Yes (Lv.2 → PR 경로)
- **브랜치**: `task/task-2728-dev6`
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2728-dev6`
- **머지 의견**: 18 regression PASS + L1 실파일 검증 PASS, forbidden 0, diff 4파일(≤5), §9-R 무손상. merge_policy=none(회장 승인 전 머지 금지) — 새 head OWNER `/gemini`(회장 직접) → CI GREEN + unresolved 0 시 `READY_FOR_CHAIR_MERGE_APPROVAL` 보고 대상.

## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회

