이 사이트의 예약 게시 글 일부는 사람이 자고 있는 동안 쓰였습니다. 스케줄러가 정해진 시각에 에이전트를 깨우고, 에이전트가 주제를 고르고, 글을 쓰고, 빌드를 돌리고, 커밋하고, PR 을 엽니다. 아침에 사람이 보는 건 이미 다 끝난 PR 하나입니다.
이걸 처음 붙일 때 가장 많이 고민한 건 프롬프트가 아니었습니다. 이 에이전트가 잘못했을 때 무엇까지 망가질 수 있는가 였습니다.
1. 무인 실행이 대화형과 다른 지점
대화하면서 쓰는 에이전트는 사실 굉장히 안전합니다. 이상한 방향으로 가면 사람이 다음 문장에서 바로 멈춥니다. 에이전트가 완벽해서가 아니라, 오류와 발견 사이의 간격이 몇 초이기 때문입니다.
스케줄 실행에서는 그 간격이 몇 시간입니다. 그리고 그 사이에 에이전트는 멈추지 않습니다. 판단이 한 번 틀리면 그 틀린 판단 위에서 계속 일합니다.
그래서 무인 실행에서 개선해야 할 대상은 판단의 정확도가 아니라 판단 사이의 구조입니다. 틀린 판단을 없앨 수는 없지만, 틀린 판단이 어디까지 번지는지는 정할 수 있습니다.
2. 피해 반경을 좁히는 경계 네 층
"어디까지 번지는가"를 정하는 경계가 넷입니다. 각 층이 막는 것이 다르고, 어느 하나가 빠지면 나머지가 대신 막아주지 못합니다.
층마다 되돌리는 비용이 자릿수씩 뜁니다. 1층에서 막으면 아무 일도 없었던 것이고, 2층이면 커밋 하나를 되돌리면 되고, 3층이면 기준 브랜치 이력을 손대야 하고, 4층까지 새면 이미 색인된 URL 을 처리해야 합니다.
그래서 안전장치를 고를 때 기준은 "이 실수를 막을 수 있는가"가 아니라 "막지 못했을 때 몇 층에서 잡히는가"였습니다. 실수는 어차피 납니다. 중요한 건 그게 어느 층에서 멈추느냐입니다.
"PR 로 게이트를 걸었으니 쓰기 경로는 넓게 열어도 된다"가 흔한 오해입니다. PR 은 사람이 보는 것을 보장하지, 사람이 알아채는 것을 보장하지 않습니다. 글 여덟 편에 섞인 eleventy.config.mjs 한 줄 변경은 잘 안 보입니다.
3. 쓰기 가능한 범위를 먼저 못 박는다
제일 먼저 정한 건 "무엇을 할 수 있는가"가 아니라 "어디에 쓸 수 있는가" 였습니다.
이 구분에는 기준이 있습니다. 글 파일은 잘못돼도 그 글 하나만 이상해집니다. 레이아웃이나 설정은 사이트 전체가 같이 이상해집니다.
특히 eleventy.config.mjs 를 뺀 게 중요합니다. 여기에는 이 저장소의 규칙을 강제하는 빌드 가드가 들어 있습니다. 검사받는 쪽이 검사하는 코드를 고칠 수 있으면 가드가 가드가 아닙니다. 빌드가 실패했을 때 가장 손쉬운 해결책이 "검사를 지우는 것"이 되어버립니다.
더 많이 열어주면 더 많은 걸 할 수 있는 게 맞습니다. 동시에 더 많은 걸 조용히 망가뜨릴 수 있습니다. 잘 하는 것과 되돌릴 수 있는 것은 다른 축입니다.
4. 결과물이 곧 배포가 되지 않게
두 번째 장치는 PR 입니다. 에이전트는 자기 브랜치에만 푸시하고, 기준 브랜치에는 직접 쓰지 않습니다. 그리고 PR 을 스스로 병합하지 않습니다.
이 한 줄이 "자동으로 글이 올라간다"와 "자동으로 초안이 준비된다"의 차이를 만듭니다. 후자에서는 최악의 경우가 "쓸모없는 PR 하나를 닫는 것"입니다. 전자에서는 최악의 경우가 사이트에 이상한 글이 올라가는 것입니다.
비용 차이는 거의 없습니다. 사람이 아침에 PR 을 훑는 데 몇 분입니다. 그런데 되돌리는 비용은 자릿수가 다릅니다. 배포된 URL 은 이미 색인될 수 있고, 그때부터는 지우는 것도 일이 됩니다.
5. 완료 조건을 기계가 판정하게 한다
사람이 없으면 "다 됐다"를 누가 판단하느냐가 문제가 됩니다. 에이전트에게 스스로 판단하게 하면 거의 항상 다 됐다고 합니다.
그래서 완료 조건을 문장이 아니라 명령어로 뒀습니다. 무엇을 문서에 적고 무엇을 검사로 옮길지는 규칙 문서를 쓰는 원칙과 같은 기준을 따릅니다 — 기계가 판정할 수 있으면 문서에 두지 않습니다.
이 규칙이 작동하는 이유는 빌드가 실제로 내용에 대한 검사를 하기 때문입니다. ko/en 두 파일이 다 있는지, 두 파일의 발행일이 같은지, 글끼리 건 링크가 미래를 향하지 않는지를 봅니다. 에이전트가 가장 많이 틀리는 지점이 정확히 여기입니다.
중요한 건 이 검사가 에이전트를 위해 만들어진 게 아니라는 점입니다. 사람이 손으로 쓸 때도 틀리던 것들을 막으려고 만든 검사인데, 그게 그대로 에이전트의 완료 판정으로 쓰입니다. 사람에게 좋은 가드가 에이전트에게도 좋습니다.
판정 가능한 문장으로 바꾸기
완료 조건을 적다 보면 대부분의 초안이 판정 불가능한 문장입니다. 그걸 판정 가능한 것으로 바꾸는 작업이 이 절의 실제 내용이었습니다.
첫 줄은 완전히 바꾸지 못했습니다. "잘 썼는가"는 기계가 판정할 수 없고, 대신 기계가 판정할 수 있는 하한선만 정해두고 나머지는 PR 리뷰로 넘겼습니다. 전부를 자동 판정으로 만들려다 보면 판정하기 쉬운 것만 남기게 됩니다 — 그건 기준을 세운 게 아니라 기준을 바꾼 것입니다.
마지막 줄은 나중에 추가했습니다. git diff --name-only 로 변경 파일 목록을 뽑아 허용 경로 밖이 있으면 멈추게 하면, 쓰기 경계가 지시가 아니라 검사가 됩니다. 지시는 잊히지만 검사는 안 잊힙니다.
6. 절반만 끝나도 그 절반은 남아야 한다
무인 실행은 중간에 끊깁니다. 시간 제한, 네트워크, 그냥 알 수 없는 이유로요. 이때 여덟 편을 다 쓰고 한 번에 커밋하는 방식이면 일곱 편이 증발합니다.
그래서 커밋 단위를 글 한 편으로 잡았습니다. 한 세트가 빌드를 통과하면 그 자리에서 커밋하고 푸시합니다. 여러 편을 몰아서 커밋하지 않습니다.
같은 커밋에서 .content-queue.md 의 해당 항목을 완료로 표시합니다. 진행 상태와 결과물이 같은 커밋에 들어가야 둘이 어긋나지 않기 때문입니다. 이 부분의 설계는 재개 가능한 작업에서 더 자세히 다뤘습니다.
여기서 얻는 건 재개 가능성만이 아닙니다. 세션이 죽어도 남은 것이 항상 유효한 상태라는 점이 더 큽니다. 3편에서 끊긴 브랜치는 "실패한 작업"이 아니라 "3편짜리 PR"입니다.
7. 계획을 결과물보다 먼저 남긴다
이건 나중에 추가한 규칙인데 효과가 컸습니다. 글을 쓰기 전에 이번 회차 계획부터 커밋하고 푸시합니다.
이유는 0편에서 끊겼을 때도 뭔가 남기 위해서입니다. 계획이 남아 있으면 다음 실행이 그걸 이어받습니다. 계획이 없으면 처음부터 다시 판단해야 하고, 같은 주제를 다시 고를 위험이 생깁니다.
부수 효과가 더 유용했습니다. 사람이 아침에 PR 을 볼 때 "왜 이 여덟 개를 골랐는가"가 저장소에 적혀 있습니다. 판단의 결과만 남기는 게 아니라 판단의 근거를 남기는 셈이라, 나중에 방향이 틀어졌을 때 어디서 틀어졌는지 찾을 수 있습니다.
8. 실패는 어떻게 남기나
사람이 없는 시간에 실패하면 실패했다는 사실 자체가 사라집니다. 로그는 세션과 함께 없어지고, 아침에 남는 건 "PR 이 없다"는 사실뿐입니다. 그런데 PR 이 없는 건 실패했을 때와 할 일이 없었을 때 똑같이 나타나는 모습입니다.
남기는 방법을 넷 놓고 비교했습니다.
결국 큐 파일에 남기고, 그 커밋을 푸시하는 것으로 정했습니다. 다음 세션이 반드시 읽는 파일이면서, 사람도 PR 이나 커밋 이력에서 보게 되는 자리라서요. 이슈를 여는 방식은 알림이 울리는 만큼 확실하지만, 다음 세션이 이슈를 안 읽는다는 게 결정적이었습니다.
기록에 넣는 것도 정해뒀습니다. 무엇을 하려다 실패했는지, 어디까지 갔는지, 사람이 무엇을 해주면 풀리는지 세 가지입니다. 마지막 항목이 없으면 아침에 사람이 로그를 다시 읽어야 합니다.
둘 다 결과가 "PR 없음"이라 밖에서는 같아 보입니다. 구분이 없으면 매일 도는 작업이 조용히 죽어 있어도 몇 주를 모릅니다.
9. 자동화하지 않기로 한 것
안전장치만큼 중요한 게 범위 밖으로 둔 것입니다.
- 병합. 사람이 합니다. 자동 병합을 붙이면 위의 모든 장치가 의미를 잃습니다.
- 이미 게시된 글 수정. 발행된 글의
date를 미래로 바꾸면 사이트에서 내려갑니다. 조용히 일어나는 삭제라 사람이 판단합니다. - 빌드 가드 수정. 검사받는 쪽이 검사를 못 고치게 합니다.
- 새 회차의 자동 시작. 직전 회차가 미완료면 그것부터 끝내고, 이어받을 게 없으면 아무것도 하지 않고 종료합니다. 할 일이 없을 때 일을 만들어내지 않는 게 중요합니다.
마지막 항목이 특히 그렇습니다. 무인 에이전트에게 "할 일이 없으면 끝내라"를 명시하지 않으면 어떻게든 할 일을 찾아냅니다. 그리고 그렇게 만들어진 작업은 대체로 필요 없는 작업입니다.
무엇을 남길지 고르는 기준
목록 자체보다 고른 기준이 옮겨 쓸 만합니다. 두 축으로 봅니다.
오른쪽 아래 칸이 사람이 하는 일입니다. 게시된 글의 date 를 미래로 바꾸는 것이 정확히 여기 있습니다 — 예약 게시 구조에서 발행일을 미래로 밀면 그 글은 다음 빌드에서 사이트에서 사라지는데, 에러도 경고도 없이 조용히 사라집니다. 되돌리기도 비싸고(색인이 이미 빠짐) 드러나기도 늦습니다.
사람이 개입하는 자리는 셋이다
결국 사람이 손대는 지점이 세 곳으로 정리됐습니다. 그 외에는 개입하지 않습니다.
- 계획 검토 — 아침에 PR 을 볼 때 계획 커밋이 맨 앞에 있습니다. 방향이 틀렸으면 여기서 잡힙니다.
- PR 병합 — 유일한 필수 개입입니다. 여기를 자동화하면 나머지 장치가 전부 장식이 됩니다.
- 막힌 항목 처리 — 큐 파일에 막혔다고 적힌 항목은 사람이 판단해야 풀립니다.
세 곳 다 비동기라는 게 중요합니다. 에이전트가 사람을 기다리며 멈춰 있지 않습니다. 사람이 언제 보든 상태는 저장소에 남아 있고, 보고 나서 다음 실행이 이어받습니다. 무인 실행에서 "사람의 확인"을 동기적으로 설계하면 그 순간 무인이 아니게 됩니다.
10. 자주 묻는 것
PR 병합까지 자동화하면 안 되나요?
검사가 판정할 수 있는 종류의 작업이면 됩니다. 문제는 이 작업의 산출물이 글이라는 점입니다. 빌드는 형식만 봅니다 — 사실관계가 틀렸는지, 논지가 이상한지, 같은 말을 두 번 하는지는 못 봅니다. 기계가 판정하지 못하는 품질이 산출물의 본질이면 병합은 사람 몫입니다. 반대로 의존성 버전 올리기처럼 테스트가 판정을 대신할 수 있는 작업이라면 자동 병합이 합리적입니다.
에이전트에게 주는 자격증명은 어떻게 제한하나요?
이 작업이 실제로 쓰는 것만 줍니다. 이 저장소 하나에 대한 쓰기 권한이면 충분하고, 다른 저장소·배포 훅·외부 서비스 키는 주지 않습니다. 권한을 좁히는 건 "에이전트를 못 믿어서"가 아니라 사고의 반경을 미리 정하는 일입니다. 사람 계정에 최소 권한을 적용하는 이유와 같습니다.
매일 돌리면 PR 이 쌓이지 않나요?
쌓입니다. 그래서 회차 개념을 뒀습니다 — 미완료 회차가 있으면 새 회차를 열지 않고 그것부터 끝냅니다. PR 도 이미 열려 있으면 새로 만들지 않고 같은 PR 에 커밋을 얹습니다. 이게 없으면 매일 새 브랜치와 새 PR 이 생기고, 사람이 처리하는 속도보다 쌓이는 속도가 빨라집니다.
에이전트가 규칙을 어기면 어떻게 아나요?
두 곳에서 걸립니다. 빌드가 잡는 것은 즉시 걸리고, 나머지는 PR diff 에서 걸립니다. 후자를 실제로 보이게 하려면 변경 파일 목록을 먼저 보는 습관이 필요합니다 — 본문 diff 를 읽기 전에 "허용 경로 밖 파일이 있는가"부터 봅니다. 이건 사람이 몇 초면 하는 확인이라 자동화보다 먼저 자리잡았습니다.
실행 시각은 어떻게 정했나요?
사람이 자는 시간에 시작해서 아침에 결과가 준비돼 있도록 잡았습니다. 중요한 건 시각 자체보다, 실행과 사람의 검토 사이에 여유가 있다는 점입니다. 실행이 끝나자마자 사람이 봐야 하는 구조면 실행이 밀릴 때마다 사람이 기다립니다.
이 정도 장치가 과하지 않나요?
넣은 것을 다 세어도 브랜치 하나, PR 하나, 빌드 명령 하나, 큐 파일 하나입니다. 새로 만든 도구는 없고 이미 쓰던 것을 순서대로 배치했을 뿐입니다. 과한 쪽은 오히려 반대 방향입니다 — 아무 경계 없이 돌려놓고 매일 아침 결과를 전부 검토하는 것이 훨씬 비쌉니다.
11. 정리
- 대화형의 안전장치는 사람이다. 무인에서는 그 자리를 구조가 메워야 합니다.
- 쓰기 범위를 먼저 좁힌다. 잘못돼도 글 하나만 이상해지는 범위로.
- 결과물이 곧 배포가 되지 않게 한다. PR 이 게이트입니다.
- 완료를 문장이 아니라 명령어로 정의한다. 빌드가 통과해야 끝난 것입니다.
- 커밋 단위를 작게 — 절반에서 끊겨도 그 절반은 유효하게.
- 할 일이 없으면 아무것도 하지 않고 끝내는 것도 명시한다.
- 실패도 남긴다. "아무 일 없었음"과 "실패했음"이 밖에서 같아 보이면 안 됩니다.
- 사람이 개입하는 자리는 비동기로 둔다. 기다리게 만드는 순간 무인이 아닙니다.
무인 에이전트를 운영하면서 바뀐 생각은 이겁니다. 에이전트를 얼마나 믿느냐가 아니라, 틀렸을 때 얼마나 싸게 되돌릴 수 있느냐가 실제 문제였습니다. 되돌리기가 싸면 덜 믿어도 돌릴 수 있고, 되돌리기가 비싸면 아무리 믿어도 마음이 놓이지 않습니다.