# mccp-gatekit — `/mccp:plan` hybrid 게이트의 기계 구간

`/mccp:plan` (1.33.6, hybrid · L3 on · cap 3) 의 5.0 → 5.2 · 5.6b → 5.7 을 셸 스크립트로 옮긴 것.
계획서 저작(Phase 1~4)과 L2 패널 발사(`Workflow`)는 오케스트레이터(에이전트)가 한다. 스크립트는 그 앞뒤를 맡는다.
정본은 `commands/plan.md` 이고, 이 파일은 그 복사본의 **계약**만 적는다. 줄 번호는 1.33.6 기준이다.

원천 경로: `/home/madsc/.claude/plugins/cache/mccp/mccp/1.33.6/commands/plan.md` · `scripts/lib/plan-review/cli.js` · `scripts/workflows/plan-review.js`
mccp 파일은 건드리지 않는다 (CLAUDE.md §0 B1).

## 파일

| 파일 | 역할 | 종료코드 |
|---|---|---|
| `env.sh <N>` | WT·PLUGIN·PLAN_REL·PRD_REL·STATE·SLUG, 헬퍼(`jread`·`record_stop`·`release_reservation`·`rounds_now`) | 2 = N 누락 |
| `pre.sh <N>` | 계획서 커밋 뒤: 5.0 → 5.1 확인 → lint → 5.-1 봉인 → 5.2 진입 정리 → 5.2a-0 → 5.2a L1 → 5.2b → 5.2c(emit·pin·dispatch-log) → 인자 출력 | 0 발사 준비 · 1 저자가 고칠 것(`AUTHOR_FIX=`, 라운드 미소비) · 12 정지(`HALT_STAGE=`) |
| (Workflow) | `workflow({scriptPath: .../scripts/workflows/plan-review.js}, <args 객체>)` | — |
| `post-a.sh <N>` | l2.json 형식 검사(봉투면 `result` 를 꺼냄) → 5.2d 정산 | 0 · 3 l2 없음/형식 오류 · 12 정지 |
| `l3-run.sh <N>` | 5.2f 발사 본체 (`exec` 로 node 가 프로세스를 대체) | 12 mode.json 해석 불가 |
| `l3-poll.sh <N>` | L3 결과 한 번 보기 | 0 done/still-running · 12 failed(5.2f 기록) |
| `post-b.sh <N>` | 5.2e decide → proof → 5.2g verify → 5.2g2 → 5.2h 기록 | 0 converged · 12 그 밖 |
| `seal-converged.sh <N>` | 5.6b write → 5.7 validate (수렴) | 0 통과 · 4 수렴 아님 · 12 실패 |
| `seal-divergent-hybrid.sh <N>` | 라운드 상한의 divergent receipt (hybrid 형태만) | 4 조건 불충족 · 5 proof 없음 · 12 실패 |
| `status.sh <N>` | `ROUNDS=<k>/<cap>` | — |
| `validate.sh <N> [cmd]` | `VALIDATE_EXIT=<n>` + 마지막 JSON 줄 (기본 `mccp:prp-implement`) | validate 와 같음 |
| `_seal-lib.sh` | seal 두 개가 쓰는 5.6b 플래그 구성·write·validate (직접 실행 안 함) | — |

`legacy/`, `codex-ask.sh`, `milestone.js`, `prompts/` 는 이 kit 의 파일이 아니다. 같은 디렉터리의 다른 작업이다. 건드리지 않았다.

## 오케스트레이터 순서 (hybrid)

1. 계획서를 쓰고 커밋한다 (Files to Change·Tasks·Validation·Risks·Acceptance·`## Codex Adversarial Review` placeholder 포함).
2. `bash .../pre.sh N`. `AUTHOR_FIX` 면 고치고 커밋한 뒤 1부터 다시. `HALT_STAGE` 면 출력대로 복구. 0 이면 `WORKFLOW_ARGS_JSON_BEGIN` 과 `_END` 사이 한 줄을 **파싱한 객체**로 넘긴다. 문자열로 넘기면 모든 필드가 `undefined` 가 된다(메모 `mccp-plan-gate-in-workflow` 1).
3. 반환 봉투의 `result` 를 `.claude/state/plan-review/l2.json` 에 쓴다. 봉투째 써도 `post-a.sh` 가 꺼낸다.
4. `bash .../post-a.sh N`.
5. hybrid: `l3-run.sh N` 을 Bash `run_in_background` 으로 띄운다 (`nohup`·`&` 금지, 메모 `mccp-l3-detached-killed-with-process-group`). `l3-poll.sh N` 을 `still-running` 이 아닐 때까지 반복한다. `failed` 면 정지다.
6. `bash .../post-b.sh N`. `DECISION verdict=… L3=…` 와 `REASON=` 을 읽는다.
   - exit 0 (converged) → `seal-converged.sh N` → `RECEIPT`·`VALIDATE_EXIT=0` 확인.
   - exit 12 (그 밖) → 흡수 후 1번부터 새 라운드. 라운드 상한(`ROUNDS=3/3`)에서 divergent 가 남으면 `seal-divergent-hybrid.sh N` (조건: hybrid · forwardCodexVerdict=true · L3 divergent · 라운드 k≥cap · proof 존재. 하나라도 빠지면 exit 4).
   - 상한에서 receipt 를 못 쓰는 경우는 전역 CLAUDE.md mccp 규칙을 따른다 (`--review-mode` 를 빼는 형태 금지, 보고 후 멈춤).

