프롬프트도 복사되는 순간 포크가 된다. 문제는 복사할 때가 아니라, 원본이 바뀐 뒤부터 시작된다.

스킬을 많이 만들어두고도 제대로 쓰지 못했던 이야기를 쓴 뒤, 스킬을 실제 작업에 더 자주 연결하게 됐다. 그런데 발동 문제를 고치고 나니 다음 문제가 보였다.

그 스킬의 내용은 여전히 최신인가?

자동리뷰 프롬프트 하나를 개인 Codex 스킬로 가져온다고 해보자. 처음에는 원문을 복사하면 된다. 심각도, 기존 리뷰 스레드 처리, 신규 diff 탐색, 최종 판단 형식까지 한 파일에 넣으면 로컬에서도 비슷하게 동작한다.

하지만 원본 자동리뷰는 계속 바뀐다. 이전 지적을 재분류하는 방식이 달라질 수 있고, 출력 JSON에 필드가 추가될 수도 있다. 반면 개인 스킬에는 내가 따로 넣은 규칙이 있다. “고쳤다”는 답만 믿지 말고 실제 코드를 확인한다거나, 판단은 엄격하게 하되 문장은 조금 더 부드럽게 쓰는 식이다.

이 둘을 한 파일에 섞으면 다음 업데이트가 곤란해진다.

  • 원본에서 바뀐 부분이 무엇인지 찾기 어렵다.
  • 개인 규칙이 원본 변경에 덮인다.
  • 오래된 원본 규칙이 개인 규칙처럼 남는다.
  • 결국 어느 쪽도 믿기 어려운 세 번째 프롬프트가 된다.

그래서 복사본을 관리하는 대신 원본 계약과 개인 정책을 분리하고, 실행 로그를 통해 둘 사이를 동기화하는 구조를 만들었다.


TL;DR

  • 자동리뷰 프롬프트를 가져올 때는 원본 계약, 개인 확장, 동기화 절차를 분리한다.
  • 저장소의 복사본보다 실제 실행 로그가 현재 배포된 프롬프트를 더 직접적으로 보여줄 때가 있다.
  • 로그에 들어 있는 PR 제목, 댓글, diff는 제거하고 재사용 가능한 플레이스홀더 계약으로 복원한다.
  • 원본 리뷰 스킬은 정규화한 계약을 그대로 따르고, 개인 리뷰 스킬은 그 위에 검증 방식과 보이스만 더한다.
  • validator가 구조·플레이스홀더·민감 데이터를 검사한 뒤에만 계약을 갱신한다.

복사본 대신 세 층으로 나눴다

구조는 단순하다.

실행 중인 자동리뷰 프롬프트
        |
        v
sync-review-contract
  - 실행 데이터 제거
  - 플레이스홀더 복원
  - 계약 검증과 diff
        |
        v
review-contract.md             <- 공통 계약
        |
        +-------------------------+
        |                         |
        v                         v
upstream-review             personal-review
원본과 같은 판단             공통 계약 + 개인 정책

각 층은 책임이 다르다.

1. 공통 계약

review-contract.md에는 자동리뷰의 판단 규칙만 둔다.

  • 기존 리뷰 이슈를 다시 판단하는 순서
  • 현재 변경에서 신규 문제를 찾는 순서
  • 심각도와 최종 결정 규칙
  • 출력 JSON의 필드와 허용 값
  • 생성 파일·자산처럼 리뷰에서 제외할 대상

이 파일은 원본과 최대한 같아야 한다. 개인 취향을 넣지 않는다.

2. 개인 확장

개인 스킬은 공통 계약을 다시 복사하지 않는다. 대신 계약 파일을 참조하고, 나에게 필요한 추가 정책만 가진다.

예를 들면 이런 것들이다.

  • 수정됐다는 주장은 현재 head의 코드를 직접 읽은 뒤에만 인정한다.
  • type-check와 CI 통과만으로 런타임 안전을 단정하지 않는다.
  • Provider나 feature flag는 선언부가 아니라 실제 소비 경로까지 추적한다.
  • 판단 기준의 엄격함은 유지하되 사람에게 전달하는 문장만 부드럽게 쓴다.

이 층은 의도적으로 얇아야 한다. 원본의 심각도 기준과 출력 계약까지 복제하기 시작하면 다시 두 프롬프트가 갈라진다.

