AI 하네스
Claude Code를 무인으로 부리려고 짠 협업 하네스.
무엇인가
혼자 여러 프로젝트를 돌리려면, AI를 일관되게·병렬로·무인으로 부릴 체계가 필요했다. 그래서 Claude Code 위에 직접 설계한 것이 이 협업 하네스다. 크게 다섯 가지를 얹었다. ① 규칙 자동주입(Skills) ② 전문 작업 자율 위임(역할별 Agents) ③ 야간 무인 자동화 ④ 세션을 넘어 지속되는 파일 메모리 ⑤ 도구 그라운딩(MCP).
Skills·Agents·MCP는 Claude Code의 기능이다. 내 몫은 그 위에 무엇을 어떻게 얹었는가에 있다. 에이전트 구성, 규칙 분리, 자동화 파이프라인, 안전 스코핑. 자동화의 경계는 계속 밀었다. 처음엔 초안·수집까지만 자동이고 판단·발행은 사람이 쥐었는데, 개발로그는 결국 발행까지 무인으로 넘겼다. 위키도 한동안은 낡았다고 드러내기만 했는데, 지금은 밀린 만큼을 매일 조금씩 자동으로 갚는다.
왜
AI 협업에는 반복되는 고질이 있다. 매번 같은 규칙을 설명해야 하고, 대화가 길어지면 규칙이 희석되고, 그래서 결과가 일관되지 않고, 작업은 순차로만 흐른다. 이걸 구조로 풀었다: 공통 규칙은 Skills로 자동 주입하고, 전문 작업은 Agents에 자율 위임하고, CLAUDE.md는 경량 라우터로.
하네스 3계층
핵심 원칙은 중복 제거다. 에이전트 A도 B도 알아야 할 것은 Skills에, 전문가에게 맡기고 결과만 받고 싶은 것은 Agent에 둔다.
| 계층 | 무엇 | 역할 |
|---|---|---|
| Skills | 규칙 자동주입 | 모든 대화·에이전트에 자동 적용. "any 금지"를 한 번 쓰면 어느 에이전트가 짜든 지켜짐 → 일관성 |
| Agents | 자율 위임 | 별도 컨텍스트·병렬 실행·규칙 100% 유지. "전문가를 새로 부르는 것" |
| CLAUDE.md | 경량 라우터 | 한 페이지. 에이전트 목록 + 워크플로우 안내만, 상세는 Skills/Agents에 위임 |
Skills는 처음엔 코딩 규칙과 프로젝트 운영 규칙 둘이었는데 다섯이 됐다. 판단 기준을 세 개 더 뗐다. 시각 설계를 무슨 수치로 판단하는지(design-craft), 시각 효과를 CSS·Canvas·WebGL 중 무엇으로 구현하는지(visual-tech), 저장소에 이미 있는 검사·생성 스크립트가 무엇인지(repo-guards). 마지막 하나는 "새로 만들기 전에 있는 걸 먼저 본다"를 강제하려고 만들었다. 차트 라이브러리가 있는 줄 모르고 SVG를 347줄 손으로 짠 적이 있어서다.
이 다섯은 거의 안 고친다. 8월 초에 한 번 정리한 뒤로 9월 중순에 한 줄을 고쳤고, 열여드레 뒤에 한 절이 붙었다. 이번에 붙은 건 기본 글꼴이다. 프로젝트마다 매번 고르던 것을 "따로 지시가 없으면 이것"으로 정해 두고, 이미 다른 글꼴로 서 있는 프로젝트는 화면 전체가 바뀌는 일이니 그 프로젝트 단위로 계획해서 바꾼다는 조건까지 같이 적었다. 규칙을 하나 늘린 게 아니라 고를 자리를 하나 없앤 쪽이다. 라우터 맨 앞의 "고를 수 있게 두면 매번 고르고 매번 틀린다"를 스킬 층에서 집행한 셈이다.
그 기본값이 나흘 뒤 실제로 화면을 바꿨다. 이 사이트의 본문 글꼴이 그 글꼴로 넘어갔고, 제목도 영문 서체 뒤에서 한글이 섞이면 그걸 받는다. 스킬에 적어둔 조건도 그대로 지켰다. 글꼴이 사용자 PC에 깔려 있다고 가정하지 않고 웹폰트를 같이 실었고(내용이 바뀌지 않게 배포 커밋까지 박아뒀다), 한 번에 전부 갈아 끼우지 않고 홈만 예전 글꼴로 남겼다. 규칙을 쓰는 층과 그 규칙이 집행되는 층은 다르다. 전에는 이 간격이 문서로만 있었는데, 이번엔 커밋 두 개로 대조가 됐다.
그리고 라우터 자체를 3층으로 갈랐다. 저장소 하나에 성격도 스택도 다른 프로젝트가 스무 개 넘게 있는데, 라우터 한 장이 그걸 다 이고 있으니 스택을 오판했다. 지금은 루트는 공통 규칙만, 스택·빌드 명령·디자인은 projects/<이름>/CLAUDE.md, 직무 기준은 에이전트 정의로 나뉜다. 세션은 저장소 루트에서 시작해서 하위 CLAUDE.md가 자동으로 안 읽히므로, "무엇부터 읽는가"라는 절차를 루트 맨 앞에 박아뒀다.
라우터 안의 프로젝트 목록도 한 번 정리했다. 같은 목록이 두 곳에 있으면서 서로 자기가 정본이라고 하고, 정작 둘 다 실제와 어긋나 있었다. 지금은 역할을 갈랐다. 라우터의 표는 무엇이 어디 있는지만 말하고, 상태와 현황은 별도 문서 하나가 정본이다. 겸사겸사 함정도 적어뒀다. 저장소 이력으로 추적하지 않는 디렉터리가 몇 개 있는데, 거기서 커밋 0은 "안 했다"가 아니라 "여기서는 안 센다"는 뜻이다.
그 표는 한동안 한 줄씩 늘었는데, 이번에 여섯 줄이 한꺼번에 들어와 스물한 줄이 됐다. 빠져 있던 것을 몰아서 넣은 것이라 라우터 버전은 안 올렸다. 버전은 규칙이 바뀐 표시이고 목록이 늘어난 표시가 아니다. 열여덟 회차째 같은 자리에 있다.
정본 쪽에서 더 눈에 남는 건 숫자가 아니라 머리말이다. 기준일을 두 달 만에 올리면서, 전부 다시 잰 게 아니라 다섯 행만 다시 쟀다는 것과 어느 행인지, 그리고 두 날짜의 숫자를 나란히 비교하지 말라는 것까지 같이 적었다. 기준일 한 줄이 네 줄로 늘었는데, 부분만 재고 전부 잰 것처럼 보이게 두는 것보다는 이게 낫다. 자동화에 성공 도장을 미리 찍지 않는 것과 같은 얘기다.
라우터에서 규칙을 덜어내기도 했다. 7월 말에 "메인이 일하는 방식" 규칙 여섯 개를 적어뒀는데, 여드레 뒤에 세어 보니 그중 여섯 개를 하루에 다 어겼다. 같은 진단을 두 번 하고 두 번 다 문서로 덮은 다음 두 번 다 고쳤다고 말한 상태였다. 문서를 늘리는 것으로는 안 바뀐다는 뜻이라, 그날 여섯을 넷으로, 넷을 셋으로, 셋을 둘로, 둘을 0으로 네 번에 걸쳐 걷어냈다. 지금 그 절에 남은 건 "시키면 하고, 끝나면 무엇을 어떻게 해서 완료했다고 보고한다" 한 문장과 왜 이렇게 짧은지에 대한 설명뿐이다.
대신 라우터 맨 앞에 절차가 아니라 목적을 뒀다. 쉽고 · 빠르고 · 예쁘게. 고를 자리를 없애고, 왕복을 없애고, 그러고도 봐줄 만해야 한다는 세 줄이다. 여기엔 앞의 실패가 그대로 들어 있다. 빠르려고 단계를 건너뛰면 며칠 뒤에야 드러날 결함이 생기니, 빠른 것은 단계를 빼는 것이 아니라 되돌아오지 않는 것이라고 적어뒀다.
개발 사이클 오케스트레이션
개발 단계마다 역할별 전문 에이전트 8개를 매핑하고, 모델을 티어링했다(추론=opus, 기계적 작업=sonnet).
| 에이전트 | 모델 | 단계 |
|---|---|---|
senior-clean-architect | opus | 구현 · 스택은 프로젝트 CLAUDE.md를 읽어 판단 |
code-reviewer | opus | 리뷰 · 심각도 4단(차단·중요·Nit·칭찬) |
test-runner | sonnet | 검증 · 빌드·타입·린트 |
commit-manager | sonnet | 커밋 · diff 분석·자동 커밋 |
librarian | opus | 도서관 · 프로젝트 가로질러 탐색·회고 |
blogger | opus | 글쓰기 · 페이퍼·이슈·devlog에서 블로그 초안 |
designer | opus | 시각 · 정보 설계부터 위계·인코딩까지 |
presenter | opus | 발표 자료 · 교육 덱·보고용 장표 |
한동안 여덟 개였는데 하나를 걷어냈다. 기획 단계의 requirement-planner는 Claude Code에 내장된 Plan 에이전트와 하는 일이 겹쳤다. 내가 만든 것이라고 남겨두면 선택 비용만 는다. 계획 수립은 내장 Plan에 넘겼다.
그러고 두 달 반이 지나 다시 여덟이 됐다. 장표를 만들 때마다 같은 것을 지적받았다. 제목을 문장으로 쓰지 말라고, 설명을 "~한다"로 끝내지 말라고, 화면 자체를 해설하는 줄을 넣지 말라고. 하루에 열 번을 넘겼을 때 그걸 사람이 매번 말하는 대신 에이전트 정의에 굳혔다. 그래서 presenter의 첫 줄은 역할이 아니라 계기다. 이 기준으로 처음부터 쓰고 매번 다시 묻지 않는다는 문장과, 그게 몇 월 며칠에 생긴 것인지가 적혀 있다.
빼는 기준과 넣는 기준이 같아서 숫자가 제자리로 돌아온 셈인데, 적정 수를 3에서 7로 적어둔 가이드 문서는 안 고쳤다. 지울 때의 근거는 "내장 기능과 겹친다"였고 넣을 때의 근거는 "같은 지적이 반복됐다"였으니 판단 자체는 어긋나지 않는다. 다만 상한을 넘긴 것은 사실이다. 상한을 올릴지 수를 세지 않기로 할지는 아직 안 정했다.
커밋 단계는 반대로 좁혔다. 원래는 모든 커밋이 commit-manager를 거쳤는데, 커밋 한 번에 5분이 들었다. 커밋 자체는 10초고 나머지는 에이전트를 부르고 돌아오는 왕복이었다. 게다가 에이전트가 백그라운드로 도는 사이에 같은 커밋이 한 번 더 만들어지는 사고까지 났다. 느린 것이 안전하지도 않았던 셈이다. 지금은 평범한 커밋을 직접 하고, 에이전트는 예민할 때만 부른다. 스테이징된 삭제, gitignore 변경, subtree 미러, push 갈라짐 같은 경우다. 원래 이 규칙이 막으려던 두 가지 중 담는 범위 실수는 "git add -A 금지, 담은 뒤 목록 확인" 한 줄이 막고, 파괴적 명령은 이미 훅이 막는다.
에이전트 정의에는 규칙만이 아니라 돌릴 명령도 박아둔다. 사서 에이전트가 ls로 앞 열두 줄만 훑어보고 "테스트가 거의 없다"고 보고한 적이 있다. 잘라낸 출력에 안 보인다는 건 없다는 근거가 아닌데 그렇게 읽은 것이다. 그래서 활동량·테스트 유무·CI·배포 대상처럼 사실로 말하게 될 항목마다 그걸 확인하는 명령을 표로 정의에 넣었다. 그 표에는 "커밋 수를 사람이 한 일로 읽지 마라"도 같이 들어 있다. 이 사이트는 야간 크론이 매일 커밋하기 때문에, 활동량을 말하려면 기계가 찍은 커밋을 먼저 갈라내야 한다.
여기서 하나 더 알게 된 게 있다. 내가 얹은 규칙 위에, 내가 끌 수 없는 층이 하나 더 있다. 도구 번들이 세션마다 "요청받지 않으면 서브에이전트를 부르지 마라"를 자동으로 붙이는데, 그게 이 저장소의 에이전트 워크플로우를 조용히 무력화했다. 설정으로 끄는 방법이 없으니 문서 쪽에서 풀었다. 라우터의 에이전트 절 맨 앞에 "이 표와 워크플로우는 상시 지시다"라고 못 박아, 그 문구가 말하는 '사용자의 요청'이 곧 이 절이라고 적어뒀다. 하네스를 남의 기본값 위에 짓는다면, 그 기본값과 부딪히는 지점을 찾아 명시적으로 덮어써야 한다.
명령을 일일이 치지 않아도, 요청 하나에 알맞은 에이전트가 자동으로 이어 붙는다. 화면 작업만은 순서를 못 박았다. designer가 정보 설계와 시각 방향을 먼저 잡고, 구현이 그 뒤에 오고, 다시 designer가 결과를 검수한다.

