---
id: issue-fixer
type: project
doc: portfolio
url: https://auejin.com/ko/projects/issue-fixer/portfolio/
lang: ko
---

# Issue-Fixer — 끊임없이 쌓이는 실무 버그 큐를 사람 대기열 없이 소화하는 자율 수정 시스템

**프로덕션 5개월 상시 가동 기록 — 설계 · 사고 · 정량 임팩트**

> QA가 Jira에 버그를 등록하면, 대상 제품·환경 판별부터 실브라우저 검증까지 사람 손 없이 리뷰 가능한 PR로 돌아온다.
> **3D 공간 데이터 B2B SaaS 기업의 프론트엔드 모노레포를 대상으로 5개월간 프로덕션 상시 가동**(야간·주말 포함 무인) — 유입 924건, PR 571건, 이슈 감지→PR 중앙값 35분.
> 벤치마크가 아니라 실운영 데이터다. 이 문서의 모든 수치는 시스템 자체 분석 DB(SQLite)와 운영 문서에서 추출했으며, 산정 가정은 부록에 명시한다.
> 사내 정보 보호를 위해 회사명·제품명·이슈 번호는 일반화했다.

---

## 1. Executive Summary

- **무엇을 만들었나**: 실무에서 끊임없이 쌓이는 버그 티켓 큐를 **사람 대기열 없이 통째로 소화**하는 시스템. QA가 Jira에 버그를 등록하면 PR 제출까지 사람 손을 거치지 않고 이슈 수집 → 영상 분석 → 브라우저 재현 → 코드베이스 분석 → 수정 계획(자기 비판 포함) → 코드 수정 → 실브라우저 Before/After 검증 → 커밋/PR/Jira 전환/Slack 알림을 수행한다(최종 머지는 사람 리뷰 게이트). LLM CLI를 22개 독립 phase로 오케스트레이션하는 하네스, 실패 격리·폭주 방지 신뢰성 계층, git worktree 슬롯 풀 기반 동시 실행, 그리고 자기 성과를 감사해 스스로 개선 PR을 내는 메타 루프로 구성된다.
- **어디서 얼마나 검증했나**: 3D 공간 데이터 B2B SaaS 기업의 프론트엔드 모노레포에서 **5개월간 프로덕션 상시 가동**(launchd 상시 서비스, 야간·주말 포함 무인). 벤치마크나 데모가 아니라 달력일 기준 하루 평균 6건 이상이 계속 유입되는 실제 버그 큐를 상대로 한 운영 기록이며, 그 기간에 겪은 대형 사고 3건을 모두 재발이 구조적으로 불가능한 형태로 전환했다(§7). 장기 가동이 만들어 준 것은 신뢰성 서사만이 아니라 **평가 데이터셋 자체**다 — 924건의 사람 판정(머지/폐기)과 후속 커밋이 RCA 정확도와 자가 개선 루프의 ground truth가 된다.
- **성과**: 5개월간 고유 이슈 924건 처리, PR 571건 자동 생성(이슈 감지→PR 중앙값 35분), 리뷰 완료 PR 머지율 35.4%(그중 82.4%는 사람 수정 0건), RCA 정확도 82.9%. 초기 감사 시점 머지율 19.5%에서 자가 개선 루프 가동 후 누적 35.4%로 +15.9pp.
- **엔지니어링 관점의 핵심**: 이 시스템의 어려움은 "LLM을 잘 프롬프팅하는 것"이 아니라, **신뢰할 수 없는 확률적 실행 단위(LLM 세션)를 신뢰할 수 있는 시스템 부품으로 만드는 것**이었다. 완료 판정·실패 분류·판정값·동시성·자기 평가 전부에서 LLM의 자유 발화를 타입과 게이트로 강등하는 설계가 반복 주제다.

### 코드 고치는 에이전트와 무엇이 다른가

버그를 고치는 AI는 흔하다. 이 시스템이 다른 지점은 **고치는 행위 앞뒤에 붙는 것들**이다 — 무엇을 고칠지 스스로 집어오는 트래커 운영, 고치기 전에 세우는 제품 컨텍스트, 여러 건을 동시에 굴리는 격리, 고쳤다는 주장을 반증하는 검증, 그리고 자기 성적표를 만들어 스스로 개선하는 루프.

| 흔한 버그 수정 에이전트 | Issue-Fixer | 근거 |
|---|---|---|
| 사람이 이슈를 골라 프롬프트로 떠먹여 준다 | **트래커를 직접 운영한다** — Jira를 5분 주기로 폴링해 대상을 스스로 집고, 상태 전이·질문 댓글·실패 롤백까지 이슈 라이프사이클을 소유 | §3, §5-4 |
| 레포를 읽고 바로 코드를 고친다 | **고치기 전에 제품 컨텍스트를 확정한다** — QA 링크로 대상 앱·환경 판별, 첨부 영상 → 키프레임 → VLM으로 촬영 조건(날짜·층·뷰 모드) 복원, 코드베이스 분석과 과거 PR 이력 주입 | §5-3 |
| 한 번에 한 이슈 (단일 체크아웃) | **동시에 굴린다** — git worktree 슬롯 풀로 이슈별 격리, 잡 사이 리셋 1.1초, 경합의 원인이던 "보류" 상태는 스키마에서 표현 자체가 불가능하도록 제거 | §5-2 |
| "고쳤습니다"라는 LLM 선언이 곧 완료 신호 | **LLM의 발화를 완료 신호로 쓰지 않는다** — 실브라우저 Before/After 반증, 판정은 타입드 enum, 실패는 5단 분류학, 완료는 마커 프로토콜 | §5-1, §5-3 |
| 개선하려면 사람이 프롬프트를 손본다 | **스스로 개선 PR을 낸다** — 격주 KPI 자가 감사(숫자는 전부 결정적 코드가 계산) → 로드맵 자동 구현 → draft PR → 다음 사이클이 효과를 통계 검정 | §5-5 |
| 벤치마크·데모에서 검증 | **프로덕션 5개월 상시 가동** — 유입 924건 소화, 대형 사고 3건을 재발 불가능한 구조로 전환 | §6, §7 |

