AGENT SKILL

codex-review

Codex CLI 코드 리뷰 모드. PASS 시 v2 code_review 증거 발급, severity escalation, Sonnet fallback, 재리뷰는 후속 회차 요청서 양식. rein 의 코드리뷰 게이트 통과 증거를 발급하는 유일한 스킬.

License 许可
MIT
所属仓库
JayJihyunKim/rein
最近更新
2026-10-15
Stars
★ 1
Forks
0

01 前言 Front Matter

name
codex-review
description
Codex CLI 코드 리뷰 모드. PASS 시 v2 code_review 증거 발급, severity escalation, Sonnet fallback, 재리뷰는 후속 회차 요청서 양식. rein 의 코드리뷰 게이트 통과 증거를 발급하는 유일한 스킬.

03 SKILL.md 正文

Codex Review Skill (Mode A)

1. 목적

코드 리뷰 전용 — rein 의 리뷰 게이트 생성 스킬. /codex-review 슬래시 명령으로 호출한다.

이 스킬은 리뷰 gate 다. PASS 회차에서 bin/rein issue-evidence code_review --verdict PASS --reviewed-digest <D> 로 v2 code_review 증거를 발급하며, 커밋 시점에 pre-bash-commit-review-gate.sh(Phase 7 웨이브 3 ③-c 신설 — 구 pre-bash-test-commit-gate.sh, ③-c 삭제 완료, 의 코드 리뷰 축 후속)가 이 v2 증거만으로 git commit 허용 여부를 판정한다. Phase 7 웨이브 3 ③-d (2026-08-24) 로 legacy stamp(trail/dod/.codex-reviewed/.review-pending) 의 write·read 경로가 전부 제거됐다 — 위 v2 발급이 이 스킬의 유일한 기록 절차이며, dual-read 전환기 서술은 더 이상 유효하지 않다. (테스트 실행 자체는 GUARD-1 이후 이 리뷰 게이트 대상이 아니다 — pre-bash-commit-discipline-gate.sh 의 동일 지점 주석 참조.)

Second opinion (brainstorm 반박, spec sanity, refactor tradeoff 이중 검증) 이 필요하면 /codex-review 가 아니라 /codex-ask 를 사용한다 — v2 증거를 발급하지 않는 별도 스킬.

Mode 대비

항목 Mode A (/codex-review) Mode B (/codex-ask)
용도 리뷰 게이트 Second opinion
리뷰 결과 기록 필수 (v2 code_review 증거 발급) 절대 금지
Resume --last 래퍼는 매번 새 세션 — 맥락은 후속 회차 요청서 양식으로 (§2, §7) 금지 — 매번 새 세션
Sandbox 래퍼가 read-only 고정 (§6.6) 항상 read-only
Fallback codex 실행 실패 시에만 Sonnet/code-reviewer/human fallback 허용 (§4) same-session Claude fallback 금지. 독립 reviewer 부재 시 degraded / no-second-opinion 으로 명시

Fallback 정책 비대칭 이유: Mode A 는 "리뷰가 어떤 형태로든 수행되어 v2 증거가 발급" 되는 것이 게이트 통과의 전제이므로 codex 실행 실패 시 차순위 reviewer 로 대체할 수 있다. Mode B 는 "독립 관점" 자체가 가치이므로 same-session Claude 가 대신하면 본질이 깨진다 — 호출자는 결과 없이 작업을 계속하거나, 별도 시점에 Mode B 를 다시 시도한다.


2. Running a review

/codex-review 는 scripts/rein-codex-review.sh wrapper 를 호출한다. Wrapper 가 context assembly, envelope 4 slots, codex exec, PASS 시 v2 code_review 증거 발급을 담당한다.

Usage

  • Interactive (code review): bash "${CLAUDE_PLUGIN_ROOT:-$PWD}/scripts/rein-codex-review.sh"

  • Automated (agent, code review, file redirect 필수):

    # 1. prompt 를 임시 파일로 작성 (Write 도구 또는 echo > file)
    # 2. file redirect 으로 wrapper 호출 — pipe (`|`) 사용 금지 (pre-bash-safety-guard 차단)
    bash "${CLAUDE_PLUGIN_ROOT:-$PWD}/scripts/rein-codex-review.sh" --non-interactive < /tmp/codex-prompt.txt
    
  • Spec review (plan review): prompt 파일 맨 앞에 [NON_INTERACTIVE] spec review for plan: <path> marker. wrapper 가 spec-review mode 로 분기.

  • Spec review (design review): prompt 파일 맨 앞에 [NON_INTERACTIVE] spec review for design: <path> marker.

Pipe vs file redirect: printf '...' | bash wrapper.sh 패턴은 pre-bash-safety-guard 가 차단한다 (이유: stdin 의 명령 source 를 hook 가 검증할 수 없음). 항상 < /tmp/<file> 형태로 호출하라.

요청서 작성 규약 — review-readiness 사전검사

정량(개수/비율/백분율)·테스트 통과 주장을 쓰려면 각 주장을 [EVIDENCE] 블록으로 선언한다. 블록 없이 주장을 쓰면 래퍼가 review-readiness 사전검사에서 exit 4 로 거부한다 (codex 미호출 — 비용 0). 주장 없는 요청서는 종전대로 블록 불필요 (완전 하위호환 — 기존 요청서 무변경). output 발췌에 [EFFORT:...] 리터럴을 포함하지 마라 — 래퍼의 기존 전역 strip 단계가 제거한다 (사전검사가 보는 "원문" 은 strip 이후의 PROMPT_BODY). 블록을 갖췄다고 모든 잔존 정량 서술이 통과하는 것은 아니다 — 블록 밖에 남은 정량/PASS 서술은 형태에 따라 두 tier 로 갈린다: 검증명사+통과어 공존(예: "테스트 통과"), 계약 문맥어 없이 또는 결과 서술어와 함께 쓰인 비율·백분율(예: "커버리지 85%"), 그리고 결과 서술어가 있고 계약 문맥어가 없는 수량+단위(예: "실패 3건 발견")는 reject-tier 로 블록이 있어도 exit 4 거부; 고정 계약 형태(예: "함수 50줄 이내", "오차 상한 ±20%")와 결과 서술어 없는 단독 수량(예: "파일 5개")은 advisory-tier 로 비차단 경고에 그친다 (문법·집합식: §4.1).

  • 증거 블록 문법 전문: §4.1
  • exit 4 판별 계약 + 호출자 행동: §4.2 (Sonnet 폴백 비대상)
  • spec-review 모드는 사전검사 비대상: §6.7
  • 수치 주장의 구성: 합계·개수를 주장하면 그 숫자가 무엇의 합인지 한 줄로 적는다. 최소 형식은 <합계> = <항목1 값>(<출처>) + <항목2 값>(<출처>) + … 이고, 출처는 파일 경로(필요하면 절)·명령 중 하나다. 예: 변경 테스트 파일 7 = 신규 3(git status --short tests/) + 갱신 4(git diff --name-only tests/). 리뷰어(Claim Audit)는 각 항목을 출처에서 확인하고, 모든 항목이 확인되고 합이 맞으면 그 숫자를 1:1 대응을 갖춘 것으로 본다. 구성이 없으면 리뷰어가 숫자마다 대응을 직접 찾아야 하고, 찾지 못하면 수정 필요로 판정한다. 사전검사(형식 검사)는 구성 줄을 따로 검사하지 않는다 — 다만 구성 줄도 블록 밖에 쓰면 정량 서술로 스캔되므로(§4.1 두 tier) "통과·발견·실패" 같은 결과 서술어를 넣지 말고 사실만 나열한다.

자가검증 증거 — 두 축 계약

코드 리뷰 모드에서 리뷰 대상에 변경(추적 파일 변경 또는 미추적 파일)이 있으면, 래퍼는 codex 를 부르기 전에 요청서에 아래 세 가지가 있는지 형식만 확인한다. 명령을 다시 실행하지 않고, 출력의 진위도 확인하지 않는다. 하나라도 없으면 exit 4 로 거부한다(§4.2 — codex 미호출, 회차 비소모). spec-review 모드와, 변경이 문서·trail 뿐이라고 판정된 경우는 확인하지 않는다.

  1. [axis:typecheck] 블록 — claim: 에 [axis:typecheck] 토큰을 정확히 하나 담고 exit_code: 0 인 [EVIDENCE] 블록(§4.1). 타입 검사·린트·구문 검사 같은 정적 검증이다.
  2. [axis:test] 블록 — claim: 에 [axis:test] 토큰을 정확히 하나 담고 exit_code: 0 인 [EVIDENCE] 블록. 1 과 다른 블록이어야 한다. 한 claim: 에 두 토큰을 같이 넣으면 어느 축으로도 세지 않는다.
  3. diff_self_review: 단독 라인 — 작성자가 diff 를 직접 읽고 확인한 내용 한 줄(비어 있으면 안 된다).

테스트 블록의 범위는 정해져 있지 않다. 래퍼는 [axis:test] 블록이 전체 스위트를 실행했는지 보지 않는다. 초회에는 전체 스위트를 권장하고, 2회차부터는 아래 "델타 증거" 형식으로 대상 테스트만 실행해도 된다.

예외 선언 — 블록·코드 펜스 밖의 단독 라인으로 쓴다:

  • verification_commands: none — 이 저장소에 실행할 검증 명령이 없을 때. 두 블록 요구를 면제하고 diff_self_review: 만 요구한다.
  • verification_state: tests-intentionally-red + expected_failure: <무엇이 왜 실패해야 하는가> — TDD 의 red 단계. [axis:typecheck] exit0 블록 + [axis:test] 블록(exit 1~123·125·126 허용, 0·124·127·128 이상은 거부) + diff_self_review: 를 요구한다.

대상 테스트 선택 도구 — rein-affected-tests.py

후속 회차 델타 증거의 대상 테스트(아래 "델타 증거" 3번 칸)를 눈으로 고르지 않고 기계로 고른다. 요청서 작성자가 직접 실행한다 — 래퍼는 이 도구를 부르지도, 테스트를 실행하지도 않는다.

python3 "${CLAUDE_PLUGIN_ROOT:-$PWD/plugins/rein-core}/scripts/rein-affected-tests.py" \
  --base <full_run_tree> --untracked-scope '<untracked_scope 의 glob>' \
  --untracked-snapshot <untracked_at_full_run 줄을 담은 파일> --format json

--base 를 생략하면 HEAD 대비 작업 트리 변경을 본다. --untracked-scope 는 델타 증거의 untracked_scope: 와 같은 glob 을 준다(여러 번 지정 가능, 생략하면 전체). --untracked-snapshot 은 untracked_at_full_run: 의 줄들(<경로> <sha256> <줄 수>, 없으면 none)을 담은 파일이다 — 도구가 미추적 항목을 델타 증거 2번 칸과 같은 규칙으로 계산한다. 외부 입력이 바뀌었거나 알 수 없으면 --external-inputs-changed 를 붙인다. --format text 는 사람용 요약, --no-install 은 아래 자동 설치를 건너뛴다. 종료 코드 0 이면 출력이 있다(전체 실행 판정 포함). 0 이 아니면 도구를 쓰지 못한 것이다 — 전체 스위트를 돌리거나 직접 골라 selection: manual 로 적는다. 도구 전체 실행 시간은 최대 600초다.

