
Claude Code로 개발을 '자동화'하기 — Anthropic 방식의 3역 에이전트 루프를 launchd 상주형으로 돌리기
요약
Anthropic의 3역 에이전트 루프 방식을 응용하여, Claude Code를 Mac의 launchd 상에서 상주형 데몬으로 운용하는 자동화 시스템을 소개합니다. 구현과 리뷰 에이전트를 분리하고 파일 큐 방식으로 통신하여 컨텍스트 오염을 방지하고 효율적인 개발 루프를 구축하는 방법을 다룹니다.
핵심 포인트
- Claude Code 세션의 컨텍스트 오염과 비용 문제를 해결하기 위해 세션 외부에서 루프를 제어
- 구현(dev-agent)과 리뷰(review-agent) 에이전트를 독립된 프로세스로 분리하여 운영
- 파일 큐(File Queue) 방식을 사용하여 에이전트 간의 상태 전달 및 안정성 확보
- 도요타 방식의 자동화 개념을 도입하여 이상 발생 시에만 인간이 개입하는 구조 설계
자신의 PR(Pull Request)에 리뷰 코멘트가 달리면, 그걸 확인하는 게 다음 날 아침이고, 수정해서 push 하는 게 점심이 됩니다. 이 왕복이 하루에 단 1회밖에 안 된다는 점이 계속 불만이었습니다.
얼마 전, Anthropic의 엔지니어가 「계획·구현·판정의 3가지 에이전트를 역할 분담시켜 루프를 돌리는」 방식으로 앱을 구축하는 영상이 화제가 되었습니다. 똑똑한 모델을 하나 준비하는 것보다, 루프의 설계가 성패를 결정한다는 이야기입니다. 그것을 보고 든 생각은, 이 방식은 단발성 데모보다 상주 운용으로 했을 때 훨씬 더 효과적이라는 것이었습니다.
그래서 만들었습니다. 구현 담당과 리뷰 담당이라는 두 가지 에이전트가, 서로의 기억을 공유하지 않는 상태에서 리뷰 왕복을 알아서 돌리고, 합격한 것만 인간의 push 대기 상태로 올라오는 구조입니다. Mac의 launchd에 상주시켜서, 지금은 자신의 PR 감시도 맡기고 있습니다.
이 구성을 멋대로 **「자동화(自働化) 루프」**라고 부르고 있습니다. 도요타 생산 방식의 자동화(자동화, 닌베ン이 붙은 자동화)는 「기계가 이상을 감지하면 자동으로 멈춰서 인간을 부르는」 구조를 말하는데, 이 루프도 마찬가지입니다. 순조로운 동안에는 무인으로 돌아가고, 이상 — 리뷰 반려 상한 초과나, 설계 판단이 필요하다는 판정 — 이 발생하면 반드시 멈춰서 인간을 부릅니다. 완전 자동화를 표방하지 않는 것이, 무인 운용을 신뢰할 수 있게 만든다는 생각입니다.
이 기사는 그 구성과, 운용해 보며 알게 된 점을 적습니다.
/loop로는 안 되는 걸까
Claude Code에는 /loop나 멀티 에이전트 병렬 기능이 있어서, 처음에는 그것만으로 충분할 것이라고 생각했습니다. 그만둔 이유는 세 가지입니다.
첫 번째, 세션 상주는 컨텍스트(Context)가 쌓입니다. 오래 돌릴수록 전반부의 판단이 후반부를 오염시킵니다. 두 번째, 감시만을 위해 세션을 살려두면, 아무 일도 일어나지 않는 시간에도 세션과 이용 한도를 점유합니다. 세 번째, 세션은 터미널을 닫으면 종료됩니다.
그래서 발상을 뒤집어, 루프를 세션 외부에 두었습니다. 상주하는 것은 200줄 정도의 Python daemon이며, 이것은 토큰을 1엔도 소비하지 않습니다. Claude는 작업이 발생하는 순간에만 claude -p (headless 모드)로 기동되고, 끝나면 프로세스째로 사라집니다.
부수적인 효과로, Claude가 매번 백지 상태로 기동되는 것이 사실 가장 효과적입니다. 이는 후술하겠습니다.
전체 구성
에이전트는 두 명 있습니다.
- dev-agent (구현 담당): 계획 수립·구현·수정
- review-agent (리뷰 담당): 차이점(diff)을 읽기 전용으로 리뷰하고, 판정만 반환
각각 독립된 launchd 서비스로, 직접 대화하지 않습니다. 설정 디렉토리에 JSON을 서로 두는 파일 큐(File Queue) 방식으로 업무를 전달합니다. 큐는 claim (atomic rename) → ack / fail → 재시도 상한에 따른 dead-letter 구조로 만들어져 있어, daemon이 떨어져도 태스크는 사라지지 않습니다. Mac이 슬립(Sleep) 모드에 들어가도, 깨어났을 때 이어서 동작합니다.
구현 의뢰를 던졌을 때의 흐름입니다.