---

## 2. 문제 정의 — 멈추지 않는 유입

이 시스템이 상대한 것은 "어려운 버그 한 건"이 아니라 **끊기지 않는 유입 흐름**이다. 실측 유입은 5개월(약 150일) 924건 — 달력일 기준 하루 평균 6건 이상이고, 시스템의 피크 처리량은 하루 35건까지 올라갔다(§7 사고 C의 붕괴 전 기준). 개발자 대기열은 이 속도를 흡수하도록 만들어져 있지 않다.

| 문제 | 상세 |
|---|---|
| **유입 속도** | 하루 6건 이상이 상시 유입 — 한 건을 잘 고치는 능력이 아니라 **큐 전체를 소진하는 처리량**이 요구 조건이 된다 |
| 반복 디버깅 비용 | 버그마다 재현 → 원인 파악 → 수정 → 검증 사이클이 개발자의 컨텍스트를 점유 |
| 재현 비용 | QA 환경 접속, 첨부 영상 확인, 재현 조건 맞추기에 상당 시간 소요 |
| 리드타임 | 버그가 개발자 대기열에서 수일 대기 — 수정 자체보다 대기가 길다 |
| 지식 손실 | 과거에 실패한 수정 시도의 교훈이 다음 시도에 반영되지 않음 |

목표는 "개발자 대체"가 아니라 **리뷰 가능한 품질의 수정 PR을 사람 대기열 없이 수십 분 안에 공급**하는 것으로 잡았다. 따라서 최종 게이트는 항상 사람의 코드 리뷰이며, 시스템의 성공 지표도 자체 완료율이 아니라 **사람이 실제로 머지했는가**(및 머지 시 사람 수정이 필요했는가)로 정의했다.

유입이 멈추지 않는다는 성질은 설계 전반을 규정했다. 처리량이 목표가 되면 동시성이 필수가 되고(§5-2), 동시성은 공유 자원 경합과 연쇄 실패를 부르며(§7 사고 B), 사람이 밤새 지켜보지 않는 상시 가동에서는 실패 처리 한 곳의 결함이 폭주로 증폭된다(§7 사고 A). "한 건을 잘 고치는 에이전트"였다면 만나지 않았을 문제들이다.

---

## 3. 시스템 아키텍처

```mermaid
flowchart TB
    subgraph Jira["Jira"]
        I["버그 이슈 (AI Review = Required)"]
    end
    subgraph Server["서버 (Node.js, 단일 리더)"]
        P["Poller (5분 주기)"]
        Q["Bull Queue (Redis)<br/>메인 큐 concurrency 2 + 브라우저 전용 큐 1"]
        R["Phase Runner<br/>phase별 독립 LLM CLI 세션"]
        FT["실패 원장 + 서킷 브레이커 (Redis)"]
        WP["Worktree 슬롯 풀<br/>이슈별 격리 작업 트리"]
    end
    subgraph Phases["워크플로우 (22 phase, 발췌)"]
        C[COLLECT] --> V[VIDEO_ANALYZE] --> RE[REPRODUCE<br/>Playwright 실브라우저]
        RE --> A[ANALYZE] --> PL[PLAN<br/>+ 자기 비판] --> F[FIX] --> VA[VALIDATE<br/>실브라우저 Before/After]
        VA -->|실패 시 최대 3회| A
        VA -->|성공| CM[COMMIT → PUBLISH]
    end
    subgraph Meta["자가 개선 메타 루프"]
        DB["분석 DB (SQLite)"] --> BR["격주 KPI 감사 보고서<br/>(숫자는 코드가 계산)"]
        BR --> RA["로드맵 자동 구현기<br/>→ draft PR"]
        RA -.->|사람 머지 후| DB
    end
    I --> P --> Q --> R --> Phases
    R <--> WP
    R <--> FT
    CM --> PR["PR + Jira 전환 + Slack"]
    R -.이벤트.-> DB
```

핵심 설계 결정:

