---
name: pr-round
description: 이 저장소에서 PR 을 열고 리뷰 한 라운드를 읽는 법. PR 을 열 때 · ready 로 바꿀 때 · 코덱스나 CodeRabbit 의 판정을 확인할 때 · 「지적이 없다」를 「깨끗하다」로 읽기 전에 본다. 봇의 신호가 어떤 모양으로 오는지, 안 붙었을 때 무엇을 의심하는지를 든다.
---

# PR 한 라운드를 읽는 법

리뷰 봇의 신호는 **문서에 없다**. OpenAI·CodeRabbit 쪽 동작이라 이 저장소가
실제로 밟아 본 것만 안다. 그래서 여기 적힌 것은 규칙이 아니라 **관측**이고,
모양이 바뀌면 다시 재야 한다.

> **말부터 정한다 — 이 문서에서 「신호」·「리뷰」·「코멘트」·「리액션」은 전부
> 「기다리는 그 봇 계정이 남긴 것」을 뜻한다.** 다른 계정이 남긴 같은 모양은
> **신호가 아니다.** 라운드를 열기 전에 **어느 `user.login` 을 기다리는지 먼저
> 정하고**, 아래 어디서든 이 말들이 나오면 그 조건이 이미 걸려 있는 것으로 읽는다.

<sub>**이 문단이 열거를 대신한다.** 같은 조건을 자리마다 붙였다가 **다섯 번 샜다** —
리액션 · 리뷰 · 무지적 코멘트 경로(4·5·7라운드), 재촉 억제(8라운드), `skip review`
제외(9라운드). 표 밖으로 올려도, 걸려야 할 자리를 세어 봐도 또 샜다. **자리를 세는
것이 실행 가능한 규칙이 아니었다** — 산문에서 「이 조건이 걸려야 할 곳」을 기계적으로
찾을 길이 없기 때문이다. 그래서 열거를 그만두고 **말의 뜻**으로 옮겼다. 빠뜨릴
자리가 생기지 않는 유일한 모양이다.</sub>

## 깨끗한 라운드의 신호는 셋이고, 매번 같은 것이 오지 않는다

코덱스가 지적 없이 끝냈다는 **신호**는 **리뷰 제출 · 무지적 코멘트 · 👍 리액션**
셋이며 **어느 것이 올지는 PR 마다 다르다.** 하나만 보면 놓친다.

**다만 셋이 다 판정은 아니다.** 아래 「판정은 커밋에 묶는다」가 가른다 — **리뷰
제출과 무지적 코멘트는 커밋을 들고 오고**(앞은 `commit_id` 필드, 뒤는 본문),
**👍 리액션만 신호에서 멈춘다.**

| PR | 무지적 코멘트 | 👍 |
| --- | --- | --- |
| #16 · #21 · #47 · #50 | 없음 | 있음 |
| #19 · #20 · **#65** | 있음 | 있음 |

**`reviews` 배열이 0건인 것은 「아무도 안 봤다」가 아니다.** 지적이 없으면 코덱스는
`reviews` 를 비운 채 **리액션**으로만 남기기도 한다 — 머지된 PR 열하나 전부에 👍 가
있었고, 없는 것은 머지되지 않고 닫힌 `#14` 하나뿐이었다. **자료구조를 사건으로
착각하지 않는다.**

**거꾸로도 마찬가지다 — 차 있는 것이 「지적이 있다」는 뜻이 아니다.** 위에서 봤듯이
**리뷰 제출은 깨끗한 신호 셋 중 하나**라, 지적이 없어도 `reviews` 가 찬다. 배열이
비었나 찼나로 세지 말고 **그 봇 리뷰의 `state` · 본문 · 딸린 라인 지적을 연다.**

<sub>실측 2026-09-14 · `#65` — 지적이 있던 라운드는 `COMMENTED` + 라인 지적이 함께
왔고(1라운드 6건), 깨끗한 라운드는 라인 지적이 **0**이었다. **가르는 것은 배열의
길이가 아니라 딸린 지적의 수다.**</sub>

리액션을 누가 남겼는지는 헤더 하나로 읽는다:

```bash
curl -s -H "Accept: application/vnd.github.squirrel-girl-preview+json" \
  https://api.github.com/repos/OWNER/REPO/issues/N/reactions
```

**리액션은 두 종류이고 같은 자리에서 교체된다.** ready → 👀 eyes(「보고 있다」)
→ 👍 +1(「끝냈다」). 나중에 조회하면 👍 하나만 남는다. **👀 를 완료로 읽으면 지적이
있는데 깨끗하다고 센다** — 둘 다 판정이 아니라 **신호**라는 것은 아래에서 가른다.