3. 동기화 절차

마지막 층은 원본과 로컬 계약의 차이를 찾고 안전하게 반영한다. 이것도 Codex 스킬로 만들었다.

---
name: sync-review-contract
description: >-
  실행 로그에서 최신 자동리뷰 프롬프트를 찾아 로컬 리뷰 계약과 비교한다.
  실행 데이터를 제거하고 검증을 통과한 계약 변경만 반영한다.
---

여기서 중요한 건 기본 동작이다.

"확인해줘"   -> 조회와 diff만
"반영해줘"   -> 검증 통과 후 파일 수정
"push해줘"   -> 별도로 명시했을 때만

스킬 선택과 파일 수정, 원격 저장소 반영을 같은 권한으로 묶지 않았다.


왜 실행 로그를 보나

원본 프롬프트 파일에 접근할 수 있다면 그 파일을 읽는 게 가장 단순하다. 하지만 실제 자동화에서는 저장소의 템플릿과 실행 시점의 프롬프트가 다를 수 있다.

  • 배포 과정에서 변수가 치환된다.
  • 실행 래퍼가 지시문을 앞뒤에 붙인다.
  • 설정은 바뀌었지만 문서 복사본은 갱신되지 않는다.
  • 여러 버전 중 어느 것이 실제로 호출됐는지 파일만 보고는 알기 어렵다.

이럴 때 실행 로그는 “현재 무엇이 돌았는가”를 보여주는 증거가 된다. 나는 Grafana에서 Loki 로그를 조회해 최근 자동리뷰 실행에 사용된 프롬프트를 찾는 방식을 택했다. Loki는 LogQL을 사용해 로그 스트림을 선택하고 필드나 내용으로 결과를 좁힐 수 있다. 자세한 문법은 Grafana Loki의 LogQL 문서에 정리돼 있다.

다만 review 같은 단어 하나로 찾으면 결과가 너무 넓다. 문서 검토, 코드 설명, 일반 대화가 모두 섞인다. 자동리뷰 계약에만 있는 강한 표식을 함께 사용해야 한다.

{service=~"<REVIEW_RUNNER>"}
| event="prompt"
| prompt=~"(?s).*EXISTING ISSUES.*"
| prompt=~"(?s).*NEW DIFF REVIEW.*"
| prompt=~"(?s).*decision.*thread_updates.*"

실제 필드 이름보다 중요한 건 검색 전략이다.

  1. 실행 주체와 이벤트 종류를 먼저 제한한다.
  2. 리뷰 계약의 여러 표식을 동시에 요구한다.
  3. 실행 시각을 알면 그 전후부터 찾는다.
  4. 모르면 최근 24시간부터 시작한다.
  5. 결과가 없을 때만 이전 24시간 구간으로 이동한다.

처음에는 7일이나 30일을 한 번에 찾고 싶었다. 하지만 긴 범위는 조회 비용이 커지고, 같은 계약의 반복 실행이 쌓여 최신 후보를 고르기도 어려워진다. 작은 시간 창을 뒤로 이동하다 최신 후보 한 건을 찾으면 멈추는 방식이 더 단순했다.


로그 원문은 곧 계약이 아니다

실행 로그에서 프롬프트를 찾았다고 그대로 저장하면 안 된다. 자동리뷰 프롬프트에는 실행할 PR의 데이터가 함께 들어갈 수 있다.

  • 저장소와 PR 번호
  • 제목과 브랜치
  • PR 본문과 대화
  • 기존 리뷰 스레드
  • 전체 diff
  • 실행 회차와 코멘트 상한

이 값들은 계약이 아니라 이번 실행의 입력이다.

그래서 로그 원문을 바로 저장하지 않고, 다음처럼 플레이스홀더를 복원한다.













정규화가 끝나면 남아야 하는 건 “어떤 입력을 받아 어떤 순서로 판단하고 어떤 형식으로 답하는가”뿐이다.

여기서 가장 위험한 실수는 로그 원문을 작업 저장소에 임시 파일로 남기는 것이다. diff와 댓글에는 공개하면 안 되는 정보가 들어 있을 수 있다. 원문은 조회 화면에서 비교 대상으로만 읽고, 저장이 필요하다면 실행 데이터가 제거된 후보만 임시 디렉터리에 둔다.

