OCR delegation plus EGM second opinion (fresh-context subagents, no conversation history), gated at 0 CRITICAL/HIGH. Runs automatically right after build. 트리거: \"리뷰\", \"코드 검토\".
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Is that skill safe to install?
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
Are you the author of Review?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/snwlee-review)
---
name: review
description: "OCR delegation plus EGM second opinion (fresh-context subagents, no conversation history), gated at 0 CRITICAL/HIGH. Runs automatically right after build. 트리거: \"리뷰\", \"코드 검토\"."
---
# review
nereus:common 규칙을 따른다. 담당 에이전트: reviewer. 인증·입력 처리·외부 호출·파일시스템·DB 쿼리를 만진 변경이면 security 에이전트도 함께 투입한다.
## 0. 사전 게이트
`node "${CLAUDE_PLUGIN_ROOT}/skills/finish/scripts/gate.mjs"`를 먼저 돌린다. evidence가 FRESH·통과가 아니거나 완료 무결성 항목이 있으면 리뷰어를 부르지 않고 build로 돌려보낸다. 리뷰어 비용을 아끼고, 리뷰어가 스텁·TODO를 지적하느라 진짜 결함을 놓치는 일을 막는다.
## 1. 계획
```bash
node "${CLAUDE_PLUGIN_ROOT}/skills/review/scripts/review.mjs" [--size small|medium|large]
```
설정 `secondOpinion`과 설치 상태로 어떤 리뷰어가 돌지 나온다. `skipped`가 있으면 사용자에게 알린다(차단은 아님).
EGM 이 계획되면 출력에 `egmPersonas`(goldfish 페르소나 절대 경로)와 서브에이전트 별칭 `egmModel`·`refuterModel` 이 함께 나온다.
`--size` 는 `.nereus/intake.md` 1행 `계획:` 의 첫 값이다. 그 줄이 없거나 지금 작업의 것이라는 확신이 없으면 빼고 돌린다 — 출력의 `sizeAssumed` 가 true 면 "규모를 몰라 medium 으로 봤다" 한 줄을 알린다.
별칭을 쓸 수 없을 때(`Agent` 오류가 모델을 알 수 없음·사용 불가·허용 안 됨이라고 말할 때만):
- EGM: 별칭 순서(haiku < sonnet < opus < fable)에서 한 단계 아래 별칭을 명시해 한 번만 재시도한다. 하한은 `sonnet` 이다. 끝내 못 쓰면 EGM 을 `skipped` 로 알리고 게이트는 OCR 결과로 판정한다. large 에서 sonnet 으로 내려가 돌았으면 보고서의 그 리뷰어에 `degraded` 를 붙인다.
- 반박은 재시도하지 않는다. `refuterModel` 로 못 돌면 반박하지 않은 것으로 둔다(의도: 보안 지적은 닫힌 쪽으로 실패한다 — 약한 반박으로 차단을 풀지 않는다) — 지적은 차단으로 남기고 보고서에 `degraded` 를 붙인다. 풀려면 opus 를 쓸 수 있는 세션에서 반박을 다시 돌리거나, 사용자가 수용·기각하고 그 결정을 `.nereus/review.md` 에 기록한다.
**제3자 검증은 EGM 이 1순위이고 기본으로 그것만 돈다**(2026-11-12 방침). 외부 CLI 의 쿠키·할당량·신뢰 디렉터리 문제에 리뷰가 묶이지 않는다.
| 설정값 | 도는 리뷰어 |
|---|---|
| `"egm"` (기본) | OCR + EGM |
| `"codex"` | OCR + EGM + Codex — 다른 모델의 시각이 필요할 때만 명시한다 |
| `"none"` | OCR만 (2차 의견 없음) |
| `["ocr", "egm"]` | 배열로 직접 지정. 빈 배열이면 리뷰 자체를 건너뛴다 |
이전 단축형 값(`"both"` 등)과 배열 속 옛 리뷰어 이름은 `egm` 으로 읽는다 — 걸러내면 2차 의견이 조용히 사라진다.
Codex 는 **PATH 존재가 아니라 실제 응답**으로 판정한다(프로브). 신뢰되지 않은 디렉터리에서 부르면 거부하므로 저장소 루트에서 `--skip-git-repo-check` 로 부른다.
## 2. 병렬 실행
리뷰 대상은 `git diff <base>...HEAD` (base 기본 `main`, 미커밋 작업이면 워크스페이스).
> **OCR 에 범위를 반드시 넘긴다.** `ocr delegate` 를 인자 없이 부르면 워크스페이스(미커밋) 모드로
> 떨어져 **커밋된 변경이 리뷰 대상에서 통째로 빠진다**. `ocrDelegateArgs(base)` 가 인자를 만든다.
> `.md` 는 `unsupported_ext` 로 제외되므로, 문서만 바뀐 범위에서 `0 reviewable` 이 나오는 것은 정상이다.
- **OCR delegation**: `ocr delegate preview --from <base> --to HEAD` 로 리뷰할 파일과 룰을 받고, 파일별 `ocr delegate rule <files>`로 룰을 받아 **이 세션의 모델이 직접** 리뷰한다. OCR이 API 키로 직접 리뷰하게 설정돼 있으면 `ocr review --format json --output .nereus/review-ocr.json`을 쓴다.
- **EGM (기본 2차 의견)**: `Agent`(general-purpose, model: `egmModel`) 서브에이전트를 **페르소나마다 하나씩 같은 메시지에서 병렬로** 띄운다.
프롬프트에는 아래만 넣는다 — 이 대화의 결정·이유·요약을 넣으면 새 맥락 리뷰가 아니다(goldfish 원칙).
```text
You are a Goldfish reviewer. First read your persona file and follow it exactly: <egmPersonas 의 경로 하나>
Mode: REVIEW (code). Review only this change (run it yourself; read surrounding code as needed):
committed: `git diff <base>...HEAD` · uncommitted: `git diff` plus untracked files from `git status --porcelain`
Spec for intent (read, do not review): <변경된 openspec change·spec-kit 디렉터리 경로 또는 "none">
You are read-only. Map your persona's severities: Blocker → CRITICAL, Major → HIGH, Nit → LOW.
Return a JSON array only: [{"file","line","severity":"CRITICAL|HIGH|MEDIUM|LOW","message"}].
```
리뷰는 보통 커밋 **전**에 돈다 — 커밋 범위만 주면 서브에이전트는 아무것도 못 본다. 두 범위 중 실제로 변경이 있는 쪽을 넣는다.
서브에이전트를 띄울 수 없으면 EGM 을 `skipped` 로 알린다. 같은 맥락에서 대신 리뷰하고 EGM 이라 부르지 않는다.
- **Codex (명시했을 때만)**: `codex review` (또는 codex 플러그인의 `/codex:adversarial-review`). 결과를 파일·줄·심각도·메시지로 정리한다.
직접 리뷰할 때는 아래 조용한 실패 5항을 반드시 훑는다 (출처: ecc `silent-failure-hunter`. 기계 게이트가 `silent_failure`으로 잡는 것은 빙산의 일각이다):
1. **빈 catch** — `catch {}`·`except: pass`. 에러를 Nothing으로 바꾼 곳.
2. **부실 로깅** — 맥락 없는 로그·잘못된 심각도·log-and-forget.
3. **위험한 fallback** — 실패를 감추는 기본값 (`.catch(() => [])`류). 그럴듯해 보여서 하류 진단을 어렵게 한다.
4. **전파 손실** — 날아간 스택트레이스·뭉뚱그린 rethrow·빠진 async 처리.
5. **누락된 처리** — 네트워크·파일·DB 경로의 타임아웃/에러 처리 없음, 트랜잭션 작업의 롤백 없음.
계획에 포함된 리뷰어만 실행하고, 결과를 `{source, file, line, severity, message}` 배열로 정규화한다.
## 2.5 security 반증 (CRITICAL/HIGH 가 있을 때만)
security 에이전트가 낸 CRITICAL/HIGH 는 발견한 맥락이 아닌 **새 서브에이전트가 반박해 본다**(출처: cloudflare/security-audit-skill — 확인하는 에이전트는 발견한 에이전트가 아니다).
대상은 `refutationTargets(findings)` 로 고르고, 항목마다 `Agent`(general-purpose, model: `refuterModel`)를 같은 메시지에서 병렬로 띄운다. 프롬프트에는 페르소나 경로(`review.mjs` 출력의 `securityRefuter`)와 그 항목 하나만 넣는다 — 다른 항목·이 대화의 판단은 넣지 않는다.
```text
You are a Goldfish verifier. First read your persona file and follow it exactly: <securityRefuter 경로>
Finding to refute (JSON): {"file","line","severity","message"}
```
돌려받은 판정 배열을 `applyRefutations(findings, verdicts)` 로 적용한다.
- `confirmed` → 그대로 차단(심각도는 내려갈 수 있다).
- `rejected` → findings 에서 빠진다. 사유와 함께 `.nereus/review.md` 에 남긴다.
- `needs_validation` → 심각도를 떼고 빠진 사실을 붙여 ledger 로. 게이트를 막지 않는다.
- 판정이 없거나 형식이 깨졌으면 차단을 유지한다. 반증을 못 했다고 차단을 조용히 풀지 않는다.
## 3. 병합과 게이트
정규화된 findings를 심각도순으로 병합해 사용자에게 보인다(`mergeFindings` 형식). 같은 위치를 두 리뷰어가 지적하면 신뢰도가 높다고 표시한다.
- **CRITICAL/HIGH 0개** → 통과. `nereus:finish`로.
- **MEDIUM 이하만** → 루프에 넣지 않는다. `.nereus/review.md` 에 한 줄씩 기록하고 `nereus:finish`로 (최종 리뷰가 merge 전 triage한다).
심각도별 액션 (`review.mjs`의 `severityAction`):
| 심각도 | 액션 | 의미 |
|---|---|---|
| CRITICAL | `fix-now` | 즉시수정. 수정 루프에 진입한다 |
| HIGH | `must-resolve` | 해결 전 진행금지. 수정 루프에 진입한다 |
| MEDIUM 이하 | `defer-ledger` | 연기 + ledger에 기록하고 `nereus:finish`로 |
| NEEDS_VALIDATION | `defer-ledger` | 배포 사실 하나가 없어 판정 보류. 빠진 사실을 ledger 에 적는다 |
| unknown | `defer-ledger` | 알 수 없는 심각도는 연기 기본값 |
### 3.1 수정 루프 상한 — 5라운드 (출처: superpowers SDD fix loop)
CRITICAL/HIGH가 남으면 fix 1회 + 스코프 재리뷰(고친 diff만, untouched 코드는 Out-of-Scope로 ledger행) 1회를 1라운드로 센다. 라운드 판정은 `review.mjs`의 `fixLoopStep(끝난라운드, 잔여blocking)`을 따른다:
- **R1–3: resume** — 같은 맥락에서 이어서 고친다 (TDD: 회귀 테스트 먼저). 컨텍스트가 끊겼으면 brief·report·findings를 통째로 넘긴 fresh dispatch로 대체한다.
- **R4–5: escalate** — fresh + 한 티어 위 모델로 바꾼다. 3번 이어 고쳤는데 안 되면 고친 주체가 자기 문제를 못 보는 것이다. 티어는 build §5 비용 티어표를 따른다.
- **R5 후에도 잔존: breaker** — 그만 고치고 각 항목을 판정해 사용자에게 확인받는다. 리뷰어가 틀렸거나 contestable하면 park + Ruling 기록. 진짜인데 하류가 안 얹히면 park + deferred. 진짜이고 load-bearing이면 (다음 작업이 얹히거나 계획 결함을 드러내면) 최소 변경을 rule로 정해 다음 build에 넘긴다. 조용히 버리지 않는다.
"고치면 될 것 같으니 한 번만 더"는 5라운드 이후의 변명이다. 라운드가 수렴하지 않으면 구조 문제다.
## 4. 기록
`.nereus/review.md`에 findings와 처리 결과를 남긴다. 판정·연기도 common의 Ruling 형식으로 같은 파일에 기록한다.