앞선 글에서 코드몽이 여러 레포를 가상 모노레포처럼 다룬다고 했습니다. repo config, worktree, queue, PR 생성까지 모든 레포를 동일한 파이프라인에 태우는 구조였습니다.
그 레포 목록에 codemon 자신이 포함돼 있다는 걸 눈치채셨을까요?
같은 파이프라인에 자기를 태우다
코드몽은 자기 자신을 수정할 수 있습니다. self-edit 전용 코드가 있어서가 아닙니다. repos를 다루는 구조 자체가 "어떤 레포든 동일하게 다룬다"는 전제로 만들어져 있었기 때문에, 코드몽을 수정 가능 레포 목록에 넣는 데 별도의 특별한 조치가 필요 없었습니다.
2편에서 소개한 repos/ 디렉토리 구조를 다시 보면, codemon이 다른 레포들과 나란히 들어 있는 걸 볼 수 있습니다.
codemon 레포를 타겟하는 별다른 분기는 없습니다. 특별한 예외 케이스 처리도 없습니다. codemon 은 코드몽이 다룰 수 있는 여러 레포 중 하나일 뿐입니다. self-targeting은 의도된 기능이라기보다, 어떤 레포든 같은 방식으로 다루는 구조에서 자연스럽게 따라온 결과였습니다.
LLM이 다루기 좋은 코드와 로그
에이전트가 자기 코드를 다룰 수 있으면, 기술 선택 기준이 달라집니다. "개발자가 편한 라이브러리"가 아니라 "LLM이 다루기 좋은 라이브러리"를 고르게 됩니다.
예를 들어 코어 라이브러리로 Effect-TS를 도입한 이유가 여기에 있었습니다. 에러를 value로 다루는 Effect의 철학은 LLM에게 특히 잘 맞았습니다. 타입 시그니처만 봐도 이 함수가 어떤 에러를 낼 수 있는지 알 수 있습니다. Effect.catchAll에서 _tag로 분기해 각각 다른 복구 전략을 타는 패턴이 코드 전반에 걸쳐 사용됐습니다. 실제로 코드몽에는 30개 가까운 TaggedError 클래스가 도메인별로 정의돼 있습니다. SessionNotFound, PrefixMismatch, BuildTriggerFailed, NotionApiError 같은 이름들이 에러의 성격을 즉시 드러내고, 각각에 대해 다른 복구 전략을 코드로 명시할 수 있게 해 줍니다.
하지만 Effect 선택만으로 끝나지 않았습니다. 코드 구조 자체도 LLM이 다루기 쉽게 설계했습니다.
모델의 출력 형식을 고정하다
모델의 출력 형식을 작은 블록으로 고정해 뒀습니다. 예를 들어 Notion API를 호출할 때, 모델은 자연어가 아니라 이런 형태로 출력합니다.
코드 변경 계획도 마찬가지입니다.
build는 [BUILD_CONFIRM], scheduler는 [SCHEDULE_TASK] — 각 블록마다 전용 parser와 validation이 붙어 있습니다. 모델이 똑똑하다고 믿긴 하지만, 앱과 맞닿는 인터페이스만큼은 문장 이해에 기대지 않고 작은 프로토콜로 잘라냈습니다.
상태를 명시하고, 실행을 좁히다
LLM이 암묵적 맥락에 의존하면 오류가 쌓입니다. 그래서 중요한 상태는 전부 명시적으로 전달하는 구조를 택했습니다.
예를 들어, prompt를 단순 문자열로 넘기지 않고 envelope로 감쌉니다.
schema version, source, mode가 명시돼 있고, 사용자 요청과 스레드 맥락이 태그로 구분됩니다. 세션을 resume할 때는 이전 prompt의 hash를 비교해서, 중간에 시스템 프롬프트가 바뀌었으면 억지로 이어붙이지 않고 새 실행으로 떨어뜨립니다. 멀티레포 정보도 repos.ts에 이름, alias, path, githubPath로 평평하게 선언해 둬서, 모델이 매 턴마다 "지금 어느 repo 얘기지?"를 대화에서 재추론할 필요가 없습니다.
side effect가 나는 구간은 더 하드하게 좁혔습니다. 코드 수정 시에는 항상 별도 worktree를 만들고, Claude SDK의 agent 실행 옵션에서 사용 가능한 도구 목록을 줄이고, 접근 가능한 파일 경로를 해당 worktree 안으로 제한합니다. 추론은 soft guidance로 두되, 실행 경계는 앱이 물리적으로 제한합니다.
네이밍도 의도적으로 직설적입니다. parseAskUserBlock, extractNotionApiRequest, buildPromptEnvelope — 함수 이름만 봐도 입력과 출력이 뭔지 바로 읽힙니다. 작은 domain function과 명시적 타입 이름으로 코드의 의도가 바로 드러나게 만드는 쪽을 택했습니다.
프로덕션 로그도 에이전트가 읽을 수 있는 구조로 쌓다
에이전트가 자기 코드를 고칠 수 있으려면, 무엇이 잘못됐는지를 알 수 있어야 합니다. 코드몽의 로그는 단순히 많이 남기는 데서 그치지 않습니다. LLM이 파이프라인을 역추적하기 좋게 구조 자체를 설계했습니다.
핵심은 세 층입니다.
첫째, Effect.fn("이름")으로 모든 effectful function에 안정적인 이름을 부여합니다. changeWorktree.create, agent.runAgentImpl, prodLogs.scanMentions 같은 도메인 의미가 있는 이름이 붙어 있어서, LLM이 trace를 읽을 때 "create worktree → run agent → push → create PR" 같은 흐름을 함수명만으로 재구성할 수 있습니다.
둘째, threadTs와 sessionId로 요청을 추적할 수 있게 합니다. threadTs는 Slack 스레드 타임스탬프로 사용자 요청 단위를 식별하고, sessionId는 에이전트 세션을 식별합니다. 하나의 Slack 요청이 agent 실행으로 이어지면, 입력 로그부터 최종 response, cost까지 같은 threadTs로 엮입니다.
셋째, 로그를 JSON line 포맷으로 저장합니다. 자유서술 로그는 사람이 눈으로 읽기엔 편하지만, 프로그래밍으로 다시 파싱하기가 까다롭습니다. 코드몽의 로그는 이벤트 종류를 나타내는 tag, 본문인 msg(안에 threadTs, sessionId 등이 포함)가 일관된 구조로 반복되기 때문에, 코드가 다시 읽어서 분석하기에 적합합니다. 실제로 prod-logs.ts는 이 로그를 다시 읽어서 mention 수, thread 수 같은 운영 지표를 계산합니다. 앱 자체가 이 로그를 데이터 소스로 사용합니다.
실제 로그는 이런 형태입니다.
같은 threadTs로 묶으면 하나의 요청이 어떤 경로를 거쳐 어떤 결과를 냈는지 한눈에 따라갈 수 있습니다.
실전: 자기 코드를 고치다
이 코드 구조와 로그 구조가 실제로 어떻게 쓰이는지, 사례를 보겠습니다.
2026년 2월, 코드몽이 자기 레포에 올린 PR #45에서 테스트가 실패했습니다. 리뷰 코멘트를 처리해 달라는 요청을 받은 코드몽은, 원인을 좁히기 시작했습니다.
먼저 로그 구조가 출발점을 줬습니다. 같은 threadTs 안에서 gh pr view 45, 리뷰 코멘트 조회, 이슈 코멘트 조회가 순서대로 tool log에 남아 있었고, 거기서 바로 mention-effect.test.ts의 두 실패 케이스와 assertion 위치가 드러났습니다. GitHub 화면을 여기저기 옮겨 다닌 게 아니라, 같은 threadTs로 묶인 tool log만 따라가도 실패 맥락이 한 줄로 연결됩니다.
그 다음부터는 코드 구조가 도움을 줬습니다. 코드몽은 실패한 테스트를 먼저 읽고, 채널 C1에서 handleMention을 호출한 뒤 say가 한 번 불리길 기대한다는 걸 확인했습니다. mention.ts의 handleMention으로 가서 초반부를 읽으니, early return이 보였습니다.
다시 slack-channel.ts로 내려가면, 이 함수의 실체가 드러났습니다.
NODE_ENV가 development가 아니면 통과시키지만, test 환경은 별도로 처리하지 않아서 기본값인 development로 떨어지고, 테스트의 채널 C1이 허용 목록에 없어 조기 종료되는 것이었습니다.
수렴 과정을 정리하면 이렇습니다: 실패 테스트 확인 → say expectation 확인 → handleMention 조기 종료 지점 발견 → isDevAllowedChannel 규칙 확인 → test 환경 예외가 없다는 걸 발견. 함수 이름이 handleMention, isDevAllowedChannel처럼 직설적이고, 책임이 작은 함수로 나뉘어 있어서 모델이 테스트 → 핸들러 → 유틸 순서로 막히지 않고 좁혀갈 수 있었습니다.
코드몽은 이 문제를 고치기 위해 자기 레포에 바로 PR #46 (fix(test): allow all channels in test environment)을 열었고, 실제 수정도 src/utils/slack-channel.ts 안에 한 줄을 추가하는 작은 패치였습니다.
그리고 흐름은 거기서 끝나지 않았습니다. 같은 PR #45 리뷰 라인에서는 questionId 초기화 누락 같은 다른 상태 관리 이슈도 추가로 드러났고, 실제로 후속 fix commit이 들어갔다가 곧바로 revert되기도 했습니다. 단순히 "자가 수정 PR 하나 올림"이 아니라, 자기 PR 리뷰를 받고 → 관련 코드와 테스트를 다시 읽고 → 작은 self-fix를 만들고 → 후속 이슈에 대해 판단을 계속 조정하는 유지보수 루프가 실제로 돌아간 것입니다.
코드만이 아니라, 자기 판단 규칙까지 고치다
코드몽이 고칠 수 있는 건 코드뿐만이 아닙니다.
코드몽이 여러 레포에 PR을 만들면서, base branch를 잘못 잡는 경우가 생겼습니다. 이를 막기 위해 PR #94에서 system prompt에 base branch 규칙을 넣었습니다.
이 규칙은 product repo에는 맞았습니다. 문제는 너무 product-repo 특화된 규칙이 일반 규칙처럼 들어갔다는 점입니다. 코드몽 자신이나 landing처럼 default branch가 main인 repo까지 같은 사고방식으로 다루면, 잘못된 base branch 판단을 유도할 수 있었습니다.
코드에 버그가 있었던 건 아닙니다. PR을 만들고 push하는 엔진은 정상이었습니다. system prompt에 적힌 base branch 규칙이 너무 좁게 하드코딩돼 있었던 것이 문제였습니다.
PR #95의 수정:
수정된 파일은 src/prompts/system.ts 단 하나입니다. 코드몽이 고친 건 자기 코드가 아니라, 자기가 다음 PR에서 어떤 branch를 선택할지 결정하는 판단 규칙이었습니다.
개선 신호가 들어오는 세 가지 경로
self-improvement가 작동하려면, "뭘 고쳐야 하는지"가 들어오는 경로가 있어야 합니다. 코드몽에는 그 경로가 세 가지입니다.
첫째, Doctor입니다. 코드몽 실행이 비정상 종료되면 자동으로 Slack에 Doctor CTA를 제안합니다. 사용자가 진단 시작을 누르면, Doctor runner가 원본 Slack thread transcript와 prod log excerpt를 모아서 구조화된 진단을 시도합니다.
fixNeeded가 true일 때만 이대로 고쳐서 PR 버튼을 보여줍니다. 진단은 자동이고, 수정은 사람이 버튼을 눌러야 시작되는 opt-in 구조입니다.
둘째, **/feedback**입니다. 사용자가 Slack에서 /feedback을 치면 누가, 어떤 채널에서, 어떤 맥락으로 남긴 피드백인지가 GitHub issue로 자동 등록됩니다. 시스템이 감지하지 못하는 개선점 — "이 응답 톤이 이상했다", "이 기능이 있으면 좋겠다" 같은 것 — 은 이 경로로 들어옵니다.
셋째, 개발자가 직접 모니터링하는 것입니다. 코드몽의 모든 호출이 기록되는 별도 Slack 채널이 있고, 개발자가 이 채널을 지켜보면서 응답 품질이나 워크플로우의 빈틈을 발견하면 바로 개선에 착수합니다. 가장 원시적이지만, Doctor나 /feedback이 잡지 못하는 미묘한 문제를 찾아내는 데는 이 경로가 가장 효과적이었습니다.
경로는 다르지만, 세 가지 모두 결국 같은 곳으로 흘러들어갑니다. 첫 섹션에서 이야기한 change request → PR → review 파이프라인입니다. Doctor가 감지하든, 사용자가 /feedback을 치든, 개발자가 모니터링 채널에서 발견하든 — 개선이 실행되는 방식은 동일합니다. 같은 파이프라인 위에서 코드몽이 자기를 고칩니다.
우리가 배운 것
에이전트가 자기 코드를 다룰 수 있으면, 기술 선택부터 달라집니다. Effect-TS를 고른 것도, 출력 형식을 파싱 가능한 프로토콜로 잘라낸 것도, 로그를 threadTs와 tag로 추적 가능한 이벤트 기록으로 설계한 것도 — 전부 "이 코드를 나중에 LLM이 읽고 고칠 수 있어야 한다"는 전제에서 나온 선택이었습니다.
구조를 잘 만들어 두면, 자가 수정은 자연스럽게 따라옵니다. 자기 자신을 고치기 위해 별도의 시스템을 만든 게 아닙니다. 다른 레포에 적용하는 것과 동일한 change request → PR → review 파이프라인 위에서, PR #46처럼 자기 코드를 고치고, PR #95처럼 자기 판단 규칙을 고칠 수 있었습니다. self-improvement를 위한 특별한 기능이 아니라, 범용적으로 설계한 구조의 자연스러운 확장이었습니다.
지금까지 코드몽이 무엇이고, 어떻게 만들어졌고 어떻게 자기 자신까지 고칠 수 있는 구조가 됐는지를 살펴봤습니다. 하지만 도구를 잘 만드는 것과 팀이 그 도구를 실제로 쓰게 만드는 것은 완전히 다른 문제입니다. 다음 글에서는 코드몽이라는 도구를 팀의 일상 워크플로우에 어떻게 녹여냈는지를 정리합니다.
핵심은 이겁니다: 쓸 수밖에 없게 만드는 것.