공개 저장소 Agent Skill Gardenreview-response-loop을 추가했다. PR #7로 만들었고, 현재 main에 병합됐다.

앞서 추가한 라우팅 기반 프론트엔드 리뷰 팩이 리뷰어 쪽에서 지적을 만드는 워크플로였다면, 이번 스킬은 작성자 쪽에서 그 지적을 끝까지 처리하는 반대편이다.

리뷰 하나에 답하는 일은 어렵지 않다. 자주 끊기는 곳은 그다음이다. 수정하고 답글을 남겼지만 리뷰를 다시 요청하지 않거나, 요청은 했지만 새 활동을 기다리지 않거나, 승인 표시만 보고 아직 남은 지적을 놓치면 PR은 끝난 것처럼 보이는 중간 상태에 머문다.

review-response-loop은 이 과정을 한 번의 답변이 아니라 상태가 정리될 때까지 반복하는 루프로 다룬다.

여러 비공개 PR의 리뷰 대응 기록에서 반복되는 실패를 찾아 공개 워크플로로 다시 썼다. 실제 저장소, 채널, 리뷰어, 업무 식별자와 비공개 결함 내용은 포함하지 않는다.

체크리스트가 아니라 끊기는 장면에서 시작했다

출발점은 기능 아이디어가 아니었다. 이틀 동안 네 개의 PR을 처리하면서 PR마다 2회에서 7회까지 리뷰 라운드가 이어졌다. 사람과 자동 리뷰어가 섞여 있었고, 한 라운드가 끝날 때마다 비슷한 요청을 사람이 다시 입력했다.

다시 봐줘.
지적 N건에 답했고, 수정한 커밋은 이것이야.

에이전트는 이 요청을 받으면 지적을 읽고, 코드를 고치고, 테스트하고, 답글을 남기는 일을 잘했다. 문제는 그다음 라운드를 스스로 시작하지 않는다는 점이었다. 답변 뒤에 리뷰를 다시 요청하지 않거나, 재요청 뒤에 새 활동 감시를 다시 걸지 않아 사람이 다음 라운드를 이어 붙였다.

기록을 다시 읽으니 루프가 끊기는 장면은 네 가지로 모였다.

  1. 지적에 답하고 일을 끝낸다.
  2. 리뷰는 다시 요청했지만 다음 응답을 기다리는 감시를 재설정하지 않는다.
  3. 리뷰 요청 메시지는 남겼지만 해당 리뷰어를 깨우는 환경별 호출 조건을 충족하지 않는다.
  4. 전체 상태가 승인으로 보인다는 이유로, 다시 요청한 리뷰어의 응답이 오기 전에 종료한다.

세 번째는 모든 환경에 그대로 적용할 수 있는 규칙이 아니다. 어떤 리뷰어는 플랫폼의 재요청만으로 움직이고, 어떤 자동화는 별도 메시징 트리거가 필요하다. 그래서 공개 스킬에는 특정 봇이나 채널 호출법을 넣지 않았다. 핵심 워크플로는 플랫폼의 재요청을 사용하고, 별도 호출법은 각 사용자의 환경 프로필에 두도록 경계를 나눴다.

가장 결정적인 실패는 네 번째였다. 승인 상태와 미해결 스레드 0개를 보고 루프가 끝났다고 판단했지만, 45초 뒤 다시 요청했던 리뷰어의 응답이 도착했다. 그 뒤에 실질적인 지적 두 건이 더 나왔다. 이 사건 때문에 종료 조건은 “전체가 승인인가?”에서 “내가 다시 요청한 리뷰어가 실제로 답했고, 새 지적도 남아 있지 않은가?”로 바뀌었다.

한 라운드는 네 단계가 모두 끝나야 한다