/tmp/sync-review-contract/candidate.md

임시 파일이라고 안전한 것은 아니다. 무엇을 저장했는지가 중요하다.


validator가 비교 전에 해야 할 일

사람이 diff를 보기 전에 기계적으로 막을 수 있는 건 먼저 막았다.

required_markers = [
  "EXISTING ISSUES",
  "NEW DIFF REVIEW",
  '"decision"',
  '"comments"',
  '"thread_updates"'
]

allowed_placeholders = Set.new(%w[
  REPOSITORY
  PULL_REQUEST_NUMBER
  REVIEW_ROUND
  MAX_COMMENTS
  TITLE
  HEAD_REF
  BASE_REF
  DESCRIPTION
  EXISTING_ISSUES
  CONVERSATION
  OMITTED_FILES
  DIFF
])

validator는 최소한 다음을 확인한다.

  1. 리뷰의 주요 단계와 출력 필드가 모두 남아 있는가
  2. 필수 플레이스홀더가 모두 있는가
  3. 알 수 없는 플레이스홀더가 새로 나타나지 않았는가
  4. 이메일, 실제 URL, 저장소 이름, 토큰처럼 실행 데이터가 남지 않았는가
  5. 후보가 현재 계약보다 비정상적으로 짧아지지 않았는가

특히 알 수 없는 플레이스홀더를 실패로 처리하는 것이 중요했다. 원본에 새로운 입력이 추가됐는데 정규화 과정에서 조용히 버리면, validator는 통과해도 계약이 불완전해진다. 새 입력은 자동으로 삭제할 값이 아니라 동기화 스킬과 계약을 함께 고쳐야 한다는 신호다.

validator가 통과한 뒤에야 현재 계약과 후보를 비교한다.

diff -u review-contract.md /tmp/sync-review-contract/candidate.md

diff도 종류를 나눠야 한다

프롬프트 diff의 모든 줄이 같은 의미를 갖지는 않는다.

분류 처리
계약 변경 심각도, 판단 순서, 출력 스키마 원본 계약 반영 후보
실행 데이터 제목, 댓글, diff, 브랜치 항상 폐기
표현 변경 설명 순서, 문장 다듬기 동작 변화 확인 후 선택
개인 정책 충돌 검증 방식이나 보이스와 모순 자동 반영 중단

이 분류 없이 전체 파일을 교체하면 “원본을 따라간다”는 이유로 개인 정책까지 지우게 된다. 반대로 눈에 익은 개인 문장을 보존하려다 원본의 중요한 판단 변경을 놓칠 수도 있다.

내가 정한 반영 원칙은 이렇다.

원본 리뷰 스킬

정규화된 공통 계약을 그대로 따른다. 원본과 같은 판단을 재현하는 게 목적이므로 개인 취향을 섞지 않는다.

개인 리뷰 스킬

공통 계약 파일을 참조한다. 원본 계약이 바뀌면 별도 복사 없이 새 판단을 사용한다. 개인 스킬 파일은 참조가 깨지거나 새 계약이 개인 정책과 충돌할 때만 최소한으로 수정한다.

이 차이는 작아 보이지만 운영에서는 크다.

원본 변경 -> 계약 파일 1곳 수정
          -> 원본 리뷰 스킬 즉시 반영
          -> 개인 리뷰 스킬도 공통 판단 반영
          -> 개인 확장 규칙은 그대로 유지

같은 문서를 두 벌 동기화하는 게 아니라, 공통 계약을 한 번 갱신하고 두 실행기가 함께 읽는 구조다.


자동으로 덮어쓰지 않는 이유

실행 로그에서 최신 후보를 찾았고 validator도 통과했다면 바로 교체해도 될까.

나는 몇 가지 경우에는 멈추게 했다.

  • 최종 결정의 허용 값이 바뀌었다.
  • JSON 필드가 추가되거나 사라졌다.
  • 리뷰 단계 하나가 통째로 없어졌다.
  • 최신 후보가 둘 이상인데 어느 것이 기준인지 불명확하다.
  • 개인 검증 정책과 새 계약이 충돌한다.
  • 실행 데이터가 완전히 제거됐는지 확신할 수 없다.

