# Notes: ops-20-statements M3 출항 재앵커

**Plan**: `.claude/plans/ops-20-statements-m3-ship.plan.md`
**Decision slug**: `ops-20-statements-m3-ship`

이 파일에 implement 게이트 리뷰를 적는 이유는 편의가 아니다. 계획서 본문은
`mccp-plan-codex` receipt 의 `plan_hash` 가 무는 대상이고, 절을 하나라도 더하면 그 receipt
가 즉시 stale 로 떨어져 체인이 끊긴다. 그래서 리뷰 기록은 notes 로 간다 —
`.claude/notes/ops-20-statements-m1-ship.md` · `.claude/notes/ops-20-statements-m2-ship.md`
와 같은 자리다.

## Codex Implementation Review

- 호출: `node ~/.claude/plugins/cache/mccp/mccp/<판>/scripts/lib/codex-invoke.js adversarial-review` (fail-closed Bash 래퍼)
- 결과: **`classification: disabled`** · `blocking: false` · exit 0
- 사유: `MCCP_CODEX_DISABLED=1` 이 전역 settings 에 박혀 있다. 이 축이 켠 우회가 아니라
  운영자 정책이며, 게이트는 이 경로에서 `codex_verdict=skipped` + `codex_disabled` 증표를
  남긴다. 이 축은 그 변수를 해제하지도 override 하지도 않았다.
- 라운드 수: 0 (호출이 spawn 전에 봉인된 정책을 읽고 short-circuit 됐다 — `codex-policy.js seal`)
- 라운드 캡: 1 — `pinnedBy: single-pass+codex-disabled`. `MCCP_REVIEW_SINGLE_PASS=deadline_pressure`
  와 `MCCP_CODEX_DISABLED` 둘 다가 캡을 1로 못박는다. R2 는 존재하지 않는다.
- 합치 결론: **없음.** Codex 가 말하지 않았으므로 합치도 이견도 기록하지 않는다.
- YAGNI Triage: 해당 없음 (finding 0건)
- Deferred to backlog: 0
- Open Questions: 없음

교차모델 검토가 이 축에서 한 번도 서지 않았다는 사실은 숨기지 않는다. 그 대가는
`resolution.codex_verdict=skipped` 로 봉인되고, cross-gate dedupe 가 그 값에 fail-closed 라
`/mccp:pr` 에서 PR-Codex 가 그대로 다시 돈다 — 이중 리뷰가 조용히 우회되지 않는다. 다만 그
PR 단계 역시 같은 운영자 정책 아래라 `codex_disabled_at_pr` 로 봉인될 것이고, 이 축이
실제로 얻는 교차모델 검토는 **0회**다.

### 2.5.1 cross-gate dedupe 를 적용하지 않은 이유

계획서에 `## Codex Adversarial Review` 절이 있고 합치 결론 줄도 있으나, dedupe 의 세 번째
전제가 거짓이다: `git diff --name-only origin/feat/ops-doc-reconcile...HEAD` 가 12파일이고
이 계획서의 Files to Change 는 receipt 3본뿐이라 부분집합이 아니다. 그 12파일은 M3 구현이
이미 HEAD 에 들여온 것이고 이 축이 새로 만드는 것이 아니지만, dedupe 규칙은 "무엇이
새로운가" 가 아니라 "diff 가 계획 범위 안인가" 를 묻는다. 전제가 거짓이므로 dedupe 를 접고
2.5.2 부터 정상 경로로 갔다.

### 새 implement-time 결정 3건

계획 단계가 미리 정하지 않은 것만 적는다.

1. **리뷰 절의 자리를 notes 로 잡았다.** 계획서는 `plan_hash` 결박이라 못 쓴다. 대안은
   receipt 를 다시 발급하는 것인데 그것이 이 축이 존재하는 이유 자체(추적 receipt 를
   안 건드린다)와 충돌한다. M1 · M2 가 같은 자리에 같은 이유를 적었다.
2. **placeholder 교체를 5.2z 러너 실행 *앞*으로 당겼다.** plan 게이트의 문면 순서는
   5.1 placeholder 삽입 → 5.2z 러너 → 5.3 교체인데, codex-disabled 경로에서는 러너가
   receipt 를 쓰고 나서 5.3 이 계획서를 고치므로 그 receipt 가 자기 손으로 stale 이 된다.
   교체를 먼저 하면 러너가 최종 본문을 해시한다. 5.6b Step A 의 두 검사(heading 존재 ·
   placeholder 부재)는 순서와 무관하게 만족된다.
3. **security-reviewer 를 호출하지 않았다.** 아래 절이 그 판단의 근거와 한계다.

## Security Reviewer

**이 축에서는 subagent 를 호출하지 않았다.** 그 사실을 먼저 적고 근거를 뒤에 적는다.

게이트 문면(2.5.5)은 subagent 를 무조건 부르라고 하지 않는다 — "보안 표면(인증·인가 ·
세션·토큰 · 암호·해시·서명·키 · 비밀·자격증명 · 입력 검증 · SQL/명령 주입 · SSRF ·
경로 탐색 · 권한 상승)에 닿을 때" 부르라고 한다. 이 축의 Files to Change 는 게이트 receipt
JSON 3본이고 그중 어느 것도 그 표면이 아니다. 그러므로 트리거가 성립하지 않는다.

이것은 자동 폴백이 아니다. 폴백은 "부르려 했으나 실패했다" 이고 그때 `security_skipped=true`
가 선다. 여기서는 부를 조건이 성립하지 않았으므로 두 부정 필드가 **둘 다 `false`** 로
남는다 — 계획서 Task 2 표가 말한 그 상태다. 선례가 같은 형태다:
`.claude/receipts/security-reviewer/` 에 `ops-20-statements-m2-ship` 파일이 없다.

