
『Low 지적 1건』을 위해 계획부터 다시 시작하기 ── 리뷰 0건 주의보를 멀티 에이전트로 3라운드 돌린 이야기 (C3 v2.39.0)
요약
멀티 에이전트 프레임워크 C3(Claude Code Conductor) v2.39.0 업데이트와 이를 활용한 워크플로우 실험 기록입니다. 리뷰 지적 사항을 완전히 제거하기 위해 에이전트 워크플로우를 3라운드 반복하며 프로세스의 복원력과 정교함을 검증했습니다.
핵심 포인트
- C3 v2.39.0: Bash 허가 다이얼로그 문제를 해결하기 위한 업데이트 포함
- 중단 내성(Interruption tolerance): 세션 복구를 위한 '현재 위치' 필드 활용
- 멀티 에이전트 워크플로우를 통한 코드 리뷰 지적 사항 제로화 시도
- Claude Code의 Bash 접두사 일치(Prefix match) 허가 방식의 한계 분석
이전 기사: https://zenn.dev/satoh_y_0323/articles/5f1a602cf200e1
C3 GitHub: https://github.com/satoh-y-0323/claude-code-conductor / PyPI: https://pypi.org/project/claude-code-conductor/ / 공식 문서: https://satoh-y-0323.github.io/claude-code-conductor/
본 기사의 스코프: 자체 제작한 멀티 에이전트 (Multi-agent) 프레임워크 C3 (Claude Code Conductor)의 v2.39.0에 관한 이야기입니다. 기능적으로는 "세션 시작 시 나타나는 Bash 허가 다이얼로그를 없앤다"라는 소소한 기능 하나입니다. 하지만 이번의 주인공은 기능이 아니라, "리뷰 지적 사항을 Low를 포함하여 0건이 될 때까지 전부 없앤다"라는 제약을 자신의 프레임워크 멀티 에이전트 워크플로우(Workflow)로 3라운드 돌린 기록입니다. 도중에 리뷰어가 "범인"을 착각하거나, 실행 담당 에이전트가 계획의 오류를 바로잡거나, 단 한 줄의 중복 때문에 계획부터 다시 시작하거나 ── 기능이 작을수록 프로세스의 민낯이 보였습니다.
시작하며 ── 인격이 망가져서, 재시작해서, "현재 위치"에서부터 다시 달렸다
이전 세션은 도중에 Opus의 도구 호출 (Tool calling)이 망가지는 희귀한 버그를 만났습니다. function_calls가 망가진 형태로 출력되어 Agent도 Write도 보낼 수 없게 되었습니다. 어쩔 수 없이 재시작해야 했습니다.
여기서 효과를 본 것이, 전전 기사에서 도입한 현재 위치: 1행 필드였습니다. 새로운 세션에서 /init-session을 입력하면 이전 세션 파일로부터 다음과 같이 복원됩니다.
현재 위치: 페이즈 D 구현 완료 (Green 확인 완료) → 다음은 release-prep부터 재개
인격 (Context)은 완전히 날아갔지만, "어디까지 끝났고 다음에 무엇을 할 것인가"만은 한 줄로 남아 있었습니다. 덕분에 "릴리스 준비부터 재개하겠습니다"라며 망설임 없이 다음 단계로 넘어갈 수 있었습니다. 중단 내성 (Interruption tolerance) 기능을 직접 만들고, 그 기능 덕분에 스스로 구원받다니. 구현한 본인으로서 이는 은근히 기쁜 순간이었습니다 (복선 회수).
그리고 그 "다음"의 내용이 v2.39.0입니다.
작은 기능: 허가 다이얼로그는 왜 없앨 수 없었나
C3는 세션 시작 시 몇 가지 뒷단 처리를 실행합니다. .claude/state/에 플래그를 쓰거나, /setup (최초 규약 설정)이 완료되었는지 판정하거나, /init-session이 이 세션에서 실행되었는지 판정하는 등의 작업입니다. 이것들은 **Bash 원라이너 (One-liner)**로 처리하고 있었습니다.
문제는 그것이 매번 Bash 허가 다이얼로그를 띄운다는 점이었습니다. "허가해도 될까?"라는 질문이 구동될 때마다 나타납니다. settings.json의 allow에 등록하면 사라질 ── 터였는데, 사라지지 않았습니다.
원인: 복합 Bash는 "접두사 일치"로 허가할 수 없다
Claude Code의 settings.json 내 allow는 Bash 명령어를 접두사 일치 (Prefix match) 방식으로 허가합니다. 예를 들어 Bash(python foo.py*)라면 python foo.py ...를 허가합니다. 그런데 허가되지 않았던 명령어는 다음과 같은 형태였습니다.
if [ -f .claude/rules/coding-standards.md ] || [ -f .claude/state/setup_done.flag ]; then ...
if, $(...), 리다이렉션(>)을 포함하는 **복합 명령어 (Compound command)**는 접두사 일치의 대상이 되지 않습니다. allow에 무엇을 적더라도 정적 분석 (Static analysis)으로 "안전"하다고 판정할 수 없기 때문에 매번 다이얼로그가 나타납니다. 이는 사양이며, claude-code-guide 에이전트에게 공식 동작을 확인해 본 결과 "복합 명령어는 각 서브 명령어의 개별 일치가 필요하며, if / $() / 리다이렉션은 정적 분석이 불가능하다"라는 답변을 받았습니다.
덧붙여 근본적인 이유를 한 단계 더 파고들자면, Claude Code에는 "샌드박스(Sandbox) 내라면 Bash를 자동 허가한다"라는 설정(autoAllowBashIfSandboxed...
)가 있지만, Windows 네이티브 환경에서는 샌드박스(Sandbox)가 no-op이므로 이 또한 적용되지 않습니다. 즉, Windows native + 복합 Bash 조합은 구조적으로 다이얼로그(dialog)를 없앨 수 없었습니다.
해법: 3개의 Bash를 하나의 스크립트로 통합하여, 한 줄로 허가하기
답은 간단했습니다. 복합 Bash를 인수에 따라 분기하는 하나의 Python 스크립트로 집약하는 것입니다.
session_guard.py mark # /init-session: 플래그를 작성하고 SETUP_DONE/NEEDED를 반환
session_guard.py check # /start: INIT_DONE/NEEDED를 반환
session_guard.py setup-mark # /setup: setup_done.flag를 작성
그리고 settings.json에 한 줄만 추가하면 됩니다.
"Bash(python .claude/skills/init-session/scripts/session_guard.py*)"
끝부분의 *를 이용한 접두사(prefix) 일치로, 3개의 서브커맨드(subcommand) 모두가 한 번에 허가됩니다. 복합 Bash가 사라졌으므로 정적 분석(static analysis)도 통과합니다. 동작(플래그의 바이트·판정 문자열)은 기존 Bash와 동일하게 유지하면서, 다이얼로그만 사라졌습니다.
기능적으로는 정말 이 정도가 전부입니다. 문제는 여기서부터였습니다.
여기서부터가 본론: 「Low까지 0건」을 달성하기
C3는 자기 자신의 개발도 C3의 워크플로우(히어링→설계→계획→TDD→리뷰)로 진행합니다. 구현이 Green 상태가 되었으므로 리뷰 단계로 넘어갑니다. code-reviewer와 security-reviewer를 병렬로 기동했습니다.
이때 저는 스스로에게 다음과 같은 제약을 걸었습니다 ── 「Low를 포함하여 단 1건이라도 지적이 나오면 전부 대응한다. planner로 되돌려 재계획 → 재구현 → 재리뷰. 지적이 0건이 될 때까지 반복한다」. C3는 직장 팀에서도 사용하고 있으므로, 내부 품질에는 투자할 가치가 있다는 판단입니다.
1차 리뷰: 11건
code-reviewer: 6건 (Medium 2, Low 4)
security-reviewer: 5건 (Medium 2, Low 3)
내용은 타당한 것이 많았습니다. _project_root()가 환경 변수 유래의 경로를 resolve()하지 않는다(다른 hook은 resolve 되어 있어 비대칭적이다), mark가 비정상 종료되었을 때 stdout이 비어 있지만 폴백(fallback)이 문서화되어 있지 않다 등입니다.
「v2.39.0이 추가했다」라고 했지만, git을 보니 기존에 존재했다
다만, 가장 중요해 보이는 1건(Medium)에서 위화감이 느껴졌습니다. code-reviewer는 이렇게 지적했습니다 ── 「v2.39.0이 setup/SKILL.md에 추가한 복합 Bash가 allow 미등록 상태이며, 이는 '다이얼로그 영구 해소'라는 목적과 모순된다」.
가슴이 철렁하는 지적입니다. 하지만 그대로 믿지 않고 git으로 사실 관계를 확인했습니다.
git status → setup/SKILL.md는 이번 변경 대상에 포함되어 있지 않음
git diff --stat setup/SKILL.md → 빈 값 (변경 사항 없음)
즉, 그 복합 Bash는 v2.39.0이 추가한 것이 아니라 이전부터 존재했던 것이었습니다. 리뷰어의 '범인 특정'(v2.39.0이 추가했다)은 오류였습니다. 이는 과거 글에서 여러 번 써왔던 교훈 ── 「리뷰/조사 에이전트는 재현율(recall)을 우선시하여 그럴듯한 오지적을 할 수 있다. 결론을 내리기 전에 1차적인 사실 확인을 할 것」 ── 을 그대로 보여주는 사례입니다.
하지만 중요한 것은, 프레이밍(framing)은 틀렸을지라도 관찰 자체는 옳았다는 점입니다. /setup의 최초 실행 시 해당 기존 Bash가 다이얼로그를 띄우는 것은 사실입니다. 'v2.39.0의 대상 외'라고 치부할 수도 있었지만, 리뷰 스코프를 'C3 전체·변경 대상 외의 허용 없음'으로 두고 있는 이상 이것 또한 해결해야 할 대상입니다. setup-mark 서브커맨드를 추가하여 /setup 측도 통합했습니다. 잘못된 근거는 정정하고, 올바른 내용은 포착한다. 이 구분이야말로 이번에 가장 효과적이었던 판단이었습니다.
제2판: 수정했더니, 새로운 Low가 1건 생겨났다
전체 11건을 TDD (Test-Driven Development)로 해결하고 재리뷰. security는 0건. code-reviewer는 최초 6건을 모두 해소 ── 했으나, 새로운 Low를 1건 찾아냈습니다.
SETUP_DONE_FLAG_REL
과
SETUP_MARKERS_REL[1]
이 동일한 경로의 튜플(tuple) (".claude", "state", "setup_done.flag")를 중복 정의하고 있습니다.
향후 어느 한쪽만 수정할 경우, 판정 대상과 쓰기 대상이 어긋나게 됩니다 (DRY 리스크).
정확한 지적입니다. Low 수준이지만, 맞습니다. setup-mark를 추가한 저의 수정이 마침 새로운 중복을 만들어냈습니다.
Low 1건을 위해, 계획부터 다시 시작하기
이 부분이 이번의 고비입니다. 단 한 줄의 중복 ── SETUP_MARKERS_REL의 두 번째 요소를 SETUP_DONE_FLAG_REL 참조로 바꾸는 것만 ── 을 위해, 규칙대로 planner (계획 담당)로 돌아가 제3판 계획을 세우고, test → impl (구현) → confirm (확인) → 재리뷰 2회를 한 바퀴 더 돌렸습니다.
솔직히 말하면 비용이 작지 않습니다. 한 라운드에 6~8회의 에이전트 기동이 실행됩니다. "Low 수준의 DRY 1건을 위해 이렇게까지 해야 하나"라는 마음이 드는 것은 당연합니다.
그럼에도 실행한 이유는 두 가지입니다. 하나는 이것이 프레임워크 본체이며 사용자가 있다는 것. 다른 하나는 "0건까지"라는 규율은 단 한 번이라도 예외를 만들면 형해화된다는 것입니다. "이건 사소하니까 괜찮아"를 허용하기 시작하면, 다음의 "사소함"도 통과하게 됩니다. 선 긋기를 규율에 맡긴다면, Low도 없애야 합니다. 비용은 직시하되, 이번에는 그렇게 판단했습니다 (이 손익에 대해서는 기사 말미에서 다시 생각해보겠습니다).
에이전트가, 계획의 오류를 바로잡다
제3판에서 흥미로운 일이 일어났습니다. planner (계획 담당)는 중복 해소를 다음과 같이 검증하라고 지시했습니다 ── "SETUP_MARKERS_REL[1] is SETUP_DONE_FLAG_REL을 is로 비교하여 테스트하라". 동일한 객체라면 True, 다른 객체라면 False라는 발상입니다.
그런데 tester (테스트 담당)는 이를 실측을 통해 부정했습니다.
CPython은 값이 같은 작은 튜플 리터럴(literal)을 인터닝 (interning, 공유)하는 경우가 있어, 별개의 리터럴로 중복 정의되어 있더라도 is가 True가 되어버릴 수 있습니다. is 비교로는 Red로 판정할 수 없습니다.
대신 tester는 소스를 ast.parse 하여, SETUP_MARKERS_REL의 두 번째 요소가 ast.Name (변수 참조)인지 ast.Tuple (리터럴 직접 작성)인지를 판정하는 방식으로 전환했습니다. 이렇게 하면 CPython의 최적화에 의존하지 않고, "참조로 통합되었는지"를 올바르게 검출할 수 있습니다.
계획이 지정한 테스트 전략을 실행역이 "그 방식으로는 검출할 수 없습니다"라고 이유를 들어 뒤집고, 더 올바른 방법으로 고쳤습니다. 계획은 완벽하지 않으며, 완벽할 필요도 없습니다. 실행 단계에서 발견한 오류를 묵인하지 않고 바로잡을 수 있는 것이 훨씬 더 가치 있습니다.
제3판: 0건
- code-reviewer: 0건 (모든 disposition 확정) security-reviewer: 0건
풀 테스트 스위트 (Full Test Suite) 1518 passed / 0 failed. 여기서 드디어 제약이 풀리고, 릴리스 승인을 사용자(나)에게 요청하는 단계로 넘어갑니다. 승인 후, commit → tag v2.39.0 → push → GitHub Release까지 순차적으로 실행하여 완료했습니다.
리뷰의 추이를 정리하면 다음과 같습니다.
| 라운드 | code-reviewer | security-reviewer |
|---|---|---|
| 최초 | 6건 (M2・L4) | 5건 (M2・L3) |
| 제2판 | 최초 6건 해소 · 신규 Low 1건 | 0건 |
| 제3판 | 0건 | 0건 |
업무·개인 개발에 활용할 수 있는 점
1. 복합 Bash는 "사전 허가"할 수 없다 ── 1스크립트 + 1행 allow로 압축하기
이것은 C3를 사용하지 않더라도 유효한, 이번의 가장 실용적인 TIL (Today I Learned)입니다. Claude Code (또는 유사한 에이전트)에서 if / $(...) / 리다이렉트(redirection)를 포함하는 Bash는,
settings.json
의 allow 접두사 일치(prefix match)로는 사전 허가할 수 없습니다. 매번 다이얼로그가 뜬다면, 로직을 하나의 스크립트(인수로 분기)로 몰아넣고, 그 스크립트 이름으로 1줄 allow 한다. 복합 구문이 사라지는 만큼 정적 분석(static analysis)도 통과하며, 허가도 한 번에 끝납니다. "허가 다이얼로그가 번거롭다"는 문제를 구조적으로 해결할 수 있습니다.
2. 리뷰의 "범인 특정"은, 결론을 내리기 전에 1차 확인을 거칠 것
리뷰어(사람이든 AI든)의 지적은, 관찰(observation)과 그 이유를 설명하는 프레이밍(framing)을 분리해서 읽으면 정확도가 높아집니다. 이번 "v2.39.0이 추가했다"는 오류였지만, "다이얼로그가 뜬다"는 관찰은 맞았습니다. git status / git diff를 한 번 실행하는 것만으로도 잘못된 이유를 버리고 올바른 내용만 골라낼 수 있습니다. 그럴듯한 문장 하나를 그대로 결론으로 삼지 마세요.
3. "전부 없앤다"는 강력한 규율이지만, 비용은 직시할 것
"Low 등급까지 0건"은 형식화되기 어려운 강력한 기준선입니다. 반면, Low 등급의 DRY(Don't Repeat Yourself) 1건을 위해 계획부터 한 바퀴를 다시 도는 비용 또한 현실입니다. 정답은 문맥에 따라 다릅니다. 이번에는 프레임워크 본체와 실제 사용자가 있는 상황이었기에 없애는 쪽을 택했지만, 일회성 스크립트라면 Low 등급은 한꺼번에 수용해도 좋습니다. 중요한 것은 "전부 없앨 것인가/수용할 것인가"를 매번 판단하는 것이 아니라 사전 규율로 정해두고, 비용을 인지한 상태에서 일관성을 유지하는 것입니다. 모호한 재량권은 대개 가장 저렴한 것을 통과시키기 마련입니다.
4. 계획은 완벽하지 않아도 된다 ── 실행자의 "잠깐"을 존중할 것
planner의 is 비교는 오류였지만, 그것으로 충분했습니다. 계획 단계에서 모든 것을 올바르게 예견할 필요는 없으며, 실행 단계에서 발견한 오류를 없앨 수 있는 경로가 있다면 됩니다. 인간 팀에서도 상류의 설계를 하류의 구현자가 "이대로는 동작하지 않습니다"라고 바로잡을 수 있는 분위기가 있는지 여부가 품질을 크게 좌우합니다. 에이전트를 배치할 때도 마찬가지로, "계획대로 한다"보다 "알아차리면 고칠 수 있다"를 설계에 넣어두어야 합니다.
5. 중단 내성(interruption tolerance)은 "이어서 실행할 수 있음"으로써 비로소 의미를 갖는다
세션이 깨져서 재시작해도, 현재 위치:라는 한 줄이 있었기에 이어서 진행할 수 있었습니다. 상태를 저장하고 있는 것과, 복구 후에 그것을 사용하여 달릴 수 있는 것은 별개입니다. 백업이나 로그를 남기고 있음에도 막상 복구할 때 "그래서, 어디서부터?"라고 된다면, 그것은 중단 내성이라고 할 수 없습니다. 복구의 입구에 "다음 수"를 한 줄로 두는 것만으로도 내성은 실용성을 갖게 됩니다.
요약
이번(v2.39.0) 기능은 "세션 시작 시의 Bash 허가 다이얼로그를 없애는" 작은 기능 하나였습니다.
- 복합 Bash (
if/$()/ 리다이렉트)는allow접두사 일치로 사전 허가할 수 없음 ── 3개의 Bash를session_guard.py의{mark|check|setup-mark}로 집약하여, **1줄allow**로 다이얼로그 문제를 영구 해결. 동작은 완전히 불변하며 하위 호환성 유지.
하지만 정말로 쓰고 싶었던 것은, 그 작은 기능을 "Low 등급까지 0건"으로 돌린 프로세스였습니다.
- 3라운드를 돌려 수렴 (초기 11건 → 제2판 신규 Low 1건 → 제3판 0건·전체 1518개 테스트 통과).
- 리뷰어의 잘못된 범인 특정("v2.39.0이 추가했다")을 git으로 1차 확인하여 정정하고, 올바른 관찰만 채택.
- tester가 planner의
is비교를 **CPython의 튜플 인터닝(tuple interning)**을 이유로 AST 분석으로 뒤집음 ── 계획의 오류를 실행역이 바로잡음. - 단 한 줄의 중복(Low)을 위해 계획부터 다시 시작하는 비용을, 인지한 상태에서 일관성 있게 유지.
예상대로 솔직한 한계도 있습니다. "Low 등급까지 전부 없애는 것"이 항상 최적인가 묻는다면, 아닙니다. 이번에는 프레임워크 본체였기에 밀어붙였을 뿐입니다. 규율은 강력하지만, 싸지는 않습니다. 그 손익을 매번 속이지 않고 바라보는 것 자체가 아마 가장 중요한 품질 관리일 것입니다.
그리고, 깨진 인격은 한 줄의 "현재 위치"로부터 이어서 달릴 수 있습니다. 작은 필드가 생각보다 큰 역할을 해주었습니다.
C3를 써보고 싶다면 ── 시작하기
pip install claude-code-conductor # C++ 컴파일러 불필요 (v2.37.0 이후)
cd your-project
c3 init # .claude/ 디렉토리에 에이전트 정의·skill·hook이 전개됨
그 후 Claude Code에서 /start를 입력하세요. 실행 전이라면 /init-session을 사용하세요.
(이전 상태 복원)과, 처음이라면 /setup
(규약 설정)을 자동으로 연쇄 실행한 후, 개발 플로우(히어링(Hearing) → 설계(Design) → 계획(Planning) → 구현(Implementation) → 리뷰(Review)) 단계로 진입합니다. v2.39.0부터는 해당 진입 처리 과정에서 허가 다이얼로그(Permission Dialog)가 나타나지 않습니다. 어댑터(Adapter)는 Claude / Codex / Cursor / OpenCode 4종을 지원합니다 (c3 init --platform ...).
"여기서 막혔다", "이 부분이 이상하다" ── 그러한 막힘(Stumbling)에 대한 보고가 가장 감사하게 느껴집니다. Issue나 PR을 통해 언제든 편하게 남겨주세요.
링크
- C3 GitHub: https://github.com/satoh-y-0323/claude-code-conductor
- C3 PyPI: https://pypi.org/project/claude-code-conductor/
- C3 공식 문서: https://satoh-y-0323.github.io/claude-code-conductor/
- v2.39.0 릴리스 노트: https://github.com/satoh-y-0323/claude-code-conductor/releases/tag/v2.39.0
- 이전 기사 (“나에게는 보이지 않는 장벽” ── 동료의 pip install이 C++ 컴파일러에서 멈춘 이야기 / C3 v2.37.0~v2.38.0): https://zenn.dev/satoh_y_0323/articles/5f1a602cf200e1
Discussion

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