판정 순서:

  1. 먼저 아래 "전체 테스트 재실행으로 돌아가는 조건" 의 의존성·빌드 설정, 테스트 공용 설정·픽스처, 소스 5개·200줄 초과(줄 수는 같은 산식 — 문서 포함 델타 전체), 소스 삭제·이름 변경, 외부 입력 변경, 변경 없음을 본다. 하나라도 걸리면 어떤 방법도 쓰지 않고 전체 실행 필요: 예 다 — 프로젝트 스크립트도 이를 뒤집지 못한다.
  2. project-script — 프로젝트가 둔 .rein/affected-tests.sh. 계약: stdin 으로 바뀐 경로를 한 줄에 하나 받고, stdout 으로 {"tests": [...], "command": "..."} JSON 을 낸다(최대 1 MiB). 종료 코드 0 = 결과 유효, 3 = 전체 실행 필요, 그 밖·120초 초과·형식 오류 = 다음 방법으로 내려간다. 고른 테스트가 빠짐없는지는 프로젝트가 책임진다 — rein 은 검증하지 않는다. 도구는 --base 에 커밋된 판을 꺼내 실행한다 — 기준 이후 바뀐 내용은 실행하지 않고, 기준에 없는 새 파일이면 건너뛴다. 스크립트는 그 판의 트리 전체를 임시 디렉터리로 꺼낸 곳을 작업 디렉터리로 실행되므로 같은 판의 보조 파일(source 하는 helper 등)도 그 판이 쓰인다. 현재(검토 대상) 저장소는 환경 변수 REIN_AFFECTED_TARGET(저장소 절대 경로)로만 전달되니, 선택 대상 파일은 이 경로 아래에서 읽어야 한다. 그 밖에 REIN_AFFECTED_BASE·REIN_AFFECTED_ROOT 를 받는다. 실행 환경에서는 BASH_ENV·ENV·CDPATH·내보낸 셸 함수를 제거하고 bash --noprofile --norc 로 실행한다.
  3. 두 스택이 함께 바뀌었거나 지원 스택(파이썬 pytest, JS/TS jest·vitest·npm test) 밖이면 전체 실행 필요: 예 다. 이후 방법 중 jest·vitest·import-search 는 저장소 전체에서 첫 인자가 고정 문자열 리터럴이 아닌 동적 import(importlib.import_module(name)·__import__(name)·require(name)·import(name)·인터폴레이션이 있는 템플릿 문자열, 따옴표 문자열에 다른 식을 이어 붙인 import("./plugins/" + name) 같은 형태 — 따옴표 문자열 하나만 인자로 오는 경우만 고정 경로로 본다)를 쓰는 파일을 찾아, 그런 테스트 파일과 그런 소스 파일에 import 로 (중간 모듈을 거쳐서라도) 닿는 테스트를 바뀐 파일과 무관하게 항상 대상에 더한다 — 동적 import 는 무엇을 불러오는지 정적으로 알 수 없기 때문이다. 테스트 파일 규칙을 바꾸는 설정(pytest 의 python_files·testpaths, jest 의 testMatch·testRegex·roots, vitest 의 include·includeSource)이 있거나, 저장소에 구문 오류가 있는 파이썬 파일·읽을 수 없거나 너무 큰 파일이 있거나, 저장소를 끝까지 훑지 못하면, 또는 더한 결과가 전체 테스트 파일의 절반을 넘으면 전체 실행 필요: 예 다(jest·vitest 의 이 절반 계산은 jest·vitest 의 실제 대상이 아니라 선택 도구 자신의 import 그래프로 센 근사다). testmon 은 실제 실행을 추적하므로 이 규칙에서 빠진다. 도구가 따라가지 못하는 경우가 남는다: JS 경로 별칭·번들러 설정 기반 모듈 해석, 함수 별칭으로 감싼 호출·exec·eval·설정 문자열 기반 플러그인 로딩 같은 패턴 밖의 동적 로딩, 그리고 JS 파일의 구문 오류(감지하지 않는다)다.
  4. jest / vitest / testmon — 정밀 도구. jest·vitest 는 package.json 의 의존성에 있고 node_modules/.bin/ 에 실행 파일이 있을 때, 명령은 jest --findRelatedTests <파일> / vitest related --run <파일> 이고, 3 의 추가 대상이 있으면 && jest --runTestsByPath <추가 경로> / && vitest run <추가 경로> 가 붙는다. testmon 은 파이썬 프로젝트에 pytest-testmon 과 기록(.testmondata)이 있을 때, 명령은 pytest --testmon 이다. 세 도구 모두 대상은 도구가 실행 시 정한다. testmon 의 기록이 없으면 전체 실행이고 명령은 pytest --testmon-noselect(선택 없이 전체를 돌리며 기록을 만든다)다.
  5. import-search — rein 내장 검색. 바뀐 모듈을 import 하는 테스트, 그리고 바뀐 모듈을 import 하는 다른 모듈을 import 하는 테스트(2단계)를 고르고, 3 의 추가 대상을 더한다. 파이썬은 import 문, JS/TS 는 상대 경로 import·require 만 본다. 다음이면 전체 실행: 바뀐 모듈을 직접 import 하는 테스트가 둘 이상(공용 모듈 — 이 방법은 완화 대상이 아니다) / 어떤 테스트에도 매핑되지 않는 소스 / 대상이 전체 테스트 파일의 절반 초과.
  6. full — 위 어느 것으로도 고르지 못했다. 사유가 함께 나온다.

도구 방법(jest·vitest·testmon)이 실행 시 "관련 테스트 없음" 으로 실패하면 전체 실행으로 간다. testmon 이 있는 프로젝트의 전체 실행(기준 실행 포함)은 pytest --testmon-noselect 로 돌린다 — selection: testmon 의 기준 실행 블록은 이 옵션을 포함해야 한다(아래 델타 증거 4번 칸).

정밀 도구 자동 설치 — 4단계에서 파이썬 프로젝트에 pytest-testmon 이 없으면 한 번 설치를 시도한다(1~3단계에서 결론이 나면 시도하지 않는다): poetry.lock 이 있으면 poetry add --group dev "pytest-testmon>=2,<3", uv.lock 이 있으면 uv add --dev "pytest-testmon>=2,<3", 그 외에는 활성 venv(VIRTUAL_ENV)가 있을 때만 그 venv 의 python -I -m pip install "pytest-testmon>=2,<3"(작업 폴더의 같은 이름 모듈을 집어오지 않도록 격리 실행)(+ requirements-dev.txt 가 있으면 한 줄 추가). venv 없는 시스템 파이썬에는 설치하지 않는다. 결과는 .rein/state/test-tools.json 에 기록하고, 기록이 있으면 다시 시도하지 않는다 — 다시 시도하려면 그 파일을 지운다. 설치에 성공한 회차는 의존성이 바뀌었으므로 전체 실행(pytest --testmon-noselect)이다. 실패하면 import-search 로 내려간다. 끄려면:

# .rein/policy/test-selection.yaml
auto_install_test_tools: false

파일이 없으면 켜짐이다. 파일이 있는데 읽을 수 없으면(형식 오류·PyYAML 없음·true/false 가 아닌 값) 꺼짐으로 보고 경고한다.

리뷰 전 프로젝트 사전 검사 — .rein/review-precheck.sh

프로젝트가 저장소 루트에 .rein/review-precheck.sh 를 커밋해 두면, 래퍼가 코드 리뷰 모드에서 codex 를 부르기 전에 이를 실행한다. 린트·정적 규칙처럼 외부 리뷰 전에 기계로 잡을 수 있는 문제를 여기서 걸러 회차를 아낀다. 파일이 없으면 아무 일도 일어나지 않는다. 검사 항목은 프로젝트가 정한다 — rein 은 연결만 한다.

  • 시점: 위 "자가검증 증거 — 두 축 계약" 확인을 통과한 뒤, 회차 예산 판정 전. spec-review 모드에서는 실행하지 않는다. 자가검증이 요구되지 않는 리뷰(문서·trail 뿐인 변경)에서도 코드 리뷰 모드면 실행한다.
  • 검토 대상 이전 판만 실행: 작업 트리의 파일은 직접 실행하지 않는다. 작업 트리 리뷰에서는 HEAD 에 커밋된 판, 커밋 범위 리뷰에서는 검토 대상 커밋들 이전(리뷰 비교 기준)의 판을 꺼내 실행한다 — 리뷰 중인 변경이 이 스크립트를 추가·수정·삭제해도 그 내용은 이번 리뷰에서 실행되지 않는다. 그 기준에 파일이 없으면 실행하지 않고 경고(WARNING: [codex-review][readiness-advisory]) 후 리뷰를 진행한다.
  • 실행: 검토 이전 판의 트리 전체를 먼저 검사(절대 경로·..·외부 심볼릭 링크 거부)한 뒤 저장소 밖 임시 디렉터리에 꺼내, 그 디렉터리를 작업 디렉터리로 bash --noprofile --norc 로 실행한다. 같은 판의 보조 파일(source 하는 helper)도 그 판이 쓰인다. 현재(검토 대상) 저장소는 환경 변수 REIN_PRECHECK_TARGET(저장소 절대 경로)으로만 전달되니, 검사 대상 파일은 이 경로 아래에서 읽어야 한다. stdin 은 비어 있다. 실행 환경에서는 BASH_ENV·ENV·CDPATH·내보낸 셸 함수를 제거한다. 기준 판이 일반 파일이 아니거나 트리 검사에 걸리면 거부한다.
  • 환경 변수: REIN_PRECHECK_TARGET(검토 대상 저장소 절대 경로), REIN_PRECHECK_PROJECT_DIR(저장소 루트), REIN_PRECHECK_CHANGED_FILES(리뷰 대상 파일 목록을 한 줄에 하나 담은 파일의 경로), REIN_PRECHECK_CHANGED_FILES_RC(0 이 아니면 목록을 얻지 못한 것 — 전체 검사로 가라), REIN_PRECHECK_DIFF_BASE(리뷰 비교 기준), REIN_PRECHECK_REVIEW_SUBJECT(working_tree 또는 commit_range).
  • 시간 제한: 기본 120초, REIN_REVIEW_PRECHECK_TIMEOUT 으로 조정(1~9999 정수). 래퍼가 스크립트를 자기 프로세스 그룹으로 띄워 직접 시간을 재므로 timeout 명령이 없는 환경(macOS 기본)에서도 지켜지고, 기한을 넘기면 그룹 전체(스크립트가 띄운 프로세스 포함)를 끝낸다. 이를 위해 setsid 또는 perl 이 필요하다 — 둘 다 없으면 실행하지 않고 거부한다.
  • 출력: 1 MiB 까지만 보관하고 나머지는 버린다. 거부 시 마지막 20줄을 보여 준다.
  • 결과: 종료 코드 0 = 통과(출력 없이 리뷰 진행). 그 밖은 exit 4 거부 — codex 를 부르지 않고 회차를 쓰지 않는다. 거부 진단행은 ERROR: [codex-review][readiness-reject] 프로젝트 사전 검사 로 시작하고 유형(실패 rc / 시간 초과 / 실행 불가 / 거부)을 밝힌다. 스크립트가 직접 124·137 로 끝나면 시간 초과로 보고된다.
  • 규약: 작업 트리를 바꾸지 않는다(래퍼는 확인하지 않는다 — 바꾸면 리뷰 대상이 바뀐다). 같은 입력에 같은 결과를 낸다. 지적은 파일 경로와 줄을 포함해 stdout·stderr 에 쓴다.

후속 회차 요청서 양식 (2회차 이상)

래퍼는 재리뷰마다 새 codex 세션을 띄운다 — 이전 회차의 검토 내용은 자동으로 전달되지 않는다(§7). 그래서 2회차부터는 호출자가 아래 다섯 칸을 요청서에 직접 싣는다. 목적은 리뷰어의 주의를 "이전 지적 + 수정분 + 그것이 닿는 계약" 에 모으는 것이다.

  1. 직전 검토 근거 — 같은 사이클의 몇 번째 확인인지, 직전 판정과 차단급 지적 유무.
  2. 열린 지적과 처리 — 지적별로 셋 중 하나: 고침(어디를) / 위험 수용(근거·기록 위치) / 반박(근거).
  3. 종결된 지적 — 이미 배제·수용해 다시 다루지 않을 항목. 새 세션은 이를 모르므로 적지 않으면 같은 지적이 회차마다 되돌아온다.
  4. 직전 검토 이후 바뀐 것 — 항목으로 나열(수정분의 범위).
  5. 영향받는 계약과 증거 — 4 가 닿는 계약과 그 검증 명령([EVIDENCE] 블록, §4.1). 테스트 증거는 아래 "델타 증거" 형식으로 대상 테스트만 실행해 갖출 수 있다.

