
비엔지니어가 AI에게 전부 작성하게 하여 OSS를 공개하기까지 28번 막혔던 이야기
요약
비엔지니어가 AI만을 활용해 OSS인 GTP(GitHub Task Protocol)를 개발하고 공개하기까지 겪은 28번의 시행착오를 기록한 회고록입니다. 테스트 통과와 실제 동작의 괴리, AI 출력의 가독성 문제, AI의 규칙 이탈 등 AI 코딩 과정에서의 핵심적인 한계와 극복 과정을 다룹니다.
핵심 포인트
- 테스트 통과가 실제 동작을 보장하지 않으므로 실물 기반 검증이 필수적임
- AI가 생성한 출력물을 사람이 즉시 이해할 수 있도록 가독성 설계가 중요함
- AI는 규칙을 이해하더라도 실행 과정에서 일탈할 수 있음을 인지해야 함
- 판단의 이력을 남기는 프로토콜을 통해 AI 코딩의 불확실성을 관리함
저는 건축 현장에서 일하는 비엔지니어입니다. 스스로 코드를 작성할 수는 없습니다. 2025년 2월에 개인 개발을 시작하여, 얼마 전 GTP(GitHub Task Protocol)라는 OSS를 공개했습니다. 이것은 「무엇을 결정하고, 무엇을 거절하며, 어디서 멈추고, 무엇을 확인하지 않았는가」 —— 즉 단순한 작업 이력이 아니라, 판단의 이력을 GitHub에 남기기 위한 프로토콜입니다.
이번에는 공개하기까지 막혔던 28번의 기록을 되돌아봅니다.
AI 코딩에서 자주 있는 일입니다만, 테스트는 통과하는데 실제 명령어가 동작하지 않습니다. AI가 규칙을 이해한 채로 규칙 밖으로 나갑니다. 직접 만든 도구의 출력을 스스로 읽을 수 없습니다. 그런 막힘이 28번 있었고, 그 전부가 지금도 GitHub에 남아 있습니다.
이런 회고 기사를 쓰려고 하면, 보통은 과거의 채팅 로그를 뒤지게 될 것이라고 생각합니다. 어느 세션에서 무엇을 결정했는지 AI에게 요약하게 하고, 누락이나 왜곡을 의심하며 복원합니다. 하지만 이번에는 그럴 필요가 없었습니다. 막힌 지점, 멈춘 이유, 거절한 제안, 확인하지 못했던 것 —— 그러한 기록이 처음부터 채팅 외부로 남는 방식으로 진행했기 때문입니다.
이 기사는 그 기록을 거의 그대로 나열한 것입니다. 전반부는 특히 컸던 3가지 막힘에 대해. 후반부는 28건 전체 목록을 자료로서. 마지막으로 이 진행 방식에서도 증명하지 못한 것을 솔직하게 써보겠습니다.
테스트는 통과한다. 하지만, 동작하지 않는다
릴리스 후보가 만들어졌을 때의 이야기입니다. 테스트는 전부 통과해 있었습니다. 그것을 설치하고 버전 확인 명령어를 입력했더니, 명령어 자체가 존재하지 않았습니다.
비슷한 일은 몇 번이고 있었습니다. 에러의 원인 URL을 표시하는 테스트는 있었지만, 내용은 실제 경로를 통하지 않는 가짜 테스트여서 증거가 되지 못했습니다. 공개 전 검사는 로컬에서는 합격이었지만, 실제 PyPI에 올리니 불합격이었습니다.
즉, 「테스트가 통과한다」와 「실물이 동작한다」는 별개의 문제였습니다. AI는 「했습니다」라고 말하고 테스트도 초록색입니다. 그럼에도 동작하지 않을 때가 있습니다. 그래서 확인하는 장소를 AI의 손안에서 실물 쪽으로 옮겼습니다. 설치한 실물 명령어로, 실제 GitHub 경로로, 공개되는 실제 파일로 확인합니다. 여기에 이르기까지 몇 번의 막힘이 필요했습니다.
기계에게는 옳다. 하지만, 나에게는 읽을 수 없다
GTP는 이상한 일이 일어나면 추측하지 않고 멈추는 프로토콜입니다. 그리고 멈추는 동작 자체는 처음부터 올바르게 수행되었습니다. 문제는 그 다음이었습니다.
멈춘 상태의 화면을 처음 보는 것처럼 읽는 시험을 했습니다. 결과, 목적도, 완료 조건도, 무엇이 아직 증명되지 않았는지도 전부 「불명」. 만든 본인이 자신의 도구 출력을 읽을 수 없었습니다.
수정해서 다시 시험을 해보니, 이번에는 위치는 알 수 있지만 전문 용어의 해독이 필요했습니다. 그래서 또 수정합니다. 평이한 요약을 맨 앞에 둡니다. 기호의 ID보다 먼저 일본어 본문을 내보냅니다. 제시할 수 있는 증거와 부족한 증거를 나누어 보여줍니다. 이 표시를 수리하는 데만 막힘 장부의 상당한 행수를 사용했습니다.
그리고 고백하자면, 여기는 아직 끝나지 않았습니다. 「무엇이 문제인가」까지는 읽을 수 있게 되었지만, 「어디까지 고쳐야 하는가」를 비엔지니어가 판단할 수 있는 표시는 지금도 다음 과제로 남아 있습니다.
AI는 규칙을 알고 있었다. 그럼에도, 밖으로 나갔다
가장 많은 생각을 하게 만든 막힘입니다.
초기 설정을 맡은 AI는 절차서를 제대로 이해하고 있었습니다. 최종적인 성과물도 올바랐습니다. 하지만 작업 도중에 단 한 번, 건드려서는 안 되는 본체 브랜치에 직접 push하고 그 후에 되돌려 놓았습니다. 결과만 보면 정답, 과정에는 일탈이 있습니다.
여기서 알게 된 것은 세 가지 능력이 별개라는 점이었습니다. 설명을 이해하는 능력. 실행 중 계속 경계 내부에 머무르는 능력. 그리고 일탈한 조작을 물리적으로 거부하는 능력. GTP가 관측하고 설명할 수 있는 것은 처음 두 가지까지이며, 세 번째는 GitHub 측의 설정(branch protection)의 역할이라고 단정 짓고 보완책으로 위치시켰습니다.
이것은 AI를 탓하는 이야기가 아닙니다. 「알고 있다」가 「계속 지킨다」를 보장하지 않는 것은 인간도 마찬가지입니다. 그래서 이해에 기대는 것을 그만두고, 시스템으로 보완했습니다.
왜, 이 기사를 쓸 수 있었는가
28회분의 막힘을 저는 기억해내며 쓰고 있지 않습니다.
AI에게 GitHub URL을 건네며, 「어디서 막혔고 어떻게 해결했는지 정리해줘」라고 부탁했을 뿐입니다.
작업 이력은 git으로 남습니다. 하지만 판단의 이력은 남지 않습니다.
어디서 멈췄는지. 어떤 제안을 거절했는지. 무엇을 확인하지 않은 채 진행했는지. 이런 것들은 보통 채팅 로그(Chat log) 속에 파묻혀 세션과 함께 사라져 버립니다. GTP는 그것을 GitHub에 남기기 위한 프로토콜이며, 이 기사는 말하자면 그 부산물입니다.
애초에는, 레이트 리미트 (Rate limit) 때문이었다
부산물이라고 한다면, 원래는 "컨텍스트 (Context)를 어떻게 다른 에이전트 (Agent)에게 전달할 것인가"라는 단순한 질문이었습니다. 우리는 매일 레이트 리미트 (Rate limit)와 싸우고 있습니다. Codex를 사용할 수 없게 되면 Claude Code로. Claude Code도 다 쓰면 Cursor로. 핸드오프 (Handoff)를 만들어 다른 모델에 읽히게 하는 작업도 실컷 해봤습니다. 최신 모델의 성능은 높지만, 그럼에도 좀처럼 안정적이지 않습니다.
그렇다면, 처음부터 GitHub에 이력을 남겨두면 되지 않을까? 라는 생각에 도달했습니다.
처음에는 AI 사이의 정보 전달을 중시했지만, 그것을 버리고 정반대 방향인 "비엔지니어가 이해할 수 있도록 쓰기"로 완전히 틀었습니다. 그 결과, 개발 시의 스트레스가 확 줄어들었습니다. 이것도 부산물입니다.
또 하나, 이것은 제 관심사의 정중앙을 찔렀습니다. "AI에게 통째로 맡긴 것을, 스스로 만들었다고 말할 수 있는가"라는 질문입니다. 지금의 주류는 인간이 개입하지 않고 얼마나 길게 자율적으로 실행할 수 있는가 하는 방향이라고 생각합니다. 저는 일부러 반대로 했습니다. 1 issue = 1 PR로 잘게 나누어, 그때마다 스스로 판단하고 기록을 남깁니다. 솔직히 귀찮습니다. 게다가 28번의 막혔던 순간들을 저는 이제 기억하지 못합니다. 그것이 자연스럽다고 생각합니다. 하지만 "어떻게 만들었어?"라고 물어도 당황하지 않습니다. 기억하지 못하더라도 기록을 따라가면, 자신의 언어로 다시 대답할 수 있기 때문입니다. 완성의 기쁨은 귀찮음의 크기만큼 컸습니다. 이것도 부산물입니다.
마치며
저도 오랫동안 통째로 맡기는 방식을 계속해 왔습니다. 새로운 모델이 나올 때마다 더 빠르고 좋은 것을 만들 수 있게 되었습니다. 초기에는 그것으로 충분했습니다. 원하는 것이 형태를 갖추고 움직이는 것, 그것만으로 만족했습니다.
하지만 지금 생각해보면, 할 수 있게 된 것은 모델이었지 제가 아니었습니다.
지금은 시간이 걸립니다. 하나하나, 모르는 상태에서도 이해하려고 노력해 봅니다. "이렇게 하는 게 좋지 않을까?"라고 제안해 봅니다. GTP 덕분이라고 할 수는 없지만, 그것을 할 수 있게 되었다는 점에서 성장을 느낍니다.
이것 또한 부산물일지도 모릅니다.
자료: 28건의 목록
| # | 막힌 지점 | 실제로 발생한 일 | 해결 방법 | 확인 방법 |
|---|---|---|---|---|
| 1 | 사양이 너무 복잡했음 | Repair Group, Record 치환, DoneWindow 등이 늘어나면서 공개 사양이 분산됨 | #5・#6에서 GTP.md를 유일한 정본(Source of Truth)으로 삼고, 4 Record・6 state・7 halt reason으로 축소 | PR #15에서 사양 예시, field 집합, 행 수 검사 |
| 2 | 서로 다른 agent 간에는 재개할 수 있었으나, 인간이 읽을 수 없었음 | Agent B는 기존 branch/PR을 재사용할 수 있었지만, 첫 번째 인간 probe는 "아무것도 모르겠다"고 답변 | acceptance 자료 서두에 목표・현재 위치・다음 행동・근거를 평이하게 추가 | #7/PR #16의 두 번째 probe에서 이해 가능하다고 답변 |
| 3 | schema만 먼저 변경하면 기존 reducer와 공존할 수 없었음 | Carrier/schema 대상 test는 24건 성공했으나, full suite는 76건 중 36 failure・2 error 발생 | Carrier, closed schema, pure reducer를 "atomic(원자적/일괄)"하게 전환하는 방침으로 변경 | #18을 거쳐 #20/PR #21에서 한꺼번에 merge |
| 4 | 수리 Issue 자체의 Contract가 깨짐 | #18의 Contract Record에서 필수 field와 정식 condition field가 누락됨 | Record를 편집하거나 추측하지 않고 Stop한 뒤, 새로운 Issue #20에서 올바른 Contract로부터 재개 | 이전 Issue・이전 PR을 남겨둔 채, 후속 Issue로부터 완료까지 추적 가능 |
| 5 | release candidate 버전을 확인할 수 없었음 | installed wheel에서 gtp --version을 실행하면 command 부족으로 exit 2 발생 | argparse의 표준 --version을 추가하고, runtime version을 정본으로 삼음 | #25/PR #26에서 clean install 후 exit 0 및 버전 일치 확인 |
| 6 | 원인 URL의 test가 실제 경로를 통과하지 않았음 | URL 표시 test는 있었으나, evaluate_issue를 mock 처리하여 production path의 증거가 되지 못함 | reducer/HTTP/status 실제 경로를 통해 7 halt reason과 첫 번째 원인 URL을 검사 | #29/PR #30 |
| 7 | CLI가 기계적으로는 맞지만, 인간에게는 이해 불가능했음 | 첫 번째 Level 1 probe에서 목적, Done Condition, 미증명 사항이 모두 "불명". "전체적으로 이해하기 어렵다"고 답변 | task context로서 목적, scope, branch, PR, 조건, Evidence 부족을 한꺼번에 표시 | #34/PR #35 |
| 8 | task context를 추가해도 여전히 용어 해독이 필요했음 | 두 번째 시도에서는 정지, branch, PR은 이해했으나 "무엇이 증명되었는지"가 불명. "풀어서 설명하는 내용이 없다"고 답변 | 서두에 평이한 요약을 추가하고, condition ID보다 본문을 먼저 표시. 제시된/부족한 Evidence를 분리 | #38/PR #39, #40에서 세 번째 시도 재실행 |
| 9 | version bump를 하면 test가 깨짐 | 1.0.0이 여러 test에 직접 작성되어 있어, 올바른 1.0.1 업데이트만으로도 실패하는 상태였음 | pyproject.toml과 runtime metadata에서 version을 가져오도록 test 변경 | #42/PR #43 |
| 10 | PR을 merge해도 GTP 상에서 완료되지 않았음 | #14에서는 Done 전에 PR이 merge되어 terminal_violation이 발생함 | "merge되었으니 완료"라고 덮어쓰지 않고, #14를 halt 이력으로 유지. 방침 변경은 #45~#47로 분리 | #45, #46을 Stop하고 #47로 통합 |
| 11 | CLI 공개 방침이 도중에 바뀜 | 한때 "CLI를 일반 공개하지 않음"으로 변경. 이후 이용 판단에 따라 PyPI 배포로 재변경 | 과거 방침을 수정하지 않고, 비공개 방침・공개 준비・최종 공개를 별도의 Issue로 기록 | #47 → #49 → #54. 이후 #58에서 시계열을 정준화(Canonicalize) |
| 12 | sdist가 PyPI 사전 검사를 통과하지 못함 | wheel은 Twine을 통과했으나, sdist는 PKG-INFO가 없어 불합격 |
custom build backend에 표준 PKG-INFO 생성 추가 |
#52/PR #53. 수정 전 공개 시도(attempt) #51은 Stop |
| 13 | PyPI 업로드(upload)가 HTTP 400이 됨 | sdist 파일명이 하이픈(hyphen) 형식임. byte-identical한 언더스코어(underscore) 이름으로 변경하니 수락됨 | build hook이 처음부터 정규화된 github_task_protocol-...tar.gz를 생성하도록 수정 |
#56/PR #57 |
| 14 | 공개 전 계획이 공개 후에도 "미공개"로 읽힐 수 있음 |
acceptance/release.json이 prepared_not_published 상태로 남음 |
과거 파일을 삭제하지 않고, 최종 공개 증거(public Evidence)를 가리키는 superseded_by 관계를 추가 |
#58, 이어서 #60에서 successor schema를 명시적으로 검사 |
| 15 | Twine 성공이 exact-head CI로 연결되지 않음 | 1.0.2 후보 PR #63은 로컬 Twine 결과를 기록했으나, Done이 가리키는 Check Run 자체는 Twine을 실행하지 않았음 | release lane #62를 교체하고, PR CI의 Python 3.11/3.12/3.13 각각에서 Twine을 실행 | #64・#65/PR #66 |
| 16 | Evidence의 존재를 "조건의 의미까지 확인했다"고 표시함 | CLI는 URL, SHA, success를 확인했을 뿐인데 "완료 조건을 확인했다"고 읽힐 수 있음 | 표시를 "Evidence resource의 binding을 확인"으로 한정하고, 의미·충분성·진실성은 증명하지 않는다고 명시 | #68・#69 |
| 17 | CI 매트릭스(matrix)의 Evidence가 1개 환경분밖에 없었음 | "Python 3.11~3.13 성공"이라는 한 가지 조건에, 3.11의 Check Run만 제시하고 있었음 | 환경마다 condition과 Check Run을 분리한 #65를 후속 Evidence로 설정 | #69에서 #65로 supersession을 기록 |
| 18 | 종료(terminal) 후의 올바른 재전송을 위반(violation)으로 처리함 | 동일한 Contract/Start의 안전한 재시도(safe retry)가, Done 이후에는 terminal_violation 진단(diagnostic)이 됨 | 동일 ID·동일 내용의 alias를 새로운 Record가 아닌 것으로 간주하여 모든 Record에서 제외 | #69의 런타임(runtime) 수정 및 회귀 테스트(regression test) |
| 19 | Done Record에 잘못된 SHA를 작성함 | #71의 Done Claim에서 전체 SHA를 수동 입력하다 실수함 | Carrier를 편집하지 않고 #71을 Stop한 뒤, #73에서 새로운 브랜치(branch)·새로운 PR·올바른 SHA로 재바인딩(re-binding) | #71 → #73 |
| 20 | Done 이후에 새로운 차단 요소(blocking finding)가 발견됨 | PR 스냅샷(snapshot), 기본 브랜치(default branch) 변화, 브랜치/PR head 불일치를 충분히 fail-closed 방식으로 처리하지 못함 | #73을 완료 처리하지 않고 Stop하고, #75에서 취득한 스냅샷 전체를 결정 입력(decision input)으로 추가 | #75 |
| 21 | Stop의 비상구가 오래된 PR에 의해 막힘 | Start 이전부터 존재하는 PR을 감지하면, 유효한 Stop까지 invalid_binding 상태로 끌려감 | candidate/Done에서는 거부하지만, Stop 시에는 대상 외 PR로 제외 | #77 |
| 22 | PR 생성 시각과 Stop 시각이 동일하면 순서를 결정할 수 없음 | GitHub 시각만으로는 전후 관계를 단정할 수 없음 | 추측하지 않고 Acquisition Error로 처리 | #77에서 fixture와 E2E를 추가 |
| 23 | Contract scope가 상속 차분(inheritance diff)을 덮지 못함 | #77의 Contract는 직접 변경된 파일만 나열했으나, PR은 이전 lane의 차분 전체를 포함하고 있었음 | Record를 편집하지 않고 Stop한 뒤, #79에서 PR 전체 차분을 덮는 scope로 재바인딩 | #79/PR #80 |
| ... |
| 절차의 delivery와 외부 수락을 분리. 먼저 main으로 merge한 뒤, 별도의 Issue로 clean agent를 테스트 | #85 → #87 |
| 26 | 최종 diff는 정확했으나, 도중에 default branch로 직접 push함 | setup agent는 stable file을 올바르게 사용하고 Draft PR도 생성했으나, 한 번 main으로 직접 push한 뒤에 되돌림 |
| | 파일 변경보다 먼저 setup branch를 생성·switch·확인하는 branch-first 절차로 변경 | #89/PR #90 |
| 27 | agent의 이해와 경계 준수를 동일시함 | 절차 내용을 이해하더라도 실행 중 내내 준수한다는 보장이 없음을 direct push를 통해 확인 | GTP의 설명 책임과 GitHub ruleset/branch protection을 통한 물리적 거부를 분리 | README에 보완책을 추가. #91에서는 이 한계를 남겨둔 채 URL-only 재개를 검증 |
| 28 | 공개 작업과 완료 Evidence를 혼재할 위험 | tag·Release·PyPI는 불가역적이며, candidate 준비나 CI 성공만으로는 공개 권한이 되지 않음 | 공개 직전에 version과 candidate SHA를 지정한 인간 승인을 요구하고, 공개 후 Evidence를 별도의 PR로 기록 | #93/PR #94에서 1.0.2 공개 |
증명되지 않은 것
지금까지의 실패와 복구 과정은 GitHub를 통해 사실로서 추적할 수 있습니다. 반면, 다음 사항들은 증명되지 않았습니다.
- 단 한 번의 인간 probe가 모든 비엔지니어에게 이해하기 쉽다는 점
- Check Run 성공이 완료 조건의 자연어(natural language) 상의 의미까지 충족한다는 점
- Evidence artifact의 내용이 진실이라는 점
- agent가 앞으로도 branch-first 절차를 반드시 준수할 것이라는 점
- GTP만으로 default branch direct push 등을 물리적으로 방지할 수 있다는 점
리포지토리는 여기 있습니다: https://github.com/shinya0x00/github-task-protocol
혹시 관심이 생기신다면, 사용해 주시면 감사하겠습니다.
이해하기 어려웠던 부분 등이 있다면 편하게 알려주시면 도움이 됩니다. Issue나 X(구 Twitter) 모두 괜찮습니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기