**👍 는 CI 를 보지 않는다.** 한 번은 👍 가 CI 완료보다 2분 빨랐다. 코덱스는
코드를 읽지 파이프라인을 읽지 않는다 — **두 조건은 따로 확인한다.**

## 판정은 커밋에 묶는다 — 시각으로는 묶이지 않는다

<sub>관제탑은 「푸시하면 리액션이 지워지므로 머지 시점에 👍 가 남아 있으면 판정한
커밋과 머지된 커밋이 같다」고 적었다. **GitHub 은 이슈 본문 리액션을 푸시로 지우지
않는다.** 그대로 믿으면 **판정 뒤에 올린 커밋을 검토된 것으로 읽는다.**</sub>

**시각 비교로 대신할 수 없다.** 이전 head 를 보는 도중에 새 커밋이 올라가면 완료
신호가 **새 커밋보다 늦게** 찍힌다. 늦게 찍혔다는 것이 **그것을 봤다는 뜻이
아니다.**

**리뷰에는 커밋이 적혀 있고, 리액션에는 없다.** 리뷰 본문 머리에 `Reviewed commit:
<sha>` 가 찍히고, `reviews` 의 각 항목과 라인 지적에 `commit_id` 가 붙는다.

```bash
curl -s https://api.github.com/repos/OWNER/REPO/pulls/N/reviews | python3 -c \
  "import json,sys; [print(r['user']['login'], r['state'], r['commit_id'][:10]) \
   for r in json.load(sys.stdin)]"
```

> **판정으로 세는 것은 커밋을 들고 온 신호뿐이다.**

**판정에는 조건이 둘이고, 둘 다 모든 경로에 공통이다.** 경로마다 다른 것은
**조건이 아니라 SHA 가 어디 적혀 있는가**뿐이다.

> **① 작성자** — 머리말의 정의 그대로다. **그 봇이 남긴 것만 신호다.** `reviews`
> 도 코멘트도 리액션도 **먼저 `user.login` 으로 거르고 나서** 아래를 본다.
>
> **② 커밋** — **들고 온 SHA 가 현재 `head.sha` 와 같아야 한다.** 이것도 모든
> 경로에 공통이다. **어디에 붙어 있는지만 경로마다 다르고**, 아래 표가 그 자리를 든다.

| 무엇이 왔나 | SHA 가 어디 붙나 |
| --- | --- |
| 리뷰 · 라인 지적 | `commit_id` **필드** |
| **무지적 코멘트** | **본문**의 `Reviewed commit: <sha>` — **`reviews` 가 비어 있어도 판정이다** |
| 👍 리액션 | **어디에도 안 붙는다** — 신호이고 판정이 아니다. **「확인 불가」로 부른다** |

<sub>**표를 「어디 붙나」만 남기게 정리하면서 「현재 head 와 같고」를 떨어뜨린 적이
있다**(`044547c`, 8라운드에 지적). 그러면 **작성자가 맞는 예전 판정이 새 head 를
통과시킨다** — 이 문서가 여덟 라운드에 걸쳐 세운 것이 그 한 줄이었다. 바로 그
커밋에서 「규칙을 고칠 때는 고친 자리만 보지 않는다」를 각주로 적고 있었다.</sub>

**작성자를 왜 보는가.** 이 저장소에는 봇이 둘이고 사람도 리뷰를 낸다 — 코덱스가
확인 불가능한 👍 만 남긴 사이에 **CodeRabbit 이나 협업자가 같은 SHA 에** 리뷰를
제출하면, 그것이 **코덱스의 판정 자리에 앉는다.**

**둘 다 표 안에 넣지 않은 이유가 있다.** 행마다 적으면 **경로를 손댈 때 조건만
따라오지 않는다** — ①이 그렇게 다섯 번 샜고(머리말 각주), ②는 표를 정리하다 통째로
빠졌다.

> **조건은 표 밖에 둔다. 그리고 열거로 지킬 수 있는 것은 거기까지다** — ①처럼
> 문서 곳곳에서 되풀이되는 조건은 **말의 뜻으로** 올려야 새지 않는다(머리말).

<sub>**코멘트 경로는 특히 약하다** — 본문 표식은 **누구나 적을 수 있고, 인용만 해도
생긴다.** 실제로 `#65` 7라운드 요청 코멘트가 지난 판정을 인용하느라 `Reviewed
commit:` 을 그대로 적었다. **그 SHA 가 현재 head 였다면 저자가 올린 코멘트가 봇의
깨끗한 판정으로 집계됐을 것이다.**</sub>

