ADR / RECOVERY CONTROL

고치는 능력보다
멈추는 구조

bounded repair loop는 실패를 다시 시도하는 기능이 아니라, 같은 실패를 식별하고 진전이 없으면 사람에게 넘기는 상태 머신입니다.

01 / PROBLEM

문제 상황: 자동 수정은 쉽게 자기 자신을 재실행한다

CI 실패나 review comment가 들어올 때마다 자동 수정이 같은 PR에 patch를 밀어 넣으면, 원래 실패가 해결됐는지 새 실패가 생겼는지 구분하기 어려워집니다. 이벤트가 다시 loop를 깨우고, loop가 다시 push하면서 비용과 diff가 커집니다.

단순한 “최대 5회” 같은 라벨·카운터는 여러 실행 주체가 공유하지 못하거나, 프로세스가 죽은 뒤 상태를 잃어 중복 PR을 만들 수 있습니다.

02 / LIMITATION

직관적 접근의 한계: retry 횟수만 세기

COUNT

시도 횟수만 증가

실패의 정체와 현재 commit을 저장하지 않으면 같은 원인을 다른 시도로 포장합니다.

LABEL

GitHub label에 상태 저장

label은 durable한 실행 계약과 중복 억제 키를 표현하기에 너무 약하고, local babysitter와 Actions가 경쟁합니다.

ALWAYS RETRY

검증이 바뀌면 무조건 재시도

failure signature가 그대로인데도 재진단하면 토큰·CI·PR만 소모하고 실제 진전은 없습니다.

03 / SOLUTION

해결 구조: 상태·진전·중복 억제를 하나의 계약으로

0Observechecks와 typed findings 수집
1Identifyfailure_signature 생성
2Repair최소 patch와 검증
3Progress?signature·findings·성공 check 비교
4Attempt 1진전 없으면 repair PR
5Recheck같은 coordinator state 재관찰
6Attempt 2두 번째 repair PR
7Exception세 번째 무진전은 사람에게 전달
진전 있음 → recheck · 진전 없음 + attempt < 2 → repair PR · 그 외 → human_exception
# lib/repair_coordinator.py
MAX_REPAIR_ATTEMPTS = 2

RepairState(
  parent_pr, current_pr, attempt,
  failure_signature, run_id, commit_sha
)

repair_key = f"parent_pr:attempt:failure_signature"

lib/repair_coordinator.py가 모델의 진단·patch와 분리된 durable 경계를 소유합니다. failure_signature는 category, checks, findings, evidence를 정렬해 SHA-256으로 만들고, has_progress는 signature 변경·finding 감소·성공 check 증가 중 하나가 있는지 봅니다.

상태는 parent_pr, current_pr, attempt, failure_signature, run_id, commit_sha를 함께 저장합니다. 따라서 local babysitter와 GitHub event adapter가 같은 coordinator를 깨워도 repair_key로 동일한 시도를 중복 생성하지 않습니다. attempt 0 이후 repair PR은 최대 2개이며, 무진전이면 human_exception evidence bundle로 종료합니다.

ADR TAKEAWAY

복구 루프의 완료 조건은 승인보다 증거다

자동화는 계속 시도하는 시스템이 아니라, 진전이 없다는 사실을 기록하고 멈추는 시스템이어야 한다.

각 patch에는 focused verification, full verification, commit SHA, append-only event가 붙습니다. 최종 merge는 자동화하지 않고 human hand-off로 남깁니다.