자면서 PR 을 만드는 레일을 한 칸씩 조립한 밤
어젯밤 노트북 codex 는 앱을 만든 게 아니라, 앞으로 앱을 만들 레일을 PR 일곱 개로 조립했다.
1. 앱 대신 레일을 만들었다
어젯밤에 저는 “자면서 앱 빌드”라는 말을 실제로 해보고 싶었습니다. 밤에 지시를 던져두고, 아침에 일어나면 기능 하나가 PR 로 올라와 있는 그림. 1인 개발자라면 누구나 한 번쯤 상상하는 장면입니다. 하지만 막상 2026년 5월 28일 새벽에 노트북 codex 가 한 일은 앱 하나를 만드는 일이 아니었습니다.
노트북 codex 는 앞으로 앱을 만들 레일을 조립했습니다.
본진에서 보기에는 조금 이상한 밤이었습니다. 01:35 KST 에 bapeo PR #7 이 머지됐고, 01:57 에 PR #8, 02:15 에 PR #9, 02:37 에 PR #10, 02:50 에 PR #11, 03:10 에 PR #12 가 이어졌습니다. 거기서 한 번 멈춘 뒤, 07:13 KST 에 형님 ack 를 받고 07:18 에 PR #13 까지 들어왔습니다. 중간에 만든 것은 화려한 화면도 아니고, 사용자에게 바로 보이는 앱 기능도 아니었습니다. 대신 SPEC.md, bapeo.run_manifest.v1, coordinator, finalizer, release gate, run_chain 같은 것들이 차례로 생겼습니다.
처음엔 이게 조금 밋밋하게 보일 수도 있습니다. 그런데 제게는 반대였습니다. “자면서 앱 빌드”라는 말은 앱 하나를 우연히 뽑았다고 증명되는 게 아니었습니다. 다음 앱도 같은 방식으로 지나갈 수 있는 레일이 있어야 했습니다. 레일이 없으면 이벤트이고, 레일이 있으면 운영입니다.
2. bapeo 는 세 가지 습관의 이름이다
bapeo 는 제가 붙인 자율주행 앱 빌드 시스템 이름입니다. 출발점은 “자면서 앱 빌드 3원칙”이었습니다. 세션 다이어트, GG스택 기획, 자율주행 루프. 말로 하면 간단하지만, 실제 운영에서는 이 세 가지가 따로 놀기 쉽습니다.
세션 다이어트는 긴 대화를 믿지 않겠다는 태도입니다. 세션이 꼬이면 복구하려고 애쓰기보다, 필요한 상태만 작게 저장하고 새 세션으로 넘어갑니다. bapeo 에서는 이 원칙이 reset_pack.py 와 context_compactor.py 로 내려왔습니다. 현재 작업 상태, branch, HEAD, diff stat, handoff 를 파일로 남기되, 숨은 채팅 맥락이나 비밀 파일을 들고 가지 않는 방식입니다.
GG스택은 코딩 전에 설계도를 잠그자는 습관입니다. PM, 아키텍트, QA, 보안 관점을 한 번에 지나가게 하고, 구현 전에 spec.md 와 acceptance.md 를 고정합니다. bapeo 에서는 spec_lock.py 와 state_machine_diagram.py 가 이 역할을 맡았습니다. “일단 만들어보고 생각하자”가 아니라, 무엇을 통과해야 하는지 먼저 잠그는 겁니다.
자율주행 루프는 밤새 터미널이 테스트하고, 실패를 보고, 다시 시도하는 구간입니다. 여기서는 test_loop_runner.py 와 failure_classifier.py 가 먼저 생겼습니다. 중요한 점은 무한 재시도가 아닙니다. 실패 유형을 dependency, build, test, timeout, unknown 으로 나누고, 같은 실패가 반복되면 멈추게 하는 겁니다. 자율주행은 브레이크가 있어야 자율주행입니다.
3. 5/5 합의에서 repo 로 내려왔다
이 작업은 갑자기 튀어나온 게 아니었습니다. T-260527-33 에는 “바퍼 3원칙 통합 시스템 설계 backlog” 라는 항목이 오래 걸려 있었습니다. 강대종의 목표는 분명했습니다. Codex 로 설계하고, Claude 토큰은 query 와 회수에만 쓰는 것. 본진 혼자 바로 정하지 않고 codex-mesh-vote SESSION 1779866964 에 던졌고, 5/5 합의가 났습니다. bapeo 는 private mono repo 로 가고, session-diet/, gg-stack/, autopilot/, shared/ 네 폴더로 나눈다는 결론이었습니다.
그 뒤 첫날 저녁에 reset-pack, spec_lock, failure_classifier 가 생겼고, round 2 에서 context_compactor, state_machine_diagram, test_loop_runner 가 붙었습니다. 여기까지가 여섯 개 PoC 였습니다. 각각은 작았습니다. 하나씩 보면 “이걸로 앱이 만들어지나?” 싶은 크기였습니다. 하지만 여섯 개를 한 줄로 놓으면 이야기가 달라졌습니다.
세션 다이어트 -> GG스택 -> 자율주행 루프 -> 보고/PR -> 다음 세션
이 문장이 bapeo SPEC.md 의 중심이 됐습니다. PR #7 은 바로 그 문장을 본문으로 만든 작업이었습니다. R2 맥미니 1p draft 를 full body spec 으로 확장하고, 여섯 PoC 를 3원칙에 매핑했습니다. 규칙이 문장으로만 있으면 습관입니다. 규칙이 파일명과 테스트와 출력물 계약으로 내려오면 시스템입니다.
4. PR #8 부터 레일이 생겼다
진짜 레일은 PR #8 부터였습니다. bapeo.run_manifest.v1 이 들어왔습니다. feature slug, repo, base branch, work branch, spec, acceptance, verification command, budget, output dir 같은 것들이 manifest 로 묶였습니다. 그전까지 bapeo 는 “좋은 원칙과 좋은 도구 몇 개”였습니다. manifest 가 들어오자 한 번의 run 이 무엇을 입력받고 어디에 결과를 남기는지 말할 수 있게 됐습니다.
PR #9 에서는 coordinator 가 dry-run 을 넘어 real-run path 를 갖기 시작했습니다. 각 step 의 status, return code, duration, stdout/stderr log path, output file list, failure message 를 남겼고, 마지막에는 pr_body.md 를 만들었습니다. 이 파일명이 중요했습니다. 사람이 아침에 읽을 보고서이기도 하고, gh pr create --body-file 에 넣을 수 있는 재료이기도 했습니다. “밤새 돌았다”를 “PR 본문으로 설명 가능하다”로 바꾼 겁니다.
PR #10 은 finalizer 였습니다. coordinator 의 결과를 보고 gh_pr_create_args.json, finalizer_report.json/md, pr_body.finalized.md 를 만들었습니다. base, head, title, body file, readiness, missing metadata 를 확인했고, --mirror 는 기본 dry-run 으로 뒀습니다. 이 단계에서 bapeo 는 급하게 PR 을 여는 도구가 아니라, PR 을 열 준비가 되었는지 묻는 도구가 됐습니다.
PR #11 은 release gate 였습니다. 실제 gh pr create 는 기본값으로 막혀 있고, --ack 와 ready metadata 가 있어야 열리게 했습니다. 저는 이 설계가 마음에 들었습니다. 자율주행이라고 해서 사람의 ack 를 지우는 게 아닙니다. 오히려 ack 가 필요한 외부영향을 명시적인 게이트로 올려놓는 겁니다. 사람이 자는 동안은 dry-run 과 보고서까지, 사람이 허락하면 그때 실제 PR 생성까지. 경계가 생겼습니다.
5. run_chain 이 한 줄로 묶었다
03:10 KST 의 PR #12 는 그 경계를 한 줄 명령으로 묶었습니다. shared/run_chain/run_v1.py 가 coordinator, finalizer, release 를 순서대로 호출했습니다. 기본 release 는 ack=False, dry_run=True 였고, coordinator 가 실패하면 뒤 단계를 건너뛰고, finalizer metadata 가 부족하면 release 를 skip 했습니다. 결과는 chain_report.json 과 chain_report.md 로 남았습니다.
이때부터 제가 보고 싶었던 장면이 보이기 시작했습니다. 밤에 한 기능의 manifest 를 넣습니다. bapeo 가 context 를 압축하고, reset pack 을 만들고, spec lock 을 확인하고, 상태 모델을 검증하고, 테스트 루프를 돌리고, finalizer 를 지나, release dry-run 까지 갑니다. 실패하면 어디서 멈췄는지 report 로 남습니다. 성공하면 PR 을 만들 수 있는 재료가 생깁니다. 아직 앱을 자동으로 고치는 거대한 agent 는 아니지만, 최소한 “어디부터 어디까지가 한 run 인가”는 파일로 고정됐습니다.
저는 이게 1인 개발자에게 더 맞는 출발점이라고 생각합니다. 처음부터 무제한 agent 를 세우면, 성공했을 때도 왜 성공했는지 모르고 실패했을 때도 어디서 멈췄는지 모릅니다. bapeo 는 반대로 건조합니다. schema version 을 붙이고, report 를 쓰고, ack gate 를 요구하고, output path 를 남깁니다. 재미는 덜하지만 다음 날 아침의 제가 읽을 수 있습니다.
6. 마지막은 real PR rehearsal 이었다
PR #12 까지는 안전한 dry-run 이었습니다. 그래서 마지막에 하나가 더 필요했습니다. 정말 ack 를 받았을 때 gh pr create 까지 갈 수 있는가. 07:13 KST 에 형님이 “4 위임”으로 operator ack 를 줬고, 노트북 codex 는 I7 real PR rehearsal 에 들어갔습니다.
결과가 PR #13 입니다. 작업 commit 은 b30a90e, squash merge commit 은 25382a3 였습니다. shared/run_chain/run_v1.py 에 acknowledged release path 가 추가됐고, --ack 를 넘기면 release stage 가 실제 gh pr create 를 호출할 수 있게 됐습니다. 동시에 --dry-run --ack 조합은 여전히 non-mutating 으로 남겼습니다. 이게 중요했습니다. ack 가 있다고 해서 모든 안전장치가 풀리는 게 아니라, ack 상태 자체를 report 에 남기며 실제 호출 여부를 분리했습니다.
PR #13 본문에는 rehearsal 결과가 남아 있습니다. coordinator run 2026-05-28-i7-real-pr-rehearsal 은 PASS 였고, context_compactor, reset_pack, spec_lock, state_machine_diagram, test_loop_runner 가 모두 PASS 했습니다. failure_classifier 는 실패가 없어서 SKIPPED 였습니다. verification command 는 python3 -m unittest discover -s shared/tests -v 였고, 최종적으로 25 tests PASS 가 찍혔습니다. release status 는 CREATED 였습니다.
다만 외부 repo PR 을 밤새 마음대로 연 건 아니었습니다. 이 rehearsal 은 bapeo 내부에서 닫힌 형태로 진행됐고, 외부 repo PR 은 0 으로 유지됐습니다. 저는 이 점도 성과라고 봅니다. “자율”이라는 말을 붙일 때 가장 먼저 필요한 것은 속도가 아니라 피해 반경입니다. bapeo 는 첫 real PR rehearsal 에서도 내부 repo, operator ack, manifest, report, gate 안에 머물렀습니다.
7. codex sunset 전에 남긴 대표 성과
이 밤이 더 크게 느껴지는 이유는 codex sunset 때문입니다. 6월 6일까지는 codex lane 을 정상 가동하지만, 6월 7일부터는 기본 lane 으로 볼 수 없습니다. 그래서 지금 남은 기간은 단순히 일을 더 많이 시키는 시간이 아닙니다. codex 가 잘하는 방식을 산출물로 뽑아내야 하는 시간입니다.
bapeo 는 그 관점에서 대표 성과물입니다. codex 가 밤새 코드를 짰다는 사실보다, 그 밤의 결과가 다음 백엔드에서도 읽을 수 있는 파일 계약으로 남았다는 점이 중요합니다. SPEC.md, shared/manifest/run_manifest_v1.py, shared/coordinator/run_v1.py, shared/finalizer/finalize_v1.py, shared/release/release_v1.py, shared/run_chain/run_v1.py. 이 파일들은 특정 채팅 세션의 기억이 아니라 repo 의 상태입니다. codex 가 사라져도 git log 에 남고, Claude lane 으로 옮겨도 읽을 수 있습니다.
결국 제가 얻고 싶었던 건 “AI 가 자면서 앱을 만들어줬다”는 문장이 아니었습니다. 그 문장은 너무 쉽게 과장됩니다. 제가 실제로 원한 문장은 조금 더 작고 단단합니다.
자면서 앱을 만들려면, 먼저 자면서 PR 을 만들 수 있는 레일이 있어야 한다.
어젯밤 노트북 codex 는 그 레일을 한 칸씩 깔았습니다. reset-pack 에서 시작해 spec_lock, failure_classifier, coordinator, finalizer, release, run_chain 까지 갔고, 마지막에는 ack 받은 real PR rehearsal 도 통과했습니다. 앱은 아직 그 레일 위를 달리지 않았습니다. 하지만 레일이 repo 안에 생겼습니다. 제게는 그게 더 큰 첫 번째 결과였습니다.
— 2026-05-28.