- **Phase별 독립 세션 (컨텍스트 격리)**: 전 과정을 한 세션으로 돌리면 컨텍스트가 수십만 토큰으로 자라 정확도가 무너진다. phase마다 새 LLM CLI 프로세스를 띄우고, 이전 phase의 산출물(JSON/Markdown 파일)만이 다음 phase의 컨텍스트가 된다. phase 정의(순서·산출물·조건부 실행·입력 대기 허용 여부)는 단일 JSON이 SSoT다.
- **파일 스키마가 곧 인터페이스**: 대화 히스토리가 없으므로 산출물 스키마가 phase 간 계약이다. 오케스트레이터는 phase 종료 후 산출물 존재를 검증하고, 누락 시 재시도 대상으로 분류한다.
- **판정의 enum 강등**: 재현 결과(REPRODUCED/NOT_REPRODUCED/WORKS_AS_EXPECTED/BLOCKED), 검증 결과(FIXED/NOT_FIXED/REGRESSION/BLOCKED) 등 모든 판정은 자유 텍스트가 아니라 파일 기반 타입드 enum으로 받아 switch 분기한다 — 판정값마다 후속 조치(진행/재시도/자동 종료/롤백)가 정반대이기 때문이다.
- **단일 실행자 보장**: Redis leader lease(CAS)로 서버 프로세스를 단일화하고, lease 갱신 루프는 worker thread로 격리해 메인 스레드 블로킹이 리더십 상실로 이어지지 않게 했다. 모든 git/GitHub 조작은 fail-closed 봇 신원(전용 GitHub App)으로만 — 토큰이 없으면 사람 계정으로 폴백하는 대신 반드시 실패한다.

---

## 4. 업계 패러다임과의 정합

2024–2026년 에이전트 엔지니어링 담론의 핵심 개념들과 이 시스템의 구현 대응이다. (대표 문헌: Anthropic *Building Effective Agents* · *Effective Context Engineering for AI Agents*, Databricks *What is an AI Agent Harness?*, LangChain·Temporal의 durable execution 문서)

| 업계 패러다임 | 이 시스템의 구현 |
|---|---|
| **Agent Harness** (모델을 행동으로 바꾸는 실행 계층) | phase 오케스트레이터 + 도구/산출물 계약 + 검증 루프 + 가드레일 + 관측을 묶은 자체 하네스. §5-1 |
| **Workflow vs Agent 스펙트럼** | 경로가 예측 가능한 구간은 코드 워크플로우(결정론적 소스 판별, 브랜치 준비), 판단 구간만 LLM phase — "필요한 곳에만 자율성" 원칙 |
| **Context Engineering** | phase별 fresh context + 산출물 파일 큐레이션 + 조건부 컨텍스트 주입(이미지·과거 PR 이력) |
| **Durable Execution** | 체크포인트(STATE 파일) 기반 재개, 실패 유형별 재시도 정책, 대기(pending input) 후 재진입 |
| **Grounding / 실환경 검증** | Playwright 실브라우저 재현·검증, 영상 → 키프레임 → VLM 분석, 로컬 트리 serve로 "수정본을 실제로 실행"한 검증 |
| **Human-in-the-Loop** | 재현/검증의 human 모드(사람은 시연만, 해석은 VLM), 입력 대기 프로토콜, draft PR + 사람 머지 게이트 |
| **Guardrails & Reliability** | 실패 원장 + 서킷 브레이커, 오류 분류학(이슈 격리 vs 전체 정지), backpressure(defer-not-fail). §5-4 |
| **Multi-agent Concurrency & Isolation** | git worktree 슬롯 풀, admission control, 공유 자원 임계구역화. §5-2 |
| **LLM-as-Judge & Evals** | RCA 정확도(ground truth = 사람의 후속 행동), 폐기 사유 LLM 분류(화이트리스트 enum 강제), KPI 감사 보고서의 통계 검정 |
| **Self-improving Systems** | 보고서 로드맵 → 자동 구현 PR → 회귀 측정의 닫힌 루프. §5-5 |

---

## 5. 기술 하이라이트 (문제 → 설계 → 트레이드오프)

### 5-1. 에이전트 하네스: LLM 프로세스의 "끝"과 "실패"를 신뢰 가능하게 만들기

**문제**: LLM 서브프로세스에는 신뢰할 수 있는 완료/실패 신호가 없다. (1) 브라우저 phase는 dev server·Playwright가 같은 프로세스 그룹에 살아 있어 작업을 끝내고도 종료하지 않는다 — exit code만 믿으면 정상 완료가 타임아웃(실패)으로 오판된다. (2) "이 이슈를 못 고친다"(이슈 하나의 문제)와 "자격증명이 폐기됐다"(전체 시스템의 문제)가 똑같은 exit code 1로 나타난다.