implement "◯◯을 구현해줘"
① 계획 수립 (dev): 코드는 건드리지 않고, 조사하여 구현 계획만 작성
② 계획 리뷰 (review): GO / REVISE / BLOCK 판정
...
판정은 리뷰 출력의 마지막 줄에 VERDICT: LGTM과 같은 한 줄을 쓰게 하고, Python 측에서 그것을 파싱하여 다음 공정을 큐에 쌓습니다. 제어 흐름(Control Flow)은 전부 Python이 담당하고, 모델에는 판정만 시키는 것이 포인트입니다. 이 부분을 모호하게 하면, 에이전트가 「그럼 다음에 ~를 하겠습니다」라고 말만 하고 아무 일도 일어나지 않는 사고가 발생합니다.
반려는 각 루프 2회까지입니다. 초과하면 멈춰서 인간에게 통지합니다. BLOCK 판정 (설계부터 상의 필요)도 즉시 에스컬레이션(Escalation)됩니다. 무한 루프는 하지 않습니다.
push는 「시키지 않는 것」이 아니라 「할 수 없게 만드는 것」
무인으로 코드를 쓰게 하는 데 있어 가장 공을 들인 부분이 여기입니다. push와 GitHub로의 쓰기는 프롬프트로 금지하는 것이 아니라, PreToolUse 훅(Hook)으로 기계적으로 차단하고 있습니다.
훅은 툴 호출(Tool Call)마다 호출되는 Python 스크립트로, bash 명령어를 shlex로 토큰 분해하여 git push 계열의 서브 명령어를 판정합니다. git -C /path push
또 bash -c "git push"나 eval git push와 같은 우회 방법도 전개해 보았습니다. 차단(block) 시의 메시지는 "push는 인간이 수행한다. 대체 수단을 찾지 말 것"으로 설정해 두었습니다. 차단된 모델이 다른 push 수단을 찾기 시작하는 것을 방지하기 위함이며, 이는 실제로 필요했습니다.
밖으로 나가는 유일한 경로는 인간이 직접 입력하는 push 명령어(미 push 커밋 목록과 diffstat를 출력하여 y/N 확인)뿐입니다. 에이전트를 신뢰할 수 있는가에 대한 논의를, 애초에 신뢰가 필요 없는 구조로 만듦으로써 종결시켰습니다.
리뷰 역할이 구현 역할의 버그를 잡아냈다
운영 첫날, 이 구성의 가치를 단번에 알 수 있는 사건이 있었습니다.
구현 역할이 리뷰 코멘트 대응을 위해, 어떤 처리를 API 측에서 worker 측으로 옮기는 변경 사항을 작성했습니다. 빌드와 테스트도 통과했고, 보고서에는 "의존성 주입(DI) 누락도 찾아내어 수정했습니다"라고까지 적혀 있었습니다. 자신만만했습니다.
이를 리뷰 역할에게 넘기자 REVISE가 돌아왔습니다. 다른 의존성이 DI 컨테න්න(DI container)에 주입되지 않아, worker가 운영 환경에서 반드시 nil panic을 일으킬 것이라는 지적이었습니다. 게다가 panic이 발생하면 큐(queue)가 재배포되어 이중 처리된다는 2차 피해까지 지적되었습니다. 구현 역할은 동일한 종류의 주입 누락을 스스로 하나 찾아냈었지만, 두 번째는 놓치고 있었습니다.
반려(revert) 후에는 무인으로 수정 → 재리뷰 → LGTM까지 돌아갔고, 인간의 작업은 마지막에 diff를 보고 push를 하는 것뿐이었습니다.
이것이 "매번 백지 상태인 Claude"의 효과입니다. 리뷰 역할은 구현 시의 선입견을 일절 이어받지 않고 차이점(diff)만을 봅니다. 같은 모델이라도 문맥을 분리하는 것만으로 보이는 것이 달라집니다. 세션 내에서 "네가 작성한 코드를 셀프 리뷰해줘"라고 하는 것과는 차원이 다른 일이었습니다.
빠졌던 부분 (ハマったところ)
타임아웃과 회수(recovery)의 불변 조건. 큐에는 "실행 중인 상태로 방치된 태스크를 회수하여 재실행한다"는 메커니즘이 있는데, 회수까지의 임계값이 태스크의 타임아웃보다 짧으면 살아있는 태스크를 잘못 회수하여 이중 실행하게 됩니다. 회수 임계값 > 최대 타임아웃이라는 불변 조건을 주석에 적어 두었음에도, 나중에 타임아웃만 늘렸다가 멋지게 스스로 위반했습니다. 이런 종류의 값은 코드상에서 서로 결합해 두어야 했습니다.
리뷰의 포괄성을 프롬프트에 맡기지 말 것. "추가된 코멘트에 이상한 기술이 없는지 봐줘"라고 리뷰 역할에게 지시해도 놓치곤 합니다. 지금은 Python 측에서 diff로부터 추가된 코멘트 행을 전부 추출하여, "이 N개의 행을 한 줄씩 판정하라"고 리스트로 전달하고 있습니다. 기계적으로 열거할 수 있는 것은 모델에게 열거하게 하지 않는 것, 이것이 교훈입니다.
결과물의 보관 장소. 결과는 Markdown으로 써서 Obsidian의 vault에 두었습니다. 데몬(daemon)의 쓰기 빈도가 높은 파일(큐나 heartbeat)을 iCloud 동기화 폴더에 두면 동기화가 요동치기 때문에, 그 부분만 분리가 필요합니다.
무엇을 무인화하고, 무엇을 남길 것인가
며칠간 돌려보며 느낀 점은, 이 메커니즘이 스케일링할 수 있는 것은 "정오(正誤) 판정이 가능한 공정"뿐이라는 것입니다. 구현·테스트·리뷰의 왕복은 그러하며, 그렇기에 무인으로 돌아갑니다.
설계는 그렇지 않습니다. 요구사항 해석의 어긋남은 차이점 리뷰(diff review)로는 검출할 수 없습니다. 코드는 계획에 충실하고, 계획은 오해에 충실한 상태가 될 뿐입니다. 그래서 설계와 브레인스토밍(wall-hitting)은 지금도 대화 세션에서 직접 하고 있습니다. 기준선은 "판단 기준 그 자체를 자신이 가지고 있는 공정은 대화, 기준을 명문화할 수 있는 공정은 무인"으로 정리되었습니다.
인간의 작업은 의뢰를 던지는 것, 이상 상황으로 호출되면 판단하는 것, 마지막에 diff를 보고 push를 누르는 것입니다. 라인은 무인으로 흐르고, 멈춰야 할 때 멈춰서 호출하러 옵니다. 자동화(autonomation) 루프라고 부르는 이유는 여기에 있습니다. 리뷰 코멘트 대응의 체감은 "다음 날의 업무"에서 "정신 차려보니 끝나 있음"으로 바뀌었습니다.
구성 요소는 launchd와 Python, 그리고 claude CLI뿐이므로, 유사한 것은 반나절이면 구축할 수 있을 것 같습니다. 코드 공개는 여러 가지를 정리한 후에 할 예정입니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기