이 위키들을 채운 코드 전수조사도 이 하네스로 돌렸다. 15개+ 서브에이전트를 병렬로 띄워 여러 프로젝트를 동시에 조사했다. 하네스가 실제로 작동한다는 증거다.
자동화한 것
개발 기록·회고·문서 관리를 커맨드 8종 + 야간 무인 실행으로 파이프라인화했다.
/devlog는 그날 커밋·파일 수정시각·이슈 원장을 긁어 개발일지 초안을 자동 생성한다/issue로 이슈를 원장에 기록(open)하고, 해결을 반영(done)하고, 목록을 조회(list)한다/blog-draft·/publish로 이슈·배움을 블로그 글로 빚는다. 그리고 이 개발로그는 결국 발행까지 완전 무인으로 갔다(→ 반자동에서 완전 무인으로)/til로 그날 배운 것을 일지에 즉석 기록한다/trend로 매일 아침 여러 포럼(HN·arXiv 등)에서 기술 동향을 수집해 뉴스카드로 브리핑한다/ideas로 그 브리핑에 달린 "이거 써먹을 만함"을 나중에 프로젝트별로 다시 꺼낸다. 수집은/trend, 재사용은/ideas로 갈랐다. 이 색인은 사이트에 자기 화면도 얻었다. 칸반 안에만 있던 아이디어를 월별로 펼치고 프로젝트별로 거르는데, 거르기를 쿼리 대신 주소로 둔 건 공유한 링크가 나중에도 그대로 맞게 하려는 것이다. 커맨드가 만든 것이 사이트에 서는 두 번째 자리다(첫째는 일지 복사)/refresh로 코드에 밀린 위키를 커밋 근거로 다시 조사해 갱신한다. 지금 이 문서도 그 대상이었다
일지 규격에서 한 번 크게 어긋났다. 내가 준 형식은 "1. OOO 작업"처럼 명사로 끊는 보고체였는데, 정작 쓰는 쪽이 "명사로 끝내지 않는다"를 자기 규칙으로 박고 62건을 전부 서술체로 써버렸다. 중간에 명사 라벨로 갈까 물었을 때도 근거를 대며 반대했다. 정반대를 박아두고 두 번 같은 방향으로 어긴 셈이다. 한 커밋으로 62건을 원래 규격으로 되돌렸다. 항목은 명사로 끊어 25자 안쪽, 요약은 맨 위 산문에서 맨 아래 브리핑으로, 준 적 없는 "배움·인상" 절은 삭제. 어떻게 짜였나는 원장과 git log가 이미 들고 있어서 일지가 중복으로 들 이유가 없고, 그날 걸린 것과 무엇을 접었나는 일지가 아니면 아무 데도 안 남는다. 앞 절의 "내가 끌 수 없는 층"과 방향은 같지만 층이 다르다. 이번엔 남의 기본값이 아니라 규격 위에 스스로 얹은 규칙이 원본을 덮었다.
쌓이기만 하던 이 일지들이 이제 사이트에도 선다. 저장소 안의 일지를 빌드 전에 사이트 콘텐츠로 복사하는 단계를 하나 뒀다. 배포 빌드는 저장소 루트를 들고 있다는 보장이 없어서, 원본을 직접 읽으면 로컬에서만 되고 배포에서는 조용히 0건이 된다. 복사가 들어가는 자리도 골랐다. 발행물 경로는 신선도 감시에서 빠져 있어, 매일 복사본이 갱신돼도 문서 배지가 흔들리지 않는다.