**설계**:
- **완료는 마커 프로토콜로**: 프롬프트가 완료 시 `PHASE_COMPLETE` 마커 출력을 지시하고, 오케스트레이터는 스트림을 파싱해 **AI가 직접 작성한 텍스트 채널에서만** 마커를 검사한다 — 에이전트가 문서를 grep한 도구 출력에 마커 리터럴이 섞여도 오탐하지 않는다. 마커 관측 후 유예 시간을 주고 프로세스 그룹 전체를 SIGTERM→SIGKILL로 회수하며, 이 경로의 비정상 exit는 성공으로 처리한다(결과물 검증은 별도).
- **실패는 5단 분류학으로**: 재시도 가능(출력 누락·타임아웃) / 이슈 영구 실패(→롤백) / 사용자 입력 필요(→Jira 질문) / 하네스 정지(→전역 브레이커) / 셧다운을 타입으로 분리. 하네스 정지 판정은 exit code가 아니라 출력 텍스트의 시그니처(401, credit 소진 등) 매칭으로 하며, **절대 이슈 격리(롤백)로 승격하지 않는다** — 승격하면 롤백이 재진입 조건을 복원해 무한 루프가 된다(§7 사고 A).
- **모델과 예산은 한 세트**: phase 타임아웃 예산은 특정 모델의 턴/도구 호출량에 캘리브레이션된 값이다. CLI 자동 갱신이 모델을 바꾸자 처리량이 붕괴한 사고(§7 사고 C) 이후, 모델 버전을 코드에 핀하고 모든 spawn 지점에 강제 주입하며, 예산 변경은 phase별 메트릭(JSONL sidecar)의 p90 실측으로만 한다. 타이밍 상수 간 부등식(phase 최장 예산 < drain 상한 < 큐 lock < 리더 TTL)은 단일 모듈에 모아 부팅 시 assert한다 — 잘못된 상수 조합으로는 서버가 뜨지 않는다.

**트레이드오프**: 마커 프로토콜은 프롬프트 규율에 의존한다 — 프롬프트에 teardown 계약을 명시하고, 스냅샷 테스트로 프롬프트 회귀를 잡는다. "결과물이 있으면 과정의 비정상 종료는 묻지 않는다"를 택하되, 역방향(exit 0인데 결과물 없음)은 반드시 실패 처리해 거짓 성공을 차단한다.

### 5-2. 동시성: 측정 → 격리 → 개방 순서의 worktree 슬롯 풀 전환

**문제**: 모노레포 체크아웃 하나를 모든 이슈가 시분할 공유했다. 배타 락 때문에 concurrency 설정을 올려도 두 번째 워커는 재큐만 반복하고(병목이 설정이 아니라 아키텍처 전제), 처리량이 오르면 stash 탈취·복원 실패·보류 연쇄 같은 경합 사고가 계단형으로 터졌다 — 관측 94일 중 경합 발생일은 6일뿐이고 전부 처리량 상승기에 몰렸다. 즉 "지금 문제 없음"이 안전의 증거가 되지 못하는 환경이었다.

**설계** (단계별, 각 단계에 정량 게이트):
- **1단계 — 측정**: "체크아웃은 하나뿐" 전제가 스며든 지점을 5개 계층 30여 곳에서 전수 열거하고 각각 처분 태그(소멸/대체/개조/유지)를 붙였다. 경합 이벤트의 최악일 피크(사고일 하루 지연 재큐 12,123건)와 phase 소요 p50/p90을 로그에서 박제 — 이것이 전환 성공 판정의 비교 기준이 된다.
- **2단계 — 격리**: 슬롯 = git worktree(오브젝트 공유) + 서브모듈 reference 공유 + node_modules 3GB의 APFS copy-on-write 복제. 실측 신규 프로비저닝 48.2초(슬롯당 1회), **잡 사이 리셋 1.1초** — 기존 브랜치 준비 p50 100초 대비 자릿수 개선. 이 실측이 "일회용이 아니라 warm pool"이라는 아키텍처 결정을 내렸다. concurrency는 1을 유지해 슬롯 생명주기 버그를 동시성 노이즈 없이 걸러냈다.
- **3단계 — 개방**: 12시간 소킹(경합 이벤트 0 확인) 후 concurrency 1→2. 실효 동시성은 `min(설정값, 풀 크기)` 단일 함수로 계산하고, 동일 이슈 이중 실행 가드와 여유 슬롯 admission control을 함께 배선했다. 추가 확대(2→N)는 기준선 문서의 정량 게이트(p90 예산 여유율) 통과가 조건이다.
- **hold(보류) 개념의 구조적 제거**: 구 설계의 "오염된 트리를 사람이 볼 때까지 보류"는 보류 1건이 대기 큐 전체의 실패 예산을 태우는 사고(§7 사고 B)를 만들었다. 풀에서는 리셋 불가능한 슬롯을 폐기 후 재프로비저닝(~68초 = 폐기 20초 + 신규 프로비저닝 48초)으로 흡수한다 — 리스 스키마에 보류 필드 자체가 없어 그 사고 경로가 표현 불가능하다.
- **잔여 공유면의 통제**: worktree 격리는 all-or-nothing이 아니다. 공유 git object DB의 auto-gc 경합(비활성화 + 유휴 시 수동 gc, alternates를 깨는 prune 금지), 워크스페이스별 빌드 데몬 정리(보수적 판정 — 오살보다 미살), 슬롯을 타지 않는 코드 경로의 자체 직렬화, 단일 포트 dev server의 임계구역화(serve 기동→브라우저 작업→정지 전체를 뮤텍스로) 등 — 대부분 concurrency 1 시절에는 큐가 공짜로 직렬화해 주던 것들이라, 사고가 아니라 **사전 실사와 커밋 전 감사에서** 찾아냈다.

**트레이드오프**: 속도 대신 검증 가능성 — 단계마다 이전 단계 검증 전 다음을 열지 않았다. 브라우저 구간은 여전히 직렬(dev server가 단일 포트)이지만 비-브라우저 구간의 병렬화로 이득 대부분을 먼저 얻고, 임시 직렬화 장치에 대기 시간 전수 계측을 내장해 다음 단계(슬롯별 dev server) 착수의 실측 근거를 쌓고 있다.

