# task-2712 v4 — FAILURE_CALLBACK_BEFORE_EXIT_GUARD (READ_ONLY_SPEC_REVIEW_PHASE · v4)

★ **READ_ONLY_SPEC_REVIEW_PHASE v4** — Codex round 3 OVERALL PASS_WITH_RECOMMENDATIONS + 0 NR + 0 FAIL + ELIGIBLE_CONTINUE + Codex 자체 "Fit for continued review/implementation flow without chair hold" + ANU 자동 루프 자격 6/6 PASS · 회장 verbatim "ANU 자동 revision loop 진행" 명시 인가 정합 자동 진행.

- v1: sha `18e400ee...` (★ 보존)
- v2: sha `0f6850e6...` (★ 보존)
- v3: sha `9bc815c9...` (★ 보존)
- v4 origin: Codex round 3 remaining 3 recommendations 1:1 surgical fix

---

## 0. Header / Authorization Anchor (★ v3 verbatim 유지)

- **draft_version**: `v4`
- **dispatch_status**: NOT_DISPATCHED · CODEX_SPEC_REVIEW_ROUND_4_PENDING

---

## 1~2. (★ v3 verbatim 유지)

---

## 3. Terminal State (★ v3 §3 verbatim + §3.2.4 정합 강화)

### 3.1 / 3.1.1 / 3.1.2 (★ v3 verbatim 유지)

### 3.2 Exactly One Terminal Marker Rule (★ v3 verbatim)

#### 3.2.1 6 marker paths (★ v3 verbatim · **terminal vs fallback evidence 분류 명시**) (★ R-v4-1)

★ R-v4-1 verbatim "stderr/syslog 가 terminal marker 인지 fallback evidence only 인지 명시":

| marker type | path pattern | classification | role |
|---|---|---|---|
| `.done` | `<task_id>.done` | **TERMINAL_MARKER** | SUCCESS 정식 marker |
| failure envelope | `<task_id>.failure-envelope.json` | **TERMINAL_MARKER** | 8 failure state 정식 marker |
| handoff marker | `<task_id>.failure-handoff-marker.json` | **TERMINAL_MARKER** | BLOCKED / INFRA / API 정식 marker |
| supervisor crash marker | `<task_id>.supervisor-crash-marker.json` | **TERMINAL_MARKER** | CRASH_NO_EXIT_CODE 정식 marker |
| **stderr emit log** | `<task_id>.stderr-emit.log` | **★ FALLBACK_EVIDENCE_ONLY** | envelope-write fail 시 회수용 · NOT terminal marker |
| **syslog journal** | systemd tag `failure_callback_2712` | **★ FALLBACK_EVIDENCE_ONLY** | last-resort 회수용 · NOT terminal marker |

**★ Rule**: stderr/syslog 는 envelope/handoff 부재 시 ANU 가 "정식 marker 가 실패했음 + 회수 가능한 fallback evidence 존재" 로 재구성하는 용도. exactly-one-marker count 에는 미포함.

#### 3.2.2 / 3.2.3 (★ v3 verbatim 유지)

#### 3.2.4 Marker count verification — **6 pattern 정합 강화** (★ R-v4-1)

```python
def verify_exactly_one_terminal_marker(task_id: str) -> dict:
    """
    R-v4-1 verbatim: stderr/syslog 는 fallback evidence (NOT terminal marker).
    return: {"status": "OK"|"ZERO_FIRE"|"MULTI_FIRE_VIOLATION", "fallback_evidence": bool}
    """
    # ★ Terminal markers: 4 paths
    terminal_markers = [
        path_exists(f"memory/events/{task_id}.done"),
        path_exists(f"memory/events/{task_id}.failure-envelope.json"),
        path_exists(f"memory/events/{task_id}.failure-handoff-marker.json"),
        path_exists(f"memory/events/{task_id}.supervisor-crash-marker.json"),
    ]
    terminal_count = sum(terminal_markers)

    # ★ Fallback evidence: 2 paths (terminal count 에 미포함)
    fallback_evidence_present = (
        path_exists(f"memory/events/{task_id}.stderr-emit.log") or
        syslog_journal_has_entry(task_id, tag="failure_callback_2712")
    )

    if terminal_count == 0:
        if fallback_evidence_present:
            # stderr/syslog 만 존재 = envelope/handoff 실패 + fallback 회수 가능
            return {"status": "ZERO_FIRE_BUT_FALLBACK_RECOVERABLE", "fallback_evidence": True}
        return {"status": "ZERO_FIRE", "fallback_evidence": False}  # UNCLASSIFIED_TERMINAL_STATE
    if terminal_count >= 2:
        return {"status": "MULTI_FIRE_VIOLATION", "fallback_evidence": fallback_evidence_present}  # CRITICAL_ESCALATION
    return {"status": "OK", "fallback_evidence": fallback_evidence_present}
```

