# 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` 보고 대상.

---

# [Gemini Triage Round] 2026-06-03 20:03~20:08 KST — PR #172 unresolved 6건 bounded 수렴

## S (Situation)
회장 자동 triage/fix loop 인가(2026-06-03). PR #172(prev head `fa5ba4b7`)에서 **같은 PR bounded triage round**로 이어서 — task-2728+1 분리 금지. Gemini 미해결 thread 6건(HIGH 1 + MEDIUM 5)을 expected_files 3코드+test 범위 안에서만 수렴. 단일소스: `memory/events/task-2728.gemini-triage-260603.json` (ANU 독립검증).

## C (Complication)
HIGH 1건이 false positive(enum 실재) 의심 + MEDIUM 5건 robustness/efficiency/concurrency/observability 결함. 모두 expected_files 밖 수정·credential·scope 확장 없이 cheap fix로 종결해야 하며, 새 HIGH/CRITICAL 재발·같은 blocker 반복 시 CHAIR_REQUIRED.

## Q (Question)
6 finding을 코드 변경 ≤3파일 + test 안에서 bounded하게 수렴하고, false positive를 단언으로 종결할 수 있는가?

## A (Answer) — finding별 처리
| id | sev | 판정 | 조치 | 결과 |
|----|-----|------|------|------|
| 3347386331 | HIGH | FALSE POSITIVE — `CancelClassification.ALREADY_FIRED` enum line 82 실재(AttributeError 불성립). 단 already_fired 전용 regression 부재=VALID | 코드 변경 없이 **already_fired 전용 regression 2건 추가**(AF-1/AF-2) | 종결 — `hasattr` 명시 단언 + enum 매핑/cause/pruned 단언 |
| 3347386351 | MEDIUM | VALID robustness — prune 루프 per-rec try-except 부재로 1건 예외가 나머지 prune 차단 | `prune_fallbacks_for_key` 루프 본문 per-rec try-except, except→`REMOVE_FAILED_WARNING`+cause `cancel_failed`+continue | 해결 — L1 스모크로 1건 장애 격리 실증 |
| 3347386334 | MEDIUM | efficiency — `read_text().splitlines()` 전체로드 | `read_records` 파일 핸들 iteration(`for raw in fh`)으로 전환 | 해결 |
| 3347386378 | MEDIUM | concurrency — append race | `_append_locked` 헬퍼(`fcntl.flock(LOCK_EX)` + 비POSIX graceful fallback) → register_fallback/mark_pruned 적용 | 해결 |
| 3347386358 | MEDIUM | observability — logger 부재 | `import logging` + `logger = logging.getLogger(__name__)` | 해결 |
| 3347386367 | MEDIUM | observability — RESULT_JSON_MISSING warning 부재 | result.json 부재 분기에 `logger.warning(...)` | 해결 |

## 수정 파일 (expected_files 내부, diff=5)
- `utils/completion_callback_fallback_cancel.py` — per-rec try-except (160 lines 변경, 들여쓰기 이동 포함)
- `utils/fallback_schedule_registry.py` — line iteration + `_append_locked`/`fcntl.flock`
- `utils/normal_completion_callback_collector_entrypoint.py` — logger + RESULT_JSON_MISSING warning
- `tests/regression/test_callback_fallback_prune_2728.py` — `_remover_already_fired` + AF-1/AF-2 (총 +84 lines)
- `memory/reports/task-2728.md` — 본 triage round 갱신

## 필수 검증 (회장 6 — 새 head 기준)
1. **regression**: `pytest tests/regression/test_callback_fallback_prune_2728.py -q` → **20 passed** (기존 18 + already_fired 신규 2, 회귀 0).
2. **fresh Gemini unresolved 0**: 새 head non-force push 후 회장 OWNER `/gemini` 재트리거 대기(ANU 회수). — *대기 항목(머지 전 게이트)*.
3. **diff name-only vs origin/main** = expected_files 5개(3 code + test + report) 내부 — ✅ 확인.
4. **forbidden 0**: finish-task.sh / critical_gap.py / terminal_state_callback.py / deploy/systemd / .github 미접촉 — ✅.
5. **ANU key literal 0**: 3 code + test 전부 `c119085addb0f8b7` 0건 — ✅ (`OK_NO_KEY_LITERAL_ANYWHERE`).
6. **capability**: `callback_fallback_prune` ACTIVE=false 유지, merge_policy=none — ✅ (코드 변경 무관, 선언 미변경).

## L1 스모크테스트 결과 (필수 기록)
- **서버 재시작**: 해당없음 (server.py 무관한 callback/fallback 유틸 라이브러리 — 서버 미기동).
- **API 응답 확인**: 해당없음 (HTTP 엔드포인트 아님).
- **subprocess/정제 L1 (실제 실행)**: `/tmp/l1_smoke_2728.py` 실행 — 동일 (task·round·head)에 3 pending(CRON_A/B/C) 등록 후, CRON_B에서 `RuntimeError` 던지는 flaky remover로 `prune_fallbacks_for_key` 호출.
  - 결과: `outcomes count = 3` (1건 예외에도 전부 처리) → CRON_A/C=`CANCELLED`(tombstone), CRON_B=`REMOVE_FAILED_WARNING`(pruned=False, PENDING 유지). 재조회 `remaining PENDING=['CRON_B']`. **L1_SMOKE_PASS** — per-rec try-except가 1건 cron API 장애를 격리하고 나머지 prune을 정상 진행함을 실행으로 실증(pytest를 넘어선 실동작 확인).
- **스크린샷**: 해당없음 (UI 없음).
- **py_compile**: 3 code 파일 COMPILE_OK.

## 발견 이슈 및 해결 (triage round)
- (해결) Gemini HIGH 3347386331 — enum 실재 검증(`grep -n ALREADY_FIRED` line 82) 후 코드 변경 없이 단언 테스트로 false positive 종결. CHAIR_REQUIRED 트리거(새 HIGH/CRITICAL·같은 blocker 반복·expected_files 밖·credential/scope) 0건.
- (참고) Pyright `reportMissingImports`(utils.* 미해결)은 worktree 루트 PYTHONPATH 미설정 정적 분석 한계 — pytest(20 passed)·py_compile·L1 런타임 정상으로 실해 없음(기존 패턴 동일).

## 모델 사용 기록 (triage round)
- 페룬(팀장, Opus): triage 판정/위임/통합/L1 스모크 직접 수행.
- 스바로그(백엔드, Sonnet): MEDIUM 5건(3 utils 파일).
- 벨레스(테스터, Sonnet): already_fired regression 2건.
- 라다/모코시 미소환(프론트/디자인 작업 없음). haiku 미사용.

## finalize 상태
- 새 head 커밋: `dd58432b` → non-force push로 PR #172 갱신.
- ANU normal callback cron 강제 등록(collector_role=ANU, ANU_KEY sealed, fallback registry 기록) — self-key 금지.
- **merge 금지** — 새 head 회장 OWNER `/gemini` 재트리거 → unresolved 0 + CI GREEN 시 ANU가 `READY_FOR_CHAIR_MERGE_APPROVAL` 보고.