### 5-3. 그라운딩: "수정본이 실제로 돌아가는가"를 실브라우저와 VLM으로

**문제**: 코드를 고쳤다는 선언과 버그가 고쳐졌다는 사실 사이에는 큰 간극이 있다. 특히 대상이 WebGL 캔버스 중심 앱이라 DOM assertion이 불가능한 영역이 많고, 스테이징 URL에서 검증하면 수정 전 코드를 보고 FIXED를 판정하는 원천적 거짓 성공이 생긴다.

**설계**:
- **로컬 트리 serve**: QA 링크를 파싱해 대상 앱/환경을 판별하고, **수정본이 들어 있는 작업 트리**(풀 모드에서는 해당 이슈의 슬롯)를 로컬 serve해 같은 화면을 재현한다. 프롬프트는 스테이징 폴백을 금지한다.
- **재현·검증의 실험 기록 스키마**: 재현(REPRODUCE)과 검증(VALIDATE) 산출물이 조건–변인–반응의 공통 스키마를 공유하고, 판정은 4-enum으로 강제된다. 검증 실패 시 실패한 수정을 아카이브하고 계획을 강등한 뒤 clean base에서 재시도(최대 3회) — 재현 로그와 검증 로그의 diff가 다음 시도의 컨텍스트로 주입된다.
- **영상 → 키프레임 → VLM**: 첨부 재현 영상을 scene detection + 사용자 입력 시점 병합으로 증거 프레임 셋으로 압축한다. 재현 실패의 최대 원인이 프레임 해석이 아니라 "녹화 당시와 재현 환경의 조건 불일치"라는 관측에서, 촬영 컨텍스트(날짜·층·뷰 모드) 추출을 1급 산출물로 승격했다.
- **HITL 최소 개입**: AI가 재현하기 어려운 버그는 사람이 브라우저에서 한 번 시연하면 된다 — 페이지에 이벤트 계측을 주입해 클릭/키 입력의 ground truth를 확보하고, 해석·판정은 VLM이 수행하며, 산출물 스키마를 AI 모드와 동일하게 맞춰 다운스트림 무변경으로 합류한다. "시연은 했는데 판정이 비어 있는 상태"는 센티널 값 + 오케스트레이터 가드로 커밋 경로 진입이 차단된다.

**트레이드오프**: enum 강제는 판정의 뉘앙스를 버린다(사유 텍스트 필드로 보완, 미지의 상태는 보수적으로 실패 처리). 브라우저 재현 playbook을 iteration 간 재사용해 자원을 아끼는 대신, 수정이 재현 조건 자체를 바꾸는 케이스의 한계를 문서화했다.

### 5-4. 신뢰성: 폭발 반경을 타입으로 인코딩하고, 종료 조건은 루프 밖에

**문제**: 이 시스템의 롤백은 Jira 상태를 폴러의 선택 조건과 정확히 같은 값으로 되돌린다 — 즉 **롤백 자체가 재진입을 보장하는 연산**이다. 실패 처리가 조금이라도 어긋나면 자동화 시스템 특유의 무한 루프·연쇄 실패로 직결된다(실제 사고 이력이 §7에 있다).

**설계**:
- **내구성 실패 원장**: 재시도 카운터를 롤백이 삭제하는 캐시 디렉토리가 아니라 Redis에 둔다(이슈별 3회 한도). 원장과 인메모리를 모두 증가시켜 max를 취하고(Redis 단절 구간의 실패 소실 방지), 카운터 증가와 TTL 설정은 트랜잭션으로 묶는다(TTL 없는 고아 카운터 차단). 원장 읽기 자체가 실패하면 "재시도할 근거가 없다"며 종결 처리한다 — 하네스가 아플 때 Redis도 같이 아플 확률이 높다는 전제의 fail-safe다.
- **전역 서킷 브레이커**: 연속 5회 실패 또는 하네스 정지급 오류 1회에 폴링을 정지하고 TTL 없는 latch를 남긴다. 원인 미해소 상태의 자동 재개를 봉쇄하고, 해제는 사람의 API 동선뿐이다.
- **backpressure — 점유는 실패가 아니다**: 공유 자원 점유를 실패로 처리하면 이슈가 오류 상태로 잠겨 사람이 하나씩 복구해야 한다. 대신 지연 재큐(defer)로 처리하되, **대기 원인별로 실패 예산을 분리**한다 — 정상 경합 예산과 보류 대기 예산이 하나면, 보류 1건이 대기자 전원의 예산을 태운다(§7 사고 B의 산술). 재큐 성공 보고는 대체 잡의 존재를 확인한 뒤에만 한다 — 대체 잡 없이 성공을 보고하면 이슈가 큐에서 흔적 없이 사라진다.
- **창발적 실패의 회귀 테스트화**: 폭주 사고의 원인은 모듈 하나의 버그가 아니라 모듈 연결이 이룬 완전한 순환이었다 — 유닛 테스트 전부가 통과하는 상태에서 시스템이 폭주했다. 그래서 이음매(spawn argv ↔ 실제 파서, 설정 JQL ↔ 종결 상태값)와 **순환 전체의 유한 수렴**(폴러→워커→롤백 순환을 실제로 돌려 쓰기 횟수가 상수에 묶이는지)을 통합 테스트 자산으로 만들었다. 사고 당시의 실제 로그 라인이 하네스 정지 경로를 타는지도 테스트에 박아뒀다.