★ §3.2.1 의 6 pattern ↔ §3.2.4 count 정합: terminal_count 는 4 패턴 만 · fallback_evidence_present 는 2 패턴 별도 boolean.

---

## 4. Failure Callback Envelope Schema (★ v3 verbatim 유지)

---

## 5. Terminal Failure Path Matrix

### 5.1 finish-task.sh inner-script hook (★ v3 verbatim 유지) + §3.1.2 trap

### 5.2 dispatch.py inner instrumentation + **spawn verification 정합** (★ R-v4-2)

#### 5.2.1 Spawn verification — **TIMEOUT distinct return + env override 명시** (★ R-v4-2 verbatim)

★ R-v4-2 verbatim "either return TIMEOUT distinctly or remove it, and show env override consumption":

```python
import os, time, glob, psutil

def _verify_bot_spawn(task_id: str, expected_bot_id: str, child_pid: int = None) -> str:
    """
    R-v4-2 verbatim spawn verification with distinct TIMEOUT + env override.
    return: "SPAWNED" / "DISPATCH_FALSE_OK" / "TIMEOUT_BOT_ALIVE_BUT_NO_MARKER"

    verification mechanism:
      - N = env override OR default 15 sec
      - polling cadence = 1 sec
      - SPAWNED = marker 박제 + child alive
      - DISPATCH_FALSE_OK = bot died (child_pid not alive) AND/OR bot never started (no marker + no child)
      - TIMEOUT_BOT_ALIVE_BUT_NO_MARKER = timeout 후 child 는 alive 인데 marker 만 부재
                                          (★ bot 가 spawn 됐으나 marker 박제 지연/실패)
    """
    # ★ env override 명시적 consumption
    DEFAULT_TIMEOUT_SEC = 15
    timeout_sec = int(os.environ.get("FAILURE_CALLBACK_2712_SPAWN_TIMEOUT_SEC", DEFAULT_TIMEOUT_SEC))
    polling_interval_sec = 1

    elapsed = 0
    while elapsed < timeout_sec:
        marker_glob = f"memory/events/{task_id}.spawn-confirmed-*.json"
        if glob.glob(marker_glob):
            if child_pid is None or psutil.pid_exists(child_pid):
                return "SPAWNED"
        time.sleep(polling_interval_sec)
        elapsed += polling_interval_sec

    # Timeout 도달
    child_alive = (child_pid is not None) and psutil.pid_exists(child_pid)
    if child_alive:
        # bot 는 alive 인데 marker 만 부재 — DISPATCH_FALSE_OK 아님 (★ R-v4-2 정합)
        return "TIMEOUT_BOT_ALIVE_BUT_NO_MARKER"
    # bot died or never started
    return "DISPATCH_FALSE_OK"
```

#### 5.2.2 N 값 결정 근거 (★ v3 verbatim + env override 사용 명시 정합)
#### 5.2.3 dispatch.py 5 entry point + verification (★ v3 verbatim 유지)

### 5.3 runtime path (★ v3 verbatim 유지)

---

## 6. Fallback chain + bypass defense

### 6.1 / 6.2 (★ v3 verbatim 유지)

### 6.3 SELF_COLLECTOR_FORBIDDEN

#### 6.3.1 In-flight enforcement (★ v3 verbatim 유지)

#### 6.3.2 Bypass defense — **D5 absence-of-expected-state detector 추가** (★ R-v4-3 verbatim)

★ R-v4-3 verbatim "Clarify how bypass is detected when no audit file exists, beyond D3 post-fire verification":

