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

★ **READ_ONLY_SPEC_REVIEW_PHASE v5** — 회장 verbatim "Option A · ANU 자체 v5 자동 진행 인가 · Codex round 4 remaining 4 전부 ACCEPT · 회귀 = ANU autonomous loop 내부 품질 결함 박제" 정합. 회장 verbatim R1~R4 + 품질 보강 Q1~Q4 1:1.

- v1: sha `18e400ee...` · v2: `0f6850e6...` · v3: `9bc815c9...` · v4: `531a3363...` (★ 모두 보존)
- pre-checklist: `memory/events/task-2712.v5-pre-checklist-260530.json` (★ 작성 전 4/4 acknowledged)
- v5 origin: Codex round 4 remaining 4 + 회장 R1~R4 + 품질 Q1~Q4 1:1

---

## 0. Header / Authorization Anchor (★ v4 verbatim + draft_version v5)

- **draft_version**: `v5`
- **dispatch_status**: NOT_DISPATCHED · CODEX_SPEC_REVIEW_ROUND_5_PENDING

---

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

---

## 3. Terminal State (★ v4 verbatim + §3.1.1 docker/nohup 추가) (★ R4)

### 3.1 / 3.1.2 (★ v4 verbatim)

### 3.1.1 Signal taxonomy — **docker SIGTERM→SIGKILL + nohup/detach 추가** (★ R4 verbatim)

| signal/kill kind | classification | exit_code | handoff |
|---|---|---|---|
| (★ v4 9 kind verbatim 유지) | — | — | — |
| **docker stop SIGTERM(15) → SIGKILL(9) sequence** (★ R4 신규) | CRASH_NO_EXIT_CODE (★ multi-phase) | -15 → -9 cascade sentinel | shell trap → before_exit_guard_hook (★ TERM) + supervisor post-mortem (★ KILL 후 fallback) |
| **nohup/detach (★ parent SIGHUP ignored)** (★ R4 신규) | **RUNTIME_RESIDUAL_PROCESS** (★ INFRA_DEFECT subcase) | original exit_code (★ 정상 종료 시) 또는 sentinel (★ 비정상) | residual process detect → INFRA_DEFECT handoff marker + .done 0 강제 |

### 3.1.3 RUNTIME_RESIDUAL_PROCESS subcase (★ R4 verbatim)

★ R4 verbatim "docker/nohup/signal residual = runtime residual terminal state 또는 INFRA_DEFECT subcase · residual process 있어도 .done 0 강제 + failure/handoff marker 로 회수":

- `INFRA_DEFECT.residual_process` subcase 분류
- detection: parent process exit 후 child process 가 여전히 alive (★ ps aux + pgrep)
- handoff: `<task_id>.failure-handoff-marker.json` (★ INFRA_DEFECT) + residual_pid 박제
- **`.done` 강제 0** (★ residual process 존재 시)

### 3.2 .done 정책 + Exactly One Terminal Marker Rule

#### 3.2.1 6 marker paths (★ v4 verbatim · classification column 유지)

(★ v4 §3.2.1 verbatim — 4 TERMINAL_MARKER + 2 FALLBACK_EVIDENCE_ONLY)

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

#### 3.2.4 Marker count verification — **return contract 보강** (★ R2 verbatim)

★ R2 verbatim "docstring return contract 에 ZERO_FIRE_BUT_FALLBACK_RECOVERABLE 명시":

```python
def verify_exactly_one_terminal_marker(task_id: str) -> dict:
    """
    R2 verbatim — explicit return contract:
      return {"status": <enum>, "fallback_evidence": <bool>}

    status enum 4 (★ ALL documented):
      - "OK": exactly 1 terminal marker present (★ 정상)
      - "ZERO_FIRE": 0 terminal marker + 0 fallback evidence (★ UNCLASSIFIED_TERMINAL_STATE)
      - "ZERO_FIRE_BUT_FALLBACK_RECOVERABLE":
          0 terminal marker BUT fallback evidence present
          (★ cron callback 미발사 + disk failure envelope 또는 handoff marker 미존재
              + stderr-emit.log 또는 syslog journal entry 존재 = ANU 회수 가능)
      - "MULTI_FIRE_VIOLATION": ≥ 2 terminal marker (★ CRITICAL_ESCALATION 후보)

    fallback_evidence boolean:
      - True: stderr-emit.log 또는 syslog journal entry 존재
      - False: 없음
    """
    # Terminal markers: 4 paths (★ v4 verbatim)
    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_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:
            return {"status": "ZERO_FIRE_BUT_FALLBACK_RECOVERABLE", "fallback_evidence": True}
        return {"status": "ZERO_FIRE", "fallback_evidence": False}
    if terminal_count >= 2:
        return {"status": "MULTI_FIRE_VIOLATION", "fallback_evidence": fallback_evidence_present}
    return {"status": "OK", "fallback_evidence": fallback_evidence_present}
```