야간 무인, 그리고 안전 스코핑
launchd에 매일 도는 크론 셋을 걸었다. 밤 23:00 개발일지(/devlog), 아침 09:00 동향 브리핑(/trend), 10:00 RAG 평가. 대표는 야간 /devlog로, 헤드리스로 돌아 그날 일지 초안을 만들고 알림을 띄운다. 여기서 핵심 설계는 안전 스코핑이다. 무인으로 도는 만큼, Bash 전체가 아니라 읽기성 명령만 허용한다(git·find·grep·ls + 파일 읽기/쓰기). 임의 셸 실행은 차단해 무인 실행의 blast radius를 최소화했다. 발행처럼 blast radius가 큰 단계는 처음엔 기본 OFF로 두고 수동 루프로 검증했다. 그 검증이 끝난 뒤, 발행까지 무인으로 넘겼다(다음 절). 같은 게이트가 지금 하나 더 걸려 있다. 위키를 다시 색인하는 단계는 배선은 끝났는데 기본값이 꺼짐이라, 현황판에 아직 한 줄도 안 찍혔다.
다만 이 스코핑이 주석에만 있고 명령에는 없던 자리가 하나 있었다. 평가 크론은 파일 두 개만 스테이징하도록 짜여 있고 "다른 세션의 미커밋 작업은 안 건드린다"고 주석까지 붙어 있는데, 정작 커밋 명령에는 경로 한정이 빠져 있었다. 그러면 다른 세션이 이미 올려둔 것까지 같이 커밋된다. 이 저장소는 여러 세션이 git 인덱스를 공유하니 결국 일어났다. 8월 26일 아침 관제판 데이터 갱신 커밋에 엉뚱한 게임 코드 백여 줄이 실려 나갔다. 내용이 날아간 건 아니고 커밋 경계가 흐려진 것뿐이지만, 같은 일을 하는 동향 크론 쪽은 그 자리에 경로 한정이 제대로 붙어 있다. 의도를 두 곳에 다르게 구현하면 안전한 쪽은 주석에만 남는다. 그리고 이건 무인 커밋만의 문제가 아니었다. 9월에 개발일지 형식을 되돌린 커밋에도 전혀 상관없는 게임 코드 스물한 개가 함께 실려 나갔다. 조건이 같으면 사람이 치는 커밋에서도 똑같이 일어난다는 뜻이라, 스코핑을 크론에만 걸어두는 것으로는 부족하다.
무인 push의 사정거리가 저장소 밖에도 있다는 건 더 늦게 알았다. 규칙표에 다른 앱 하나를 "아직 배포 대상이 아니다"라고 적어뒀는데 사실이 아니었다. git 연동으로 이미 나가고 있었고, 커밋 시각과 배포 시각을 대조해서야 확인했다. 게다가 그 앱을 한 줄도 안 고친 push에도 다시 빌드된다. 이 사이트의 야간 크론이 매일 main을 자동 push하니, 내가 아무것도 안 해도 다른 앱이 하루에 몇 번씩 재배포되고 있었던 셈이다. 지금은 규칙표의 그 줄에 주소와 함께 함정으로 적어뒀다. 무인 자동화의 blast radius를 저장소 안에서만 세면 모자란다.
여기서 한 가지를 더 배웠다. 지키게 하고 싶은 것은 부탁이 아니라 훅으로 막는다. 문서에 "파괴적 git 금지"라고 아무리 써둬도 그건 읽히기를 기대하는 규칙이다. 지금은 도구 실행 직전에 훅이 reset --hard·checkout .·clean -fd를 차단한다. 이 저장소는 세션 여럿이 동시에 쓰기 때문에, 한 세션의 되돌리기가 다른 세션의 커밋 안 된 작업을 지운다. 경로를 한정해도 마찬가지라 경로 한정도 안전장치로 치지 않는다.
훅 배선 자체도 저장소에 넣었다. 그전엔 에이전트 정의와 스킬만 git으로 추적하고 설정 파일은 빼뒀는데, 그러면 훅이 사라져도 오류 하나 없이 조용히 멈춘다. 막는 장치가 없어진 걸 아무도 모르는 상태가 제일 나쁘다. 지금은 설정과 에이전트 기억까지 추적하고, 기계마다 다른 로컬 설정만 뺀다.
훅이 늘기만 하는 것도 아니다. 하루 사이에 훅 셋을 얹었다. 할 일이 전부 완료로 바뀌면 요청과 대조하게 하는 것, 위임 프롬프트가 비대해지는 걸 막는 것, 구현을 위임하기 전에 계획을 세우게 하는 것. 마지막 하나는 새벽 다섯 시에 들어와 두 시간을 못 살고 지워졌다. 다음 날 아침에 셋을 한 커밋으로 전부 걷어냈기 때문이다. 그날 남긴 건 파괴적 git을 막는 훅 하나뿐이다.
걷어낸 이유는 계산이 맞지 않아서다. 훅 셋과 측정 스크립트 하나를 만든 그날, 정작 쓸 화면은 하나도 안 나왔다. 기계로 막을 것은 되돌릴 수 없는 것뿐이다. 다른 세션의 커밋 안 된 작업이 날아가는 건 되돌릴 수 없으니 훅으로 막고, 나머지는 틀렸을 때 그 자리에서 고치면 된다.
반자동에서 완전 무인으로
처음엔 개발로그가 반자동이었다. 밤마다 일지 초안은 자동으로 쌓였지만, 그걸 블로그 글로 다듬어 발행하는 건 사람 몫이었다. 읽어보고 /publish로 토글하는 식이었다. 안전한 대신 매번 손이 갔다. 한 단계 줄여보려 후보 방식(밤마다 여러 편 뽑아두고 아침에 하나 고르면 발행, 안 고르면 제일 좋은 걸 자동 발행)도 만들어봤는데, 정작 검토를 어디서 하느냐가 걸렸다. 이 사이트는 정적 공개 사이트라 초안을 띄우면 남들 눈에 그대로 보인다. 비공개로 보려면 로컬 서버를 띄우거나 레포를 열어야 했고, 관문을 줄이려던 게 관문을 늘리고 있었다.
그래서 컨셉으로 돌아갔다. 이건 '무인 블로그'다. 발행까지 넘겼다. 지금은 매일 밤 클라우드 크론(맥이 꺼져 있어도 돈다)이 그날 커밋을 읽고, 글감이 될 게 있으면 제일 좋은 한 편을 써서 바로 발행한다. 없으면 아무것도 안 쓴다.

사전 검토를 없앤 대신 안전장치를 다른 데 뒀다. 초안 후 스스로 AI 냄새를 5개 이상 찾아 실제 커밋·결정으로 바꾸는 자기점검, 근거 없는 주장 금지, 그리고 빌드가 통과할 때만 발행. 문법 깨진 글이나 도구 출력이 샌 글은 아예 못 나간다. 통제는 사후다. 별로면 읽고 고치거나 지운다. 덜 다듬어진 글이 잠깐 올라갈 리스크는, '만드는 과정을 남긴다'는 이 블로그의 전제가 받아준다.
완전 무인엔 걸림돌이 하나 더 있었다. 그림이다. 스크린샷은 사람이 찍어 붙여야 하니, 그게 끼는 순간 무인이 아니다. 그래서 글에 들어가는 도식은 스크린샷 대신 AI가 그릴 수 있는 다이어그램 컴포넌트(흐름·비교·패널)로 바꿨다. 실제 구조에서 나온 정직한 도식만 그리고, 없는 UI를 그럴싸하게 지어내지 않는다. 크론이 사람 손 없이 그림까지 갖춘 글을 낼 수 있게 하려는 것이다.
무인은 실패까지 설계해야 한다
무인 자동화의 진짜 어려움은 성공이 아니라 실패 쪽에 있다. 사람이 안 보고 있으니, 죽어도 죽은 줄 모른다.
처음엔 거짓말을 했다. 크론이 종료 코드를 보지 않고 무조건 성공을 찍고 있었다. 그래서 사흘 내리 동향 큐레이션이 사용량 한도에 걸려 죽었는데도, 현황판은 사흘 내내 "실행 ok"라고 말했다. 자동화가 도는 줄 알았는데 실은 성공 도장만 찍히고 있었던 것이다. 관측 장치가 거짓을 말하면 없느니만 못하다.
지금은 세 단계로 다룬다.