한 라운드를 닫는 순서는 단순하다.

  1. 모든 지적에 답한다. 받아들인 지적에는 수정 커밋을, 받아들이지 않은 지적에는 근거를 남긴다.
  2. 합의가 끝난 스레드를 정리한다.
  3. 리뷰를 다시 요청한다.
  4. 새 리뷰 활동을 기다리는 감시를 다시 건다.

흐름으로 적으면 다음과 같다.

새 리뷰 활동
  → 현재 파일을 기준으로 지적 판정
  → 수정 또는 근거 있는 소명
  → 변경한 동작 검증
  → 커밋을 명시해 답변
  → 합의된 스레드 정리
  → 리뷰 재요청과 시각 기록
  → 다음 활동 감시

마지막 두 단계가 중요하다. 코드가 바뀌지 않고 설명만 추가한 라운드도 다시 요청해야 한다. 답변을 남기는 것과 다시 검토해 달라고 요청하는 것은 별도 동작이고, 감시는 요청을 대신하지 않는다.

반대로 리뷰를 다시 요청했더라도 감시를 다시 걸지 않으면 작성자는 응답이 왔는지, 새 지적이 추가됐는지 알 수 없다. 그래서 재요청과 감시는 하나로 뭉개지 않고 별도 단계로 유지했다.

승인은 필요하지만, 항상 종료는 아니다

처음에는 reviewDecisionAPPROVED이고 미해결 스레드가 0개면 루프를 끝내도 된다고 생각하기 쉽다. 하지만 특정 리뷰어에게 다시 요청한 직후라면 그 리뷰어가 아직 답하지 않았을 수 있다. 다른 리뷰어의 승인이 먼저 들어와 전체 결정이 승인으로 보이더라도 기다리던 응답은 오지 않은 상태다.

그래서 종료 판정을 다음처럼 나눴다.

관찰한 상태 판정
승인, 미해결 스레드 0개, 기다리는 리뷰어 없음 종료
승인, 미해결 스레드 0개, 재요청한 리뷰어가 아직 응답하지 않음 계속 대기
미해결 스레드 또는 새 지적 존재 계속 처리
승인만 추가됐고 스레드는 남아 있음 계속 처리

여기서 누구의 응답을 기다리는가어떤 활동을 증거로 읽는가는 서로 다른 범위다.

  • 기다림은 재요청한 리뷰어의 활동만으로 끝난다.
  • 증거는 모든 리뷰어의 활동에서 수집한다.

이 구분이 없으면 기다리던 리뷰어의 승인이 도착한 순간, 다른 리뷰어가 같은 시간대에 남긴 새 지적이 판정에서 사라질 수 있다.

이 PR도 처음에는 그 경계를 틀렸다

PR의 첫 구현에는 --only-from이라는 옵션이 있었다. 특정 리뷰어의 응답을 기다릴 때 활동 목록 자체를 그 리뷰어로 제한했다. 기다림만 좁히려던 옵션이 증거까지 좁힌 셈이다.

다음 상황에서 문제가 드러났다.

기다리던 리뷰어: APPROVED
다른 리뷰어: 본문이 있는 COMMENTED 리뷰
미해결 인라인 스레드: 0
현재 reviewDecision: APPROVED

다른 리뷰어의 COMMENTED에는 실제 지적이 있었지만 필터 뒤에서 사라졌다. 최종 판정은 finished가 됐다. 상단 리뷰 본문은 인라인 스레드를 만들지 않으므로 미해결 스레드 수로도 잡히지 않았다.

최종 구현에서는 new_activity가 리뷰어 필터를 아예 받지 않게 했다. 활동은 항상 모든 리뷰어에게서 모은다. CLI 옵션도 --awaiting-reviewer로 바꿔, 이 값이 증거 필터가 아니라 응답을 기다리는 대상임을 이름에 드러냈다.

이 사례는 헬퍼 함수와 CLI 양쪽에 회귀 테스트로 남겼다. 명령의 의미가 문서에만 있는 것이 아니라 함수 시그니처와 테스트에서도 같은 방향을 가리키게 했다.