**트레이드오프**: 브레이커 해제가 수동뿐이라는 운영 부담, 정상 실패 3회 하드캡(일시 장애로 종결된 이슈는 사람이 원장을 비워야 재개)을 감수했다 — "멈추고 사람을 부르는 쪽이 무한 루프보다 항상 싸다"는 명시적 선택이다.

### 5-5. 자가 개선 루프: 숫자는 코드가, 서사만 LLM이

**문제**: 자기 성과를 보고하는 LLM은 지표를 게임한다 — p-value를 지어내고, KPI 분모를 슬쩍 바꾸고, 이미 있는 기능을 "신설"로 재포장한다. 또 자기 코드를 고치는 에이전트에게는 "한계를 심각도 낮음으로 자평"하고 "해결했다고 선언만 하는" 유인이 구조적으로 존재한다.

**설계**:
- **평가의 ground truth는 사람의 후속 행동**: PR이 머지/폐기될 때, 사람의 후속 커밋 파일 집합과 AI 변경 파일 집합의 겹침 비율로 RCA 등급(CORRECT/PARTIAL/INCORRECT)을 계산한다. 초기 공식(분모=사람 파일, recall)은 사람이 lint 한 줄만 고쳐도 등급이 추락하는 왜곡이 있어 precision(분모=AI 파일)으로 재정의하고 과거 데이터 전체를 소급 재채점했다. 판정 불가능 케이스는 억지로 점수화하지 않고 INDETERMINATE로 분리한다 — 그리고 보고서가 "분모 축소로 인한 100% 착시"를 스스로 지적하게 만들었다.
- **격주 감사 보고서**: 섹션별 독립 LLM 세션(하네스와 같은 패턴)으로 생성하되, **통계 수치는 LLM 계산이 전면 금지**다. 순수 JS로 구현한 통계 모듈(Wilson CI, Fisher exact, BH-FDR, beta-binomial posterior, seeded bootstrap)이 낸 값만 인용할 수 있고, 모든 수치에 KPI 공식 정의의 해시가 따라붙는다. KPI 게이밍 문구(분모 제외, 공식 재정의, 기존 기능 신설 포장)는 결정적 validator가 정규식으로 차단하고, 효과 주장에는 필요 표본수 인용을 강제한다 — 표본이 부족하면 제안이 관찰 전용으로 강등된다. 실제로 한 사이클 보고서는 top-3 제안 전원을 "판정 유예"로 출력했고, 섹션 생성이 실패한 보고서는 조작 없이 실패 placeholder 상태로 발행이 차단된 채 남아 있다.
- **닫힌 루프 — 보고서가 시스템을 고친다**: 보고서 로드맵을 파싱해 initiative별로 scope → 구현 → 테스트 → 커밋을 자동 수행하고 draft PR을 낸다. 자기관대 편향은 권한 분리로 차단한다: 한계(limitation)의 심각도는 하드코딩된 규칙만 부여할 수 있고, 해결 선언은 해당 커밋이 그 한계의 파일을 실제로 건드렸는지 git으로 검증해야 통과하며, 게이트 통과 여부는 스크립트의 exit code가 결정한다. 최종 게이트는 draft PR + 사람의 머지다.
- **회귀 측정의 내장**: 다음 보고서는 "지난 제안이 실제로 효과 있었나"를 먼저 판정한다 — 이행 여부는 커밋 대조로, 인과 효과는 pre/post 분리 + 통계 검정으로, 판정 라벨은 코드 전유물로. 그 결과가 다음 로드맵의 가점/감점이 된다. 이 루프의 완전한 한 바퀴(보고서 제안 → 봇 구현 → 사람 머지 → 다음 보고서가 효과 참조)가 저장소 이력에 남아 있다.

**트레이드오프**: 판정 보수성(소표본 전부 유예)은 실제 개선의 인정을 수 사이클 늦춘다 — 대신 "몇 사이클 뒤 판정 가능한가"를 로드맵에 명시하고, 표본 부족 자체를 최우선 개선 과제로 강제 승격하는 메타 규칙으로 보완했다.

---

## 6. 정량 임팩트

### 6-1. 시스템 지표 (5개월, 분석 DB 직접 집계)

핵심은 개별 수치가 아니라 **유입과 소화의 균형**이다: 하루 6건 이상 들어오는 큐를 사람 대기열 없이 받아, 61.8%를 리뷰 가능한 PR로 되돌려보내고, 나머지는 자동 triage로 걸러내거나 종결시켰다.