흡수는 **post-b 판정 뒤에** 한다. 판정 전에 계획서나 `l2.json` 을 고치면 DD13 해시 바인드가 `unavailable` 을 낸다 (메모 `hybrid-plan-gate-run-gotchas` 4).

## 저자가 지킬 명령 본문 의무 (1.33.6 에서 찾은 것)

- **머리 형식** (PRD 산출물 형식, 줄 ~451): `# Plan: …` · `**Source PRD**: <path>` · `**Selected Milestone**` · `**Complexity**`.
- **`## User Intent` 표** (Phase 1.5, 줄 155~176): 사용자가 말한 제약·금지만. 판단 근거를 넣지 않는다. ID `^UI\d+$` 유일 · Kind ∈ constraint/exception/exclusion/direction · 제약문 3어 이상 · placeholder(`{…}`·TODO·TBD·N/A·`-`) 금지 · 지시형 문구(ignore·you must 등) 금지 · 200행 이하. 사용자 제약이 없으면 행을 지어내지 말고 그렇다고 적는다.
- **L1 필수 절**: Summary · Files to Change · Tasks · Validation · Risks · Acceptance (C1). Files 표 헤더 `File|Action|Why`, Action 은 CREATE/UPDATE/DELETE. CREATE 대상은 없어야 하고 UPDATE/DELETE 대상은 있어야 한다 (C3).
- **인용** (C6): `경로:줄` 은 해석 가능해야 한다. `.claude/…` 경로는 줄 번호 대신 검사 번호로 가리킨다. `3.9:1` 같은 비율도 인용으로 읽히니 `3.9 대 1` 로 쓴다 (메모 `l1-citation-drops-leading-dot`).
- **placeholder 두 줄**: `## Codex Adversarial Review` 와 `<!-- placeholder: will be replaced by Phase 7.3 -->`. `pre.sh` 가 확인만 하고 넣지는 않는다 (해시가 바뀌면 안 된다).
- **plan-kit lint** (WT CLAUDE.md §1): Task 의 `Gate: #N`·`Validate:` 행, Validation 블록 `set -euo pipefail`, `# 검사 #N` 블록의 `# 기대:`·`# 음성:`, NOT Building 절, Acceptance 의 게이트 표기. `pre.sh` 가 `node .claude/templates/plan-kit/lint-plan.mjs` 를 돌린다 (스모크에서만 뺀다).
- **5.0**: 설계 신호가 1 이면 critique 를 돌리고 `## Design Critique` 절을 남긴 뒤 커밋한다. 신호 0 은 서브모듈 안의 UI 를 못 본다 (메모 `impeccable-detect-blind-to-submodules`). 그 사실은 notes 에 적는다.
- **동결**: 5.2c 발사 뒤 계획서는 receipt 가 써질 때까지 고치지 않는다 (명령 본문 invariant ii).
- **리뷰 기록은 계획서 밖**: `.claude/reviews/plan-review-<slug>.md` 와 `.claude/state/plan-review/` 에 있다. 구현 리뷰 절을 계획서에 붙이지 않는다 (메모 `implement-review-goes-to-notes`).
- **한 worktree 에서 게이트를 둘 이상 돌리지 않는다**: `plan-review/` 상태 디렉터리는 단일 소유다 (명령 본문 5.2f 주석).

## 슬러그 (`derive-decision`) — 계획서 경로로 유도한다

`node scripts/receipt/cli.js derive-decision --command mccp:plan --args .claude/PRPs/plans/admin-rbac-m<N>.plan.md`

- 실측: N=1 → `admin-rbac-m1` (원장 `.claude/state/review-rounds/mccp-plan-codex__admin-rbac-m1.json`).
- 비교 실측: PRD 경로 `.claude/prds/admin-rbac.prd.md` → `admin-rbac` (마일스톤 공유 원장). 두 번째 마일스톤부터 예산이 0 이 되므로 쓰지 않는다 (메모 `mccp-round-ledger-per-prd`).
- 계획서 경로로 유도하면 `/mccp:prp-implement <계획서 경로>` 가 찾는 슬러그와도 맞는다 (메모 `mccp-freeform-plan-slug-first-word`).
- `--prd` 는 emit 에만 넘긴다 (계획서의 Source PRD 를 패널이 읽도록). 슬러그는 바꾸지 않는다.