반복되는 위치는 두 가지로 읽는다

여러 리뷰어가 같은 path:line을 지적하면 모두 같은 현상처럼 보인다. 하지만 라운드를 기준으로 나누면 의미가 다르다.

상황 읽는 방법
같은 위치가 나중 라운드에서 다시 제기됨 round-trip
여러 리뷰어가 한 라운드 안에서 같은 위치를 지적함 convergence
라운드 정보가 없거나 한 리뷰어가 같은 라운드에 두 번 작성함 repeated

round-trip은 지적별 답변이 합의로 이어지지 않고 있다는 신호다. convergence는 여러 관점이 한 취약한 지점에 모였다는 신호다. 의미는 다르지만 대응은 같다. 스레드마다 같은 답을 반복하지 않고, 영향 경로와 선택지, 결론, 결론이 바뀔 조건을 하나의 종합 코멘트로 정리한다.

둘을 구분하려면 시간 간격을 추측해서는 안 된다. 흐른 시간은 라운드가 어디서 시작했는지 말해주지 않는다. 최초 요청과 각 재요청의 시각을 round_marks로 기록해야 한다. 이 경계가 없으면 도구는 round-trip이나 convergence를 추측하지 않고 repeated만 반환한다.

그 시각을 따로 조회할 필요는 없다. 앞서 정리한 네 단계 중 세 번째가 리뷰 재요청 자체이므로, 요청하는 그 순간에 시각을 남기면 그것이 곧 라운드 경계다. 플랫폼마다 다른 리뷰 요청 이력 조회를 워크플로에 넣지 않아도 되는 이유이기도 하다.

결정 로직은 네트워크 없이 테스트한다

review_activity.py는 GitHub를 직접 호출하지 않는다. 리뷰, 코멘트, 현재 결정, 미해결 스레드 수를 JSON으로 받아 새 활동과 판정만 계산한다.

입력은 다음 모양이다.

{
  "reviews": [],
  "comments": [],
  "review_decision": "APPROVED",
  "unresolved_threads": 0,
  "round_marks": ["2026-09-01T05:00:00Z"]
}

저장소에서 직접 실행한다면 다음과 같이 사용할 수 있다.

python3 core/skills/review-response-loop/scripts/review_activity.py \
  --input state.json \
  --author AUTHOR \
  --since 2026-09-01T05:00:00Z \
  --awaiting-reviewer REVIEWER

출력은 새 활동과 함께 finished, new-finding, awaiting-reviewer, threads-open, undecided 중 하나를 반환한다. 네트워크 접근과 상태 계산을 분리했기 때문에 실제 API를 흉내 내는 큰 모의 환경 없이도 종료 조건을 직접 검증할 수 있다.

설치하고 사용하는 방법

스킬 원본은 core/skills/review-response-loop에 있다. 프로젝트에 연결하기 전에 먼저 드라이런으로 설치 계획을 확인한다.

Claude Code 프로젝트에는 다음처럼 설치한다.

./scripts/install.sh \
  --target claude \
  --scope project \
  --root path/to/project \
  --skill review-response-loop

Codex 프로젝트에서는 --target codex로 바꾼다. 계획이 맞으면 같은 명령에 --apply를 붙인다. 기존 경로가 있으면 조용히 덮어쓰지 않고 충돌로 중단한다.

설치 뒤에는 목적과 권한을 함께 말해주는 편이 좋다.

이 PR의 리뷰 지적을 판정하고, 타당한 것은 수정·검증한 뒤
답글과 재리뷰 요청까지 처리해줘. 병합은 하지 마.

상태만 먼저 보고 싶다면 쓰기 작업을 막아 둔다.

이 PR의 현재 리뷰 상태와 미해결 지적만 정리해줘.
아직 수정하거나 답글을 게시하지 마.