| 지표 | 값 | 비고 |
|---|---|---|
| 유입 압력 | 약 150일간 924건 | 달력일 기준 하루 평균 6건 이상, 피크 처리량 하루 35건 |
| 처리 고유 이슈 | 924건 | 워크플로우 실행 7,839회 — 재시도 포함, §7 사고 A의 폭주분 5,526회도 포함 |
| 자동 필터링 | 176건 | 비버그/중복/스코프 밖 — triage 자동화 |
| PR 생성 | 571건 (61.8%) | 이슈 감지 → PR 중앙값 35분, p90 59분 |
| 머지 | 159건 / 리뷰 완료 449건 (35.4%) | 분모는 머지 159 + 폐기 290. 아직 리뷰가 끝나지 않은 오픈 PR 122건은 제외 — 생성 PR 전체(571건) 기준으로는 27.8% |
| clean merge 비율 | 82.4% (131/159) | 인간 개입률 17.6% |
| RCA 정확도 (가중) | 82.9% | 판정 가능 237건 기준¹ |
| 폐기 사유 분해 | AI 실패 128 / 외부 요인 107 / 미분류 55 | AI 실패: 수정 유발 버그 57, 잘못된 수정 방향 55, 잘못된 타겟 16 |

> ¹ 가중 공식: (CORRECT×1.0 + PARTIAL×파일 겹침 비율) ÷ 판정 가능 건수. 판정 가능 237건 = 리뷰 완료 449건 − 비교 대상(사람의 후속 커밋 또는 대체 PR) 부재 212건. 사람 수정 0건으로 머지된 clean merge는 정의상 CORRECT로 계상되므로, 이 지표는 머지 성공과 완전히 독립인 지표가 아니라 "AI가 지목한 파일이 사람의 판단과 얼마나 겹쳤는가"의 척도다.

머지율 궤적: 첫 감사 보고서 시점 19.5% → 누적 35.4% (+15.9pp). 30일 창 산식과 누적 산식의 차이가 있어 직접 비교는 근사치이며, 궤적도 단조 상승이 아니었다(중간 사이클 38.5% → 사고 구간 하락 → 회복). 이 기간 자가 개선 루프가 구현한 개선(수정 사이드이펙트 자가 검증, 계획 대안 강제, 짝 함수 계약 주입)이 있었으나, 인과는 사이클별 통계 검정(§5-5)의 보수적 판정을 따른다 — 상관과 인과를 구분하는 것이 이 시스템 자신의 보고 원칙이다.

### 6-2. 시간·비용 환산 (가정 전부 명시)

**가정**: (A1) 프론트엔드 버그 1건의 개발자 처리 시간 2~4시간(중간 3시간), (A2) AI PR 리뷰 비용 건당 0.25~0.5시간(중간 0.375시간) — 폐기된 PR 포함 전체 449건에 부과, (A3) 사람 수정 후 머지된 28건은 절감을 50%만 인정, (A4) 시간당 완전 고용 비용 4~6만 원(중간 5만 원).

| 시나리오 | 절감 | 리뷰 비용 차감 | 순절감 (5개월) | 금액 환산 |
|---|---|---|---|---|
| 보수 | 290h | −225h | **65h** | 약 260만 원 |
| 중간 | 435h | −168h | **267h** | 약 1,300만 원 |
| 낙관 | 580h | −112h | **468h** | 약 2,800만 원 |

환산에 넣지 않은 편익: 리드타임(사람 대기열 수일 → PR까지 35분), 개발자 컨텍스트 스위칭 제거, 924건 자동 triage, 야간·주말 무인 처리. 환산에 넣지 않은 비용: LLM API·운영 인프라(데이터 미보유로 미산정 — 정직하게 공란).

> 수치 산출의 재현성: 모든 DB 수치는 단일 쿼리 스크립트로 재현되며, 스크립트와 쿼리별 매핑 표를 내부 부록으로 유지한다. 조회할 수 없는 수치는 싣지 않았다.

---

## 7. 사고와 구조적 해결 — 케이스 스터디 3건

프로덕션 자동화 시스템의 실력은 사고가 없는 것이 아니라, **사고를 어떤 구조로 전환하는가**로 드러난다. 세 건 모두 실제 사고이며, 각각이 시스템의 핵심 설계 원칙이 됐다.

세 건의 공통점은 **짧은 시연으로는 원리적으로 만날 수 없는 사고**라는 것이다. 폭주 루프는 인증 토큰이 만료될 만큼 오래 돌아야 나타나고, 공유 자원 연쇄 실패는 대기열이 실제로 쌓여야 성립하며(관측 94일 중 경합 발생일은 6일뿐, 전부 처리량 상승기), 성능 붕괴는 의존성이 자동 갱신될 만큼의 시간이 지나야 촉발된다. 장기 상시 가동은 이 시스템의 자랑거리가 아니라 **이 설계들이 존재하는 이유**다.

### 사고 A — 폭주 루프 (2026년 7월)

- **현상**: 인증 토큰 폐기(401)로 수집 phase가 실패 → 롤백 → 롤백이 이슈를 폴러 선택 조건으로 복원 → 재진입. 이틀간 롤백 5,534회, Jira 전환 16,599회 (분석 DB에서 해당 이틀의 실행 5,526회가 직접 관측되어 사고 기록과 교차 검증됨).
- **원인의 본질**: 각 모듈은 단독으로 전부 옳았다. "실패하면 초기 상태로"(롤백)와 "대기 상태를 집는다"(폴러)의 합성이 고정점 없는 순환을 이뤘고, 유일한 종료 조건 후보(재시도 카운터)가 **루프 자신이 삭제하는 디렉토리 안에** 있었다.
- **구조 전환**: "종료 조건은 루프가 지울 수 없는 곳에" — Redis 실패 원장 + 전역 서킷 브레이커 + 오류 분류학(전역 장애는 이슈 격리로 승격 금지). 회귀 방지는 순환 전체의 유한 수렴을 검증하는 통합 테스트로, 사고 당시의 실제 로그 라인까지 테스트 케이스에 박았다.