---

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

### 4.3.2 syslog journal operational spec — **retention unavailable fallback 명시** (★ R4 verbatim)

★ R4 verbatim "syslog retention 없거나 불완전 → disk marker 기준 failure recovery 가능 명시":

- syslog retention "systemd default" (★ ~30일) host-dependent
- **★ syslog retention 부재/불완전 시 fallback rule (★ R4 verbatim)**:
  - syslog journal 미존재/만료 시 **disk marker 기준 failure recovery 우선**
  - disk marker (★ §3.2.1 terminal 4 + stderr-emit.log) 가 1차 회수 source
  - syslog 는 disk marker 부재 + envelope-write fail + stderr capture fail 의 last-resort 만
  - ANU L3 scan 시 syslog 없어도 disk marker 우선 회수 strict

---

## 5. Terminal Failure Path Matrix (★ v4 verbatim 유지)

---

## 6. Fallback chain + bypass defense

### 6.1 / 6.2 (★ v4 verbatim)

### 6.3 SELF_COLLECTOR_FORBIDDEN

#### 6.3.1 / 6.3.2 / 6.3.4 (★ v4 verbatim)

#### 6.3.3 Bypass detection — **4 terminal marker class 모두 포함 + .done semantic** (★ R1 verbatim · self-review 완료)

★ R1 verbatim "scan 대상 marker class를 4개 모두 포함한다 · .done은 success terminal marker로만 count · failure marker와 .done이 동시에 증가하면 exactly-one-terminal-marker violation으로 판정 · .done 증가 + failure envelope 없음은 SUCCESS일 때만 허용 · failure terminal state에서 .done 증가가 있으면 violation으로 판정 · pseudo-code와 prose가 1:1 일치":

### 6.3.3.A Prose semantic spec (★ pseudo-code 작성 전 prose 1:1 대조 self-review)

| marker class (★ §3.2.1 4 terminal) | count target | semantic |
|---|---|---|
| `*.failure-envelope.json` | failure_marker_count | 8 failure state 정식 marker (★ §3 SUCCESS 외 8) |
| `*.failure-handoff-marker.json` | failure_marker_count | BLOCKED / INFRA / API_FAIL 정식 marker |
| `*.supervisor-crash-marker.json` | failure_marker_count | CRASH_NO_EXIT_CODE 정식 marker |
| `*.done` | success_marker_count | SUCCESS 정식 marker (★ 단독 count) |

★ **exactly-one 정합 rule (★ R1 verbatim 1:1)**:
- ✓ OK 1: `success_marker_count=1` + `failure_marker_count=0` (★ SUCCESS path)
- ✓ OK 2: `success_marker_count=0` + `failure_marker_count=1` (★ failure path)
- ★ VIOLATION 1: `success_marker_count≥1` + `failure_marker_count≥1` (★ "failure marker + .done 동시 증가")
- ★ VIOLATION 2: `success_marker_count≥2` 또는 `failure_marker_count≥2` (★ multi-fire)
- ★ VIOLATION 3: failure terminal state 에서 `.done` 발생 (★ "failure terminal state 에서 .done 증가")

### 6.3.3.B Pseudo-code (★ §6.3.3.A prose 1:1 mirror)