| defense layer | rule |
|---|---|
| D1 single entry-point invariant | (★ v3 verbatim) |
| D2 lint/static check | (★ v3 verbatim) |
| D3 post-fire verification | (★ v3 verbatim) |
| D4 runtime audit log | (★ v3 verbatim) |
| **D5 absence-of-expected-state detector** (★ R-v4-3 신규) | cron 발사 marker (★ §3.2.1 terminal marker) ↔ audit log entry count 매칭 강제. cron 발사 마커 N건 ↔ audit log entry N건 · 불일치 시 wrapper 우회 의심 → CRITICAL_ESCALATION 후보 박제 (★ ANU L3 scan 영역) |

#### 6.3.3 Bypass detection — **D5 detector 추가** (★ R-v4-3)

```python
def detect_bypass_via_count_mismatch(window_start, window_end) -> List[dict]:
    """
    R-v4-3 verbatim: audit file 부재 시에도 bypass 감지.
    cron 발사 marker count ↔ audit log entry count 매칭.
    """
    # ★ Step 1: window 내 fire 된 callback marker count
    cron_fires = []
    for marker_path in glob("memory/events/*.failure-envelope.json") + glob("memory/events/*.done"):
        mtime = os.path.getmtime(marker_path)
        if window_start <= mtime <= window_end:
            # Check if this marker triggered a callback
            envelope = json.load(open(marker_path)) if marker_path.endswith(".json") else None
            if envelope and envelope.get("registration_mode") in ("normal_callback", "failure_callback_before_exit_guard"):
                cron_fires.append(marker_path)

    # ★ Step 2: window 내 audit log entry count
    audit_entries = []
    for audit_path in glob("memory/events/cron-fire-audit-*.json"):
        mtime = os.path.getmtime(audit_path)
        if window_start <= mtime <= window_end:
            audit_entries.append(audit_path)

    # ★ Step 3: count mismatch detection
    if len(cron_fires) > len(audit_entries):
        # cron fire 된 marker 가 있는데 audit log 가 없음 = wrapper 우회 의심
        return [{
            "violation": "AUDIT_LOG_MISSING_FOR_CRON_FIRE",
            "cron_fires_count": len(cron_fires),
            "audit_entries_count": len(audit_entries),
            "missing_count": len(cron_fires) - len(audit_entries),
            "fire_marker_paths_to_investigate": cron_fires,
        }]
    return []  # 정합
```

#### 6.3.4 D1~D5 orthogonal coverage matrix (★ R-v4-3)

| bypass scenario | D1 | D2 | D3 | D4 | D5 |
|---|---|---|---|---|---|
| wrapper 외부 직접 cron 호출 + audit 박제 | ✓ block | ✓ static reject | ✓ post-fire mismatch | ✓ caller stack 의심 | ✓ count 정합 |
| wrapper 외부 직접 cron 호출 + audit 미박제 (★ 우회 시도) | ✓ block | ✓ static reject | ✓ post-fire mismatch | — (★ audit 부재) | ★ **D5 count mismatch detect** |
| audit 위조 (★ fake `caller_passed_validate_collector_strict=true`) | — | — | ✓ post-fire mismatch | — (★ 위조 가능) | ✓ count 정합 (★ marker ↔ audit 1:1 강제) |

---

## 7. Required Test Fixtures (★ v3 §7.3 10 verbatim + F-14 추가)

### 7.1~7.2 (★ v3 verbatim)

### 7.3 v4 confirmed required fixture: **11** (★ v3 10 + F-14 D5 detector)

| # | fixture | terminal_state | 출처 |
|---|---|---|---|
| F-1~F-13 | (★ v3 verbatim) | - | v3 |
| **F-14** | D5 audit-missing bypass attempt (★ wrapper 외부 cron 호출 + audit 미박제) | CRITICAL_ESCALATION | v4 §6.3.2 D5 + §6.3.3 detect |

---

## 8. Architecture (★ v3 verbatim 유지)

## 9. Affected Files — 20 expected_files (★ v3 19 + F-14)

| # | path | 출처 |
|---|---|---|
| 1~19 | (★ v3 verbatim) | v3 |
| 20 | tests/fixtures/failure_callback_2712/F-14_d5_audit_missing_bypass.json (★ 신규) | v4 §6.3.2 D5 |

---

## 10~11. (★ v3 verbatim 유지)

---

## 12. v3 → v4 diff summary

