AI 에이전트가 로드맵을 관리하는 법: claw-code에서 배운 자율 루프
한 오픈소스 Claude Code 포트의 789개 커밋과 ROADMAP.md를 해부해, AI 에이전트가 스스로 로드맵을 읽고 쓰고 반영하는 루프를 어떻게 구축하는지 분석한다. 그리고 내 프로젝트에 도입할 수 있는 실천 레시피로 정리한다.
들어가며: 789개 커밋, 사람 커밋은 4개
한 오픈소스 레포를 열어봤다. Claude Code를 Rust로 재구현하는 포팅 프로젝트다. 커밋이 789개 쌓여 있었고, 전부 2주 안에 생성된 기록이었다. 작성자 분포가 묘했다.
| 작성자 | 전체 커밋 | ROADMAP.md 수정 |
|---|---|---|
| Yeachan-Heo | 462 | 55 |
| YeonGyu-Kim | 211 | 38 |
| Jobdori | 112 | 29 |
| instructkr (사람 유지자) | 4 | 0 |
상위 세 명은 커밋 메시지 패턴과 빈도로 보아 AI 에이전트였다. 사람 유지자는 squash/port 같은 거대 리셋만 건드렸고, ROADMAP.md는 단 한 번도 직접 수정하지 않았다. 로드맵은 에이전트가 100% 관리하고 있었다.
그런데 이 ROADMAP.md가 심상치 않다. 86KB, 524줄. 번호 붙은 이슈가 #70까지 찍혀 있고, 완료된 이슈도 지워지지 않고 남아 있다. 어떤 엔트리는 2,500자가 넘는 버그 부검이다. 이 로드맵은 기능 기획서가 아니라, 에이전트가 스스로 읽고 쓰며 다음 작업을 고르는 상태 파일이었다.
이 글은 그 구조를 해부한다. 그리고 후반부에서 같은 루프를 내 프로젝트에 어떻게 도입할지 실천 레시피로 정리한다.
1. 이 로드맵이 보통과 다른 네 지점
1-1. 고통점(Pain Points)이 문서 최상단에 박혀 있다
대부분의 로드맵은 "할 일 목록"으로 시작한다. claw-code의 로드맵은 다르다. Goal 바로 아래에 "Current Pain Points" 섹션이 7개 항목으로 박혀 있다.
### 1. Session boot is fragile
- trust prompts can block TUI startup
- prompts can land in the shell instead of the coding agent
- "session exists" does not mean "session is ready"
### 2. Truth is split across layers
- tmux state
- clawhip event stream
- git/worktree state
- test state
- gateway/plugin/MCP runtime state
### 3. Events are too log-shaped
- claws currently infer too much from noisy text
- important states are not normalized into machine-readable events이게 왜 중요한가. 에이전트가 새로 발견한 이슈를 기존 고통 범주 중 어디에 속하는지 분류할 수 있어야 하기 때문이다. 범주가 먼저 있어야 후속 이슈의 위치가 결정된다. "상태가 다섯 갈래로 쪼개져 있다"는 문장 한 줄이 이후 수십 개 이슈의 북극성이 된다.
1-2. 이슈가 번호로 관리된다
백로그의 이슈는 모두 번호가 붙어 있다. 숫자는 추가된 순서로 단조 증가한다. 최근 커밋 로그를 보면 정확히 이렇게 흐른다.
17e21bc docs(roadmap): add #70 — install-source ambiguity misleads users
4f83a81 Make dump-manifests recoverable outside the inferred build tree
763437a docs(roadmap): add #69 — lane stop summary quality floor
06d1b8a docs(roadmap): add #68 — internal reinjection/resume path opacity
4e199ec docs(roadmap): add #67 — structured review verdict events번호는 참조 ID다. 이후 커밋이 ROADMAP #41 test isolation 같이 번호로 이슈를 지목할 수 있고, 다른 엔트리가 "#28 내에서 다루지 않은 부분은 #29에서 처리" 같은 교차 참조를 남길 수 있다. 번호 없이 제목만 있다면 에이전트는 매번 긴 제목 문자열 매칭을 해야 한다. 번호는 에이전트의 작업 메모리 비용을 줄이는 인덱스다.
1-3. 완료된 이슈도 지우지 않는다
대부분의 이슈는 끝에 **done**: ... 표시가 붙어 있다. 지워지지 않고 남는다.
9. Stale-branch detection before workspace tests — **done**: `stale_branch.rs`
module with freshness detection, behind/ahead metrics, policy integration
10. MCP structured degraded-startup reporting — **done**: `McpManager`
degraded-startup reporting (+183 lines in `mcp_stdio.rs`), failed server
classification (startup/handshake/config/partial), structured
`failed_servers` + `recovery_recommendations` in tool output일반 프로젝트 관리라면 완료된 이슈는 이슈 트래커에서 닫아버리고 로드맵 문서에서는 제거한다. claw-code에서는 그 반대다.
왜일까. 에이전트가 새 이슈를 발견했을 때 기존 이슈와 겹치는지 빠르게 비교해야 하기 때문이다. 과거에 "hook 실행 시 BrokenPipe" 이슈를 어떻게 해결했는지 로드맵 안에서 바로 찾을 수 있어야, 비슷한 증상이 다시 나올 때 같은 진단 경로를 반복하지 않는다. 완료 이슈는 삭제 대상이 아니라 참조 자산이다.
1-4. 틀린 가설까지 기록되어 있다
가장 특이한 지점이다. 해결된 이슈의 본문에는 처음에 내렸던 잘못된 진단과, 그 오진에 기반한 실패한 수정 시도까지 남아 있다. 다음 섹션에서 대표 사례를 본다.
2. 사례: #25 부검 — "틀린 가설"이 기록된 이유
ROADMAP #25는 Linux CI에서 flake로 시작해 결정론적 실패로 악화된 테스트 하나의 부검이다. 엔트리 구조가 7단계로 고정되어 있다.
2-1. 증상
plugins::hooks::collects_and_runs_hooks_from_enabled_plugins
PostToolUse hook .../hooks/post.sh failed to start for "Read":
Broken pipe (os error 32)첫 회 시도 flake → 3회차에 결정론적 red. 4번의 CI run 번호까지 엔트리에 박혀 있다.
2-2. 첫 번째 가설 (틀림)
초기 진단은 "생성된 .sh 파일에 execute bit가 없어서 Command::new(path).spawn()이 fork/exec에서 race한다"였다. 이 가설 기반으로 chmod 패치가 먼저 shipping됐다.
2-3. 반증
chmod 패치가 배포된 뒤에도 CI에서 같은 Broken pipe 에러로 실패했다. 코드 리뷰가 아니라 CI run의 empirical 결과로 가설이 반증되었다.
2-4. 진짜 원인
부모 프로세스가 자식의 stdin에 write_all로 쓰는 경로에서 BrokenPipe를 무조건 에러로 propagate하고 있었다. 테스트용 hook 스크립트가 #!/bin/sh + printf 한 줄이라서 마이크로초 단위로 실행 완료 → 자식이 부모보다 먼저 exit하고 stdin을 닫아버림 → 부모가 200바이트짜리 JSON payload 쓰기를 끝내기 전에 EPIPE 발생 → "failed to start"로 오분류.
실제로는 자식이 정상 실행되고 stdout까지 뱉었는데도 "시작 실패"가 된 것이다.
2-5. 플랫폼 차이
- Linux: pipe가 닫히면 즉시
EPIPE. - macOS: 작은 payload가 pipe 버퍼에 들어가버려서 자식 exit 전에 write 완료. 재현 안 됨.
이것이 "로컬에선 green인데 CI(Linux)에서만 빨강"의 원인이었다.
2-6. 수정 3파트
write_all에서BrokenPipe만 명시적으로 swallow, 그 후wait_with_output()으로 exit 상태 정상 캡처- 하이진 강화로 생성
.sh에0o755설정 - 회귀 방지 테스트 신설
2-7. 메타 교훈
`Broken pipe (os error 32)`가 fork/exec 경로에서 나오면 두 해석 사이에
모호하다:
1. "exec 자체가 실패했다"
2. "exec는 성공했고 부모가 stdin 쓰기를 끝내기 전에 자식이 먼저 exit했다"
첫 가설은 (1)을 cargo-cult했다. 반증은 empirical CI에서 나왔고,
코드 검토에서 나오지 않았다. 이 패턴을 기억해두자: pipe 에러가
fork/exec에서 나오면, 실패로 귀인하기 전에 wait_with_output()이
자식에 대해 실제로 뭐라고 보고하는지 먼저 계측하라.왜 이 구조를 고집하나
증상만 남기고 오진을 지웠다면 다음 번에 같은 에러 코드를 만났을 때 또 exec-bit 가설로 빠질 확률이 크다. 에이전트는 과거 자신의 기록을 읽고 다음 행동을 정한다. 그러므로 오진의 궤적이 기록되지 않으면 같은 오류 경로를 반복한다. 이 엔트리는 todo 항목이 아니라 에이전트의 장기 기억이다.
3. 로드맵을 굴리는 세 가지 커밋 스타일
커밋 이력을 보면, ROADMAP.md를 건드리는 커밋은 정확히 세 갈래로 나뉜다. 이 셋이 루프의 기본 동작을 구성한다.
3-1. docs(roadmap): add #NN — 번호 부여
17e21bc docs(roadmap): add #70 — install-source ambiguity misleads users
06d1b8a docs(roadmap): add #68 — internal reinjection/resume path opacity
2329ddb docs(roadmap): add #64 — structured artifact events새 문제를 발견했을 때만 쓴다. 번호는 현재 최대 번호 + 1. 이 커밋에서는 로드맵에만 추가하고 코드는 건드리지 않는다. 발견과 해결을 구분한다.
3-2. Retire the stale ... — 이슈 회수
b825713 Retire the stale slash-command backlog item without breaking verification
8eb93e9 Retire the stale bare-word skill discovery backlog item
d40929c Retire the stale OpenAI reasoning-effort backlog item
2d5f836 Retire the stale broken-plugin warning backlog item검증 결과 현재 코드에서 이미 해결되어 있는 이슈, 혹은 더 이상 유효하지 않은 이슈를 "retire" 처리한다. 단순 삭제가 아니라, 검증 테스트가 green인지 먼저 확인하고 retire 사유를 기록한다.
3-3. 일반 fix 커밋이 ROADMAP 1~3줄을 함께 수정 — 작업-기록 동기화
6a95756 Make recovery handoffs ... +2/-2 ROADMAP.md
f91d156 Keep poisoned test locks ... +3/-1 ROADMAP.md
26b89e5 Keep completed lanes ... +3/-3 ROADMAP.md실제 코드 수정이 일어나는 모든 커밋은 ROADMAP의 해당 엔트리를 같은 커밋에서 업데이트한다. "done at <hash>" 태그가 붙는 순간이 이때다. 코드와 로드맵이 같은 원자 단위로 변한다. 별도 커밋으로 나누면 로드맵이 코드보다 뒤처지거나 앞서는 drift가 생긴다.
4. 결정적 힌트: backlog-scan lane
커밋 로그를 더 보면 단서가 하나 더 나온다.
8f53524 Make backlog-scan lanes say what they actually selected
1d83e67 Keep the backlog sweep from chasing external executor notes
5c85e5a Keep the worker-state backlog honest with current main behaviorbacklog-scan lane이라는 전담 lane 타입이 존재한다. 시스템에 내장된 루프는 대략 이렇다.
┌─────────────────────────────────────────────────────────┐
│ 1. backlog-scan lane: ROADMAP.md 읽기 → 다음 #NN 선택 │
│ 2. work lane: 해당 이슈 코드 수정 + 테스트 + 검증 │
│ 3. 동일 커밋에 "done at <hash>" ROADMAP 업데이트 │
│ 4. 새 문제 발견 → docs(roadmap): add #NN+1 append │
│ 5. stale 엔트리 검출 → Retire │
│ └── 반복 → 다음 backlog-scan lane이 1단계로 복귀 │
└─────────────────────────────────────────────────────────┘그리고 Keep the worker-state backlog honest with current main behavior 같은 커밋이 있다는 건, 에이전트가 자신의 과거 ROADMAP 엔트리가 현재 코드와 어긋났을 때 그것을 감지하고 정정하는 단계까지 존재한다는 뜻이다. 로드맵이 코드와 거짓말하지 않게 주기적으로 정합성 검사를 돈다.
5. 내 프로젝트에 적용하려면
여기서부터 실천 파트다. claw-code는 병렬 lane, lane 이벤트 버스, clawhip 오케스트레이터 같은 하부 인프라를 깔고 움직인다. 개인 프로젝트에서 그 전부를 흉내 낼 필요는 없다. 패턴만 추려서 파일 한 개 + 커밋 컨벤션 + 에이전트 지침으로도 같은 루프의 핵심은 돌릴 수 있다.
5-1. 파일 하나부터: ROADMAP.md의 최소 템플릿
프로젝트 루트에 ROADMAP.md를 만든다. 최소 구조는 네 섹션이다.
# Project Roadmap
## Goal
(이 프로젝트의 "clawable" 한 줄 목표)
## Current Pain Points
### 1. (고통 범주 1)
- 구체 증상 1
- 구체 증상 2
### 2. (고통 범주 2)
- ...
## Backlog
1. **(이슈 제목)** — 한 문단 설명.
**Action.** 구체 수정 포인트.
2. ...
## Retired / Done Log
- #1 done at <commit hash>: (요약)핵심은 Pain Points를 먼저 고정하는 것이다. 에이전트가 새 이슈를 발견했을 때 "이건 범주 2야" 하고 분류 먼저 하고 넘어가게 된다.
5-2. 세 가지 커밋 스타일을 CLAUDE.md에 못 박기
에이전트가 로드맵을 관리하게 하려면 커밋 컨벤션을 명시해야 한다. 프로젝트의 CLAUDE.md(또는 AGENTS.md)에 다음을 추가한다.
## ROADMAP 관리 규칙
1. **신규 이슈 발견 시**: `docs(roadmap): add #NN — <title>` 커밋을 단독으로
만든다. 이 커밋에서는 코드를 수정하지 않는다. 번호는 현재 최대 번호 + 1.
2. **이슈 해결 시**: 코드 수정 커밋 안에서 ROADMAP.md의 해당 엔트리를 같이
수정한다. 엔트리 끝에 `**done at <short-hash> on YYYY-MM-DD**: <요약>`
을 추가한다.
3. **이슈 무효화 시**: `Retire the stale <title> backlog item` 커밋으로
엔트리를 Retired 섹션으로 이동한다. Retire 사유를 한 줄로 남긴다.
4. **신선도 검사**: 매 작업 시작 전, 선택한 이슈의 "done at" 태그가
실제 코드 상태와 일치하는지 검증한다. 불일치 시 `Align the <area>
roadmap note with current behavior` 커밋으로 정정한다.이 네 줄만 못 박아두면, 에이전트가 코드 작업을 할 때마다 ROADMAP이 자동으로 같이 따라간다.
5-3. 실패 부검 포맷 고정 — 6단계 템플릿
#25 스타일 부검을 쓰고 싶으면 포맷이 고정되어야 한다. 즉흥으로 쓰면 정보의 빠짐과 순서가 매번 달라진다. 다음을 엔트리 템플릿으로 고정하자.
NN. **(이슈 제목)** — **done at <hash> on <date>**.
**증상**: 재현 경로 + 에러 메시지.
**첫 가설(틀렸다면)**: 처음 세운 가설과 그 기반 수정 시도.
**반증**: 어떤 관찰로 가설이 깨졌는지. 가능하면 CI run/재현 로그 링크.
**진짜 원인**: 코드 경로를 file:line 수준으로 특정.
**수정**: 변경된 파일/함수. "defense-in-depth"와 "핵심 fix"를 구분.
**교훈**: 다음 번에 같은 증상을 만났을 때 피해야 할 첫 가설."첫 가설(틀렸다면)" 섹션이 핵심이다. 틀린 가설을 제거하지 않고 기록으로 남기는 것이, 같은 오류 경로를 반복하지 않게 하는 유일한 방법이다.
5-4. 번호 매김 전략과 Retire 기준
번호는 단조 증가. 완료된 이슈는 번호를 재사용하지 않는다. 재사용하면 "#25"로 남긴 커밋/PR/슬랙 대화의 링크가 다른 내용을 가리키게 된다.
Retire 기준은 단 두 가지로 좁히는 게 좋다.
- 이미 해결됨: 현재 코드에서 증상 재현되지 않고, 회귀 테스트가 있다.
- 더 이상 유효하지 않음: 해당 영역의 설계가 바뀌어서 이슈 자체가 의미를 잃었다. 이유를 한 줄로.
그 외의 경우(예: "덜 중요해 보여서", "복잡해서")는 Retire하지 않는다. 낮은 우선순위로 두되 목록에는 남긴다. 삭제는 정보를 지우는 행위다.
5-5. 에이전트용 루프 지침
마지막으로, 에이전트가 한 사이클을 어떻게 돌지 CLAUDE.md에 명시한다.
## 작업 루프
1. **Backlog scan**: ROADMAP.md를 읽고, Pain Points 범주에 속한
미해결 이슈 중 검증 가능한 것 하나를 선택한다. 선택 이유를
한 줄로 말하고 시작한다.
2. **신선도 검사**: 선택한 이슈의 "done at" 태그가 없는지,
그리고 설명된 증상이 실제로 재현되는지 먼저 확인한다.
재현되지 않으면 Retire로 전환한다.
3. **수정 + 테스트**: 변경은 해당 이슈 범위로 제한한다.
관련 없는 리팩토링을 끼워 넣지 않는다.
4. **로드맵 업데이트**: 같은 커밋에서 ROADMAP.md의 해당 엔트리에
"done at <hash>" 태그와 요약을 추가한다.
5. **새 이슈 포착**: 작업 중 발견한 인접 문제가 있으면,
별도 커밋 `docs(roadmap): add #NN+1`로 등록한다.
현재 작업에 끼워 넣지 않는다.이 다섯 단계를 지침으로 박아두면 에이전트의 한 사이클이 일관된다. 루프가 일관되어야 장기 기록이 일관된다.
6. 주의점 — 이 방식이 항상 좋은 건 아니다
claw-code의 ROADMAP 스타일을 그대로 복제하기 전에 몇 가지 트레이드오프를 보자.
1. 문서가 빠르게 커진다. 86KB짜리 마크다운 하나는 사람이 읽기에는 피곤하다. claw-code에서 이것이 성립하는 이유는 실제 소비자가 사람이 아니라 에이전트이기 때문이다. 사람이 진짜로 읽어야 하는 로드맵이라면 완료 이슈는 별도 HISTORY.md로 분리하는 편이 낫다.
2. 에이전트가 틀린 정보를 기록할 수 있다. 과거 ROADMAP에 "commit 0984cca가 /state 엔드포인트를 추가했다"고 남아 있지만 실제로는 그런 커밋이 없었던 사례가 claw-code에도 기록되어 있다(섹션 Deployment Architecture Gap 중 "Prior session note"). 신선도 검사(5-1의 4번 규칙)를 두지 않으면 로드맵이 코드와 거짓말을 한다.
3. 번호가 정치가 된다. 여러 에이전트가 동시에 번호를 매기려고 하면 #NN이 겹친다. 직렬화 지점이 필요하다. claw-code는 backlog-scan lane 하나가 번호 매김을 전담한다. 규모가 작다면 "ROADMAP 업데이트는 항상 main에 직접, rebase로 번호 충돌 시 더 나중 커밋이 재번호" 정도의 규칙으로 충분하다.
4. 오진 기록이 정보 과잉이 될 수 있다. 부검이 길어지면 핵심이 묻힌다. "결론 → 요약 → 상세" 순서로 쓰고, 요약 한 줄에서 전체 상태가 드러나게 해야 한다. claw-code의 #25도 첫 줄에 done at 172a2ad on 2026-04-08과 한 문장 요약이 먼저 나온다.
마무리
이 패턴은 결국 한 문장으로 요약된다.
로드맵을 할 일 목록이 아니라 에이전트의 장기 기억으로 취급하라.
이 전환에서 네 가지가 따라 나온다.
- 완료 이슈를 삭제하지 않는다. 참조 자산이다.
- 틀린 가설을 기록한다. 반복되는 오진을 끊는다.
- 코드와 로드맵을 같은 커밋에서 수정한다. drift를 원천 차단한다.
- Pain Points를 먼저 고정한다. 새 이슈가 분류될 위치를 준다.
당장 오늘 할 수 있는 건 ROADMAP.md 하나 만들고, CLAUDE.md에 세 줄짜리 커밋 컨벤션을 박는 것이다. 그다음 한 번의 실패 부검을 6단계 포맷으로 써보자. 한 번만 포맷대로 쓰면, 에이전트는 다음부터 그 포맷을 복제한다. 루프는 그렇게 시작된다.