### 세션 규칙과의 관계

UI3 이 Agent 도구를 금지한다. 위 판단은 그 금지를 **우회한 것이 아니라** 금지와 무관하게
성립한다 — 트리거가 섰다면 이 축은 STOP 했어야 하고, `security_force_override` 로 접지
않는다는 것이 UI8 이다. 두 조건이 같은 방향을 가리켰다는 사실을 여기 적어 둔다. 트리거가
섰는데 금지 때문에 못 부른 경우와 트리거 자체가 안 선 경우는 다른 상태이고, 이 축은 후자다.

### 기계 sweep — 판단을 주장으로 두지 않는다

M2 출항이 security-reviewer 로 잡은 부류가 정확히 **절대 호스트 경로 · 사용자명 유출**
2건이었다. 그 부류는 subagent 없이 셸로 전수할 수 있으므로 이 축의 신설 산출물 전건에
돌렸다.

| 대상 | 패턴 | 결과 |
|---|---|---|
| `.claude/plans/ops-20-statements-m3-ship.plan.md` | 사용자명 · `C:\Users` · `/home/` · `%USERPROFILE%` · `AppData` | **0건** |
| `.claude/receipts/mccp-plan-codex/ops-20-statements-m3-ship.json` | 사용자명 · `C:\Users` | **0건** |
| 같은 receipt 의 `meta.cwd` | 절대 경로 여부 | `"."` — repo-relative |

`meta.cwd` 를 따로 잰 것은 `/mccp:pr` Phase 3.0a 가 ship receipt 의 절대 `meta.cwd` 를
`[MCCP-EVIDENCE-STOP]` 으로 막기 때문이다. 계획서 §Validation 에는 이 검사가 없다 — 이
축이 게이트 문면에서 읽고 추가로 돌린 것이고, 그래서 계획서의 검사 목록이 아니라 여기
기록한다.

**이 sweep 이 증명하지 못하는 것.** 두 패턴 부류(호스트 경로 · 사용자명)만 본다. 인증 ·
암호키 · SSRF · 주입 · 권한 상승은 이 축의 diff 에 표면이 없다는 **판단**으로 넘겼고,
그 판단을 독립 검토한 리뷰어는 없다. 계획서 Task 2 가 "사후 검사의 한계" 로 적은 것과 같은
층위의 한계이며, 메우려면 subagent 가 필요하고 그것이 UI3 이 막은 것이다.

## 사전 블록 실측 — 게이트 진입 전

계획서 §Validation 사전 블록을 게이트 체인 착수 **전에** 1회 돌렸고 `PRE OK` rc 0 이었다.
그 시점에 `.claude/receipts/` · `ops/` · `.claude/prds/` 가 전부 비어 있었다 (Validation
6 · 7). 계수 24행은 리터럴 기준선과 `cmp -s` 로 일치했고 `diff -u` 는 돌지 않았다.

하네스 넷(`m0` · `m1` · `m2` · `m3`) `reverify` 전건 rc 0 · `provenance_proven 0` 이다.
M2 출항 때와 달리 이번에는 `m0.sh reverify` 를 **실제로 재실행했다** — 그때 fork 고갈로
인용에 기댔던 한 칸이 이번에는 측정이다. `bash -n` 넷 다 rc 0.

## 병합을 게이트보다 먼저 끝냈다

착수 시점 HEAD `0de61c1` 이 base 보다 1커밋 뒤져 있었다. ship receipt 는 `head_sha` 를
묶으므로 봉인 뒤 병합하면 그 자리에서 stale 이 되고 재봉인이 불가능하다. 그래서 병합을
먼저 했고, 그 병합이 내용을 하나도 바꾸지 않음을 트리 해시로 확인했다 —
`72270a0^{tree}` 와 `001ea0e^{tree}` 가 둘 다 `3477dd453137cc82e7da8a2e1ad249f3350cec4d`
이고 `git diff 0de61c1 8f83a22 --stat` 이 비어 있다. 병합 후 HEAD `8f83a22`, 0 behind.

## 계획 게이트 요약 — L2 패널은 돌지 않았다

`MCCP_PLAN_REVIEW=codex` 로 내려 돌았다. 전역 기본값은 `multi-agent` 이고 그 경로는 4렌즈
패널을 Agent 도구로 띄우므로 UI3 이 막는다. `MCCP_INTENT_ARBITER=author` 도 같은 이유로
함께 내렸다.

| 축 | 값 |
|---|---|
| `mode` | `codex` — 5.2z 분기, 5.2a~5.2h 미실행 |
| `review_*` 필드 | 미기록 (패널이 안 돌았으므로 stamp 하지 않는다) |
| intent gate | `skipped` — `intent_gate_verdict=skipped authorizes the gate` |
| `codex_verdict` | `skipped` |
| marker | `exit_code=0` · nonce 일치 |

**이 축은 L2 반증 패널의 검토를 받지 않았다.** M1 · M2 출항은 각각 5라운드 · 5라운드를
받았고 이 축은 0라운드다. 그 차이의 원인은 계획서가 더 단순해서가 아니라 세션 규칙이
패널을 못 띄우기 때문이며, 계획서의 형태는 M2 판을 그대로 가져온 것이라 그 5라운드가
흡수한 교정(리터럴 기준선 전건 대조 · 디렉터리 전건 루프 · `receipt_hash` 정합 · 사전·사후
짝 · `$MCCP` 환경변수화)은 상속돼 있다. 상속과 검토는 다르다 — 이 축이 M2 판에 **더한**
부분(census 결박 계수 24행 · `m3.sh` 추가 · 병합 근거 절 · security 트리거 미성립 논증)은
어떤 반증 렌즈도 통과하지 않았다.