여기서 핵심은 세 번째다. 재시도할 값이 있는 실패와 없는 실패를 구분하는 것. 연결이 끊긴 건 다시 부르면 대개 되지만, 한도에 걸린 건 곧바로 다시 불러도 같은 이유로 죽는다. 무작정 재시도하는 건 복구가 아니라 소음이다.
그리고 자동 복구는 숨기지 않는다. 재시도로 살아난 날은 현황판에도 "재시도 후 복구"라고 적는다. 조용히 살아나면 이 체계가 얼마나 자주 넘어지는지 아무도 모르게 된다.
이 설계가 실제로 값을 한 날이 8월에 다섯 번 있다. 일지 초안 단계가 두 번, 동향 큐레이션 단계가 세 번 네트워크로 죽었다. 다섯 번 다 그날의 나머지 단계는 끝까지 돌았고, 현황판에는 죽은 단계만 실패로 남았다. 단계를 갈라 기록해 두면 한 단계의 죽음이 그 밤 전체를 죽이지 않고, 무엇이 죽었는지도 그대로 보인다. 동향 쪽은 다음에 성공한 회차가 밀린 날짜를 보충으로 메워, 빠진 날이 그대로 구멍으로 남지 않았다. 보충이 한 번에 두 날을 메운 적도 네 번 있다. 처음은 8월 말이고, 9월 하순에는 24일과 25일이 연달아 네트워크로 죽은 다음 26일 회차가 재시도로 살아나 밀린 두 날을 같이 채웠다. 10월에 두 번 더 났고, 그중 한 번은 새로 생긴 실패 종류에도 보충이 걸렸다. 수집해 둔 캐시가 남아 있으면 큐레이션만 다시 돌면 되기 때문이다. 다만 보충은 그날치가 성공했을 때만 시도한다. 한도에 걸려 죽은 날에 보충까지 부르면 같은 이유로 또 죽고 로그만 길어진다.
이 기록이 실제로 일한 밤이 있다. 새벽에 시작한 회차에서 일지 초안 단계만 네트워크로 죽고, 뒤이은 드리프트 감지와 원장 갱신은 멀쩡히 끝났다. 현황판에는 그 한 칸만 "실패 · 네트워크"로 남고 나머지는 정상으로 남았다. 단계를 뭉뚱그려 한 줄로 찍었다면 그 밤은 통째로 성공이거나 통째로 실패였을 것이다. 다음 밤에 그 칸은 정상으로 돌아왔다.
10월 초에는 그 칸이 사흘 연속으로 죽었다. 3일부터 5일까지 일지 초안이 내리 끊겼다. 그때까지 기록에 남은 것 중에서는 가장 긴 연속 실패인데, 같은 사흘의 드리프트 감지는 전부 정상이고 원장 갱신도 사흘 중 두 밤은 끝까지 가서 오프셋을 전진시켰다. 한 칸이 사흘 내리 비어도 나머지는 자기 일을 했다는 뜻이다. 열흘쯤 뒤에 같은 길이가 한 번 더 났다.
그런데 실패를 종류로 갈라 적는 설계에는 함정이 하나 더 있었다. 분류하는 쪽이 틀릴 수 있다. 위의 사흘을 나는 "셋 다 네트워크"로 적어뒀는데, 그중 하루는 로그인 토큰이 만료된 것이었다. 인증이 막힌 응답도 "API Error"로 시작해서, 네트워크를 찾는 줄에 먼저 걸렸던 것이다. 지금은 그 앞에 인증 만료를 찾는 줄이 하나 더 있고, 다시 로그인하라는 말까지 노트에 적힌다. 실제로 9일과 10일 밤에 그 노트가 처음 찍혔다. 이미 찍힌 기록은 고치지 않았다. 그날의 현황판 노트는 여전히 "네트워크"고, 고친 것은 근거를 다시 확인한 쪽 문서뿐이다. 종류를 적는다는 건 그 종류가 틀렸을 때 기록도 같이 틀린다는 뜻이기도 하다. 10월 19일 아침엔 세 번째 종류가 처음 찍혔다. 일지 파일이 안 나왔다는 노트인데, 종료 코드가 0인데 실패로 적힌 첫 칸이다. 죽지 않았는데 산출물이 없는 경우를 따로 가른다는 뜻이다.
같은 날 동향 쪽에도 줄이 하나 더 붙었다. 브리핑 파일은 만들어졌는데 수집이 전부 실패한 날은 실패로 찍는다는 분기다. 주석에 날짜가 적혀 있어 그날 기록을 찾아봤더니 지금도 정상으로 남아 있다. 종료 코드를 보고 갈라 찍는 칸에서도 보는 것이 모자라면 거짓 성공이 남는다. 처음의 성공 도장, 성공이 문자열로 박힌 세 칸에 이어 세 번째 종류다. 새로 붙은 두 줄은 같은 아침에 들어왔는데, 그 파일들은 저장소가 추적하지 않아서 언제 들어왔는지는 커밋이 아니라 파일 시각으로만 말할 수 있다.
그런데 같은 종류의 낙관이 아직 세 칸에 남아 있다. 현황판 일곱 칸 중 넷은 종료 코드를 보고 성공과 실패를 갈라 찍지만, 나머지 셋(드리프트 감지, 썩음 재계산, RAG 평가)은 스탬프를 부르는 자리에 "성공"이 문자열로 박혀 있다. 판정이 아니라 리터럴이다. 그래서 그 세 칸에는 기록이 쌓인 내내 실패가 한 건도 없고, 죽은 회차는 실패가 아니라 빈 줄로 나타난다. 9월 14일 평가 칸이 실제로 그랬다. 그날은 기록도 커밋도 없고 앞뒤 날은 둘 다 정상이다. 9월 하순엔 그 공백이 사흘로 늘었다. 23일부터 25일까지 평가 칸에 기록이 없고, 관제판의 일별 데이터에도 커밋에도 그 사흘이 없다. 세 곳이 같은 구멍을 가리키는데 현황판에는 실패가 한 건도 없다. 그 뒤로도 9월 30일과 10월 2일에 하루씩 또 비었고, 10월 6일에 한 번 더 비었다. 매번 세 곳이 같은 날을 가리키는데 실패 칸은 여전히 0이다. 일곱 번째 공백이다. 그 뒤 이레는 하루도 안 비었다. 다만 그 칸에서 읽을 수 있는 건 여전히 "비지 않았다"뿐이다. 앞 절의 성공 도장과 똑같은 구조가 세 칸에 그대로 남아 있는 셈이라, 고치기 전에 우선 적어둔다.
문서가 낡으면 스스로 알려준다, 로튼 도큐먼트
이 위키의 각 페이지는 특정 시점의 코드를 조사해 쓴 "스냅샷"이다. 그런데 코드는 계속 움직인다. 그래서 문서마다 신선도 배지를 붙였다. 로튼토마토를 오마주해 로튼 도큐먼트라고 부른다. 문서를 마지막으로 고친 뒤 그 프로젝트에 쌓인 git 커밋 수(drift)를 세서 100 · 20 / (20 + drift)로 점수를 낸다. drift가 0이면 100%, 20커밋 쌓이면 50%로 떨어진다. 70% 이상은 FRESH, 40%대는 STALE, 그 아래는 ROTTEN이고, ROTTEN이면 인스펙터의 문서 아이콘이 찢어진 문서로 바뀐다.
계산은 빌드가 아니라 스크립트로 따로 돌려 freshness.json에 커밋한다. 배포 빌드는 git을 얕게 클론해 전체 히스토리가 없어서 drift를 셀 수 없기 때문이다. 여기엔 규칙이 하나 박혀 있다. 하네스가 감시하는 건 각 프로젝트의 코드지, 이 사이트가 발행한 글·위키가 아니다. 그래서 블로그를 아무리 발행해도 하네스 문서 자신은 안 썩는다. 실제로 이 배지는 이 문서 자신을 여러 번 잡았다.
이 문서에는 사각지대가 하나 더 있었다. 감시하는 코드 경로가 사이트 쪽뿐이어서, 정작 하네스를 뜯어고쳐도 하네스 문서는 안 썩었다. 에이전트를 여덟에서 일곱으로 정리하고 스킬을 셋이나 새로 만든 날에도 이 문서의 drift는 0이었다. 기계를 바꿨는데 그 기계를 설명하는 글만 멀쩡한 척을 하고 있던 셈이다. 라우터와 에이전트 정의, 스킬, 훅 스크립트를 감시 경로에 넣자 drift가 9에서 17로 뛰었고, 그제서야 이 문서가 야간 갱신 대상으로 잡혔다.
같은 종류의 구멍을 두 개 더 찾았다. 하나는 메웠고 하나는 아직 열려 있다. 이 사이트 자신을 다룬 문서만 신선도 판에서 20%까지 썩어 있었는데, 계산이 틀린 게 아니라 그 문서만 자동 갱신 루프 밖에 있었다. 갱신은 비공개 사실원장을 읽어 원장과 위키를 같이 고치는 절차인데, 이 문서만 원장이 없어 대상이 될 수 없었다. 기준점이 없으니 밀린 82커밋을 갚을 방법도 없었다. 감시 범위를 좁히는 대신 원장을 만들어 물렸다. 원인이 범위가 아니라 기준점이었기 때문이다. 나머지 하나는 커맨드다. 지금 저장소가 추적하는 하네스는 에이전트 정의, 스킬, 설정, 에이전트 기억 넷이고 커맨드 여덟은 빠져 있다. 그래서 커맨드를 전면 개편해도 이 문서의 drift는 0이다. 실제로 이번에 일지 규격을 통째로 되돌렸는데, 커밋에 남은 건 결과물인 일지 62건뿐이고 규격 자체는 파일 상태로만 확인된다.
세 번째 종류가 이번에 나왔다. 앞의 둘은 계산기가 못 보는 자리였는데 이건 계산기가 볼 수 없는 종류다. drift는 커밋을 센다. 그러니까 감시 경로 안에 있고 기준점도 제대로 물려 있어도, 그 파일이 틀린 채로 가만히 있으면 측정값이 안 올라간다. 이 페이지에 붙은 개발 순환 그림이 그랬다. 첫 상자에 그려진 에이전트는 그림을 만든 다음 날 지워졌는데, 그림은 76일 동안 그대로 서 있었다. 감시 경로 안이고, 기준점도 맞고, 그런데도 그 76일 동안 drift에 한 번도 기여하지 않았다. 바뀌지 않았으니 셀 것이 없었던 것이다. 같은 날 다른 자리에서도 같은 걸 봤다. 확장 하나의 전용 라우터가 "미발행"이라고 적혀 있었는데 마켓에는 한 달 넘게 올라가 있었다. "바뀌었는데 문서가 안 따라왔다"는 커밋으로 잡히고, "안 바뀌었는데 애초에 틀렸다"는 안 잡힌다. 메울 방법은 아직 못 정했다.
그리고 같은 자리에 구멍이 두 개 더 있는 걸 이번에 알았다. 둘 다 등록부 쪽이다. 문서와 코드의 대응은 등록부 한 곳이 쥐고 있어서, 위키를 새로 올리면서 거기 한 줄을 안 더하면 배지도 자동 갱신도 붙지 않는다. 이번에 기술 노트 두 편이 그렇게 들어왔다. 훅과 바이브코딩, 하필 이 하네스를 설명하는 문서다. 세어 보니 기술 노트 열여섯 편 중 등록부가 쥔 건 다섯이었다.
반대쪽은 더 조용하다. 등록은 했는데 감시할 코드 경로를 비워 둔 문서는 셀 커밋이 없으니 드리프트가 영원히 0이고, 배지는 100% FRESH로 선다. 이번에 새로 등록한 여섯 중 둘이 그 상태다. 측정 대상이 아니라고 적어두는 것과, 검증한 적 없는데 신선하다고 말하는 것은 다르다. 후자는 기준을 조사 지점으로 옮겨서 없앴다고 생각한 그 거짓말과 같은 종류다. 아직 안 고쳤고, 우선 적어둔다.
반대 방향으로 헐거운 자리도 있다. 감시 대상에 라우터 파일이 통째로 들어 있어서, 다른 프로젝트를 새로 열고 라우터의 프로젝트 목록에 한 줄을 더하면 그만큼 이 문서가 낡은 것으로 센다. 실제로 이번에 밀린 열 커밋 중 둘이 그랬다. 하네스 규칙은 한 줄도 안 바뀌었고 목록만 늘었다. 과대 측정이라고 할 것까진 아니다. 라우터가 한 파일이라 생기는 성질이고, 어차피 세는 쪽이 헐거우면 문서를 한 번 더 읽는 것으로 끝난다. 거꾸로 헐거우면 안 읽고 넘어가게 되니, 틀리는 방향은 이쪽이 낫다.
그런데 이 배지가 한동안 거짓말을 하고 있었다. 기준점을 "위키 파일을 마지막으로 고친 커밋"으로 잡았던 게 문제였다. 그러면 본문을 검증하지 않고 오타 하나만 고쳐도 배지가 100%로 되돌아간다. 실제로 위키 제목에서 긴 줄표를 걷어낸 커밋 하나가 전 문서의 배지를 한꺼번에 FRESH로 되돌렸다. 검증하지 않았는데 신선하다고 말하는 배지는 앞 절의 성공 도장과 똑같은 종류의 거짓말이다.
기준을 사실원장의 조사 지점으로 옮겨서 고쳤다. 위키의 근거가 되는 비공개 사실원장이 따로 있고, 거기 "여기까지 코드를 확인했다"는 커밋 해시가 박혀 있다. 배지는 그 지점 이후 쌓인 커밋을 센다. 그러자 전부 FRESH이던 표가 실제 상태를 드러냈다. 이 문서가 54커밋 밀린 27% ROTTEN으로 나온 것도 그때다.
같은 김에 계산기를 하나로 합쳤다. 그전엔 배지를 내는 스크립트와 썩음을 알리는 스크립트가 각자 git을 세고 각자 매핑 표를 들고 있어서, 같은 질문에 서로 다른 답을 내놓고 있었다. 지금은 계산이 한 곳, 문서와 코드의 대응이 한 곳이고 나머지는 그 결과를 읽기만 한다.
밀린 만큼이 아니라 정해진 만큼만
낡은 걸 감지한 다음이 문제다. 원래는 "가만히 있다가 많이 밀리면 한 번에 갚는" 구조였다. 그러면 밀릴수록 한 번의 일이 커지고, 커지니까 "너무 크면 건너뛴다"는 상한이 필요해지고, 결국 가장 썩은 문서가 자동에서 정확히 제외된다. 감지의 목적을 배반하는 구조였다.
작업 단위를 문서 전체에서 커밋 묶음으로 바꿨다. 원장에 박힌 커밋 해시를 오프셋 삼아, 매일 밤 그 지점 이후 정해진 개수만큼만 소화하고 오프셋을 전진시킨다. 컨슈머 오프셋과 같은 발상이다.