요청 문장은 다음 취지로 쓴다: "후속 확인이다 — 필수 검사 축은 그대로 수행하되 4·5 를 우선 확인해 달라. 중대한 결함은 범위와 무관하게 보고해 달라." "그 밖의 새 결함도 함께 봐 달라", "수정이 새 충돌을 만들지 않았는지 전체를 확인해 달라" 같은 문장은 쓰지 않는다 — 매 회차 전체 재탐색을 요청하는 것과 같다.

델타 증거 — 후속 회차의 테스트 증거

2회차부터는 매 회차 전체 테스트 스위트를 다시 돌리지 않는다. 대신 "전체 스위트를 돌린 시점의 트리 식별자 + 그 뒤 바뀐 파일(델타) + 델타가 닿는 테스트만 실행한 기록" 을 싣는다. 자가검증 두 축 계약의 [axis:test] 블록은 이 대상 테스트 실행으로 채운다(래퍼는 테스트 범위를 보지 않는다 — 위 "자가검증 증거 — 두 축 계약"). 코드 리뷰 봉투의 Claim Audit 은 이 형식을 갖춘 요청서에 전체 스위트 재실행을 요구하지 않는다.

  1. 기준 트리 식별자 — 전체 스위트를 돌린 직후에 git stash create 를 실행해 출력된 커밋 id 를 적는다. 이 명령은 작업 트리와 stash 목록을 바꾸지 않고 추적 파일의 현재 상태(스테이지 포함)를 커밋 객체로 고정한다. 출력이 비어 있으면(추적 변경 없음) git rev-parse HEAD 의 값을 쓴다. 이 커밋 객체는 어느 ref 에도 걸리지 않는다 — 가비지 수집 유예(기본 2주)는 보장이 아니므로, 요청서를 쓰기 직전에 git cat-file -e <id> 로 아직 있는지 확인하고 없으면 전체 스위트를 다시 돌려 새 식별자를 만든다. 전체 스위트 직후에 git ls-files --others --exclude-standard 의 결과를 untracked_scope: 에 적은 범위(glob 목록, 예: src/** tests/** — 생략하면 전체)로 거르고, 남은 파일마다 <경로> <sha256> <줄 수> 한 줄(sha256sum·wc -l)을 untracked_at_full_run: 에 적는다. 기준과 현재 비교는 항상 같은 untracked_scope 를 쓴다 — 집합이 다르면 문서·trail 파일이 added 로 오판된다 — git stash create 는 미추적 파일 내용을 고정하지 않으므로 이 지문·줄 수 스냅샷이 미추적 파일의 기준이다(없으면 none).
  2. 델타 — git diff --name-status <기준 트리 식별자> 의 출력을 그대로 싣고, git diff --numstat <기준 트리 식별자> 의 추가·삭제 합을 delta_lines: 에, 소스 파일(테스트·문서 제외) 수를 delta_source_files: 에 적는다. --name-status 에는 미추적 파일이 나오지 않으므로 untracked_at_full_run 스냅샷과 지금 git ls-files --others --exclude-standard 를 같은 untracked_scope 로 거른 목록을 비교해 세 경우를 ?? <경로> <added|modified|deleted> 로 직접 추가한다: added(지금 줄 수) — 스냅샷에 없던 파일, modified(스냅샷·지금 중 큰 줄 수) — sha256 이 다른 파일, deleted(스냅샷 줄 수) — 스냅샷에 있었는데 지금 미추적 목록에 없는 파일. 추적 전환 규칙: 스냅샷 파일이 지금 추적 파일이 됐으면(git ls-files --error-unmatch <경로> 성공, --name-status 에 A 로 나타남) deleted 가 아니다 — --name-status 항목으로만 한 번 센다. 세 경우의 줄 수를 delta_lines 에 더하고, 소스 파일이면 delta_source_files 에도 센다(산식: numstat 합 + ?? 줄 수 = delta_lines, --name-status 소스 + ?? 소스 = delta_source_files — 리뷰어가 같은 산식으로 재계산해 대조한다). 판단이 어려우면 범위 안 미추적 파일을 전부 modified 로 올린다.
  3. 대상 테스트 — 델타 파일마다, 그 파일을 import 하거나 그 동작을 실행하는 테스트 파일을 전부 나열한다(산정 책임은 요청서 작성자). 위 "대상 테스트 선택 도구" 로 골랐으면 selection: 에 출력의 선택 방법을 적고, 도구 실행을 axis 토큰 없는 [EVIDENCE] 블록으로 싣는다 — command: 는 도구 호출 그대로(--base <full_run_tree>, --untracked-scope, --untracked-snapshot, --format json 포함), output 은 JSON 출력 전체다. 리뷰어는 그 JSON 의 base·untracked_scope·changed·delta_lines·delta_source_files 를 이 델타 증거의 값과 대조한다 — 도구를 실행한 뒤 파일이 더 바뀌었으면 대조가 어긋나므로 도구를 다시 실행한다. JSON 이 증거 블록 상한(60줄·8000바이트)을 넘으면 도구 방법을 쓸 수 없다 — selection: manual 로 쓰거나 전체 실행으로 간다. 도구가 전체 실행 필요: 예(full_run_required: true)를 내면 델타 증거를 쓰지 않는다. 눈으로 골랐으면 selection: manual. 나열한 테스트를 실행한 명령을 [axis:test] 블록의 command: 로 싣는다 — 도구로 골랐으면 JSON 의 command 와 같아야 한다. jest·vitest·testmon 은 도구가 실행 시 대상을 정하므로 targeted_tests: 에 * -> <선택 방법> 선택 한 줄로 적는다.
  4. 기준 실행 기록 — 기준 시점의 전체 스위트 실행을 axis 토큰 없는 [EVIDENCE] 블록으로 싣는다(claim: 예: 기준 트리 <id> 에서 전체 스위트 실행, output 은 요약 몇 줄). 이 블록은 exit_code: 0 이어야 한다 — 실패한 실행은 기준이 아니다. 새 세션의 리뷰어는 이전 요청서를 볼 수 없으므로 매 회차 다시 싣는다. selection: testmon 이면 기준 실행 블록의 command: 에 --testmon-noselect 가 있어야 한다(선택 없이 전체를 돌리며 testmon 기록을 만든 실행 — 그 기록이 이후 회차의 선택 근거다).
  5. 외부 입력 — 테스트가 읽는 저장소 밖 입력과 gitignore 대상 입력(실데이터 디렉터리, 환경 변수, 인터프리터·의존성 설치 상태)이 기준 실행 이후 바뀌지 않았음을 external_inputs: unchanged 로 선언한다. 바뀌었거나 알 수 없으면 델타 증거를 쓰지 않고 전체 재실행으로 돌아간다.

형식:

delta_evidence:
  full_run_tree: <git stash create 출력 — 비어 있으면 git rev-parse HEAD>
  untracked_scope: <미추적 비교 범위 glob 목록 — 예: src/** tests/**, 생략하면 전체>
  untracked_at_full_run:
    <경로> <sha256> <줄 수>   # 전체 스위트 직후, untracked_scope 로 거른 미추적 파일마다 한 줄, 없으면 none
  delta:
    <git diff --name-status <full_run_tree> 출력 그대로>
    ?? <경로> <added|modified|deleted>   # 스냅샷과 지금 상태의 비교
  delta_lines: <git diff --numstat <full_run_tree> 추가+삭제 합 + 미추적 added/modified/deleted 줄 수>
  delta_source_files: <델타 중 소스 파일 수 — 테스트·문서 제외>
  targeted_tests:
    <델타 파일> -> <테스트 파일>, <테스트 파일>
  selection: <project-script|jest|vitest|testmon|import-search|manual>   # 생략하면 manual
  external_inputs: unchanged

칸이 하나라도 비거나 산식이 맞지 않으면 델타 증거가 아니다 — 리뷰어는 Medium "델타 증거 불완전 — 전체 실행 후 재요청" 으로 돌려주므로 전체 재실행으로 돌아간다. 이어서 [EVIDENCE] 블록 두 개 — 기준 실행 기록(axis 토큰 없음, exit_code: 0) 1개, 대상 테스트 실행(claim: 에 [axis:test]) 1개 — 와, 선택 도구를 썼으면 도구 실행 블록(axis 토큰 없음, output 은 JSON) 1개, 그리고 [axis:typecheck] 블록과 diff_self_review: 라인을 싣는다. "대상 테스트 통과" 같은 문장을 블록 밖에 쓰지 않는다 — 검증명사와 통과어가 함께 있는 라인은 블록이 있어도 사전검사가 거부한다(§4.1 reject-tier). 통과 사실은 블록의 claim: 에만 쓴다.

전체 테스트 재실행으로 돌아가는 조건 — 델타가 다음 중 하나라도 포함하면 델타 증거를 쓰지 말고 전체 스위트를 다시 실행해 그 결과를 [axis:test] 블록으로 싣는다(새 기준 트리 식별자도 이때 만든다):

  • 의존성·빌드 설정 파일 변경 — lockfile, pyproject.toml, package.json, setup.* 류. 작성자가 아는 의존성 설치 상태·런타임 버전 변경도 여기에 든다.
  • 테스트 공용 설정·픽스처 변경 — conftest.py, tests/fixtures/**, tests/helpers/** 류.
  • 둘 이상의 테스트 파일이 import 하는 공용 모듈 변경 — selection: 이 import-search 이거나 manual(칸 없음 포함)일 때 적용한다. 도구 보증 방법(project-script·jest·vitest·testmon)으로 골랐으면 적용하지 않는다 — 대상의 완전성은 프로젝트 스크립트(프로젝트가 책임, rein 은 검증하지 않는다) 또는 도구(jest·vitest 의 import 그래프, testmon 의 실행 기록)가 보증하며, jest·vitest 를 쓸 때는 선택 도구가 저장소의 동적 import 흔적 파일과 그 파일에 import 로 닿는 테스트를 실행 명령에 더한다.
  • 소스 파일(테스트·문서 제외) 5개 초과 변경(delta_source_files > 5), 또는 델타 전체의 변경 200줄 초과(delta_lines > 200).
  • 소스 파일 삭제 또는 이름 변경(--name-status 의 D·R).
  • 테스트가 읽는 외부 입력(저장소 밖·gitignore 대상 데이터, 환경 변수, 인터프리터·의존성 설치 상태)이 기준 이후 바뀌었거나 알 수 없음(external_inputs 가 unchanged 가 아님).
  • 델타 증거의 칸이 하나라도 비었거나, 기준 실행 블록이 exit_code: 0 이 아니거나, 기준 커밋이 더 이상 없음(git cat-file -e 실패).
  • 대상 테스트 선택 도구가 전체 실행 필요: 예 를 냈음.

이 숫자(5개·200줄)는 측정으로 정한 값이 아니라 운영 가설이다 — 놓친 결함이 드러나면 이 절에서 조정한다. "전체 검토로 복귀하는 조건"(아래)은 리뷰 범위의 조건이고, 이 절은 테스트 실행 범위의 조건이다 — 둘은 따로 판정한다.

전체 스위트 실행 횟수 — 리뷰 회차마다가 아니다. 기준 실행 1회, 그리고 최종 트리가 기준 트리와 다르면(기준 이후 코드가 바뀌었으면) 커밋 직전에 최종 트리에서 1회 더 — 복귀 조건이 한 번도 걸리지 않으면 사이클당 1~2회다. 복귀 조건이 걸릴 때마다 전체 실행 1회와 새 기준 트리가 추가되므로 상한은 "1~2회 + 복귀 횟수" 이지 고정된 숫자가 아니다. 최종 트리가 기준과 같으면 기준 실행이 곧 커밋 전 실행이라 추가 실행이 없다. 커밋 전 실행에서 실패해 코드를 고치면 그 수정은 리뷰를 받지 않은 변경이므로 종전대로 재리뷰 대상이다.

전체 검토로 복귀하는 조건 — 하나라도 해당하면 이 양식을 쓰지 말고 초회와 같은 전체 검토를 요청한다: 새 인터페이스(호출 규약·출력 형식·파일 위치) 추가, 보안 경계(아래 (b) 의 범주 — 한정 열거 아님) 변경, 작업 범위(DoD·Scope 항목) 변경, 직전 검토 이후 변경이 검토 대상의 대부분을 차지.

함께 지킬 운영 규율:

  • 작업 기준서(DoD)의 범위를 검토 단위에 맞춘다. 기준서가 아직 구현하지 않은 범위 항목까지 가리키면 매 회차 그 항목들이 다시 평가된다.
  • 문서 리뷰에서 "구현 단계 이관" 참고사항과 오류 경로·상태 수명 지적을 문서 본문의 계약으로 편입하지 않는다(§6.7). 편입하면 다음 회차에 "문서 내부 충돌" 로 되돌아오고, 그 지적은 판정에 반영된다.
  • 후속 회차의 강도는 조건부 기본값이다 — 아래 셋이 모두 맞으면 [EFFORT:medium] 으로 부른다(§6.1): (a) 직전 독립 검토 보고에 High 가 없고 필수 검사 축이 수행됐다, (b) 변경이 직전 지적 처리에 한정되고 새 인터페이스·보안 경계·범위 변경이 없다 — 보안 경계는 보안 규칙이 다루는 범주 전부다(입력 검증·경로·권한·비밀 취급·인증·세션·TLS·인증서·암호화·역직렬화·코드 실행·외부 신뢰 경계 등, 한정 열거가 아니다) 또는 §6.3 floor 경로, (c) 새 판단이 필요한 변경이 없다. "High 를 고쳤다" 는 (a) 충족이 아니다 — 직전 보고에 High 가 있었으면 그 회차는 대상 밖이다. 셋 중 하나라도 어긋나면 medium 을 쓰지 않는다: 직전 보고에 High 가 있었거나(그 High 를 고쳤더라도 — 그 수정은 아직 독립 검토를 받지 않았다) (b)·(c) 에 어긋나면 §6.3 재승급 규칙대로 [EFFORT:high] 를 넣고, 직전 보고가 필수 축을 수행하지 않았으면 그 검토는 완결이 아니므로 마커 없이 전체 검토와 산출 강도(§6.3, 위험 경로 floor 포함)로 복귀한다. 이 기본값은 medium 이 놓치는 결함 폭을 재지 않은 운영 가설이며, 이후 리뷰·CI·버그에서 드러난 누락과 대조한다.
  • 회차 상한(§10)과 별개로, 누적 대기가 길어지면 재호출 대신 사람에게 남은 위험 수용 / 범위 축소 / 중단 중 결정을 요청한다. 시간이 많이 들었다는 사실은 통과 사유가 아니다.

한계: 래퍼는 후속 회차에도 검사 축 전부를 응답에 요구한다. 이 양식은 범위 축소를 보장하지 않는다. 효과를 볼 때는 소요 시간만이 아니라 놓친 결함도 함께 본다 — trail/review-events/ 의 소요는 판정으로 끝난 호출만 담으므로 총 대기 지표가 아니다.

Mode 별 기록 규칙 (CRITICAL)

  • Code review mode: PASS 시 bin/rein issue-evidence code_review --verdict PASS --reviewed-digest <D> 로 v2 증거 발급 (commit gate 통과용). digest 는 리뷰 대상 diff 범위(HEAD~1 체인 기반)에서 산정한다 (Phase 7 웨이브 3 ③-d — 이전에는 stamp 의 diff_base: 필드에서 읽었지만, stamp 자체가 제거되면서 매 실행 시 직접 재산정으로 전환됐다, GI-codex-review-diff-base 후속).
  • Spec review mode (plan 또는 design): code_review v2 증거를 절대 발급 안 함. legacy marker 는 이미 존재하지 않으므로 건드릴 대상도 없음. verdict 만 stdout 으로 방출. caller 가 bash "${CLAUDE_PLUGIN_ROOT:-$PWD}/scripts/rein-mark-spec-reviewed.sh" <path> <reviewer> 를 별도로 호출해 trail/dod/.spec-reviews/*.reviewed 를 생성할 책임 (spec-review 축은 이번 웨이브의 존속 예외).
  • Rationale: code_review v2 증거는 code commit gate 전용. spec review 가 이를 발급하면 코드 변경 없이도 gate 통과 → rein 규율 붕괴.

Legacy interactive model selection

아래는 wrapper 가 없던 시기의 manual invocation 절차로, 현재는 wrapper 가 담당한다. 참고용으로 남긴다.

  1. 모델은 단일 출처(plugins/rein-core/config/codex-models.sh 의 CODE_GATE_MODEL — 게이트 고정 모델, 프로필 구조)가 결정한다. 래퍼가 이 값을 -m 으로 전달하므로 수동 모델 선택은 불필요하다 — 모델 rename 시 그 파일만 수정한다. (legacy alias CODE_MODEL 은 신값을 노출한다.)
  2. Ask the user (via AskUserQuestion) which reasoning effort to use: low, medium, or high.
  3. Select the sandbox mode required for the task; default to --sandbox read-only unless edits or network access are necessary.
  4. Assemble the command with the appropriate options:
    • -m, --model <MODEL>
    • --config model_reasoning_effort="<low|medium|high>"
    • --sandbox <read-only|workspace-write|danger-full-access>
    • --full-auto
    • -C, --cd <DIR>
    • --skip-git-repo-check
  5. Run the command, capture stdout/stderr, and summarize the outcome for the user.
  6. After every codex command, immediately use AskUserQuestion to confirm next steps, collect clarifications, or decide whether to resume the same review cycle (§7).

Quick Reference

Use case Sandbox mode Key flags
코드 리뷰 (초회) read-only 래퍼가 --sandbox read-only 를 항상 전달 (§6.6)
코드 리뷰 (fix 후 재리뷰, 같은 사이클) read-only 래퍼 재호출(새 세션) + 후속 회차 요청서 양식 (§2)
로컬 편집 (수동 직접 호출, 래퍼 아님) workspace-write --sandbox workspace-write --full-auto
다른 디렉토리에서 실행 task needs -C <DIR> 추가

실행 모드 — Bash 도구로 호출 시 foreground 전용

plugins/rein-core/rules/background-jobs.md 예외 절에 따라, 어떤 경로로 codex 를 호출하든 Bash 도구에서는 foreground 실행이 강제된다. Bash 도구의 auto-background 전환이 stdin 을 unix socket 으로 붙이면 codex sandbox/auth 초기화가 hang 한다 (증상: 네트워크 연결 0개, CPU 0, ~/.codex/sessions/ 미갱신).

공통 규칙 (wrapper / direct 모두):

  • Bash 도구 호출 시 run_in_background: false 필수
  • 시간상한은 래퍼가 코드로 강제한다 (문서 규약 아님): 1차 상한 low 120s / medium 180s / high 300s (spec-review 동일). 1차 상한 도달 후 30초 창 단위로 생존 검진하며, 신호는 3축이다 (2026-07-27~):
    • edge(창 사이 실제 변화: 결합 스풀 성장 또는 codex 자손 PID 집합 변동) → 무기한 유예 (절대 상한 없음)
    • level(상태 유지·변화 없음: 미완료 자식 명령 표식 또는 자손 존재) → 유한 lease(기본 20창) 안에서만 유예
    • 위 어느 것도 아닌 창이 연속 6창(180s) 이면 TERM → 10s grace → KILL → reap 후 exit 5 로 종료 (판별 계약 + 호출자 행동: §4.2, 하니스 상호작용: §4.3)
    • 배경: codex 는 자식 명령 실행 중 출력을 내지 않아, 리뷰어가 테스트를 돌리는 정상 구간이 과거 '무성장 단일 축' 판정으로 오살됐다 (2026-07-27 실측·수리)
  • 워치독 override env 5종 REIN_WATCHDOG_{CAP,INTERVAL,GRACE,STALL_WINDOWS,LEVEL_LEASE}_OVERRIDE 는 테스트 전용이다 — 운영 호출에서 설정 금지
  • 600s harness limit 을 넘어가는 리뷰는 기존대로 prompt 쪼개기 (§4.3)

Wrapper 경로 — bash "${CLAUDE_PLUGIN_ROOT:-$PWD}/scripts/rein-codex-review.sh" [--non-interactive] < /tmp/<prompt>.txt:

  • wrapper 는 stdin 으로 prompt 를 받는다. 항상 file redirect (< /tmp/<file>) 사용 — pipe (printf '...' | bash wrapper.sh) 는 pre-bash-safety-guard 가 차단 (stdin 명령 source 검증 불가)
  • prompt 작성: Write 도구로 /tmp/codex-prompt.txt 등에 작성 후 redirect
  • 출력은 wrapper 가 PASS 시 v2 발급 + stdout 요약을 동기 반환. > /tmp/<output>.log 2>&1 형태로 직접 파일에 쓰고 Read 로 확인. | tail -N 추가 pipe 는 불필요
  • v2 발급은 foreground 동기 실행 가정 위에서 동작. background 전환 시 발급이 누락된 채 프로세스만 죽을 수 있음 — legacy stamp 안전망이 없으므로(③-d), 이 경우 "리뷰는 됐는데 기록이 없다"가 곧 "리뷰가 안 된 것"과 동일하게 취급된다 — 재실행 필요

Direct codex exec 경로 (§2 "Legacy interactive model selection" 의 수동 조립):

  • stdin < /dev/null 로 명시 close
  • 출력은 > <file> 2>&1 로 직접 파일에 쓰고 Read 로 읽기. | tail -N 금지 (EOF 까지 버퍼링)
  • codex 실행 후 v2 발급은 수동으로 caller 가 bin/rein issue-evidence code_review --verdict <V> --reviewed-digest <D> 를 직접 호출한다 (review_round 는 §10 카운터가 담당하며 발급 인자에 포함되지 않는다)

Hang 감지 + 복구: §8 Error Handling 참고.


3. Escalation 규칙

리뷰 결과 severity + 수정 규모에 따라 다음 단계를 결정한다:

리뷰 결과 수정 규모 다음 행동
High 이슈 있음 무관 수정 후 codex 재리뷰 (Round 증가)
Medium만 있음 > 3줄 수정 후 codex 재리뷰 (Round 증가)
Medium만 있음 ≤ 3줄 수정 후 sonnet 셀프리뷰
Low만 있음 무관 수정 후 sonnet 셀프리뷰
이슈 없음 — 통과 — v2 code_review 증거 발급 (§5)
3회차에도 High — 사람에게 에스컬레이션 — 기록 없음 (PASS 가 아니므로 발급 대상 아님)

문서 지적의 차단 기준 (래퍼가 두 봉투 — 문서 봉투의 판정 규율 (iv), 코드 봉투의 Claim Audit — 에 같은 문장으로 방출한다): 수정 필요(NEEDS-FIX)로 이어지는 지적은 결정·범위·사실의 오류여야 하고, 무엇을 잘못 전달하는지와 그 근거가 지적에 있어야 한다. 같은 뜻의 표현 선택·배치·명칭 취향은 "참고사항" 으로 분리되며 verdict 를 승격시키지 않는다 — 참고사항은 위 표의 다음 행동을 결정하지 않는다. 실질 결함(잘못된 사용법, 영향 범위 과장, 필수 보안·안전 제약의 누락 — 인증·TLS 검증·안전한 역직렬화 등은 범위의 오류이며 "구현 단계 이관" 참고사항으로 돌리지 않는다 —, 완료 주장, 수치 불일치, 누락된 추적 연결)은 계속 차단이다. Scope ID 의 측정 가능성도 문자열 형식이 아니라 Scope Items 표의 설명·검증 방법까지 읽어 판정한다 — 세 요소(대상·방향·상황)가 설명에 있으면 문자열만으로 플래그하지 않고, 설명에도 없을 때만 Medium 이다.

sonnet 셀프리뷰: 변경 diff 를 직접 확인하고, PASS 판정이면 §5 발급을 그대로 수행한다 (발급 자체에는 reviewer 구분 필드가 없다 — 사람 보고 문구에서만 "self-review" 로 구분한다).

Round 관리: 회차는 래퍼가 사이클 단위로 계수하며 상한을 코드로 강제한다 (§10). 같은 사이클 내 재리뷰도 래퍼는 새 세션으로 실행한다 — 이전 검토의 맥락은 호출자가 후속 회차 요청서 양식(§2)으로 전달한다 (§7).

2026-08-04 정정: 이전 판은 "stamp 의 review_round 필드에 회차를 기록한다" 고 서술했으나 그 필드는 존재한 적이 없다. 래퍼에 회차 개념 자체가 없었고, 상한은 산문 지시로만 존재해 두 번 무력화됐다 (13회차 무보고 연속 실행 / 종결 지시 후 5회차 폭주). §10 이 이를 코드 계약으로 대체한다.


4. Sonnet Fallback

Codex 실패 (에러/타임아웃) 시 Sonnet 기반 대체 리뷰 경로:

  1. Codex 리뷰 실행 — §2 절차로 codex exec
  2. 성공 시 — §5 v2 발급 후 종료
  3. 실패 시 (에러/타임아웃) — sonnet 폴백 리뷰 실행
    • code-reviewer 스킬 호출, 또는
    • general-purpose 에이전트 (model: sonnet) 로 이관
  4. 폴백 성공 시 — §5 v2 발급을 그대로 수행하고, 사람 보고 문구에서 "sonnet 폴백, 사유: codex_timeout" (또는 해당 에러 코드) 처럼 밝힌 뒤 종료 (발급 인자 자체에는 fallback 사유 필드가 없다)
  5. 폴백도 실패 시 — 작업 중단, 사용자에게 보고

중요: "codex 리뷰 결과가 부족해서" 등은 폴백 사유가 아니다. 에러/타임아웃 같은 실행 실패 만 폴백을 허용한다.

모델 설정 에러(종료 코드 3)는 폴백 대상이 아니다. 래퍼가 exit 3 으로 종료하면 codex 가 모델명을 거부한 것(upstream rename 등)이다. sonnet 폴백으로 넘기면 잘못된 모델 설정이 가려지므로, 폴백하지 말고 plugins/rein-core/config/codex-models.sh 의 CODE_GATE_MODEL 을 codex 의 최신 모델명으로 갱신하도록 사용자에게 안내한다. 래퍼가 경로와 현재 값을 stderr 로 출력한다.

review-readiness 거부(종료 코드 4 + 거부 진단행)도 폴백 대상이 아니다. codex 실행 실패가 아니라 요청서 결함이므로, sonnet 폴백으로 새면 요청서 결함이 가려진다 (exit 3 과 동일한 비대상 원리). 준비도 거부는 codex spawn 이전에만 발생하며 (비용 0), v2 발급을 시도하지 않고 존속하는 .spec-reviews/* 도 건드리지 않는다. 호출자는 stderr 안내대로 요청서를 수정한 뒤 재호출한다 — 문법은 §4.1, 판별 계약과 호출자 행동은 §4.2.

4.1 증거 블록 문법 ([EVIDENCE])

리뷰 요청서(래퍼 stdin 의 PROMPT_BODY)에서 정량/PASS 주장의 재현 증거는 다음 고정 형식으로 선언한다. 래퍼의 review-readiness 사전검사가 codex 호출 전에 형식을 결정론적으로 검증한다 (code-review 모드 한정 — spec-review 모드는 비대상, §6.7):

[EVIDENCE]
claim: 테스트 21건 GREEN
command: bash tests/skills/run-all.sh
exit_code: 0
output:
...(생략)...
ok 21 - test-codex-model-profile-routing
21 passed, 0 failed
[/EVIDENCE]

문법 규칙 (결정론적 — 위반 = exit 4):

  1. fence 상태 전이 (단일 좌→우 스캔): fence 토글( )은 증거 블록 밖에서만 유효하다 — 블록 밖에서 fence 열림 상태의 [EVIDENCE]/[/EVIDENCE] 단독 라인은 블록 경계로도 형식 위반으로도 취급하지 않는다 (문법 예시·diff 인용 보호). 반대로 **증거 블록 안(output 영역 포함)의 는 일반 출력 텍스트**이며 fence 상태를 토글하지 않는다 — output 에 미폐쇄 가 있어도 뒤따르는 실제 [/EVIDENCE] 가 정상 폐쇄로 인식된다. 정량/PASS 패턴 스캐너는 이 스캔이 남긴 같은 마스킹 결과를 공유한다 (마스킹 1회). 인라인 백틱은 라인 단독 마커가 될 수 없으므로 블록 파서에는 무관 (패턴 스캐너에만 적용).
  2. [EVIDENCE] / [/EVIDENCE] 는 각각 한 줄에 단독으로 위치한다 (선행 공백 허용).
  3. 블록 내부 필드는 고정 순서 claim: → command: → exit_code: → output: 4종, 각 정확히 1회. 순서 위반·누락·중복 = 형식 위반.
  4. claim: = 이 블록이 뒷받침하는 자연어 주장 한 줄 (비어있으면 위반). command: = 재현 명령 한 줄 (비어있으면 위반).
  5. exit_code: = 0–255 정수. 그 외 값 = 위반.
  6. output: 라인 다음 줄부터 [/EVIDENCE] 직전까지가 출력 원문이다 — 어떤 escaping 도 불필요 (여러 줄·특수문자 그대로). 단 출력 영역 안에 [EVIDENCE] 단독 라인이 나타나면 중첩 금지 위반. 출력 0줄 허용 (명령이 무출력인 경우) — output: 라인 자체는 필수.
  7. 출력 상한: 블록당 output 60줄 이하 그리고 8000바이트 이하. 바이트 정의: output: 다음 줄부터 [/EVIDENCE] 직전 줄까지 output 영역의 UTF-8 원시 바이트 수 — 각 줄의 LF 개행 바이트를 포함하고, 문자 수가 아니라 바이트 수다 (한글 등 다중바이트 문자는 인코딩된 바이트만큼 계수). 경계 포함: 정확히 60줄·정확히 8000바이트는 통과, 61줄 또는 8001바이트부터 위반. 초과 = 형식 위반 (래퍼는 절단·변조하지 않는다 — 작성자가 관련 발췌로 줄인다. stderr 안내: tail/grep 발췌 권장).
  8. 블록 수 상한: 요청서당 유효 블록 16개 이하. 초과 = 형식 위반 (envelope 크기 보호).
  9. 다중 주장 = 다중 블록 (주장 1 : 블록 1). 하나의 명령 출력이 여러 주장을 뒷받침하면 블록을 주장별로 나누고 output 발췌를 각각 최소화한다 (규약 — 래퍼는 주장:블록 대응의 의미를 검증하지 않는다).

검증 깊이 (확정): 형식만. 래퍼는 command: 를 재실행하지 않고, output 과 exit_code 의 진위를 확인하지 않는다. 형식은 갖췄지만 거짓인 증거는 기존대로 codex 가 잡는다 — 유효 블록은 PROMPT_BODY 원문 위치에 그대로 보존되고, envelope 의 evidence_manifest: 슬롯(블록별 claim/command/exit_code 요약)과 Claim Audit sub-item 7 로 codex 에 구조화 전달된다.

코드 리뷰 모드에서 요구되는 자가검증 두 축([axis:typecheck]·[axis:test] 블록 + diff_self_review:)과 예외 선언은 §2 "자가검증 증거 — 두 축 계약", 후속 회차의 테스트 증거 범위는 §2 "델타 증거" 를 본다.

블록 밖 잔존 정량 매칭 — 두 tier 처분 (reject-tier / advisory-tier): 형태 3종(Q1 수량+단위 / Q2 비율·백분율 / Q3 검증명사+통과어 공존)과 문맥어 2종(C = 계약 문맥어: 최소/최대/상한/하한/임계/목표/허용/오차/이내/이하/이상/threshold/limit/tolerance/target/quorum/budget/±, R = 결과 서술어: 발견/실패/오류/누락/남음/잔존/감지/검출/재현/fail·found·detected·remaining·flagged·missing·reproduced)를 각각 독립 변수로 평가한다 — 한 라인이 여러 형태에 동시에 매칭돼도 어느 하나가 reject 조건이면 reject-tier 다.

  • reject-tier (증거 블록이 있어도 [readiness-reject] 로 exit 4 거부): Q3(예: "테스트 통과 확인", "build passed") · Q2 중 C 가 없거나 R 이 있는 것(예: "커버리지 85%", "상한 20% 초과로 3/5 실패") · Q1 중 R 이 있고 C 가 없는 것(예: "실패 3건 발견", "2 files flagged").
  • advisory-tier (WARNING: [codex-review][readiness-advisory] 비차단 경고 + envelope unbacked_quant_flags: 슬롯): Q1 중 R 이 없거나 C 가 있는 것(예: "함수 50줄 이내", "최대 재시도 3회", "실패 상한 3회") · Q2 중 C 가 있고 R 이 없는 것(예: "오차 상한 ±20%", "완화 목표 2%", "quorum 2/3 이상").
  • 동률 규칙: C 와 R 이 한 라인에 같이 있으면 형태의 기본 처분을 유지한다 — Q2(비율·백분율)는 reject 기본, Q1(수량+단위)은 advisory 기본. 문맥어는 신호가 한쪽뿐일 때만 기본 처분을 뒤집는다.

4.2 exit code 4 / 5 / 6 — 판별 계약 + 호출자 행동 (준비도 거부 / 래퍼 timeout / 회차 예산 소진)

호출자 행동 표:

exit 의미 호출자 행동
4 (+거부 진단행 — ERROR: [codex-review][readiness-reject] 로 시작하는 stderr 라인) review-readiness 거부 — 블록 0개 + 정량/PASS 매칭, 또는 블록 ≥1 이어도 블록 밖 reject-tier 매칭(§4.1) — codex 미호출 (비용 0) 프로젝트 사전 검사(.rein/review-precheck.sh, §2 "리뷰 전 프로젝트 사전 검사")의 실패·시간 초과·실행 불가·거부도 같은 접두로 이 행에 속한다. stderr 안내대로 요청서를 수정(증거 블록 추가, 계약 형태로 고쳐 쓰기, 또는 형식 교정)해 재호출. Sonnet fallback 비대상 — codex 실행 실패가 아니라 요청서 결함이므로 fallback 으로 새면 결함이 가려진다 (exit 3 과 동일한 비대상 원리). 재리뷰 카운트(§3 escalation)에 포함하지 않는다 — 리뷰가 수행되지 않았다.
4 (거부 진단행 0 — advisory 경고·발췌 내용 무관) codex 실행 실패 passthrough (드묾 — CODEX_RC passthrough 와의 이론적 겹침) 기존 실행 실패 경로와 동일 — Sonnet fallback 후보 (§4 본문).
5 (+진단행 — ERROR: [codex-review][review-timeout] 로 시작하는 stderr 라인 ≥1) 래퍼 소유 timeout (정지 판정 종료 — verdict 없음, v2 발급 미시도) 즉시 대체 리뷰 (Sonnet fallback, 사유 codex_timeout 재사용). 재시도·재호출 없음. 재리뷰 카운트(§3 escalation) 비포함 — 리뷰가 완료되지 않았다.
5 (진단행 0) codex 자체 exit 5 passthrough 기존 실행 실패 처리 (Sonnet fallback 후보) 그대로.
6 (+진단행 — ERROR: [codex-review][round-budget-exceeded]) 이 사이클의 리뷰 회차 예산 소진 — codex 미호출 (비용 0), v2 발급 미시도 (§10) 재호출 금지. 지금까지의 지적과 남은 쟁점을 요약해 사람에게 넘긴다. Sonnet fallback 비대상 — codex 실행 실패가 아니라 예산 소진이며, 폴백으로 새면 상한이 무의미해진다. 예산이 더 필요하다고 판단하면 사람의 승인을 받은 뒤에만 [MAX_ROUNDS:<n>] 을 선언해 재호출한다 (선언은 기록에 남는다).

호출자 판별 계약 (기계 계약 — 사람 판독 아님):

거부 진단행    := 정확히 "ERROR: [codex-review][readiness-reject]" 로 시작하는 stderr 라인 (라인 시작 anchored — substring 검색 금지)
준비도 거부    := exit == 4 AND 거부 진단행 ≥ 1
codex 실행실패 := exit == 4 AND 거부 진단행 0  → 기존 실행 실패 처리 (Sonnet fallback 후보) 그대로

앵커행        := 정확히 "ERROR: [codex-review][review-timeout]" 로 시작하는 stderr 라인 (라인 시작 anchored — substring 검색 금지)
래퍼 timeout  := exit == 5 AND 앵커행 ≥ 1  → 즉시 대체 리뷰 (Sonnet fallback, fallback_reason: codex_timeout). 재시도·재호출 없음. 재리뷰 카운트(§3 escalation) 비포함 — 리뷰가 완료되지 않았다
codex passthrough := exit == 5 AND 앵커행 0 → 기존 실행 실패 처리 (Sonnet fallback 후보) 그대로

예산 진단행    := 정확히 "ERROR: [codex-review][round-budget-exceeded]" 로 시작하는 stderr 라인 (라인 시작 anchored — substring 검색 금지)
예산 소진      := exit == 6 AND 예산 진단행 ≥ 1  → 재호출 금지, 사람에게 핸드오프. Sonnet fallback 비대상. 리뷰가 수행되지 않았으므로 회차도 소모되지 않는다
codex passthrough := exit == 6 AND 예산 진단행 0 → 기존 실행 실패 처리 (Sonnet fallback 후보) 그대로

카운터 진단행  := 정확히 "ERROR: [codex-review][round-budget-unavailable]" 로 시작하는 stderr 라인 (라인 시작 anchored)
카운터 불가    := exit == 7 AND 카운터 진단행 ≥ 1  → **예산 소진이 아니다.** 카운터 디렉토리 쓰기 불가 또는 count 손상으로 예산을 강제할 수 없는 상태다. 사람 핸드오프가 아니라 **인프라 복구**(권한 수정 / 손상 파일 제거) 후 재호출. Sonnet fallback 비대상 — 예산을 강제할 수 없는 채로 리뷰를 통과시키면 안 된다

substring 이 아니라 라인 시작 anchored 접두사로 판별하는 이유: 발췌·기존 진단 등 다른 stderr 내용에 예약 문자열이 섞여도(래퍼의 발췌 소독 — 발췌 내 예약 태그를 [readiness-…] 로 치환 — 이 1차 차단, 접두사 anchoring 이 2차 방어) 판별이 오염되지 않는다. advisory 는 별도 접두사 WARNING: [codex-review][readiness-advisory] (비차단) 이므로, advisory 경고가 방출된 뒤 codex 자신이 exit 4 를 반환하는 조합에서도 stderr 에 거부 진단행이 없어 호출자가 passthrough 로 정확히 분류한다.

exit 5 앵커도 동일 원리로 라인 시작 anchored — substring 검색 금지: 래퍼가 스풀 내용을 방출하는 모든 경로에서 방출 전에 라인 시작의 예약 접두사 ERROR: [codex-review][review-timeout] 을 ERROR: [codex-review][review-…] 로 소독하므로, 결합 로그(> log 2>&1)에서 라인 앵커 매치는 래퍼 소유 stderr 라인뿐이다 (codex 자체 출력의 리터럴 오염으로 인한 오분류 배제).

4.3 하니스 600s 상호작용 (워치독 생존 계약)

  • 워치독은 래퍼 프로세스 내부 메커니즘 — 하니스(Claude Code Bash)가 래퍼 프로세스를 살려두는 동안만 유효하다. 하니스가 Bash timeout 으로 래퍼를 죽이거나 process group 을 정리하면 워치독 생존은 비보장 — 이 경우 exit 5 계약은 발화하지 않고, 호출자는 하니스 timeout 을 기존 실행 실패로 처리한다 (§4 본문).
  • 권장 호출 패턴: run_in_background: false + Bash timeout: 600000(max) + 출력 직접 파일 리다이렉트 (> /tmp/<output>.log 2>&1 — background-jobs.md 형식) 유지.
  • 절대 상한이 없으므로 정당한 장기 리뷰는 600s 를 넘을 수 있다. 600s 를 상회하는 리뷰가 하니스에 잘리는 것은 기존 "600s 한계 넘으면 prompt 쪼개기" 규약의 영역 — 워치독이 해소하는 것은 "행 방치" 이지 "하니스 상한" 이 아니다.

5. 리뷰 결과 기록 — v2 발급 (필수, PASS 회차 한정)

Phase 7 웨이브 3 ③-d (2026-08-24) 갱신. 이전에는 이 절이 trail/dod/.codex-reviewed legacy stamp 를 생성하는 절차였다. ③-d 로 legacy 리뷰 표식 3종(.codex-reviewed/.review-pending/.security-reviewed)의 write·read 경로가 전부 제거되면서, PASS 판정 회차에서 bin/rein issue-evidence code_review --verdict PASS --reviewed-digest <D> 를 호출하는 것이 유일한 기록 절차가 됐다. 이 호출이 없으면(또는 exit 0 이 아니면) pre-bash-commit-review-gate.sh(구 pre-bash-test-commit-gate.sh 후속, ③-c 삭제 완료)가 커밋을 차단한다 — 더 이상 대신 참조할 legacy stamp 가 없다.

Plan A Phase 6 이후 / ③-a~③-d 계승: scripts/rein-codex-review.sh wrapper 가 code-review mode PASS 시 이 발급을 자동 호출한다. Sonnet 셀프리뷰 / 수동 경로에서도 caller 가 동일한 발급 호출을 직접 수행한다.

5.1 발급 인자

v2 발급은 --verdict와 --reviewed-digest 두 인자만 받는다 (plugins/rein-core/rein/cli/issue_evidence.py::issue()). 과거 legacy stamp 가 담던 부가 정보(reviewer/cycle/scope/files_reviewed/fallback_reason/resolution/remaining_issues/실행 증빙 5필드 model/effort/effort_source/policy_version/codex_version)는 더 이상 durable 하게 저장되지 않는다 — 이 정보가 필요하면 §9 "사용자 안내" 문구에 그대로 담아 그 회차의 채팅 응답에만 남긴다. reviewed-digest 는 §2 "Mode 별 기록 규칙"이 서술한 대로 diff 범위(HEAD~1 체인 기반)로 매 실행 직접 산정한다.

  • reviewer 구분(codex / sonnet-fallback / self-review)은 §9 사용자 안내 문구로만 남는다.
  • review_round(같은 사이클 내 N번째 리뷰)는 발급 인자가 아니다 — 회차 계수·상한 강제는 언제나 §10 의 카운터가 전담한다 (legacy stamp 시절에도 래퍼 발급 stamp 에는 이 필드가 없었다).

5.2 정상 통과 (codex)

"${CLAUDE_PLUGIN_ROOT:-$PWD/plugins/rein-core}/bin/rein" issue-evidence code_review \
  --verdict PASS --reviewed-digest "$REIN_REVIEWED_DIGEST"

성공(exit 0) 시 사람 보고에는 "codex 리뷰 통과, 기록됨"을 담는다 (§9).

5.3 Sonnet Fallback

발급 호출 자체는 5.2 와 동일하다 — reviewer/fallback_reason 을 구분할 인자가 없으므로, §9 사용자 안내 문구에서 "codex 실행 실패로 sonnet 폴백 리뷰로 통과, 사유: codex_timeout(또는 해당 코드)" 를 명시한다.

5.4 Sonnet 셀프리뷰 (Low/경미한 Medium)

발급 호출은 5.2 와 동일하다. §9 사용자 안내 문구에서 "직전 codex 리뷰의 Low/Medium(≤3줄) 지적을 수정한 뒤 self-review 로 통과" 를 명시한다.

5.5 사람 에스컬레이션 (기록 없음)

3라운드 후에도 High 가 남아 있으면 발급을 시도하지 않는다 — PASS 가 아니므로 대상이 아니다. 잔존 이슈는 §9 사용자 안내 형식으로 사람에게 직접 보고한다.

5.6 발급 실패/거부 처리

발급이 exit 0 이 아니면(digest-mismatch 거부, 인프라 실패 등) 이 회차는 기록되지 않는다 — legacy stamp 안전망이 없으므로, code-reviewer SKILL(plugins/rein-core/skills/code-reviewer/SKILL.md)의 "PASS 시 v2 발급" 절과 동일한 fail-closed 판정을 따른다: digest-mismatch(또는 응답을 긍정적으로 그 외 사유로 파싱할 수 없는 경우)는 리뷰 대상 트리가 회차 도중 바뀌었다는 뜻이므로 digest 를 재캡처해 재리뷰해야 하고, 그 외 실패는 인프라 복구 후 재발급을 시도한다.


6. Non-interactive mode

자동 호출 (예: plan-writer agent 가 spec review 트리거) 시 사용. Prompt 에 marker 가 있으면 사용자 질문(AskUserQuestion) 을 skip 하고 default 로 codex exec 실행.

6.1 Invocation contract

prompt 본문 어디든 다음 marker 가 있으면 non-interactive mode 활성:

  • [NON_INTERACTIVE] — AskUserQuestion 호출 skip
  • [MODEL:<name>] — model override (기본값은 단일 출처 plugins/rein-core/config/codex-models.sh 의 CODE_GATE_MODEL — 게이트 고정 모델)
    • 허용 값: codex 가 지원하는 임의 모델명. 하드코딩 화이트리스트를 두지 않는다 (모델 rename 시 문서 수정 불필요).
  • [EFFORT:<level>] — reasoning effort override (마커 부재 시 래퍼가 변경 규모로 산출 + 위험도 floor 적용; 측정 실패 시 fail-closed 페어의 CODE_FAIL_CLOSED_EFFORT(=high))
    • 허용 값: low, medium, high
    • 거부 값 (각자 사유 + 산출 진입):
      • ultra — 게이트에서 명시 거부. ultra 는 자동 하위작업 위임(auto subagents)으로 동작해 게이트 판정이 비결정적이 되고 quota/timeout 충돌을 일으킨다. stderr 에 ultra 전용 사유 메시지 출력.
      • max / xhigh — timeout 실측 전 미지원 (후속 재검토 대상). stderr 에 각자 사유 메시지 출력.
      • 셋 다 거부 후에는 마커 없음과 동일하게 변경 규모 산출 경로로 진입하며(floor 포함), 마커는 strip 되어 codex 프롬프트/인자 어디로도 전달되지 않는다. 기타 무효값(low2 등)은 기존 generic warning 유지.
  • [SANDBOX:<mode>] — 래퍼는 이 마커를 읽지 않는다. 샌드박스는 항상 read-only 다 (§6.6).

6.2 Marker 처리 절차

  1. prompt 에서 marker 패턴 (\[NON_INTERACTIVE\], \[MODEL:[^\]]+\] 등) 추출
  2. 추출 후 prompt 본문에서 marker 제거 (codex exec 로 전달 안 함)
  3. 추출된 값 또는 default 로 codex exec 명령 조립
  4. 실행 후 결과 캡처

구현 상태 (v1.1.2~):

  • [EFFORT:<level>] — ✅ wrapper (scripts/rein-codex-review.sh) 에 구현됨. 유효 값은 low|medium|high. ultra/max/xhigh 는 각자 전용 사유의 stderr 메시지(ultra=자동위임 게이트 금지, max/xhigh=timeout 실측 전 미지원) 후 산출 진입, 기타 무효값은 기존 generic warning + 변경 규모 기반 산출 진입(_compute_effort). marker 는 유효성과 무관하게 codex 로 전달되는 prompt 에서 제거.
  • [NON_INTERACTIVE] — ✅ wrapper 의 --non-interactive CLI 플래그로 처리. marker 자체는 prompt 에 보존 (spec-review 모드 감지에 사용).
  • [MODEL:<name>] — ⏳ marker override 자체는 wrapper 미구현(legacy — prompt-controlled downgrade 차단 목적의 의도된 미구현). 단 기본 모델은 단일 출처 plugins/rein-core/config/codex-models.sh 의 CODE_GATE_MODEL 을 wrapper 가 -m 으로 항상 전달한다 (config 전 후보 부재 시 래퍼 내장 canonical gpt-5.6-sol+high 로 명시 폴백 — 무모델 호출 없음, §6.3). [SANDBOX:<mode>] — 의도된 미구현. prompt 에 있어도 래퍼는 무시하고 --sandbox read-only 를 전달한다 (§6.6).

6.3 래퍼 자동 effort 산출 기준

/codex-review 호출 시 래퍼(rein-codex-review.sh)가 변경 규모로 effort 를 결정론적으로 산출한다(마커 부재 시). caller 가 [EFFORT:<level>] marker 를 직접 prepend 하면 그 값이 산출을 오버라이드하고, 그 외에는 래퍼 산출이 적용된다. ~/.codex/config.toml 은 어느 경로에서도 건드리지 않는다 (사용자 환경 보존). 측정 실패/불가 시 fail-closed 모델+effort 페어(CODE_FAIL_CLOSED_MODEL+CODE_FAIL_CLOSED_EFFORT = gpt-5.6-sol+high)로 폴백 (effort_source: fail_closed).

코드 산출이 권위다. 아래 표는 산출 휴리스틱의 의도를 설명하며, Claude 본체/사용자의 수동 [EFFORT:] 는 산출을 오버라이드(상향/하향)하는 용도다.

조건 Effort 근거
docs-only, 3-10줄 diff, 기존 패턴 반복, markdown/comment 수정 low reasoning 최소. 2분 내 완료 → harness auto-background 방지.
단일 모듈 코드 변경 <100줄, hook 수정 1-2개, 아키텍처 변화 없음, 로컬 로직 추가 medium 표준 리뷰. 대부분 2-3분 내 완료.
다중 파일 + 보안 표면 / 새 데이터 흐름 / 아키텍처 변경 / 복잡한 상태 관리 / 동시성 이슈 high 심층 reasoning 필요. 3분 초과로 background 전환 수용.

판단 경계:

  • 경계 케이스는 한 단계 낮춰서 시작 → 리뷰 결과가 "더 깊은 분석 필요" 면 재리뷰 round 에서 high 로 승급.
  • security-reviewer 와 codex-review 는 별개 agent. codex-review 의 effort 는 보안 review 와 무관.
  • 사용자가 prompt 에 명시적 [EFFORT:...] 를 붙이면 Claude 의 자동 판단을 오버라이드. Claude 는 이를 존중.
  • 재승급 불변식 (E5): 재리뷰의 high 승급은 회차 숫자가 아니라 남은 위험으로 정한다 — 직전 회차 보고에 High 가 있었거나(그 High 를 고쳤더라도 — 수정은 아직 독립 검토 전이다) 후속 회차가 §2 (b)·(c) 에 어긋나면(지적 처리를 넘는 새 인터페이스·보안 경계·범위 변경, 새 판단) caller 가 [EFFORT:high] 마커를 주입한다. §2 후속 회차 조건 (a)~(c) 를 모두 만족하면 조건부 기본값 [EFFORT:medium], 직전 보고가 필수 축을 수행하지 않았으면(직전 검토가 완결이 아님) 마커 없이 산출 강도(floor 포함)로 전체 검토한다. 마커는 산출보다 우선이므로 어떤 산출 결과도 재승급된 high 를 약화시키지 않는다.

위험도 floor (경로 기반 하한선):

변경 크기 ≠ 위험도(3줄 인증 우회 = 고위험, 500줄 생성 문서 = 저위험)의 보완으로, 산출된 effort 에만 path 기반 하한선을 적용한다:

  • 위험 경로 패턴 5종: hooks/** · security/** · config/** (임의 깊이 디렉토리 세그먼트 매칭), .github/workflows/** (repo 루트 앵커), scripts/rein-*.sh (scripts/ 세그먼트 하위).
  • 변경 파일 중 한 줄이라도 매칭 + 산출 effort 가 low 면 medium 으로 승격 (도장에 effort_source: computed+floor 기록).
  • low→medium 단방향만: medium/high 산출은 무변경 — floor 는 하한선이지 가산기가 아니다.
  • 마커 > floor: 유효 [EFFORT:] 마커가 있으면 산출·floor 블록 자체에 진입하지 않는다 — [EFFORT:low] + 위험 경로 = 최종 low. [EFFORT:high] 재승급도 마커 경로라 산출/floor 가 관찰하지 못한다 (E5 불변식 보존).
  • spec-review 모드 미적용: 문서 리뷰는 코드 경로가 아니므로 위험 경로 매칭 자체를 건너뛴다.
  • 측정 실패 폴백(fail_closed)은 이미 high 라 floor 와 구조적으로 상호 배타.

Canonical fallback (config 부재 시):

config(codex-models.sh) 의 전 후보가 부재해도 "빈 모델 → -m 생략 → codex 기본 모델 degrade" 경로는 없다(폐지됨). 래퍼 내장 canonical 상수 gpt-5.6-sol + high 로 명시 폴백하고 stderr 경고를 정확히 1회 출력한다(config 정상 로드 시 무발화). codex 호출에 -m 은 항상 전달된다(무모델 호출 0건 — 게이트가 "codex 기본 모델" 이라는 외부 가변값에 좌우되지 않도록). 이 경로로 실행되면 도장의 policy_version 이 0 으로 기록된다.

재리뷰 일관성 (같은 사이클 = 같은 모델):

같은 리뷰 사이클 내 재리뷰는 모델을 바꾸지 않는다 — effort 는 §2 의 후속 회차 규칙과 이 절의 재승급 규칙을 따른다. 게이트 모델이 CODE_GATE_MODEL 단일 고정이므로 이 일관성은 구조적으로 보장된다 — 규모/회차별 모델 혼용은 결함 탐지 성향·severity 보정·PASS 기준이 달라져 게이트 판정 비일관을 만들므로 하지 않는다.

6.4 사용 예 (agent 내부 자동 호출)

입력 prompt:

[NON_INTERACTIVE] spec review for plan: docs/plans/2026-04-20-v011-workflow-hardening.md
Validate scope coverage and implementation feasibility.

→ skill 처리 후 실제 codex exec:

# 모델은 래퍼가 단일 출처(codex-models.sh)의 CODE_GATE_MODEL 을 -m 으로 전달.
codex exec -m "$CODE_GATE_MODEL" --sandbox read-only --config model_reasoning_effort="high" \
  --skip-git-repo-check "spec review for plan: ..."

6.5 사용자 직접 호출과의 차이

[NON_INTERACTIVE] marker 없이 /codex-review 호출 시 기존 interactive 경로 (§2 "Running a Task" 의 AskUserQuestion 절차) 따름. 자동 mode 는 agent 내부 호출 전용.

6.6 보안 노트

래퍼는 codex 에 --sandbox read-only 를 항상 넘긴다(코드·문서 리뷰 공통). 생략하면 codex 가 사용자 ~/.codex/config.toml 의 sandbox_mode 를 상속하는데, 그 값이 danger-full-access 이면 리뷰어 명령이 저장소 밖 파일을 쓰고 네트워크에 접근할 수 있게 된다. 프롬프트 마커([SANDBOX:…])·환경 변수로 바꾸는 경로는 없다 — 자동 호출 경로의 권한 상승을 막기 위해서다.

  • workspace-write 를 쓰지 않는 이유: 리뷰어 명령이 저장소 안의 게이트 제어 파일 — 통과 기록 저장소, 훅 정책, 리뷰 회차 카운터, 문서 리뷰 표식, .claude 설정 — 을 쓸 수 있게 된다. 이 경로들은 리뷰 대상 지문에 들어가지 않으므로, 리뷰 대상 내용에 심은 지시로 리뷰어가 게이트를 약화해도 감지되지 않는다.
  • 대가: 리뷰어는 임시 파일을 쓰는 테스트를 다시 돌리지 못한다(/tmp 도 쓸 수 없어 mktemp 가 실패한다). 리뷰어는 요청서의 증거 블록(§4.1)을 1차 증거로 대조한다 — 테스트 결과는 증거 블록으로 싣는다.
  • 리뷰어 셸 명령에는 네트워크가 없다. 다만 사용자 codex 설정에 등록된 MCP 서버는 이 샌드박스 밖에서 돌며 네트워크를 쓸 수 있다 — 샌드박스는 셸 명령만 묶는다.

6.7 기록 분리 — spec review vs code review (CRITICAL)

non-interactive mode 는 두 경로 로 쓰인다:

  • Code review 자동화 (향후) — agent 가 구현 완료 후 codex-review 를 호출하는 경우. 기존 §5 발급 규정 따름 → PASS 시 v2 code_review 증거 발급.
  • Spec review 자동화 (v1.0.0 plan-writer 경로) — agent 가 plan/design 을 검증받는 경우. code_review v2 증거를 절대 발급하지 않음. legacy marker 는 이미 존재하지 않으므로 건드릴 대상도 없음. 코드리뷰 gate 는 별개 절차.

Spec review 모드 감지: prompt 첫 줄이 [NON_INTERACTIVE] spec review for plan: 또는 [NON_INTERACTIVE] spec review for design: 형식으로 시작하면 spec-review 서브플로우로 분기.

spec-review 모드는 review-readiness 사전검사 대상이 아니다 — 사전검사·정량/PASS 휴리스틱·evidence_manifest: 슬롯 전부 skip (사전검사는 code-review 모드 한정, §2 요청서 작성 규약 / §4.1 참조). spec-review 지시문의 "PASS"·"coverage" 류 관행 토큰이 false-positive 로 자동 경로를 차단하지 않도록 하기 위함이다.

문서 리뷰의 판정 축 (2026-08-04): spec-review 모드는 코드 리뷰와 다른 슬롯 세트를 받는다. 래퍼가 결정 건전성 / 범위·추적성 / 주장 감사 3개 슬롯과 판정 규율 4종을 방출하며, 코드 결함·테스트 정합 슬롯은 방출하지 않는다.

  • 판정 축은 결정·범위·추적성 이다. 구현 수준 결함(오류 경로·자원 수명주기·버퍼링 방식·자료구조 선택)은 verdict 에 반영하지 않고 "구현 단계 이관" 참고사항으로 분류한다 — 문서는 실행할 수 없어 그런 주장을 검증할 수단이 없고, 코드 리뷰 게이트가 실물을 놓고 판정한다.
  • 같은 성격의 지적은 적용 대상 전체를 한 회차에 일괄 열거한다 (입력·대상별 분할 지적 금지).
  • 저장소에 없는 일반 규칙을 근거로 반려하지 않는다 — "저장소 계약 위반" 과 "일반론 참고사항" 을 분리 표기하며 후자는 verdict 를 승격시키지 않는다.
  • 수정 필요로 이어지는 지적은 결정·범위·사실의 오류여야 하고, 무엇을 잘못 전달하는지와 근거가 지적에 있어야 한다. 같은 뜻의 표현 선택·배치·명칭 취향은 "참고사항" 으로 분리해 verdict 를 승격시키지 않는다 — 실질 결함(잘못된 사용법·영향 범위·필수 보안·안전 제약의 누락(범위의 오류 — "구현 단계 이관" 참고사항이 아니다)·완료 주장·수치 불일치·누락된 추적 연결)은 계속 차단한다. 코드 봉투의 Claim Audit 도 문서 주장에 같은 문장을 쓴다 (§3).

배경: 이전 판에는 모드 분기가 없어 문서 리뷰에도 코드 리뷰 슬롯이 그대로 나갔다. 즉 계획서를 놓고 "resource leak 을 찾아라" 라고 지시한 셈이고, 리뷰어가 파일 핸들 수명주기를 소송한 것은 지시대로 수행한 결과였다. 그 지적에 대응하려면 구현 세부를 문서 본문에 쌓아야 해서 리뷰 표면이 자가증식했다 (실측: 설계 리뷰 41회·계획 리뷰 26회, 구현 코드 0줄).

Spec review 서브플로우 동작:

  1. codex exec 실행 → verdict 캡처 (PASS / NEEDS-FIX / REJECT)
  2. code_review v2 증거를 발급하지 않음 (코드리뷰 게이트 오염 방지)
  3. legacy marker 는 이미 존재하지 않으므로 건드릴 대상도 없음
  4. PASS 시 caller (plan-writer 등) 가 책임지고 bash "${CLAUDE_PLUGIN_ROOT:-$PWD}/scripts/rein-mark-spec-reviewed.sh" <path> <reviewer> 호출 — 이는 spec-review 전용 stamp (trail/dod/.spec-reviews/<hash>.reviewed) 생성 (spec-review 축은 이번 웨이브의 존속 예외)
  5. NEEDS-FIX/REJECT 시 caller 가 handoff (spec-review stamp 생성 안 함)

왜 분리?: code_review v2 증거는 pre-bash-commit-review-gate.sh(구 pre-bash-test-commit-gate.sh 후속, ③-c 삭제 완료) 의 코드 commit gate 축 전용. spec review 에서 이 증거가 발급되면 코드 변경 없이도 gate 통과 가능해져 rein 규율이 깨짐.

Sonnet fallback 분기도 동일: spec review 모드에서 codex 실패로 code-reviewer fallback 이 호출되더라도 code_review v2 증거 발급 금지. code-reviewer skill 이 spec-review context 를 인식할 수 있도록 prompt 전달 경로에서 [NON_INTERACTIVE] spec review prefix 보존.


7. Resume 정책

래퍼는 세션을 이어가지 않는다. scripts/rein-codex-review.sh 는 초회든 재리뷰든 매번 새 codex exec 세션을 띄운다 — 이전 회차의 대화는 다음 회차에 전달되지 않는다. 같은 사이클 재리뷰에서 이전 검토의 맥락을 살리는 수단은 세션 재사용이 아니라 후속 회차 요청서 양식(§2) 이다.

수동으로 codex exec resume --last 를 쓰는 것은 권하지 않는다 — 래퍼를 거치지 않으면 회차 계수(§10)·이벤트 기록·증거 발급(§5)이 모두 빠진다.

사이클 경계: 새 DoD, 새 PR, 새 기능 개발로 전환되면 후속 회차 양식을 쓰지 않고 초회 전체 검토로 시작한다. Second opinion 용도로는 절대 이 스킬이 아니라 /codex-ask 를 사용한다 (v2 증거 미발급 + resume 금지).


8. Error Handling

  • codex --version 또는 codex exec 가 non-zero exit → 즉시 중단 + 보고. 재시도 전 사용자 지시 요청.
  • 고위험 플래그 (--full-auto, --sandbox danger-full-access, --skip-git-repo-check) 사용 전 AskUserQuestion 으로 허가 획득 (이미 허가되지 않은 경우).
  • 경고/부분 결과가 포함된 출력은 요약 후 AskUserQuestion 으로 조정 방향을 확인.
  • Codex 실행 실패 → §4 Sonnet fallback 경로. 실패 사유는 §9 사용자 안내 문구에 담는다 (더 이상 저장할 stamp 필드가 없다).
  • Hang 증상 탐지 (§2 실행 모드 절 참고):
    • codex 프로세스가 수 분 이상 진행 없음 + CPU 0 + 네트워크 연결 0개 → Bash 도구 auto-background 전환으로 stdin 이 unix socket 에 붙어 sandbox/auth 초기화가 멈춤
    • 체크: lsof -p <pid> -i 결과 비어있으면 API 요청 미도달. ps -o stat,%cpu,etime -p <pid> 로 Sleep + 0% CPU 확인
    • 대응: kill 후 foreground + stdin close + 직접 파일 출력 형태로 재호출 (wrapper 경로 권장)
    • 기록 상태 정리 (Phase 7 웨이브 3 ③-d 갱신): legacy stamp 가 없으므로 "hang 이 발생한 사이클이 절반만 기록된 상태로 남는" staleness 자체가 구조적으로 사라졌다 — v2 발급은 원자적 all-or-nothing(호출 자체가 성공/실패)이다. 재호출 전에는 그 회차가 v2 발급까지 도달했는지(사람 보고 문구 또는 §9 결과)만 확인하면 된다.

9. 사용자 안내

이 SKILL 의 결과를 사용자에게 보고할 때 (호출자 = Claude 가 wrapper output 을 user-facing chat 으로 emit 할 때) 다음 짧은 형식을 먼저 출력한다. wrapper 자체는 raw codex output 을 그대로 emit 하며, 안내 prepend 는 호출자 책임 (호출 환경이 다양하므로 wrapper 강제 안 함). 형식은 한 문장 또는 두 문장 — 결과 1줄 + 다음 액션 1줄.

Round PASS (verdict=PASS):

코드 리뷰 통과. [차단급 결함 없음 또는 Low advisory N건]. 다음은 [보안 리뷰 또는 다음 단계].

Round NEEDS-FIX (verdict=NEEDS-FIX):

리뷰에서 N건 수정이 필요해요 — [Severity 요약, 예: "Medium printf 형식 + Low 테스트 stderr 미검증"]. [고치고 재리뷰 또는 sonnet 셀프리뷰 — escalation 규칙 §3 따라].

Sonnet fallback 통과 (§4 — codex 실행 실패 한정. §3 의 sonnet self-review 와 다른 path):

codex 가 떠지지 않아 sonnet 폴백 리뷰로 통과시켰습니다 (사유: <코드>). 통과 증거가 기록됐어요.

3회차에도 High 잔존 → 사람 에스컬레이션:

3회차 리뷰에도 High N건이 남아 사람 에스컬레이션이 필요해요. 이 회차는 기록되지 않았습니다 — 잔존 이슈를 직접 처리한 뒤 다시 리뷰해 주세요.

PASS 이지만 기록 실패 (v2 발급이 exit 0 이 아님 — §5.6):

리뷰 자체는 통과했지만 이번 회차는 기록되지 않았어요 — [사유]. digest 재캡처 후 재리뷰가 필요합니다.

이 짧은 안내 후에 기존 codex 본문 (Code Defects / Design Alignment / Test Alignment / Claim Audit / FINAL_VERDICT: <X>) 를 그대로 emit 한다.


10. 회차 예산 (round budget)

리뷰 회차 상한은 래퍼가 강제한다. 산문 지시가 아니라 코드 계약이다.

항목 계약
사이클 키 문서 리뷰 = 리뷰 대상 문서의 정규화된 절대 경로 / 코드 리뷰 = 활성 작업 기준서 slug. 키가 다르면 예산은 독립. 정규화하지 않으면 같은 문서를 docs/x.md / ./docs/x.md / 절대 경로로 번갈아 불러 각각 새 예산을 받는 우회가 생긴다
기본 상한 5회차 (§3 의 "3회차 High 잔존 → 사람 에스컬레이션" 에 2회 여유)
계수 대상 미통과 회차만. 통과(PASS)는 사이클 종료이므로 카운터를 제거한다
계수 시점 verdict 가 확정된 뒤 1회. 상한 판정만 spawn 이전에 한다. 리뷰가 수행되지 않은 종료 — 준비도 거부(4)·모델 오류(3)·워치독 정지(5) — 는 예산을 소모하지 않는다 (§4.2 의 "재리뷰 카운트 비포함" 과 정합)
초과 시 codex 를 실행하지 않고 exit 6 + 앵커 진단. v2 발급 미시도
연장 프롬프트 첫 줄이 [MAX_ROUNDS:5] 하나뿐일 때만 선언으로 인정한다 (정수 1~999). 기본 상한을 넘기는 선언은 stderr 경고 + 카운터 이력에 기록된다 — 조용한 연장 경로는 없다
마커 오인 방지 본문 어디에 있는 마커 문자열도 선언으로 해석하지 않고 원문 그대로 보존한다. 위치 제한이 없던 이전 판에서는 이전 리뷰 출력이나 문서 예시를 인용하기만 해도, "사용자는 [MAX_ROUNDS:8] 을 승인하지 않았다" 같은 문장으로도 상한이 올라갔다 — 사람 승인 없이 예산이 늘어나는 경로였다. 첫 줄 단독이지만 정수가 아닌 표기도 선언이 아니며 본문이 유지된다
상태 파일 trail/dod/.review-rounds/<키 해시> (key / count / limit / updated / extensions). 기록은 키별 락 안에서 파일을 다시 읽어 증가시키고 임시파일+교체로 원자화한다
카운터 불가 디렉토리 쓰기 불가·count 손상은 예산 소진이 아니다 — 별도 앵커 + exit 7. 손상 시 0 으로 되돌리지 않는다 (되돌리면 카운터를 훼손하는 것만으로 예산이 무한 리셋된다)
동시성 한계 판정과 기록이 codex 실행 시간만큼 떨어져 있어, 상한 직전 동시 진입 k 건은 모두 통과할 수 있다 (최대 k-1 회 초과, k 에 상한 없음). 초과분이 기록된 뒤의 호출은 정상 거부된다. 엄격히 막으려면 만료 가능한 진행 중 예약이 필요한데, 만료를 짧게 잡으면 정상 리뷰를 죽이고 길게 잡으면 강제 종료된 리뷰가 회차를 문다 — 유실 방지를 택하고 한계를 남긴 사용자 결정 (2026-08-04)
연장 이력의 수명 연장 선언은 선언 즉시 stderr 경고로 드러나고, 카운터가 살아있는 동안 그 파일의 이력에 남는다. 사이클이 통과로 끝나면 카운터와 함께 이력도 정리된다 — 세션을 넘는 영구 감사 기록이 아니다
위협 모델 이 예산은 정직한 에이전트의 규율 장치다. 카운터 파일을 지우면 0회차로 돌아가고, 그 디렉토리를 지키는 훅은 없다. 대체 리뷰어 경로가 예산을 확인하지 않는 것도 문서 규정으로만 막혀 있다. 적대적 우회 하드닝은 범위 밖이며, 막는 대상은 "고치다 보니 무한히 재리뷰하게 되는" 발산이다. 다만 의도치 않은 파일 파괴(카운터 파일 또는 그 디렉토리가 심볼릭 링크인 경우)는 거부한다 — 판정 초입에서 검사해 mkdir·읽기 이전에 exit 7 로 중단하고, 미통과 기록은 락 안에서, 통과 정리는 rm 직전에 각각 재검사한다 (통과 정리 실패도 표식 없이 exit 7)

호출자(에이전트) 규약: exit 6 을 받으면 재호출하지 않는다. 지금까지의 지적과 남은 쟁점을 요약해 사람에게 넘긴다. 연장이 필요하다는 판단은 사람의 승인 사항이며, 에이전트가 스스로 [MAX_ROUNDS:] 를 올려 재시도하는 것은 이 계약의 위반이다.

배경: 회차 상한이 산문 지시로만 존재하던 동안 두 번 무력화됐다 — 보고 없이 13회차 연속 실행, 그리고 사람의 종결 지시 이후 5회차 추가 실행(설계 문서 도장이 무효화되며 병렬 작업자 전원이 편집 차단됨). 산문으로 한 줄 더 쓰는 처방은 이미 두 번 반증됐으므로 채택하지 않았다.

04 同仓库更多 Skills