<sub>**「무지적 코멘트에는 커밋이 안 붙는다」고 적었다가 다음 라운드에 반증됐다**(실측
2026-09-14 · `#65` 6라운드). 리뷰 본문에 `Reviewed commit:` 이 찍히는 것은 이미
관측해 놓고, **같은 것이 코멘트 경로에도 있는지는 재지 않고 「없다」고 단정했다.**
`reviews` 배열이 비어 있는 것을 「커밋을 안 들고 온다」로 읽은 것이다 — **자료구조를
사건으로 착각하지 않는다**가 이 문서 위쪽에 이미 있다.</sub>

**head 는 PR 에서 읽는다**(`GET /pulls/N` → `head.sha`). `git rev-parse HEAD` 는
사람이나 다른 레인이 그 가지에 푸시해도 안 움직여, **내 작업 공간이 조용한 것을 PR 이
조용한 것으로 읽는다.**

**「확인 불가」는 실패가 아니라 아는 만큼 적는 것이다.** 저자에게 그대로 알린다 —
「코덱스가 신호는 남겼지만 **어느 커밋을 봤는지 확인할 수 없다**」. 그 다음은 저자가
정한다.

**다만 그 자리는 생각보다 좁다.** 지적이 있으면 리뷰가 오고, 지적이 없으면 무지적
코멘트가 온다 — **둘 다 커밋을 들고 온다.** 👍 만 오는 경우가 남지만, `#65` 의 깨끗한
라운드는 **👍 와 무지적 코멘트가 1초 차로 함께** 왔다. **「깨끗한 라운드가 전부 확인
불가로 떨어진다」는 걱정은 재 보니 기우였다.**

### 없는 정보를 주변 사실로 복원하려 들지 않는다

리액션에 커밋을 붙여 보려고 조건을 쌓았다가 **네 라운드에 걸쳐 구멍이 넷 나왔다** —
묵은 👍 가 통과했고, **남의** 👍 가 통과했고, 로컬 HEAD 가 원격을 못 봤고, **이전
head 를 검토하던 실행이 늦게 끝나도 통과**했다. 마지막 것이 핵심이다.

<sub>**경로를 지우거나 더해도 그 경로의 결함은 따라온다.** 「남의 것을 세지
않는다」는 처음에 리액션 쪽 조건이었다. 리액션 경로를 지우자 **같은 결함이 리뷰
쪽에** 남았고, 무지적 코멘트 경로를 더하자 **거기에 또** 났다 — 세 번이다. 그래서
그 조건을 표 밖의 **공통 조건**으로 올렸다. **규칙을 고칠 때는 고친 자리만 보지
않는다.**</sub>

> **리액션은 커밋도 시작 시각도 들고 오지 않는다.** 없는 정보는 스냅샷으로 복원되지
> 않고, **조건을 하나 더 붙일 때마다 다음 구멍이 생긴다.**

그래서 조건을 늘리는 대신 **판정의 정의를 좁혔다.** 「깨끗한 PR 이 영원히 안 닫힌다」는
걱정은 자리를 잘못 짚은 것이다 — **닫는 것은 저자이고**, 문서가 할 일은 **모르는 것을
안다고 적지 않는 것**이다.

<sub>실측 2026-09-14 · `#65` 라운드 넷 — `reviews[].commit_id` 가 매번 당시
head(`02f8c68` · `917a7c2` · `96da03c` · `f968256`)로 왔고 라인 지적도 같았다.
**지적이 있는 라운드에서는 커밋이 붙은 신호가 항상 왔다.**</sub>

## 봇 둘이 지적을 내는 모양이 다르다

코덱스는 **라인 스레드**, CodeRabbit 은 **리뷰 본문 안 `<details>` 블록**이다.
스레드만 훑는 절차는 CodeRabbit 을 통째로 놓친다 — `#17` 에서 실제로 2건을
놓쳤다.

## 「지적 0」이 「깨끗함」이 아닌 경우가 둘 있다

봇이 **안 붙은** 것과 봇이 **붙었는데 지적이 없는** 것은 화면에서 똑같이
초록이다. 안 붙는 이유가 하나가 아니므로 **사유 문구를 읽는다.**

| 사유 | 문구 | 어떻게 부르나 |
| --- | --- | --- |
| PR 작성자가 봇 | *“Review skipped — Bot user detected”* · 코덱스는 *“create a Codex account”* | 사람이 ready 를 누르면 코덱스는 붙는다(`#47`) |
| 저장소 별이 열 개 미만 | *“This repository does not receive automatic reviews because it has fewer than 10 stars.”* | `Trigger review` 버튼 또는 `@coderabbitai review` |