```python
def detect_bypass_via_count_mismatch(window_start, window_end) -> List[dict]:
    """
    R1 verbatim: 4 marker class 모두 scan · .done = success terminal marker only.
    Prose ↔ code 1:1 대조 self-reviewed (★ §6.3.3.A 참조).
    """
    success_marker_count = 0
    failure_marker_count = 0
    fire_markers = []

    # ★ R1 verbatim: 4 marker class 모두 scan
    # (1) failure-envelope.json — failure
    for path in glob("memory/events/*.failure-envelope.json"):
        mtime = os.path.getmtime(path)
        if window_start <= mtime <= window_end:
            envelope = json.load(open(path))
            if envelope.get("registration_mode") in (
                "normal_callback",
                "failure_callback_before_exit_guard",
            ):
                failure_marker_count += 1
                fire_markers.append(path)

    # (2) failure-handoff-marker.json — failure (★ v4 누락 영역 정정)
    for path in glob("memory/events/*.failure-handoff-marker.json"):
        mtime = os.path.getmtime(path)
        if window_start <= mtime <= window_end:
            failure_marker_count += 1
            fire_markers.append(path)

    # (3) supervisor-crash-marker.json — failure (★ v4 누락 영역 정정)
    for path in glob("memory/events/*.supervisor-crash-marker.json"):
        mtime = os.path.getmtime(path)
        if window_start <= mtime <= window_end:
            failure_marker_count += 1
            fire_markers.append(path)

    # (4) .done — success only (★ R1 verbatim 'success terminal marker only')
    for path in glob("memory/events/*.done"):
        mtime = os.path.getmtime(path)
        if window_start <= mtime <= window_end:
            success_marker_count += 1
            fire_markers.append(path)

    # Audit log count
    audit_entries = [
        path for path in glob("memory/events/cron-fire-audit-*.json")
        if window_start <= os.path.getmtime(path) <= window_end
    ]

    violations = []

    # ★ Rule: exactly-one-terminal-marker violation
    if success_marker_count >= 1 and failure_marker_count >= 1:
        violations.append({
            "violation": "EXACTLY_ONE_TERMINAL_VIOLATION_FAILURE_AND_DONE_CONCURRENT",
            "success_count": success_marker_count,
            "failure_count": failure_marker_count,
            "fire_markers": fire_markers,
        })

    # ★ Rule: D5 bypass (★ wrapper 우회 — audit log missing for cron fire)
    total_fires = success_marker_count + failure_marker_count
    if total_fires > len(audit_entries):
        violations.append({
            "violation": "AUDIT_LOG_MISSING_FOR_CRON_FIRE",
            "total_fires": total_fires,
            "audit_entries_count": len(audit_entries),
            "missing_count": total_fires - len(audit_entries),
            "fire_markers": fire_markers,
        })

    return violations
```

### 6.3.3.C Self-review checklist 4/4 (★ R1 verbatim "pseudo-code 와 prose 가 1:1 일치")

| # | 검토 | 결과 |
|---|---|---|
| 1 | 4 marker class 모두 scan? | ✓ PASS (★ §6.3.3.A 4 row + §6.3.3.B 4 glob loop) |
| 2 | `.done` = success terminal marker only? | ✓ PASS (★ §6.3.3.A success_marker_count + §6.3.3.B (4) `.done` → success_marker_count++) |
| 3 | failure + `.done` 동시 = violation? | ✓ PASS (★ §6.3.3.A VIOLATION 1 + §6.3.3.B `EXACTLY_ONE_TERMINAL_VIOLATION_FAILURE_AND_DONE_CONCURRENT`) |
| 4 | prose ↔ pseudo-code 1:1? | ✓ PASS (★ §6.3.3.A 4 row class ↔ §6.3.3.B 4 glob loop 1:1) |

---

## 7. Required Test Fixtures — **F-9/F-10/F-11 explicit elevation** (★ R3 verbatim)

### 7.1~7.2 (★ v4 verbatim)

### 7.3 v5 confirmed required fixture: **14** (★ v4 11 + F-9/F-10/F-11 elevation · "explicit decision: ELEVATED")

★ R3 verbatim "F-9, F-10, F-11은 required regression fixture/test로 승격한다":

