0. Contents
1. Overview
1.1 승인에서 결과 표시까지
배경
- 02편은 PR 이 열리는 데서 끝난다. 그 시점까지 클러스터에는 아무 변화가 없다
- 승인 카드를 누르면 Kukie 가 PR 을 머지하고, ArgoCD 가 그 커밋을 sync 한다
- 카드를 만든 뒤 PR 이나 레포가 바뀔 수 있고, sync 는 몇 분이 걸린다
이 글의 질문
- 승인한 내용과 지금 내용이 같은지 어떻게 확인하는가
- 끝나는 데 몇 분 걸리는 일을 HTTP 요청 하나로 어떻게 다루는가
- 머지 직후 AI 답변이 나중에 틀린 말이 되는 문제를 어떻게 막는가
1.2 kubectl 승인과 PR 승인
| 항목 | kubectl 경로 | PR 경로 |
|---|---|---|
| 승인이 하는 일 | 클러스터에 바로 적용 | PR 머지 |
| 클러스터 반영 | 실행과 동시에 | ArgoCD sync 뒤 |
| 응답 시점의 결과 | 끝난 결과 | 머지까지만 |
| 되돌리기 | 다시 kubectl | revert PR |
이 차이가 5장의 장애와 6장의 답변 문제로 이어졌다.
2. Approval Card — 승인 카드
2.1 승인 전 PR 생성
- 승인 전에 PR 을 먼저 연다. 렌더 비교를 통과한 PR 만 카드가 된다
- 그래서 카드에 PR 번호와 링크, 실제 diff 를 싣고, 사용자는 GitHub 에서도 같은 내용을 본다
- 승인과 머지가 같은 일이다. 승인을 누르기 전에는 main 이 바뀌지 않는다
2.2 카드에 싣는 내용
| 칸 | 내용 |
|---|---|
| 클러스터에서 실제로 바뀔 것 | 렌더 diff. 지금 main 과 머지 뒤의 비교 |
| 레포에서 바뀌는 파일 | git diff. 접어서 보여 준다 |
| 대상 앱 | 경로, 형식, 자동 sync 와 self-heal 여부 |
| 예상 효과, 부작용 | AI 가 변경 툴을 부를 때 적은 값 |
| 승인 전에 볼 것 | 별도 검토 단계가 Plan 과 diff 만 보고 쓴 내용 |
| 진행 막대 | 승인 → PR 머지 → ArgoCD sync → 앱 상태 |
2.3 거절과 만료 시 PR 닫기
| 경우 | 동작 | 확인 |
|---|---|---|
| 거절 | 에이전트가 PR 을 닫고 이유를 코멘트로 남긴다 | 시험 PR #5 |
| 결정 없이 카드 만료 | 서버가 Plan 만 닫는다. PR 은 남는다 | 단위 테스트 |
| 만료된 PR 이 파일을 쥠 | 같은 파일의 다음 제안이 이 PR 을 닫고 진행한다 | 단위 테스트 |
세 번째 줄은 리뷰에서 나왔다. 카드가 결정 없이 끝나면 에이전트는 통보를 받지 못한다. 2편의 “같은 파일을 고치는 Kukie PR 이 열려 있으면 기다린다” 규칙 때문에 아무도 닫지 않으면 PR 경로가 영영 막힌다.
3. Recheck — 승인 순간 재확인
3.1 경로 재판단
- 승인 순간 02편 3장의 경로 판단을 다시 돌린다
- 판단 결과가 kubectl 이거나, 대상 앱과 레포의 지문이 카드를 만들 때와 다르면 실행하지 않는다
- 02편의 판단은 AI 를 막는 일이고, 여기는 사용자가 승인한 것과 같은지 대조하는 일이다
3.2 파일 지문과 base 대조
Git 은 파일 내용으로 blob id 를 만든다. 내용이 한 글자만 달라도 값이 달라지므로, 카드를 만들 때 저장한 값과 비교하면 파일이 그대로인지 알 수 있다.
3.3 일시 실패와 재시도
| 상황 | 결과 | 이유 |
|---|---|---|
| 조회 실패, 시간 초과 | 재시도 (RESUME_RETRYABLE) | 바뀐 증거가 아니다. 아무것도 안 했다 |
| 전제가 바뀜 | STALE, PR 닫음 | 다시 판단해 새로 제안해야 한다 |
| GitHub 가 머지를 거부 (405, 409) | 실패, PR 유지 | 사용자가 GitHub 에서 처리한다 |
| 머지 요청이 닿았는지 모름 | 올려서 재시도가 판단 | 결과 기록이 없으면 UNKNOWN 으로 닫는다 |
처음에는 승인 순간 조회가 실패해도 STALE 로 닫았다. 일시적 실패로 승인한 변경과 PR 을 버리는 문제가 리뷰에서 나와 나눴다.
3.4 GitHub 에서 먼저 머지된 PR
- 승인한 내용 그대로 머지됐으면 변경은 이미 들어갔다. STALE(“실행 안 함”)은 사실과 다르다
- 그래서 파일 지문과 머지 커밋의 첫 부모가
base_sha와 같으면 반영으로 보고, sync 지켜보기로 넘어간다 - 첫 부모만 본다. squash 와 rebase 는 부모가 하나이고, 머지 커밋은
[base, PR head]두 개이기 때문이다 - 다른 내용이나 다른 base 위에 머지됐으면 직접 확인하라고 안내한다
3.5 서버 경로 누락
시험 레포의 PR 에 커밋을 더하고 승인하는 시험을 실제로 돌렸다. 머지는 막혔는데 카드가 “결과를 확인하는 중이에요"에서 끝나지 않았다.