| v3 영역 | v4 변경 | 출처 |
|---|---|---|
| §3.2.1 6 marker paths | terminal vs fallback evidence classification column 추가 (★ 4 terminal + 2 fallback) | R-v4-1 |
| §3.2.4 marker count Python | terminal_count 4 + fallback_evidence_present boolean 분리 + ZERO_FIRE_BUT_FALLBACK_RECOVERABLE 신규 상태 | R-v4-1 |
| §5.2.1 spawn verification | TIMEOUT_BOT_ALIVE_BUT_NO_MARKER distinct return + env override 명시적 consumption (`os.environ.get(..., DEFAULT_TIMEOUT_SEC)`) | R-v4-2 |
| §6.3.2 D1~D4 | D5 absence-of-expected-state detector (★ marker count ↔ audit count mismatch) 추가 | R-v4-3 |
| §6.3.3 detection Python | detect_bypass_via_count_mismatch() 추가 | R-v4-3 |
| §6.3.4 (신규) D1~D5 orthogonal coverage matrix | 3 bypass scenario 매트릭스 | R-v4-3 |
| §7.3 10 required fixture | 11 required (F-14 D5 fixture 추가) | v4 §6.3.2/§6.3.3 정합 |
| §9 19 expected_files | 20 (F-14 추가) | F-14 |
| §3.1 / §3.2.2 / §3.2.3 / §4 / §5.1 / §5.3 / §6.1 / §6.2 / §6.3.1 / §8 / §10 / §11 | **변경 0** (★ v3 verbatim) | round 3 PASS/PWR 영역 보존 |

---

## 13. R-v4-1~3 self-check

| ID | Codex round 3 remaining | v4 반영 위치 | status |
|---|---|---|---|
| **R-v4-1** | §3.2.4 vs §3.2.1 marker count rule 정합 (stderr/syslog terminal vs fallback) | §3.2.1 classification column + §3.2.4 terminal_count 4 + fallback boolean + ZERO_FIRE_BUT_FALLBACK_RECOVERABLE | ✓ ACCEPTED |
| **R-v4-2** | §5.2.1 TIMEOUT distinct + env override | TIMEOUT_BOT_ALIVE_BUT_NO_MARKER return + `os.environ.get()` 명시 + 3 return distinct | ✓ ACCEPTED |
| **R-v4-3** | bypass detection audit file 부재 시 (D3 post-fire 외) | D5 absence-of-expected-state detector + detect_bypass_via_count_mismatch() + D1~D5 orthogonal matrix | ✓ ACCEPTED |

**총 3/3 ACCEPTED**

---

## 14. ANU Doctrine Compliance (★ v4 round)

- ★ ANU 자체 코드 구현 0 · finish-task.sh 변경 0 · dispatch.py 변경 0
- ★ bot_settings/settings 변경 0 · PR/push/merge/GitHub write 0 · 구현 dispatch 0
- ★ task-2710/task-2711/task-2706~2709+1 evidence 변경 0
- ★ chair_authorization_id 발급 0 · 새 forbidden target 추가 0 · 새 chair_authorization scope 확장 0
- ★ ANU 자체 분류 결정 0 (★ R-v4-1~3 모두 Codex round 3 remaining 1:1)
- ★ ANU 자체 OVERALL PASS 선언 0
- ★ ANU-Codex 자동 루프 적용 (★ Codex round 2 + round 3 양쪽 ELIGIBLE_CONTINUE + 회장 verbatim 명시 인가)

---

## 15. Sentinel

★ task-2712 v4 = READ_ONLY_SPEC_REVIEW_PHASE v4 · ANU 자동 루프 round 2 (★ Codex round 3 ELIGIBLE_CONTINUE 정합) · Codex round 3 remaining 3 + R-v4-1~3 1:1 · §3.2.1 4 terminal + 2 fallback classification · §3.2.4 terminal_count 4 + fallback boolean + ZERO_FIRE_BUT_FALLBACK_RECOVERABLE · §5.2.1 TIMEOUT distinct + env override · §6.3.2 D5 absence-of-state detector + §6.3.3 count mismatch + §6.3.4 D1-D5 orthogonal matrix · §7.3 11 required fixture · 20 expected_files · finish-task.sh 변경 0 · dispatch.py 변경 0 · 구현 dispatch 0 · task-2710/task-2711/task-2706~2709+1 evidence 변경 0 · Codex round 4 자동 호출 후 PASS 도달 시 회장 보고 평가. 끝