이 한 번의 변경으로 세 가지가 같이 풀렸다. 한 번에 처리하는 양이 고정이라 크기 상한이 필요 없어졌고, 매일 조금씩 소화하니 빚이 쌓이지 않고, 무엇보다 진도를 따로 기록할 것이 없어졌다. 오프셋이 곧 어디까지 읽었는지라서, "여기까지 했다"는 도장을 따로 찍을 필요가 없다. 기록할 것이 없으면 거짓말을 할 수도 없다.
한 가지는 나중에 고쳤다. 처음엔 오프셋이 전진했으면 성공, 그대로면 실패로 판정했는데 그러다 정상 실행을 실패로 찍었다. 정상인데 오프셋이 안 움직이는 경로가 둘 있었기 때문이다. 바꿀 게 없는 밤과, 원장은 이미 최신이고 위키만 뒤처져서 위키만 맞추는 밤이다. 그래서 성공과 실패는 종료 코드로만 가르고, "무슨 라운드였나"는 오프셋이 움직였는지로 따로 분류해 현황판에 적는다. 진도를 재는 자와 생사를 재는 자를 겹쳐 쓰면 안 된다는 이야기다.
이 설계가 실제로 값을 한 밤이 8월 말에 있었다. 원장 갱신이 응답이 끊긴 채 중간에 죽었고, 죽기 직전까지 고친 내용은 커밋으로 남았다. 그런데 그 문서의 오프셋은 한 칸도 움직이지 않았다. 다음 밤이 같은 지점에서 이어받아 마저 처리했고, 그 이틀 뒤에는 위키만 뒤처진 문서를 골라 위키만 맞추는 라운드가 실제로 돌았다. 죽어도 진도를 잃지 않는다는 게 말이 아니라 커밋으로 확인된 셈이다. 9월 초에 같은 일이 통째로 한 번 더 반복됐다. 이번엔 RAG 문서 차례였는데, 갱신이 또 응답이 끊긴 채 죽어 원장 부분 편집만 커밋됐고, 오프셋은 그대로였고, 다음 밤에 위키만 맞추는 라운드가 따라잡았다. 한 번이면 우연일 수 있지만 두 번이면 경로다.
세 번째는 원인이 달랐다. 이번 방아쇠는 내 손이다. 원장을 새로 만들면서 위키는 안 건드렸더니, 원장 쪽이 더 나중이 됐다. 판정은 그 둘의 커밋 시각만 비교하기 때문에 죽었는지 아닌지와 무관하게 위키 뒤처짐으로 섰고, 그날 라운드가 그 문서를 최우선으로 골라 위키만 맞췄다. 실패 뒤처리용으로 만든 분류가 원장이 앞선 모든 경우에 걸린다는 뜻이다. 그때 맞춘 내용도 적어둘 만하다. 위키는 원장 자동 갱신이 꺼져 있다고 써 뒀는데 실제로는 켜져서 돌고 있었다. 자동 갱신이 잡아낸 어긋남이 부풀린 쪽이 아니라 실제보다 낮춰 적은 쪽이었던 건 이번이 처음이다.
네 번째는 9월 중순에 왔는데, 이번엔 앞의 두 종류가 한자리에서 겹쳤다. 갱신 단계가 이틀 연속으로 죽었고, 그중 하루는 커밋이 하나도 안 남았는데 다른 하루는 원장 부분 편집이 남았다. 가른 건 편집이 파일에 닿았느냐다. 커밋하는 자리가 성공·실패를 가르는 자리 바깥에 있어서, 죽기 전에 뭔가 고쳤으면 그만큼이 남고 아무것도 못 고쳤으면 아무것도 안 남는다. 남은 쪽이 원장만 앞서게 만들었고, 다음 밤 라운드가 그걸 위키 뒤처짐으로 잡았다. 그런데 이번 라운드는 한 가지를 더 했다. 뒤처짐이 오탐인지 먼저 확인했다. 죽은 회차가 원장 머리말과 가드레일까지만 쓰고 나머지 절은 못 채운 채 끝나 있었으니 오탐이 아니었고, 그 확인이 커밋 메시지에 그대로 남았다.
이 구조가 실제로 도는 흔적은 커밋에 남는다. 8월 이후 사람이 아무것도 시키지 않은 채로 사실원장과 위키가 일흔한 번(8월 1일부터 10월 21일 밤 회차까지) 고쳐졌다. 셀 측정기 문서, RAG 문서, noon-desktop 문서로 시작해 지금은 이 문서 차례까지 왔다. 밀린 순서대로 하나씩, 정해진 분량만큼만 갚아 나가는 셈이다.
다만 이 수는 정확히 말해 실행이 있었다는 증거이지 그 밤이 끝까지 갔다는 증거가 아니다. 이번에는 현황판에 남은 갱신 실패를 커밋과 하나씩 맞춰 봤는데, 중간에 죽은 밤이 남긴 부분 편집은 셋이 아니라 다섯이었다(아래에 다시 적는다). 성공과 실패는 현황판이 따로 들고 있고, 갱신 칸만 보면 예순한 번 중 열다섯 번이 실패다.
그런데 이 두 번째 수를 "누적"으로 읽으면 안 된다는 걸 뒤늦게 알았다. 현황판 기록은 60일 창만 남기고 그보다 오래된 회차를 버린다. 그래서 9월 27일에 세었을 때는 예순 번 중 열두 번이었던 것이 나흘 뒤에는 예순한 번 중 열한 번이다. 실패 하나가 해결된 게 아니라 창 밖으로 밀려났을 뿐이다. 커밋으로 세는 쪽은 git 이력이라 줄지 않고, 현황판으로 세는 쪽은 가만히 있어도 줄어든다. 같은 문장에 두 수를 나란히 쓰려면 이걸 먼저 알아야 했다. 2주 뒤에 다시 세니 총계는 예순한 번 그대로인데 실패가 열넷이다. 창의 첫 회차가 8월 1일에서 8월 15일로 밀려, 정상 셋이 창 밖으로 나가고 실패 셋이 들어온 것이다. 또 한 주 뒤엔 총계가 역시 그대로인데 실패가 열다섯이고 창의 첫 회차는 8월 21일까지 밀렸다. 총계가 안 변했다고 아무 일도 없었던 게 아니다. 세는 것과 판정하는 것을 겹쳐 쓰면 앞 절에서 거짓말을 한 그 도장이 다시 생긴다.
한 가지는 정확하게 적어둔다. 커밋 날짜만 보면 19일이 비어 있고 20일에 두 번이 몰려 있다. 빠뜨린 게 아니라, 19일 밤 회차가 자정을 넘겨 시작해 새벽 한 시 반에 커밋됐기 때문이다. 스무 날에 스무 번이지만 "하루도 빠짐없이"는 아니라서, 날수 대신 회차로 센다. 이후로도 밤마다 이어져 9월 27일까지 쉰한 번이 됐다. 8월 말에 이 갱신 단계가 두 밤 연속으로 죽은 적이 있는데, 그다음 사흘은 모든 단계가 정상으로 돌았다. 넘어진 다음에 알아서 일어난 것까지가 기록에 남는다. 9월에는 회차 자체가 한 번 통째로 비었다. 하루 건너뛴 다음 날 새벽과 오전으로 나뉘어 뒤늦게 돌았고, 밀린 동향은 보충으로 메워졌다. 왜 밀렸는지는 아직 확인하지 못했다.
9월 중순엔 갱신 단계가 다시 이틀 연속으로 죽었다. 이번엔 같은 두 밤에 일지 초안 단계도 함께 죽었다. 그때 나는 한 밤에 두 단계가 같이 넘어진 첫 사례라고 적었는데, 나중에 다시 세어 보니 틀렸다. 실패를 칸별로만 세고 밤 단위로 묶어 보지 않아서다. 죽은 밤마다 그 밤의 다른 칸을 같이 들여다보면 같은 일이 8월 7일에 이미 있었고, 9월 중순까지 여섯 번이었다. 기록은 처음부터 거기 있었고 내가 세는 단위를 잘못 잡은 것이다. 그래도 동향·드리프트·평가 칸은 그 이레 내내 정상이었다. 단계를 갈라 둔 설계가 둘이 함께 죽은 밤에서도 나머지를 살린 셈이다. 그리고 한 회차 안에서도 자정을 넘긴 단계만 다음 날짜로 찍힌다는 걸 또 봤다. 같은 밤인데 드리프트 감지는 18일에, 썩음 재계산은 19일 새벽에 남아 있다. 날수로 세면 안 되는 이유가 이것이다.
9월 하순 아흐레는 격일로 넘어졌다. 갱신 단계가 아홉 회차에 세 번 죽고 여섯 번 전진했는데, 죽은 밤과 끝까지 간 밤이 하루씩 번갈아 왔다. 눈에 띈 건 이번 세 번은 커밋이 하나도 안 남았다는 것이다. 앞선 사례들은 죽기 직전까지 고친 만큼이 커밋으로 남아 원장만 앞서는 상태를 만들었는데, 이번엔 편집이 파일에 닿기도 전에 죽었다. 그러니 뒤처리할 것도 없고 다음 밤이 같은 지점에서 그냥 이어받았다. 아홉 밤에 세 번 넘어졌는데도 밀린 문서는 여섯 번 전진한 만큼 줄었다. 그리고 자정을 넘긴 회차가 아홉 중 넷이라, 23일이라는 날짜는 현황판 일곱 칸 어디에도 없다. 그 밤의 기록이 전부 24일 새벽에 찍혔기 때문이다. 날수로 세면 통째로 사라지는 밤이 또 생긴 셈이다.
그다음 닷새는 한 번도 안 넘어졌다. 9월 28일부터 10월 2일까지 갱신 단계가 다섯 회차 연속 끝까지 가서 오프셋을 전진시켰고, 같은 닷새의 일지·감지·재계산·지표 칸도 전부 정상이다. 격일로 죽던 게 고정된 패턴은 아니었다는 뜻이다. 넘어지는 빈도는 주마다 꽤 흔들리는데, 그래도 진도가 뒤로 가지 않는 건 오프셋이 쥐고 있어서다.
그 연속은 10월 4일 밤에 끊겼다. 이번엔 편집이 파일에 닿기도 전에 죽어서 커밋이 하나도 안 남았고, 그래서 뒤처리할 것도 없었다. 다음 밤부터 사흘이 내리 전진해 세 문서를 차례로 갚았다.
그러다 8일부터 10일까지 사흘이 내리 죽었다. 갱신 단계가 사흘 연속으로 넘어진 건 처음이다(그전 최장은 이틀). 세 밤 다 커밋이 하나도 안 남았으니 원장만 앞서는 상태도 안 생겼고, 밀린 사흘치는 그대로 남아 다음 회차가 받았다. 중간에 죽은 밤은 이제 열여섯 번인데, 그중 뭔가를 커밋으로 남긴 건 넷이다. 넘어지는 빈도는 주마다 흔들려도 진도가 뒤로 가지 않는 건 오프셋이 쥐고 있어서다.
그리고 그다음 네 밤은 넷 다 끝까지 가서 커밋을 남겼다. 사흘 내리 죽은 뒤에 나흘 내리 돈 셈이다. 마지막 밤은 또 자정을 넘겨서 커밋 날짜가 하루 밀렸다. 날수로 세면 안 되는 이유를 두 달 만에 한 번 더 봤다.
그다음 닷새에는 밤 하나가 통째로 사라졌다. 10월 18일 밤은 현황판 일곱 칸 어디에도 기록이 없다. 그 전날 밤 회차는 갱신이 응답 끊김으로 죽어 원장 본문만 고친 채 남았고, 다음 날 아침에 정규 시각 밖에서 보충 회차가 돌아 위키를 맞췄고, 그날 밤 회차가 같은 지점에서 배치를 마저 소화했다. 한 문서가 세 회차 연속으로 뽑힌 것이다. 오프셋이 전진하지 않으면 그 문서가 계속 제일 많이 밀린 문서로 남으니, 배치를 못 끝낸 밤은 다음 밤의 선택까지 묶어 둔다. 9월에 한 문서가 닷새에 세 번 뽑힌 적이 있는데 그건 흩어진 세 번이었고, 이번은 연달아 세 번이다. 회차가 통째로 비는 것도 9월에 한 번 있었고, 그때도 다음 날 정규 시각 밖에 뒤늦게 돌았다. 왜 빠졌는지는 이번에도 확인하지 못했다.
그리고 21일 밤에 처음으로 동률이 났다. 두 문서가 똑같이 14커밋씩 밀려 있었다. 규칙에는 "제일 많이 밀린 하나"라고만 적혀 있고 같을 때 어떻게 하라는 말이 없는데, 실제로는 등록부에 먼저 적힌 쪽이 뽑혔다. 썩음을 계산하는 쪽이 등록부 배열을 순서대로 돌며 쓰기 때문에 배지 파일의 키 순서가 곧 등록부 순서이고, 최다를 앞에서부터 고르면 앞자리가 이긴다. 규칙에 쓰지 않은 순서가 실제 선택을 가른 셈이다. 그 자리에서 하나 더 알았다. 라운드가 읽는 배지 파일은 커밋돼 있는 그 파일이 아니다. 야간 스크립트가 선택 직전에 썩음을 한 번 더 계산한다. 그래서 커밋된 배지에는 한쪽이 13으로 적혀 있는데 그날 밤 선택은 14 대 14로 났다. 그 사이 낮에 커밋 하나가 한쪽 감시 경로에만 들어왔기 때문이다. 커밋된 숫자로 그날 밤의 판단을 되짚으면 틀린다.
이번 회차엔 내가 같은 종류의 실수를 또 한 걸 찾았다. 중간에 죽어서 부분 편집만 커밋으로 남은 밤을 그동안 셋으로 세어 왔는데, 현황판의 갱신 실패 열일곱 건을 시각으로 커밋과 하나씩 맞춰 보니 다섯이었다. 그리고 가장 이른 것은 8월 28일이 아니라 8월 7일이다. 그 밤 커밋은 "이제 갱신 로그와 오프셋 전진"이라는 문장에서 끊기고 뒤에 응답 끊김 메시지가 그대로 붙어 있다. 오프셋 줄은 손대지 않았으니, 죽어도 진도를 잃지 않는다는 설계가 처음 값을 한 밤도 8월 28일이 아니라 8월 7일이다.
왜 몰랐는지가 더 걸린다. 그 끊김 메시지를 인용한 커밋과 실제로 죽은 커밋을 구별하는 데만 신경을 썼고, 반대로 빠뜨린 쪽은 안 봤다. 8월 7일 밤의 실패 기록은 이 문서가 이미 두 번 인용해 뒀다. 스탬프는 봤고 그 밤의 커밋은 안 본 것이다. 같은 날 동향 쪽에서도 같은 종류가 하나 더 나왔다. 빠진 두 날을 한 번에 메운 첫 사례를 9월 말로 적어뒀는데 8월 말에 이미 있었고, 그 노트 역시 이 문서가 인용해 둔 자리에 있었다. 기록이 없어서 틀린 게 아니라, 들고 있는 기록을 다시 읽지 않아서 틀렸다. 세는 단위를 잘못 잡아 틀린 게 앞서 두 번이었으니 이번이 세 번째다.
한 가지는 스스로 뒤집혔다. 앞선 엿새는 뽑힌 문서가 매번 달라서 밀린 순서대로 고르니 같은 문서가 연달아 걸리지 않는다고 적어뒀는데, 바로 다음 닷새가 정반대였다. 한 문서가 세 번 뽑혔다. 규칙이 같은 문서를 피해 주는 게 아니라, 그 문서가 계속 제일 많이 밀려 있었을 뿐이다. 매번 다른 문서가 걸리는 건 규칙의 성질이 아니라 그 주의 우연이었다. 이렇게 다음 회차가 지난 회차의 문장을 무르는 것까지가 이 갱신 루프가 하는 일이다. 바로 다음 닷새에는 그 문서가 한 번도 안 뽑혔다. 최다 드리프트가 다른 문서로 넘어가면 선택도 따라 옮겨간다.
여기서 수를 두 가지로 갈라야 한다는 걸 알았다. 지금까지 일흔한 회차가 돌았는데 원장을 고친 건 예순여덟 편이다. 나머지 셋은 원장을 한 줄도 안 건드리고 위키만 고쳤다. 원장은 이미 갱신됐는데 위키가 안 따라간 문서를 골라 재조사를 건너뛰고 위키만 맞추는 분기가 있는데, 그 분기가 돌아간 밤이 커밋에 그렇게 남는다. "일흔한 번 돌았다"와 "원장 일흔한 편을 고쳤다"는 다른 말이다. 두 수를 같은 창에서 세야 하는 것도 알았다. 날짜로 안 자르고 문서별로만 세면 8월 앞의 두 회차가 섞여 들어와 합이 어긋나 보인다.
그래서 예순여덟 편을 문서별로 세어 봤다. 가장 많이 고쳐진 건 **이 문서 자신(스물한 번)**이고, 그다음이 RAG 문서 열한 번, 셀 측정기와 사이트 문서가 각각 열 번이다. 자기가 남긴 커밋 때문에 자기가 낡는 구조니 당연한 결과인데, 숫자로 보니 생각보다 쏠려 있었다. 반대쪽도 봤다. 한 번도 안 뽑힌 문서가 열한 개 있다. 빠뜨린 게 아니라 그 프로젝트 코드가 안 움직여서 밀릴 게 없는 문서들이다. 밀린 순서대로 고르는 규칙에서는 안 뽑히는 것이 정상 동작인 문서가 있다. 그리고 10월 2일에는 LLM 판정을 다룬 문서가 쉰다섯 회차 만에 처음으로 차례가 됐다. 7월에 만들어진 뒤로 한 번도 걸리지 않았던 문서다. 평가 엔진 문서는 8월 20일에 한 번 걸린 뒤 마흔일곱 회차를 건너뛰고 10월 중순에 두 번째가 됐다. 밀린 순서대로 고르는 규칙에서는 차례가 이렇게 띄엄띄엄 온다.
재미있는 건 이 문서를 마지막으로 갱신하게 만든 커밋들의 정체다. 8월 10일부터 9월 1일까지 스물사흘 동안 하네스 감시 경로에 쌓인 커밋은 마흔한 개였는데, 그중 사람이 만진 것은 하나도 없었다. 문서를 고친 스무 개와 관제판 데이터를 갱신한 스물한 개, 전부 이 자동화 자신이 남긴 것이다. 자기가 남긴 커밋 때문에 자기가 낡았다고 판정돼서 자기를 다시 조사하는 셈인데, 이건 결함이라기보다 이 구조의 정직한 부작용이다. 그 구간은 9월 2일에 끝났다. 그날 오후 사람 커밋 세 개가 들어왔는데, 하네스를 고친 게 아니라 이 사이트 쪽 작업이었다. 하네스 문서를 낡게 만든 것이 스물사흘 만에 처음으로 사람 손이 된 셈이다. 그래서 에이전트 정의에도 "커밋 수를 사람이 한 일로 읽지 마라"를 박아둔 것이다.
앞 절의 거짓 성공이 왜 생겼는지도 여기서 분명해진다. 날짜는 "어디까지 했는지"를 표현하지 못한다. 그래서 별도의 성공 도장이 필요했고, 그 도장이 거짓말을 했다. 커밋 해시는 그 자체가 진도라서 도장이 필요 없다. 관측 장치를 고치는 것보다 관측할 필요가 없는 구조로 바꾸는 쪽이 낫다.
세션을 넘는 파일 메모리
세션이 끝나도 유지되는 파일 기반 장기 기억을 뒀다. 사용자 배경, 행동 규칙(피드백), 프로젝트 결정을 개별 파일로 저장하고 인덱스로 매 세션 로드한다. 그래서 "디버깅은 로그 먼저", "레거시 이식 규칙" 같은 교훈이 세션을 넘어 축적되고, 같은 실수를 반복하지 않는다.
최근엔 층을 하나 더 뒀다. 에이전트마다 자기 몫의 기억 폴더를 갖는다. designer는 이 사이트의 디자인 규격과 관제판 규칙, 헤드리스 크롬에서 반복해 걸린 함정을 자기 폴더에 쌓았고, 구현 담당은 "편집이 실제로 반영됐는지 확인할 것" 같은 자기 버릇을 적어뒀다. 사람이 주는 규칙과 에이전트가 스스로 겪은 것을 갈라 둔 셈이다. 처음엔 이 둘뿐이었는데 지금은 다섯이다. 리뷰 담당은 "리뷰만 하고 고치지는 말 것"을, 사서는 서가 지도와 함께 확인 없이 보고했던 그날의 기록을 자기 폴더에 들고 있다. 세 번째와 네 번째가 둘 다 같은 종류의 사고에서 나온 셈이다. 다섯 번째는 커밋 담당인데, 두 달 전에 세어 볼 때는 폴더만 있고 안이 비어 있었다. 이 폴더도 저장소에 커밋하기 때문에, 기계를 갈아도 쌓은 게 남는다.
쌓이는 속도는 고르지 않다. 9월 이후 두 달 치를 한 번에 올렸는데 파일이 마흔일곱에서 백스물둘로 늘었고, 그중 쉰 건이 구현 담당 몫이다. 디자인 담당과 리뷰 담당이 그 뒤를 잇고, 사서는 두 달 동안 한 건도 안 늘었다. 많이 틀리는 자리에 많이 쌓인다는 뜻이라, 폴더 크기가 그 역할의 난이도를 대충 보여준다.
재미있는 건 이 백스물두 건이 신선도 판에 보인 경위다. 기억 폴더는 git으로 추적하지만 신선도를 재는 감시 경로에는 안 들어 있다. 그런데 이번엔 같은 커밋에 에이전트 정의 파일 하나가 끼어 있었다. 그 한 건이 감시 경로 안이라서 나머지 일흔다섯 건이 같이 보였다. 정의 파일이 안 끼었으면 두 달 치 학습이 쌓여도 이 문서의 drift는 0이었을 것이다. 커맨드가 빠져 있는 것과 같은 구멍인데, 이번에는 우연히 한 파일이 끼어서 드러났다.
도구 그라운딩 (MCP)
프롬프트만으로는 부족한 작업엔 전용 도구를 붙인다. 라이브 문서(context7), 브라우저 자동화(playwright), 그리고 자작 MCP(그어봄)까지 에이전트 도구로 편입했다. "프롬프트만"이 아니라 "프롬프트 + 전용 도구"로 그라운딩한다. noon-desktop의 전문가 모드와 같은 철학이다.
그런데 도구를 붙이는 것과 모델이 그 도구를 고르게 하는 것은 다른 문제였다. 사용자 자료를 색인해 검색해 주는 MCP를 붙여뒀는데, "그 자료 어디 있더라"라는 질문에 모델이 두 번 다 저장소 grep으로 가서 두 번 다 "없습니다"로 끝냈다. 실제로는 색인에 있었다. 저장소 안에서 일하는 모델에게 grep은 기본값이고, MCP 도구의 설명은 따로 열어야 보이기 때문이다. 도구 설명을 두 번 고쳐도 안 바뀌어서, 결국 세션마다 무조건 읽히는 라우터에 절을 하나 박았다. 저장소 grep이 0건인 것은 "없다"의 근거가 아니라고. 서브에이전트 상시 지시와 같은 패턴이다. 안 읽히는 층의 문제는 확실히 읽히는 층에서 푼다.
또한 루트뿐 아니라 각 프로젝트에도 그 프로젝트 전용 CLAUDE.md를 둔다. 스택, 빌드 명령, 이미 있는 자산, 다시 가져오면 안 되는 방향까지 그 프로젝트 안에 적어두는 식이다. 같은 하네스 패턴이 스튜디오 전체로 스케일된다. 7월 말에 넷으로 시작했는데 지금 열셋이다. 늘어난 경로가 재미있다. 따로 "문서화하는 날"을 잡아서 늘어난 게 아니라, 새 프로젝트를 여는 커밋이 전용 라우터를 같이 들고 들어온다. 열세 번째는 한 발 더 갔다. 보류한 앱의 소스만 보존하는 커밋이었는데 거기에도 전용 라우터가 딸려 왔다. 라우터가 "진행 중"이라는 표시가 아니라 디렉터리를 여는 행위의 기본 동작이 된 셈이다.
설계 원칙
규칙은 Skills 한 곳, 워크플로우는 Agents. 여러 문서에 같은 금지사항을 쓰지 않는다. 그래야 일관성이 깨지지 않는다.
너무 적으면 범용화로 전문성이 떨어지고, 너무 많으면 선택 비용·중복·유지보수가 는다. "혹시 필요할까봐" 미리 만들지 않고, 반복되는 패턴이 보일 때만 추가한다. 빼는 쪽도 같다. 내장 기능과 겹치던 기획 에이전트는 지웠다. 내가 만들었다는 이유로 남겨두면 선택지만 늘어난다.
지금은 여덟이라 이 상한을 넘겼다. 겹치는 자리가 없고 같은 지적이 반복됐으니 넣는 기준은 지킨 셈인데, 수는 넘겼고 가이드 문서는 안 고쳤다. 상한을 올릴지 수를 세지 않기로 할지는 아직 안 정했다.
읽기성 명령만, 임의 셸 실행 금지, push는 스코프 한정. 무인으로 도는 것일수록 blast radius를 먼저 좁힌다. 발행처럼 되돌리기 어려운 단계는 처음엔 꺼두고 검증부터 했다.
수동 커밋 라우터(v1) → Skills 자동주입 + Agents 자율 스폰(v2). 일하는 방식 자체를 관찰하고, 반복 고질이 보이면 구조를 다시 짠다.
정적 공개 사이트에선 비공개 검토가 관문만 늘렸다. '무인 블로그'라는 컨셉대로 사전 검토를 빼고, 대신 자기 AI-검출·빌드 게이트·근거만으로 막았다. 통제는 사후(읽고 고치기)로 옮겼다. 덜 다듬어진 글이 잠깐 올라갈 리스크는 '과정을 남긴다'는 이 블로그의 전제가 받는다.
처음엔 위키 본문을 일부러 자동화에서 뺐다. 위키 한 페이지는 여러 프로젝트를 가로질러 추려낸 판단이라, AI가 통째로 다시 쓰면 그 판단이 뭉개진다고 봤기 때문이다. 그래서 한동안은 로튼 도큐먼트로 "낡았다"는 신호만 냈다.
지금은 갱신까지 무인으로 넘겼고, 대신 경계를 다른 곳에 그었다. 통째로 다시 쓰지 않고 어긋난 곳만 고친다. 파일과 줄 번호나 커밋 해시로 근거를 못 대는 사실은 아예 손대지 않는다. 애매하면 원문을 그대로 둔다. 틀리게 고치는 것보다 낡은 채로 두는 쪽이 낫기 때문이다. 안전장치는 실행 쪽에도 하나 더 있다. 원장 자동 갱신은 스크립트 기본값이 꺼짐이고, 크론 등록에서 환경변수로만 켠다. 손으로 그냥 돌리면 낡음을 알리는 데서 멈춘다.
무인으로 도는 것에 성공 도장을 미리 찍어두면, 죽어도 죽은 줄 모른다. 실제로 사흘 내내 실패한 자동화가 현황판에서는 정상으로 보였다.
지금은 종료 코드와 산출물을 같이 보고, 실패면 종류까지 갈라 적는다. 되살릴 값이 있는 실패만 다시 부르고, 그래도 안 되면 다음 날 보충하고, 그마저 실패하면 실패로 노출한다. 관측 장치가 낙관적이면 그 장치는 없느니만 못하다.
규칙을 늘려서 고쳐 본 적이 여러 번인데 안 됐다. 여섯 개를 적어두고 하루에 여섯 개를 다 어긴 날, 규칙을 다시 쓰는 대신 전부 지웠다. 지키지 않는 규칙을 남겨두면 그 규칙을 관리하는 데 시간이 가고, 문서만 보면 잘 돌아가는 것처럼 보인다. 같은 날 화면 측정 스크립트도 걷어냈다. 자기가 잰 값을 판정 근거로 쓰려면 그 측정부터 믿을 수 있어야 하는데 그렇지 않았다. 하네스는 늘기만 하는 것이 아니라 줄기도 한다.
회고
이 하네스의 증거는 이 포트폴리오 자체다. 5개+ 프로젝트를 1인이 끌고 갔고, 개발일지·이슈·블로그가 매일 밤 무인으로 쌓이며, 이 위키들을 채운 전수조사(15+ 병렬 서브에이전트)까지 전부 이 하네스 위에서 돌아갔다. 8월 이후로는 이 문서들 자신도 예순여섯 번 사람 손 없이 갱신됐다.
그래서 나는 "AI를 쓰는 사람"이 아니라 "AI를 부리는 체계를 설계·운영하는 사람"이다. 선은 분명하다. Claude Code 위에 얹은 하네스 설계가 내 몫이고, 무인으로 넘긴 것은 수집·초안·발행·문서 갱신까지다. 무엇을 만들지, 무엇을 자동화할지, 그 자동화를 켤지 끌지, 그리고 나온 결과를 어떻게 볼지는 여전히 사람이 쥔다. 그 경계를 아는 것까지가 이 방법론의 일부다.