본문이 `skip review` 인 코멘트는 **라운드로 세지 않는다** — 세면 분모가 기운다.
머리말의 정의대로 **그 봇이 남긴 것일 때만** 그렇다. **누가 인용만 해도 같은 문자열이
생기므로**, 남의 것을 빼면 **붙었는데 안 붙은 것으로 오진한다.**

## 자동 부착은 들쭉날쭉하다 — 재촉에도 규칙이 있다

ready 로 바꾼 뒤 붙기까지 **6초 ~ 19분 무응답**까지 봤다.

> **전환 후 5분 안에 아무 반응이 없으면 재촉할 때다. 다만 👀 가 이미 붙어
> 있으면 재촉하지 않는다** — 이미 보고 있는 것이고, 부르면 중복 리뷰가 된다.

**여기 「👀」도 머리말의 정의대로 그 봇의 것이다.** 협업자나 다른 봇이 남긴 👀 를
「보고 있다」로 읽으면, **그 봇이 시작도 안 했는데 저자에게 알리는 것을 막는다.**

<sub>이 자리가 같은 조건이 샌 **네 번째**였다(8라운드에 지적) — 그때는 조건을 판정에만
걸어 두었다. 다섯 번째(`skip review`)가 나온 뒤 **열거를 그만두고 머리말의 정의로
옮겼다.**</sub>

**리액션이 비어 있는 것도 무응답이 아니다.** 판정이 나면 👀 가 **교체되어 사라진다** —
19분 뒤 리액션이 0건이라 무응답으로 읽을 뻔했는데, 판정은 **5분 만에 리뷰로** 와
있었다(실측 2026-09-14 · `#65` 8라운드). **재촉을 알리기 전에 판정 경로 셋을 먼저
본다.**

**재촉은 에이전트가 하지 않는다.** `@codex review` 를 남기는 것은 **사람
계정**이어야 하고, 봇 계정이 남기면 거절당한다. 그러니 에이전트가 할 일은
**저자에게 「5분이 지났고 👀 도 없다」고 알리는 것**까지다 — `authority-line` 이
리뷰 요청을 사람에게 남기는 이유가 이것이다. 에이전트가 직접 부르면 **요청이
갔다고 믿는 채로 PR 이 검토 없이 앉아 있게 된다.**

## 매 PR 에서 따로 확인하는 셋

**작성자 · 리뷰어 존재 · `merged_by`.** 셋이 따로인 이유는 각각 다른 것을 숨기기
때문이다 — 작성자는 봇이 안 붙는 사유를, 리뷰어 존재는 「지적 0」의 뜻을,
`merged_by` 는 **사람이 눌렀는지 봇이 눌렀는지**를 가른다. 마지막 것은 PR
화면에서 눈에 띄지 않는다.

## 가지는 조각마다 새로 뗀다

머지된 가지를 재사용하려면 리셋이 필요하고, 리셋은 force-push 를 부르고,
**force-push 는 분류기가 막는다**(published history rewrite). 새 가지를 떼면
force 가 필요 없다. **취향이 아니라 이 환경에서 유일하게 통하는 길이다.**

## 스택된 PR 은 아래가 들어가면 충돌**할 수 있다**

base 를 다른 PR 의 가지로 두면 그것이 머지된 뒤 `dirty` 가 될 수 있다 — **되는지는
변경이 실제로 겹치는가에 달렸고, 안 겹치면 멀쩡하다.** 그러니 **고치기 전에 묻는다.**

```bash
curl -s https://api.github.com/repos/OWNER/REPO/pulls/N | python3 -c \
  "import json,sys; print(json.load(sys.stdin)['mergeable_state'])"
```

`dirty` 일 때만 수리한다. 해소는 base 를 **머지해서** 푸는 것이지 rebase·force-push
가 아니고, 해소 커밋이 아래 PR 이 넣은 것을 지우지 않았는지 **diff 로 확인해야
한다** — CI 초록은 그것을 못 잡는다(검사가 사라지면 그냥 덜 돈다).

**멀쩡한데 수리하지 않는다.** 수리 커밋은 head SHA 를 바꾸고, 그러면 위의 「판정은
커밋에 묶는다」가 **리뷰를 한 번 더 요구한다.** 필요 없는 라운드를 스스로 만드는
셈이다.

**환경의 결함이 아니다.** GitHub 은 base 가 전진해 충돌이 생겨도 웹훅을 쏘지
않는다.

<sub>실측 2026-09-09 ~ 2026-09-14 · PR `#14`~`#26` · `#47` · `#50` · `#65`.
👀 의 시각만은 레인 실시간 관측이라 사후 재현이 안 된다. 봇의 동작이 바뀌면 이 문서가 아니라
**다시 잰 값**이 맞다.</sub>