| # | fixture | terminal_state | failure_kind | expected marker class | .done 허용 |
|---|---|---|---|---|---|
| F-1~F-8 | (★ v4 verbatim 유지) | - | - | - | - |
| F-12 SIGTERM/Ctrl-C trap | CRASH_NO_EXIT_CODE | trap_SIGINT_or_SIGTERM | supervisor-crash-marker.json | NO |
| F-13 bypass attempt cron direct | CRITICAL_ESCALATION | wrapper_bypass_direct_cron | failure-envelope.json (CRITICAL) + audit missing | NO |
| F-14 D5 audit-missing bypass | CRITICAL_ESCALATION | audit_log_missing_for_cron_fire | failure-envelope.json (CRITICAL) + count mismatch | NO |
| **F-9 syslog retention unavailable/incomplete** (★ R3 verbatim elevation) | INFRA_DEFECT | syslog_retention_unavailable_or_incomplete | failure-envelope.json + stderr-emit.log (★ disk marker 우선 회수) | NO |
| **F-10 docker/nohup/signal residual process** (★ R3 verbatim elevation) | INFRA_DEFECT.residual_process (★ §3.1.3) | docker_stop_sigterm_sigkill_or_nohup_residual | failure-handoff-marker.json (INFRA_DEFECT + residual_pid) | NO (★ §3.1.3 강제 0) |
| **F-11 callback registration attempted no cron fire but fallback marker exists** (★ R3 verbatim elevation) | (any failure) | callback_registration_attempted_no_fire_fallback_exists | failure-envelope.json + cron 미발사 + handoff-marker.json 존재 = ZERO_FIRE_BUT_FALLBACK_RECOVERABLE | NO |

★ **R3 verbatim explicit decision: ELEVATED** — F-9/F-10/F-11 candidate → required 명시적 승격 (★ v4 ambiguity 정정)

---

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

## 9. Affected Files — **23 expected_files** (★ v4 20 + F-9/F-10/F-11)

| # | path | 출처 |
|---|---|---|
| 1~20 | (★ v4 verbatim) | v4 |
| 21 | tests/fixtures/failure_callback_2712/F-9_syslog_retention_unavailable.json (★ R3 elevation) | v5 |
| 22 | tests/fixtures/failure_callback_2712/F-10_docker_nohup_residual_process.json (★ R3 elevation) | v5 |
| 23 | tests/fixtures/failure_callback_2712/F-11_callback_attempted_no_fire_fallback_exists.json (★ R3 elevation) | v5 |

---

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

---

## 12. v4 → v5 diff summary

| v4 영역 | v5 변경 | 출처 |
|---|---|---|
| §3.1.1 9-kind signal taxonomy | + docker stop SIGTERM→SIGKILL sequence + nohup/detach (RUNTIME_RESIDUAL_PROCESS subcase) | R4 |
| §3.1.3 (신규) RUNTIME_RESIDUAL_PROCESS subcase | INFRA_DEFECT.residual_process subcase + .done 강제 0 | R4 |
| §3.2.4 docstring return contract | 4 status enum 명시 (OK / ZERO_FIRE / **ZERO_FIRE_BUT_FALLBACK_RECOVERABLE** / MULTI_FIRE_VIOLATION) + fallback_evidence boolean 정의 | R2 |
| §4.3.2 syslog retention | syslog retention 부재/불완전 시 disk marker 기준 failure recovery 우선 명시 | R4 |
| §6.3.3 D5 detector | 4 marker class 모두 scan (★ v4 누락 2개 정정) + .done success only + exactly-one violation + prose 1:1 + self-review 4/4 PASS | R1 + Q3 |
| §6.3.3.A (신규) prose semantic spec | 4 marker class row + exactly-one rule 3 violation enum | R1 + Q3 |
| §6.3.3.B (신규) pseudo-code | prose §6.3.3.A 1:1 mirror · 4 glob loop · EXACTLY_ONE_TERMINAL_VIOLATION_FAILURE_AND_DONE_CONCURRENT 추가 | R1 + Q3 |
| §6.3.3.C (신규) self-review checklist 4/4 | 4 row · ALL PASS 박제 | R1 + Q3 |
| §7.3 11 required fixture | 14 required (★ F-9/F-10/F-11 explicit elevation · "ELEVATED" 명시) | R3 + Q4 |
| §9 20 expected_files | 23 expected_files (F-9/F-10/F-11 fixture 3개 추가) | R3 |
| §3.1.2 / §3.2.1 / §3.2.2 / §3.2.3 / §4 (외 §4.3.2) / §5 / §6.1 / §6.2 / §6.3.1 / §6.3.2 / §6.3.4 / §8 / §10 / §11 | **변경 0** (★ v4 verbatim) | round 4 PASS 영역 보존 |

---

## 13. R-v5-1~4 + Q1~4 self-check + **v4 ANU autonomous defect ack** (★ Q1 verbatim)

### 13.1 R1~R4 + Q1~Q4 self-check

