# 인프라 결함 2축 조사 (read-only) — A축 truncate / B축 callback·finalize

상태명: `INFRA_DEFECT_TWO_AXIS_INVESTIGATION_ACTIVE_FALSE`
작성: ANU 직접 (2026-07-02 KST). read-only 조사 + 감시강화 설계만. 코드수정/PR/dispatch/추가복원 0. 근거: 회장 GPT 판정(우선순위=인프라 결함>task-2781 PR, 2축 분리 보고).

---
## A축 — replacement_pr_runner truncate root-cause 후보 + 재현 감시 강화안

### A-1. 확정 사실 (현장 포착)
- 00:13:59 KST, canonical 2파일(`utils/replacement_pr_runner.py` 33557→0, `tests/regression/test_replacement_pr_runner_2510.py` 24403→0)이 **동일 mtime으로 동시** 0바이트.
- finish-task-start 00:12:42 → truncate 00:13:59 (**+77초**) → escalation 00:15:48. = finish-task **초기(QC/finalize 서브프로세스) 단계**.

### A-2. 배제된 원인 (read-only 실증)
- **git 조작 아님**: `git reflog` 00:12~00:16 canonical HEAD 이동 0건(마지막 09:45 ff-pull). checkout/reset/stash-pop 배제.
- **finish-task.sh 직접 write 아님**: 스크립트 내 두 파일 대상 write 0. git 관련은 `git stash list`(count)·`git worktree list`(read-only)뿐. destructive checkout/restore/clean/reset 없음.
- **git-hooks 아님**: pre-commit/pre-push에 포매터·두 파일 write 0(락파일 read만).
- **QC 아님**: qc_verify.py가 merge-executor/replacement import 0.
- **명시적 truncate 코드 아님**: scripts/utils/dispatch 어디에도 두 파일 open('w')/`:>`/truncate 0.

### A-3. 남은 후보 (좁혀진 지목)
두 파일은 **real merge executor 체인**이 import: `utils/real_merge_hooks.py`·`repository_policy_adapter.py`·`automation_contracts.py`·`merge_queue_executor.py`. truncate는 finish-task 서브프로세스가 **직접 0바이트 write**한 것(git 아님). 1순위 후보:
1. finalize 중 merge-executor 체인을 import/실행하는 서브프로세스(예: `run_auto_pr_merge_readiness_dry_run_2609.py` — __pycache__가 replacement_pr_runner 참조)가 해당 파일을 stub/재생성하며 0바이트 write.
2. merge_queue_executor/replacement_pr_runner 자체가 import·dry-run 시 자기 경로에 0바이트 write(자기훼손).
- ※ task-2781은 merge SKIP(project_path 없음)인데도 발생 → merge 미수행 경로에서도 트리거되는 import-time 부작용 의심.

### A-4. 재현 감시 강화안 (30초 폴링→정밀, 설계만·다음 finish-task 시 arm)
30초 폴링은 순간 truncate의 범인 프로세스를 놓침(fuser/lsof 빈값). 강화 옵션(권한순):
- **(권장) auditctl 파일 워치**: `auditctl -w <file> -p wa -k truncwatch` → `ausearch -k truncwatch`로 **pid·comm·exe·syscall** 확보. sudo 필요.
- **bpftrace**: `openat`(O_TRUNC)/`ftruncate` 시스템콜을 inode 필터 → pid+comm+ustack. sudo 필요.
- **fanotify(FAN_MODIFY)**: pid 제공. CAP_SYS_ADMIN 필요.
- **(무권한 fallback) inotifywait -m -e modify,close_write,open** + 이벤트 즉시 `ps --forest`(finish-task 트리 상시 존재)로 서브프로세스 특정.
- **(선택·승인필요) 트립와이어**: 감시 중 2파일 `chattr +i`(immutable) → truncate 시도가 **실패**하며 finish-task 로그에 정확한 명령/라인 노출. ★파일 속성 변경이므로 회장 승인 필요.