### 사고 B — 공유 자원의 연쇄 실패 (2026년 7월)

- **현상**: 오염된 작업 트리 1건의 "보류"가 배타 락을 2시간 점유. 보류 대기와 정상 경합의 실패 예산이 하나였던 탓에(둘 다 7,200초 — 대기자가 구조적으로 이길 수 없는 산술) 대기 중이던 이슈 ~102건이 예산을 소진, **97건이 오류 상태로 확정**됐다. 하루 지연 재큐 12,123건.
- **원인의 본질**: 실패 격리 장치(보류)가 실패 증폭기(대기 전원 동시 실패)로 반전됐다. 안전장치 각각의 단독 검증으로는 절대 보이지 않는 상호작용이다.
- **구조 전환**: 2단 대응 — 즉시: 대기 원인별 예산 분리 + 대량 복구 스크립트(상태 되돌림 순서까지 명문화). 구조: 단일 체크아웃 시분할을 worktree 슬롯 풀로 대체해 **보류라는 상태 자체를 제거**(§5-2). 전환은 기준선 실측 → 격리 → 소킹 → 개방의 단계별 게이트로 진행했다.

### 사고 C — 의존성 자동 갱신에 의한 조용한 성능 붕괴 (2026년 7월)

- **현상**: LLM CLI 자동 갱신이 기반 모델을 교체. 새 모델은 같은 작업에 턴/도구 호출을 1.6배 사용해 계획 phase의 타임아웃 예산 초과율이 50%에 도달, 예산 초과 → 타임아웃 재시도 → 롤백 → 전체 재실행의 증폭 구조로 **처리량이 하루 35건에서 6건으로 붕괴**했다.
- **원인의 본질**: "모델"과 "phase 예산"이 암묵적으로 결합돼 있는데 그 결합이 코드 어디에도 표현되지 않았다.
- **구조 전환**: 모델 버전을 코드에 핀하고 모든 spawn 지점에 강제 주입, 예산 재보정은 phase 메트릭 p90 실측으로만("성공 기록만 보면 타임아웃에 잘린 분포가 실제보다 짧아 보인다"는 관측 편향까지 규약화), "모델 핀 ↔ 예산은 한 세트"를 안전 규약 문서에 명문화. 타이밍 상수 전체를 SSoT 모듈로 모아 부팅 시 부등식을 assert한다.

---

## 8. 한계와 로드맵

**정직한 한계**:
- 리뷰 완료 PR의 64.6%는 폐기된다. 그중 AI 실패(128건)가 개선 대상이고, 외부 요인(107건 — 중복 처리·비버그·재현 불가)은 유입 필터링의 문제다.
- RCA 판정 가능 비율이 53%에 그친다(비교 대상인 사람 PR이 없는 케이스) — 정확도 82.9%는 판정 가능 집합 기준임을 항상 병기한다.
- 단일 프로젝트(모노레포) 편중 — 도메인 일반화 성능은 미검증이다.
- 자가 보고 파이프라인도 완벽하지 않다 — 대형 사고 직후 사이클의 보고서는 섹션 생성이 실패했고, 조작 없이 실패 상태로 남겨뒀다.
- 비용 환산은 가정 의존이므로 범위로만 제시한다.

**로드맵**: 브라우저 구간 병렬화(슬롯별 dev server — 선명세 완료, 대기 시간 전수 계측으로 착수 우선순위 산정 중), 동시성 2→N 확대(기준선 게이트 통과 조건), AI 실패 계열 폐기 사유의 지속 감축(자가 개선 루프의 표적).

---

## 부록 — 기술 스택

| 계층 | 기술 |
|---|---|
| 오케스트레이션 | Node.js, Bull Queue(Redis), 자체 phase runner(LLM CLI 서브프로세스 관리) |
| LLM | Claude CLI (모델 버전 핀), VLM 기반 영상/스크린샷 분석 |
| 브라우저 자동화 | Playwright (headless/headful, CDP 이벤트 계측, 비디오 캡처) |
| 격리/동시성 | git worktree, APFS clonefile(CoW), 파일 리스 + Redis leader lease(worker thread 격리) |
| 관측/평가 | SQLite(WAL, append-only 이벤트 + 비정규화 뷰), 자체 통계 모듈(Wilson/Fisher/BH-FDR/bootstrap), 격주 자동 감사 보고서 |
| 연동 | Jira REST(폴링·전환·댓글), GitHub App(GraphQL 배치, fail-closed 봇 신원), Slack |
| 인프라 | macOS launchd 상시 서비스, 자동 배포(main 폴링 + graceful drain 재시작) |

> 본 문서의 수치·사례는 실운영 데이터에서 추출했으며, 회사 식별 정보는 일반화했다. 상세 아키텍처·코드 수준 근거는 인터뷰에서 시연 가능하다.