- 카드는 Plan 상태를 5초마다 읽는다. 끝난 상태가 오지 않으면 약 11분까지 돈다
- 에이전트의 단위 테스트는 가짜 서버를 쓴다. 가짜 서버에는 후속으로 미룬 이 경로가 이미 있었다
- 머지 안전에는 문제가 없었다. 사용자가 멈춘 것으로 오해할 수 있는 표시 문제다
4. Merge Guardrails — 머지의 제약
4.1 브랜치 보호와 PR 유지

4.2 동시 변경 잠금
| 대상 | 방식 | 막는 일 |
|---|---|---|
| 같은 방 | 방 잠금. 처리 중 승인과 재개는 409 BUSY | 같은 카드를 두 번 눌러 머지 두 번 |
| 같은 파일 | 레포·파일 단위 키 | 서로 덮어쓰는 PR 두 개 |
| 같은 PR | 머지 호출의 head_sha | 재확인과 머지 사이의 새 커밋 |
4.3 머지 기록 우선 저장
- 기록을 끝내 못 하면 지켜보기를 시작하지 않는다. 얹을 자리가 없기 때문이다
- 이 경우 결과의
stderr에 경고를 붙이고, 재실행은 차단한다 - 지켜보는 동안 앱은 Plan 의
sync_result를 읽어 진행을 표시한다