---
## B축 — normal callback / finalize 실패 root-cause + 최소 수정안

### B-1. 분류 확정
- **CALLBACK_NOT_DELIVERED**: `.done` 없음(`.done.escalated`만·사유 state_file_missing)·result.json 없음·ANU 콜백 cron 0·schedule_history 실발사 0(5328933C=executor뿐, 0BCB9FBD=무관 오디오 recurring).
- **dev2 "before-exit guard가 ANU key로 발사·정상 종단" = FALSE_CLAIM**(실증 schedule/result.json/inbox 전무).

### B-2. root-cause 조사 (GPT 4항목)
1. **project_path 없는 ANU-gated run에서 왜 .done.escalated?** finish-task.sh:878(task-2472) `state file missing → merge 차단 + escalation_marker(kind=escalated, blocking=state_file_missing)` → line 893 ESCALATED이면 `.done` 차단. state 파일 `.tasks/state/task-2781.json`은 taskctl.py transition이 생성하나 **미생성**. ★단 task-2778/2779 state도 부재한데 머지됨 → state는 정상 dispatch서 안 만들어짐(정상흐름 gap), project_path 없는 escalation 경로에서만 이 게이트가 걸림.
2. **terminal_state_callback이 dispatched marker 없이 fail-open 종료**: emit은 `{task_id}.dispatched-*.json`에서 attempt/schedule_id를 읽음(harness/v36/terminal_state_callback.py:79). **dispatched marker 부재** → 콜백 등록 컨텍스트 없음 → fail-open(정상 import 시 항상 exit 0)으로 조용히 종료, `.terminal-callback-failed.json`도 안 남김(성공처럼 보임).
3. **result.json 생성 책임**: dispatch 계약(OS-LEVEL PICKUP CLOSEOUT)상 executor가 result.json(`callback_schedule_created:false` 명시) 작성, OS-level pickup runner가 owner-proof 후 closeout. 이번엔 **executor가 result.json 자체를 안 씀**(escalation 경로가 result.json 생산을 건너뜀).
4. **모든 종단에 ANU terminal callback 남는가? → NO**: 정상완료(.done)는 before-exit guard가 커버하나, **escalation 종단(.done.escalated)은 ANU terminal callback 보장 결선이 없음** = 핵심 결함. dispatched marker 부재 시 콜백 컨텍스트 소실까지 겹침.

### B-3. 최소 수정안 (설계만·별도 승인)
- **핵심**: escalation 종단(.done.escalated / BLOCKED / state_file_missing)에서도 **ANU terminal callback 강제 결선**(성공/실패/차단/에스컬레이션 4종 모두 terminal status가 ANU에 도달). fail-open이 "성공처럼" 조용히 끝나는 것을 escalation에선 fail-closed 마커+ANU 콜백으로 전환.
- **보조1**: dispatched marker 부재 시 콜백 컨텍스트를 dispatch 시점 산출물(schedule_id)로 폴백 확보(현재는 marker 없으면 컨텍스트 소실).
- **보조2**: escalation 경로에서도 result.json(최소 envelope) 생산 보장.
- ※ 원인=dispatch incomplete(`progress_watcher_registered:false`) 상관 가설은 미확정 → 수정 전 추가 관측 필요.

---
## 진행안 (A/B축 보고 이후·별도 승인 후보로만)
1. (승인 시) 다음 finish-task에 A-4 정밀 감시 arm → 범인 명령/스크립트 라인 특정.
2. (승인 시) B-3 최소 수정안을 bounded task로 분리(escalation→ANU terminal callback 결선).
3. task-2781 PR은 위 인프라 결함 처리 후. 브랜치 task/task-2781-dev2 + worktree 보존.

## 금지 (현 단계)
코드수정·PR·CI/ruleset 변경·Merge Queue 활성화·추가 dev dispatch·추가 파일복원/수정(1회 승인 복원 외)·chattr 등 파일속성 변경 — 전부 미실행. read-only 조사+감시강화 설계까지만.