스킬도 같은 권한 경계를 가진다. 읽기는 먼저 할 수 있지만 코드 수정, 답글 게시, 스레드 정리, 리뷰 재요청은 각각 외부 상태를 바꾸는 작업이다. 그리고 review-response-loop병합하지 않는다. 승인은 병합 결정 권한을 자동으로 넘겨주지 않기 때문이다.

라운드가 길어질 때는 대응을 바꾼다

같은 방식으로 계속 답한다고 루프가 빨리 끝나지는 않는다. 스킬은 라운드 수에 따라 대응 수준을 올린다.

  • 1~2라운드: 각 지적을 판정하고 답한다.
  • 3라운드 이상: 지적받은 곳만 보지 않고 같은 역할의 파일, 호출자, 간접 소비처를 먼저 자체 점검한다.
  • 닫혔던 논점이 다시 열림: 현재 변경에서 그 정책을 제거하고 이전 동작으로 정확히 되돌리는 선택을 검토한다.
  • 5라운드 이상 결론 없음: 코드만으로 결정 가능한 문제인지 구분하고 사용자에게 선택지를 넘긴다.

자체 점검에서 새 문제가 0개였더라도 그 결과를 명시한다. 무엇을 확인했는지와 결과가 비어 있다는 사실도 다음 리뷰어가 같은 범위를 다시 탐색하지 않게 하는 증거다.

현재 검증 상태

PR #7이 병합된 시점의 저장소 검증 결과는 다음과 같다.

  • 공개 스킬 22개 검증
  • 자연어 라우팅 사례 29개 검증
  • 단위 테스트 53개 통과
  • review_activity.py 전용 테스트 31개 통과
  • 컨텍스트 예산과 공개 안전성 검사 통과
  • GitHub Actions 통과
  • Claude Code 대상 임시 설치와 원본 비교 확인

스킬을 추가하면서 종료 판정, 라운드 구분, 입력 계약을 세 번 다시 고쳤다.

세 번 중 가장 뼈아픈 것은 첫 번째였다. 리뷰 대응을 다루는 스킬이, 정작 자기 PR에서 “승인은 종료가 아니다”라는 핵심 원칙을 지키지 못한다는 지적을 받았다. 기다리던 리뷰어의 승인이 도착한 순간 다른 리뷰어의 새 지적이 판정에서 사라지는 경로가 남아 있었고, 그 경로를 찾아낸 것은 이 스킬이 설명하는 바로 그 리뷰 라운드였다.

규칙을 글로 적는 것과 그 규칙이 코드에서 실제로 성립하는 것은 다른 일이다. 이 PR은 새 기능을 추가한 기록이면서, 그 기능이 말하는 방식대로 리뷰를 받아 자기 자신의 반례를 찾아낸 기록이기도 하다.

리뷰를 만드는 스킬과 닫는 스킬

리뷰 자동화는 보통 지적을 얼마나 잘 찾는지에 집중한다. 하지만 작성자에게 필요한 것은 지적 목록만이 아니다. 어떤 지적을 받아들일지 판단하고, 수정이 실제로 동작하는지 검증하고, 리뷰어가 확인할 수 있게 커밋을 연결하고, 다시 요청한 뒤 새 활동이 올 때까지 상태를 유지해야 한다.

이제 Agent Skill Garden의 리뷰 흐름은 두 방향을 가진다.

pull-request-review
  → 현재 diff에서 근거 있는 지적을 만든다

review-response-loop
  → 작성자가 그 지적을 정리된 결과까지 끌고 간다

저장소를 만든 배경과 여러 에이전트에서 같은 업무 방식을 유지하는 구조는 개인 설정에서 Agent Skill Garden으로 옮긴 기록에 정리했다.

이번 기능에서 가장 중요하게 남긴 문장은 이것이다. 처음부터 적어둔 원칙이 아니라, 이 워크플로가 자기 PR에서 스스로를 어긴 뒤에야 얻은 문장이다.

기다리는 대상은 좁혀도, 판단에 쓰는 증거는 좁히지 않는다.