| ID | recommendation | v5 반영 위치 | post-authoring 검증 |
|---|---|---|---|
| **R1** | §6.3.3 4 marker class + .done success only + violation + 1:1 | §6.3.3.A prose / §6.3.3.B pseudo / §6.3.3.C self-review 4/4 | ✓ PASS |
| **R2** | §3.2.4 ZERO_FIRE_BUT_FALLBACK_RECOVERABLE docstring | §3.2.4 4 status enum docstring | ✓ PASS |
| **R3** | F-9/F-10/F-11 explicit elevation | §7.3 14 fixture + "ELEVATED" 명시 + §9 23 expected_files | ✓ PASS |
| **R4** | syslog/docker/nohup residual 명시 | §3.1.1 docker/nohup 2 row + §3.1.3 INFRA_DEFECT.residual_process + §4.3.2 disk marker 우선 | ✓ PASS |
| **Q1** | v4 회귀 = ANU autonomous spec 작성 결함 박제 | §13.2 ack (본 round) | ✓ PASS |
| **Q2** | pre-checklist + post-checklist verification | pre = task-2712.v5-pre-checklist-260530.json · post = 본 §13.1 4/4 PASS + round 5 marker | ✓ PASS |
| **Q3** | pseudo-code ↔ prose self-review | §6.3.3.C 4/4 PASS 박제 | ✓ PASS |
| **Q4** | F-9/F-10/F-11 explicit decision 박제 | §7.3 "ELEVATED" 명시 + 본 §13.1 R3 | ✓ PASS |

**총 8/8 PASS** (★ R1~R4 + Q1~Q4)

### 13.2 v4 ANU autonomous defect acknowledgment (★ Q1 verbatim)

★ Q1 verbatim "v4 회귀 원인을 'ANU autonomous spec 작성 결함'으로 v5 self-check에 포함":

| defect ID | 영역 | root cause | v5 정정 |
|---|---|---|---|
| ANU-V4-DEFECT-001 | §6.3.3 D5 detector | 4 marker class 중 2개 누락 (failure-handoff-marker + supervisor-crash-marker) + `.done` envelope=None 처리 미흡 | §6.3.3.A prose 1:1 + §6.3.3.B 4 glob loop + §6.3.3.C self-review 4/4 |
| ANU-V4-DEFECT-002 | §7.3 F-9/F-10/F-11 결정 | v4 §7.2 candidate 상태 유지 + §7.3 F-1~F-13 wholesale 인계 = ambiguity | §7.3 explicit "ELEVATED" 명시 + 14 fixture |
| 근본 원인 | ANU autonomous spec 작성 quality control 미흡 | pseudo-code 작성 시 §3.2.1 marker class 와 1:1 대조 self-review 누락 | Q3 self-review 강제 + Q2 pre/post checklist 강제 |

---

## 14. ANU Doctrine Compliance (★ v5)

- ★ 코드 구현 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
- ★ ANU 자체 분류 결정 0 (★ R1~R4 + Q1~Q4 모두 회장 verbatim 1:1)
- ★ ANU 자체 OVERALL PASS 선언 0
- ★ Q1~Q4 품질 강화 적용 (★ v4 autonomous defect 박제 + checklist 강제)

---

## 15. Sentinel

★ task-2712 v5 = READ_ONLY_SPEC_REVIEW_PHASE v5 · 회장 Option A · ANU autonomous loop round 3 · Codex round 4 remaining 4 + 회장 R1~R4 + 품질 Q1~Q4 1:1 · §3.1.1 docker SIGTERM→SIGKILL + nohup · §3.1.3 RUNTIME_RESIDUAL_PROCESS subcase · §3.2.4 4 status enum docstring (ZERO_FIRE_BUT_FALLBACK_RECOVERABLE 명시) · §4.3.2 syslog 부재 시 disk marker 우선 · §6.3.3.A prose + §6.3.3.B pseudo + §6.3.3.C self-review 4/4 (★ 4 marker class 모두 scan + .done success only + violation + 1:1) · §7.3 14 required fixture (★ F-9/F-10/F-11 explicit ELEVATED) · 23 expected_files · §13.2 v4 autonomous defect 박제 (ANU-V4-DEFECT-001/002) · finish-task.sh 변경 0 · dispatch.py 변경 0 · 구현 dispatch 0 · task-2710/task-2711/task-2706~2709+1 evidence 변경 0 · Codex round 5 자동 호출 후 자격 재평가. 끝