## 확인한 함정과 반영 위치

| 메모 | 함정 | 반영 |
|---|---|---|
| hybrid-run-gotchas 1 · plan-gate-in-workflow 1 | 봉투의 `result` · 인자 문자열 이스케이프 | `post-a.sh` 가 봉투를 풀고 `reviewedPlanHash`·`results` 없으면 exit 3. 인자 전달은 README 순서 2 |
| hybrid-run-gotchas 2 · l3-detached | `&&` 사슬 끝 `&` · `nohup` 이 프로세스 그룹과 함께 죽음 | `l3-run.sh` 는 전경 `exec`, 도구 백그라운드로만 띄운다. 논스·마감은 파일 |
| hybrid-run-gotchas 4 | 흡수 뒤 decide → DD13 unavailable | `post-b.sh` 는 decide 먼저. 흡수는 README 6 |
| hybrid-run-gotchas 5 | L1 실패도 emit 이 라운드를 먹는다 | `pre.sh` 가 L1 을 reserve·emit 보다 먼저, L1 위반이면 exit 1 (스모크 확인: `ROUNDS=0/3`) |
| hybrid-run-gotchas 6 | 감사 사유의 placeholder 낱말 | `_seal-lib.sh` 사유 문구에서 test·tmp·temp·foo 등 제외 |
| hybrid-run-gotchas 8 · round-ledger 1·3 | rounds 는 원장과 맞춘다 · 단일통과가 캡을 1 로 고정 | `status.sh` 는 봉인된 cap 을 읽는다. `pre.sh` 는 `review-rounds seal` 로 캡 3 을 싣는다. 단일통과(`MCCP_REVIEW_SINGLE_PASS`)는 gatekit 이 쓰지 않는다 |
| round-ledger 12 · 6 | 마일스톤 간 원장 공유 | 슬러그를 계획서 경로로 유도 (위) |
| receipt-write-overwrites-flags | 재작성 시 플래그 소실 | seal 두 개는 항상 전 플래그를 함께 준다. 같은 슬러그에 두 번 쓰면 나중 것이 전부를 덮는다 |
| divergent-receipt-multi-agent-shape | `--review-mode` 를 빼면 triple 이 강제 · `--codex-verdict` 와 multi-agent 는 모순 | `seal-divergent-hybrid.sh` 는 hybrid 형태만. `--review-mode` 를 빼지 않는다 |
| plan-codex-runner-seals-divergent | codex 모드 러너는 자체 봉인 | gatekit 은 hybrid 라 러너를 쓰지 않는다. `plan-codex-runner.js` 를 어디서도 부르지 않는다 |
| panel-prompt-injection | 패널 프롬프트에 CLAUDE.md §3 주입 불가 (args 의 prompt 는 안 읽힌다) | 주입하지 않는다. 범위 밖 지적은 리뷰 기록으로 받는다 |
| l1-citation-drops-leading-dot · mccp-plan 메모의 `3.9:1` | C6 오탐 | README 저자 의무 |
| evidence-claim-blocks-after-clear | `/clear` 뒤 첫 write 가 TTL 15분 동안 막힌다 | 막히면 `claimed_at`+15분 대기 (우회 금지) |
| intent gate (9·14) | hybrid write 가 intent gate 로 거부된다 (PRD 모드 등) | `_seal-lib.sh` 는 **거부됐을 때만** `MCCP_SKIP_INTENT_GATE` 사유로 같은 write 를 한 번 더 부른다. `INTENT_OVERRIDE=` 로 보인다 |
| 0-byte l3 · 한도 오진 | 4~5초의 `exit-nonzero` 는 한도일 수 있다 | `l3-poll.sh` 가 `died-without-record` 와 `stderr` 경로를 보고한다 |
| pin / debt | 발사 뒤 정산하지 않으면 예약이 남는다 | `pre.sh` 가 발사 직전 pin(mark-debt) · `post-a.sh` 가 reconcile 로 해제 · 발사 전 중단은 `--actual 0` 반환 |

## 명령 본문과 다르게 옮긴 자리