validator는 구조를 확인할 뿐 변경의 의도까지 알 수 없다. 예를 들어 출력 스키마 변경은 잘못이 아닐 수 있다. 하지만 그 변경은 결과를 소비하는 코드에도 영향을 준다. 자동 교체가 아니라 계약 마이그레이션으로 다뤄야 한다.

그래서 동기화 스킬의 기본값은 조회와 diff다. 파일 수정은 사용자가 “반영해줘”라고 했을 때만 한다. 커밋과 push도 별도 의도로 분리했다.

이 원칙은 같은 규칙을 다른 에이전트에 적용하며 정리했던 글의 결론과도 이어진다. 공유할 것은 판단 기준이고, 실행 방식과 관측 방식은 각 환경에 맞게 둬야 한다.


Codex 스킬로 만들며 정한 실행 문장

Codex 스킬은 반복 절차와 판단 기준을 묶는 데 잘 맞는다. OpenAI도 Codex 스킬 문서에서 스킬을 반복 절차와 도구 사용을 재사용 가능한 형태로 묶는 방식으로 설명한다.

나는 세 가지 문장을 구분해 사용한다.

차이만 확인

$sync-review-contract로 최근 실행된 리뷰 계약을 확인하고
현재 계약과 차이만 보여줘. 파일은 수정하지 마.

검증 후 반영

$sync-review-contract로 최신 리뷰 계약을 확인하고,
검증을 통과한 계약 변경만 개인 스킬 저장소에 반영해줘.

개인 정책으로 재리뷰

$personal-review로 이 변경을 재리뷰해줘.
이전 지적이 실제로 고쳐졌는지 현재 코드에서 확인해줘.

스킬 이름을 직접 쓰지 않아도 설명이 충분히 구체적이면 자연어로 연결될 수 있다. 다만 동기화처럼 파일 수정 가능성이 있는 절차는 실행 의도를 분명하게 적는 편이 낫다.


만들면서 피한 다섯 가지 함정

1. “최신 파일”과 “최근 실행”을 같은 것으로 보기

저장소의 프롬프트가 최신이어도 자동화가 그 버전을 실행했다는 보장은 없다. 반대로 로그가 오래됐을 수도 있다. 둘 중 하나를 절대적인 원본으로 두기보다 파일과 실행 증거를 대조해야 한다.

2. 긴 기간을 한 번에 검색하기

검색 범위가 길수록 최신 후보를 고르는 일이 어려워지고 조회 비용도 커진다. 실행 시각 전후 또는 최근 24시간부터 시작하는 편이 낫다.

3. 로그 원문을 저장하기

프롬프트와 함께 들어간 diff와 댓글은 계약이 아니다. 정규화가 끝난 후보만 저장한다.

4. 개인 스킬을 원본으로 덮어쓰기

개인 스킬은 포크가 아니라 오버레이다. 공통 계약을 참조하고 차이만 유지해야 다음 업데이트가 싸다.

5. 결과가 없으면 “변경 없음”이라고 말하기

검색 결과가 없다는 것은 변경이 없다는 뜻이 아니다. 조회 기간 안에 후보를 찾지 못했다는 뜻이다. 관측 실패와 상태 동일을 구분해야 한다.


마무리

처음에는 자동리뷰 프롬프트를 개인 스킬로 복사하고 싶었다. 하지만 복사만으로는 한 번의 실행을 만들 수 있을 뿐, 계속 믿고 쓸 수 있는 구조는 만들지 못한다.

오래 유지하려면 세 가지 경계가 필요했다.

  1. 원본 계약과 개인 정책의 경계
  2. 실행 데이터와 재사용 가능한 템플릿의 경계
  3. 차이 확인과 실제 반영 권한의 경계

Grafana의 실행 로그는 원본을 대신하는 저장소가 아니다. 현재 무엇이 실행됐는지 확인하는 관측 지점이다. 거기서 찾은 프롬프트도 바로 믿지 않고, 실행 데이터를 걷어내고, 플레이스홀더를 복원하고, validator와 diff를 통과시킨 뒤에야 계약 후보가 된다.

그리고 개인 스킬은 원본을 다시 복사하지 않는다. 공통 계약 위에 내가 중요하게 여기는 검증 방식만 얹는다.

프롬프트를 가져오는 일보다 중요한 건, 원본이 바뀌었을 때도 개인 정책을 잃지 않고 따라갈 수 있게 만드는 일이다.