실전 예제 — Quicknote
한 문장 아이디어에서 시작해 완료 계약 ok까지 실제로 완주한 기록이다. 수치는 전부 .gatekit/의 진짜 상태 파일에서 가져왔다.
이 문서를 읽는 방법: 따라 칠 명령과, 그때 화면에 무엇이 나오고 사용자가 무엇을 판단하게 되는지를 함께 적었다. 특히 5·6단계는 "일이 잘 안 풀렸을 때 도구가 어떻게 반응하는가"를 보여주는 부분이라, 처음 쓰는 사람에게는 오히려 그쪽이 더 쓸모 있다.
버전 참고: 이 기록은 discover/interview가 자유 대화로 바뀌고 프로토타입 확정 게이트가 추가되기 전에 진행됐다. 큰 흐름(스펙 → 작업 → 완료조건 → 빌드 → 검증)과 수치는 그대로 유효하지만, 인터뷰와 목업 단계에서 실제로 받게 될 질문 방식은
05-commands.md의 현재 설명을 따른다.
출발점
메모를 여러 텍스트 파일에 흩어 적어두다가, 지난주 회의 메모를 찾는 데 20분이 걸렸다. 그 불편에서 시작했다.
1단계 — interview
/gatekit:interview 메모를 적고 나중에 빨리 찾을 수 있는 개인용 노트 앱spec/01-prd.md와 spec/03-architecture.md가 나왔다. 측정하지 못한 값은 지어내지 않고 "미측정"으로 표에 넣고 가정 원장 1번 행에 올렸다.
| 지표 | 현재 값 | 출처 | 측정일 |
|---|---|---|---|
| 지난주 회의 메모를 찾는 데 걸린 시간 | 20분 | 사용자 진술(사례 1건) | 2026-09-10 |
| 흩어져 있는 메모 파일 수 | 미측정 | — | 2026-09-10 |가정 원장에는 6개 행이 들어갔다. 제목 길이 상한, 목록 정렬 방식, 삭제 복구 없음, 검색 방식, DB 파일 위치 등 사용자가 말하지 않은 판단이 전부 기록됐다.
여기서 사용자가 하는 일: 나온 기능 목록과 가정 원장을 읽고 "이게 내가 원한 게 맞나"를 판단한다. 가정 원장은 AI가 혼자 정한 것들의 목록이므로, 틀린 게 있으면 지금 말하는 게 가장 싸다. 측정 못 한 값이 "미측정"으로 남는 건 정상이다 — 지어내지 않았다는 뜻이다.
2단계 — mockup
/gatekit:mockup mockup.htmlmockup.html에서 화면과 CSS 커스텀 속성을 추출해 spec/02-screens.md와 spec/tokens.json을 만들었다. 토큰은 목업이 실제로 쓴 값 그대로다.
{"version": 1, "source": "mockup.html",
"color": {"accent": "#0E5C55", "danger": "#B3382C"}}3단계 — tasks
/gatekit:tasksspec/04-tasks.md에 태스크 8개가 나왔다. 전부 수직 슬라이스이고, 각각 게이트를 1개씩 갖는다.
| 태스크 id | 라운드 | write_scope |
|---|---|---|
task-db-schema | 1 | db.py, tests/test_db.py |
task-note-create | 2 | app.py, tests/test_notes_create.py |
task-note-list | 3 | app.py, tests/test_notes_list.py |
task-note-update | 4 | app.py, tests/test_notes_update.py |
task-note-delete | 5 | app.py, tests/test_notes_delete.py |
task-note-search | 6 | app.py, tests/test_notes_search.py |
task-static-frontend | 7 | static/, tests/test_static.py |
task-full-suite-gate | 8 | 전체 스위트 확인 |
app.py를 여러 태스크가 건드리므로 라운드를 다르게 배정했다. 범위를 넓혀 충돌을 없애는 대신 순차화한 것이다.
여기서 사용자가 하는 일: 거의 없다. 이 단계는 대부분 자동이다. 다만 작업 개수와 라운드 수를 한 번 보는 게 좋다 — 라운드가 많으면 그만큼 순차 실행이라 오래 걸린다. 근거 없이 나뉜 라운드가 보이면 합칠 수 있는지 물어봐도 된다.
4단계 — gate
/gatekit:gatespec/05-gate.md에 기준 8개가 나왔다. 태스크 8개와 1:1로 대응하도록 id에 태스크 id를 넣었다. ## 완료로 보지 않는 조건에는 일반 조항 6개에 더해 프로젝트별 조항이 들어갔다. 표준 라이브러리 밖 패키지를 import한 경우, 서버가 127.0.0.1 밖에 바인딩된 경우, 검색 성능 기준을 실측 없이 통과 보고한 경우 등이다.
여기서 사용자가 하는 일 — 이 예제에서 가장 중요한 판단: 기준 8개를 표로 보여주면서 승인을 묻는다. 던져야 할 질문은 하나다. "이 8개가 전부 통과하면 내가 원한 게 정말 만들어진 건가?" 부족하면 기준을 더 넣어달라고 하면 된다. 승인하는 순간 소스 코드 쓰기가 열리고, 세션을 끝낼 때 이 명령들이 진짜로 실행된다.
승인 후 계약이 파생됐다.
python3 "${CLAUDE_PLUGIN_ROOT}/bin/gatekit.py" contract derive
python3 "${CLAUDE_PLUGIN_ROOT}/bin/gatekit.py" approve spec/05-gate.md
python3 "${CLAUDE_PLUGIN_ROOT}/bin/gatekit.py" approve check spec/05-gate.md5단계 — build
/gatekit:build잡 20260910T060008Z-b1dc가 시작됐다. 백엔드 claude, 병렬 3, 태스크 타임아웃 900초, 최대 재시도 2회.
게이트가 잡은 것 — 워커는 exit 0이었다
task-note-delete의 첫 시도에서 워커는 종료 코드 0으로 성공을 보고했다. 그런데 게이트가 테스트를 실행하자 실패했다.
File "tests/test_notes_delete.py", line 148
</content>
^
SyntaxError: invalid syntax워커가 테스트 파일 끝에 </content> 문자열을 남겼다. 파이썬 파일이 아예 import되지 않는 상태였다. 워커의 자기 보고만 믿었다면 이 태스크는 통과로 기록됐을 것이다.
같은 파일이 깨져 있었으므로 task-full-suite-gate의 python3 -m unittest discover도 함께 실패했다. 67개 테스트를 돌리고 수집 오류 1건으로 exit 1이었다.
잡 상태에는 이렇게 남았다.
{"state": "failed", "exit": 0, "gates_verdict": "fail",
"gates_passed": 0, "gates_total": 1,
"detail": "worker exited 0 but gates verdict is fail (0/1 ok)"}재위임
python3 "${CLAUDE_PLUGIN_ROOT}/bin/gatekit.py" jobs redelegate task-note-delete
python3 "${CLAUDE_PLUGIN_ROOT}/bin/gatekit.py" jobs redelegate task-full-suite-gate이전 시도는 attempt-1/로 보관되고, 실패한 게이트의 출력이 프롬프트에 덧붙어 다시 실행됐다. 두 태스크 모두 재위임 2회차에서 통과했다. task-full-suite-gate의 최종 상태는 73개 테스트 전부 OK, 게이트 1/1이다.
최종 결과는 8/8 passed다.
여기서 사용자가 하는 일: 실패한 작업이 보이면 재시도를 시킨다. 판단할 게 하나 있다 — 실패 원인이 코드인가, 완료 조건 자체인가. 위 사례는 코드가 문제였으므로 재시도가 맞았다. 만약 조건이 없는 파일을 가리키고 있었다면 재시도가 아니라
spec/04-tasks.md를 고쳐야 한다. 같은 작업이 3번 연속 실패하면 자동으로 멈추고 진단을 남기므로, 무한정 재시도가 반복되지는 않는다.
6단계 — verify와 예산 문제
/gatekit:verify독립 평가자가 계약을 실행했다. 집계 판정은 ok가 아니라 unverified였다. 기준 8개 중 6개가 ok, 2개가 타임아웃으로 unverified였다.
| 기준 | 판정 | 실측 | 당시 예산 |
|---|---|---|---|
task-note-update-works | unverified | 6.61~6.65초 (3회 측정) | 5초 |
task-full-suite-gate-works | unverified | 24.5초 | 10초 |
중요한 점은 이것이 구현 결함이 아니었다는 것이다. 두 테스트 모두 손으로 돌리면 통과했다. 예산이 실측보다 작았을 뿐이다. 그런데 gatekit은 이것을 통과로 반올림하지 않았다. 검사가 끝나지 못했으므로 unverified다.
여기서 사용자가 하는 일 —
unverified를 처음 만났을 때: 처음 보면 "도구가 까다롭게 군다"고 느끼기 쉬운데, 읽는 법은 간단하다.unverified는 "틀렸다"가 아니라 "확인을 못 했다"다. 그래서 고치는 방향도 다르다 — 코드를 건드리는 게 아니라, 왜 확인이 안 됐는지(대개 시간 초과)를 보고 그 원인을 없앤다. 아래 7단계가 그 과정이다.
평가자는 E2E 단계도 손으로 전부 실행했다. 임시 DB와 포트로 서버를 띄우고 실제 HTTP 요청을 보냈다.
QUICKNOTE_DB=/tmp/e2e_notes.db QUICKNOTE_PORT=8099 python3 app.py10개 E2E 단계가 전부 ok였다. 노트 101건을 API로 시드한 뒤 측정한 검색 왕복 시간은 0.0115초로, 수용 기준 1초를 충족했다.
7단계 — 예산 상향과 재승인
실측값을 근거로 개별 timeout_s를 15초·45초로 올리고, 전체 예산을 gatekit-budget 펜스로 선언했다.
{"total_budget_s": 180}전체 스위트 실측 49초에 여유를 둔 값이다. 추측이 아니라 측정 후 선언이다.
05-gate.md를 고쳤으므로 승인이 만료되고 계약이 stale이 됐다. 다시 파생하고 다시 승인했다.
python3 "${CLAUDE_PLUGIN_ROOT}/bin/gatekit.py" contract derive
python3 "${CLAUDE_PLUGIN_ROOT}/bin/gatekit.py" approve spec/05-gate.md --note "budget fence added after measuring 49s total"
python3 "${CLAUDE_PLUGIN_ROOT}/bin/gatekit.py" contract run --json이번에는 8/8 ok, 총 소요 49.4초였다.
최종 상태
| 항목 | 값 |
|---|---|
| 태스크 | 8/8 passed |
| 완료 계약 기준 | 8/8 ok |
| 재위임된 태스크 | 2개 (task-note-delete, task-full-suite-gate) |
| 선언 예산 | 180초 |
| 실측 총 소요 | 49.4초 |
| 가장 긴 기준 | 24.5초 |
이 실행이 보여준 것
- 워커의 exit 0은 근거가 아니다. SyntaxError가 남은 파일을 두고 워커는 성공을 보고했다. 게이트가 잡았다.
unverified가 통과로 반올림되지 않았다. 두 기준이 실제로는 통과 가능했지만 예산 안에서 확인되지 않았다. 도구는 그 사실을 그대로 보고했다.- 예산은 측정 후에 선언했다. 느린 테스트를 감추려고 올린 것이 아니라 실측 49초를 근거로 180초를 선언했다.
- 파일을 고치자 승인이 만료됐다. 게이트 파일 수정은 자동으로 재승인을 요구했다.
- 평가자가 만든 쪽이 아니었다. E2E 10단계를 별도 read-only 에이전트가 실제 서버에 대고 손으로 실행했다.