1. `pre.sh` 가 5.2 진입 전에 `codex-policy seal` 과 `review-rounds seal` (5.-1) 을 돈다. 명령 본문은 5.-1 로 이 순서를 정한다. 빠지면 자식 프로세스가 캡을 못 본다.
2. `pre.sh` 는 5.2 진입 IPC 정리(`GK_PURGE`, 명령 본문 5.2 의 목록과 같다)와 `mode.json` 생성을 한다. `dispatch-log` 는 지우지 않는다.
3. 5.0 은 검출만 한다. critique 루프는 LLM 몫이라 `signal=1` 이면 계획서에 `## Design Critique` 가 있어야 통과한다 (`AUTHOR_FIX=5.0`).
4. lint 를 추가했다 (WT CLAUDE.md §1). 명령 본문에는 없다.
5. `pre.sh` 는 emit 에 `--prd` 를 넘긴다. 명령 본문의 `--prd <prd path or omit>` 중 앞쪽이다.
6. L1 위반(exit 1)은 명령 본문대로 5.2e 가 divergent 를 낸다. 그래서 `post-b.sh` 를 부르면 판정이 기록되고 halt 5.2e 로 끝난다. 라운드는 소비되지 않는다.
7. `post-a.sh` 는 봉투를 자동으로 푼다 (명령 본문은 `result` 를 직접 써야 한다고 한다).
8. `l3-poll.sh` 는 한 번만 본다 (명령 본문은 540초 루프). 대기는 오케스트레이터가 한다.
9. `seal-converged.sh` 와 `seal-divergent-hybrid.sh` 의 감사 우회는 거부되었을 때만 쓴다. 명령 본문 5.6b 는 항상 쓰지 않는다.
10. `seal-divergent-hybrid.sh` 는 명령 본문에 없는 조건을 더했다: 라운드 상한 도달, L3 divergent, proof 존재. 상한 전에 divergent 를 봉인하지 않게 하려는 것이다.
11. `pre.sh` 의 `GATEKIT_SMOKE` · `GATEKIT_PLAN_REL` 은 스모크 전용이다. 실행에서는 설정하지 않는다.

## 스모크 (실측, 2026-10-19)

- 슬러그 `admin-rbac-m1` 확인 (위).
- 더미 계획서 `gatekit-smoke-m0` (커밋하지 않음):
  - `GATEKIT_SMOKE=1 pre.sh 0` → L1 위반 5건(C1 ×5) → `AUTHOR_FIX=5.2a` · exit 1 · `status.sh` = `ROUNDS=0/3`.
  - 같은 더미에서 `post-b.sh 0` → `DECISION verdict=divergent source=multi-agent … L3=none`, `HALT_STAGE=5.2e` · exit 12 (판정 기록 1건 생성 후 삭제).
  - 필수 절을 채운 더미(placeholder 포함) → L1 0 → `WORKFLOW_ARGS_JSON` 출력 · `ROUND=1/3` · 원장 1 (emit 이 소비). 이 더미 원장은 emit 까지만 해당.
  - `post-a.sh 0`: 봉투(`result.skipped`) → `L2_STATE=ok skipped=1 actual=0` · exit 0 (reconcile 0 으로 pin 해제). 형식 오류 `{"x":1}` → exit 3. l2 없음 → exit 3.
  - `post-b.sh 0` (L3 nonce 없음) → `HALT_STAGE=5.2f` · exit 12. `l3-poll.sh 0` → `L3_STATE=failed:not-launched` · exit 12.
  - `seal-converged.sh 0` (수렴 아님) → exit 4. `seal-divergent-hybrid.sh 0` (조건 불충족) → exit 4.
  - 페이크 아티팩트(converged / divergent 각각)로 `_seal-lib.sh` 의 `build_flags` 만 호출 → 5.6b 플래그 순서 확인(`--codex-verdict`·triple·`--review-mode hybrid`·`--review-l3-invoked`·`--review-l3-reason`·`--quiet`). write 는 부르지 않았다.
- 정리: 더미 계획서·리뷰 기록·원장 파일 삭제, `.claude/state/plan-review/` 삭제, `review-rounds cli clear`, 실험용 `.git/worktrees/admin-rbac/mccp/` 삭제. `git status --porcelain` 은 시작 때와 같다 (서브모듈 3줄).

## 검증하지 않은 것 (미검증)

- 실제 L2 Workflow 발사 (반환 봉투 형태는 메모와 명령 본문에 근거, 실측 없음).
- 실제 L3 Codex 호출 (`l3-run.sh` 의 `exec` 경로와 nonce 흐름). 한도·비용 때문에 돌리지 않았다.
- 실제 receipt write·validate, 라운드 상한 divergent 봉인. 플래그 구성만 확인했다.
- `impeccable-detect` 가 실제 계획의 설계 신호를 잡는지 (더미는 `no-signal`).
- 계획서 발사 전 `pre.sh` 의 `--ignore-submodules=all` 판정은 서브모듈 포인터 차이를 제외한다. 시작 시점의 `M` 3줄을 그대로 둔다.