5. Incident — 머지 승인 HTTP 504
5.1 첫 실제 시험의 60초 끊김
시험 레포의 첫 PR 을 승인했더니 화면에 504 가 떴다. 머지는 성공했고, 클러스터 반영도 정상이었다.
| 시각 (UTC) | 일어난 일 |
|---|---|
| 12:22:23 | PR #1 머지 성공 |
| 머지 뒤 | 웹 로그에 승인 요청 499, 60.0초 |
| 12:26 | ArgoCD 반영 정상 |
변경은 성공했는데 사용자에게는 실패로 보였다. 다시 누르면 머지를 또 시도할 위험도 있었다. 그때까지 테스트와 픽스처로만 확인했고, 픽스처에는 CloudFront 의 시간 제한과 ArgoCD 의 조회 주기가 없었다.
5.2 sync 대기와 CloudFront 제한
제한을 늘리는 방법은 쓰지 않았다. CloudFront 를 늘려도 ALB 의 유휴 제한(60초)이 남고, 그동안 사용자는 도는 버튼만 본다.
5.3 응답과 지켜보기의 분리
| 항목 | 고치기 전 | 고친 뒤 |
|---|---|---|
| 승인 요청이 하는 일 | 머지 + sync 대기 | 머지까지 |
| sync 대기 | 요청 안, 180초 | 요청 밖, 600초 |
| 결과를 받는 곳 | 응답 본문 | Plan (sync_result) |
| 앱의 읽기 상한 | 60번 | 130번, 약 11분 |
- 앱은 진행 스트림이 아니라 Plan 을 읽는다. 새로고침해도 같은 상태를 보여 주기 위해서다
- 끊긴 요청의 핸들러는 취소되지 않는다. uvicorn 소켓을 띄운 시험으로 확인했다
- 배포 순서는 서버 → 에이전트 → 앱이다. 에이전트가 먼저 나가면
sync-result가 404 라 Plan 이syncing에 남는다
5.4 재시작 복구와 중복 머지 방지
에이전트가 재시작되면 메모리의 승인 티켓과 지켜보기가 사라진다. 기동할 때 서버에 끝나지 않은 PR 경로 계획을 물어 닫는다.
| 재시작 때 상태 | 처리 | 상태 |
|---|---|---|
머지 기록 있음 (syncing) | 이어 지켜보지 않음. 사용자 토큰이 없어 연결 정보를 받지 못한다 | interrupted |
| 머지 기록 없음, GitHub 에서 머지됨 | 머지 사실을 기록 | APPLIED + interrupted |
| 머지 기록 없음, PR 열림 | PR 을 남기고 다시 하라고 안내 | FAILED |
| 머지 기록 없음, 알 수 없음 | 사용자가 PR 을 확인 | UNKNOWN |
6. Stale Answer — 카드와 다른 AI 답변
6.1 카드 “정상"과 답변 “아직 4개”
504 를 고친 뒤 시험 PR #3 에서 카드와 답변이 서로 다른 말을 했다.
6.2 충돌한 지시
6.3 코드 문장으로 교체
02편과 같은 판단이다. 안내문과 프롬프트 5번도 고쳤지만, 화면에 나가는 문장은 코드가 정한다. 실행 단계 블록을 코드가 채우는 자리가 이미 있어 같은 곳에 넣었다.
| 항목 | 값 |
|---|---|
| 답변 | “PR #N 머지가 끝났어요. 반영 결과는 위 카드에서 볼 수 있어요.” |
| 원인·근거 칸, 다음 행동 안내 | 비움 |
| 답변 미리보기 | 같은 문장을 보낸다. AI 의 글이 잠깐 보였다 바뀌지 않게 하기 위해서다 |
| 다음 질문 | 평소대로 AI 가 조회해 답한다 |
시험 PR #4 에서 이 문장이 나왔고, 머지 뒤 응답은 약 10초에서 약 5초로 줄었다. 한 번 잰 값이다.
6.4 교체하면 안 되는 경우
첫 구현은 “머지 결과가 syncing 이면 교체"였다. 리뷰 세 판에서 지적 9건이 나왔고, 대부분 교체하면 설명이 사라지는 경우였다.
| 경우 | 문제 | 처리 |
|---|---|---|
| 기록을 못 한 머지 | 지켜보기가 돌지 않는데 “카드에서 보라"고 안내. 경고도 사라짐 | AI 가 쓴다 |
| 함께 보낸 변경의 실패, 거절 | 실패 설명이 덮임 | AI 가 쓴다 |
| 답 양식의 형식 재시도 | 실패로 착각해 수정이 통째로 꺼짐 | 세지 않는다 |
| GitHub 에서 먼저 머지된 PR | Kukie 가 머지한 것처럼 읽힘 | 문장을 “머지가 끝났어요"로 바꿈 |
| PR 번호 없음 | “PR #None” | AI 가 쓴다 |
형식 재시도는 코드로 읽어서는 확신할 수 없어, 실제 흐름을 재현하는 시험을 넣어 확인했다.
7. Discussion
7.1 확인 범위
| 내용 | 확인 방법 |
|---|---|
| 승인 → 머지 → 응답 | 시험 레포 PR #1~#4, #7 |
| sync 결과와 카드 “정상” | PR #2, #3 |
| STALE, 머지 차단, 카드 안내 | PR #6, #8 |
| 거절과 PR 닫기 | PR #5 |
| 머지 직후 답변 문장 | PR #4 |
| 202 응답 | 시험만. 실제 응답은 모두 45초 안에 끝났다 |
| 재시작 복구, 중복 클릭, 먼저 머지된 PR | 시험만 |
| main 이 앞선 경우, 브랜치 보호 | 시험과 픽스처만 |
| Kustomize 앱의 머지 | 실제 기록 없음 |
7.2 채팅 승인과 GitHub 리뷰 생략
- Kukie 의 승인은 GitHub 리뷰를 대신한다. App 이 리뷰 없이 머지한다
- 브랜치 보호가 리뷰를 요구하면 머지하지 않고 PR 을 남긴다. 팀의 규칙을 우회하지 않기 위해서다
- 카드의 diff 를 사람이 읽는다는 전제가 남는다. 작은 변경에서는 편하고, 큰 변경에서는 약한 지점이다
7.3 AI 호출을 없애는 구조
지금 구조는 AI 를 한 번 부른 뒤 그 글을 버린다.
- 쓰지 않을 답에 비용과 대기 시간이 든다
- 대화 기록의 출력 툴 인자에 원래 글이 남아, 다음 턴에 모델이 볼 수 있다
- 머지 앞에서
ToolFailed로 끝난 다른 변경은 정상 재시도와 구분되지 않아, 그 설명이 덮일 수 있다
머지가 끝나면 모델을 부르지 않고 코드가 바로 답하면 앞의 둘이 사라진다. 수정 범위가 커서 비용과 대기 시간이 문제가 될 때 한다.
핵심 메시지
- 승인의 대상: 커밋이 아니라 내용과 전제
- 파일 지문, base, 브랜치, 경로가 모두 같아야 머지한다
- 긴 작업: 제한을 늘리지 않고 요청과 분리
- 응답은 머지까지, 결과는 Plan 에 쌓고 화면이 읽는다
- 기록 순서: 머지한 사실이 먼저
- 어디서 죽어도 재시도가 다시 머지하지 않는다
- 고정되는 글: 바뀌는 상태를 적지 않는다
- 카드가 맡는 상태는 답변에 쓰지 않고, 코드로 막는다
- 가짜 서버의 한계: 시험을 통과해도 경로가 없을 수 있다
- 504 와 STALE 은 모두 실제 시험에서 처음 드러났다
