설계 결정
docs/ARCHITECTURE.md가 모듈 경계·파일 형식·어휘의 단일 진실 원천이다. 모든 모듈·커맨드·훅·테스트가 이 파일과 일치해야 한다. 구현이 벗어나야 한다면 docs/decisions/에 ADR을 쓰고 이 파일을 먼저 고친 뒤 코드를 고친다.
gatekit은 클린룸 구현이다. 훅 강제 게이트, 가정 원장, 해시 앵커 승인, 실행 가능한 완료 계약, 워커 잡 디렉터리 같은 일반적인 엔지니어링 패턴을 빌려 왔지만 다른 프로젝트에서 복사한 코드는 없다.
ADR-0001 — 하나의 플러그인, 하나의 패키지
맥락: interview·mockup 파이프라인, 태스크·게이트 시스템, 워커 잡 러너, 이를 강제하는 훅들. 각각을 별도 플러그인으로 나눌 수도 있었다.
문제: Claude Code는 ${CLAUDE_PLUGIN_ROOT}를 플러그인마다 따로 해석하고, 플러그인 간 파일 경로는 존재하지 않는다. 한 플러그인의 훅 스크립트가 상대 경로로 다른 플러그인 디렉터리에 닿을 수 없다. 나누면 원장·판정 어휘·훅 I/O 계약 같은 횡단 요소가 중복되거나, 형제 플러그인 설치 위치에 대한 부서지기 쉬운 절대 경로 가정에 기대게 된다.
결정: gatekit은 정확히 하나의 플러그인(plugin/)이고 그 안에 정확히 하나의 파이썬 패키지(plugin/gatekit/)가 있다. marketplace.json은 항목 하나만 나열한다.
대가: plugin/이 작은 트리 여러 개가 아니라 큰 트리 하나가 된다. ARCHITECTURE.md §1이 그 트리를 탐색 가능하게 유지하려고 존재한다.
ADR-0002 — 파이썬 표준 라이브러리만
맥락: pyyaml, jsonschema, rich 같은 편의 라이브러리는 각각 커널이나 CI 게이트의 어딘가에서 구현 노력을 줄여 줬을 것이다.
문제: 훅은 세션의 모든 프롬프트와 도구 호출에서 실행된다. 없는 패키지를 import하지 못하는 훅은 우아하게 격하되지 않는다. 첫 줄에서, 모든 호출마다, 누군가 알아채고 패키지 매니저를 돌릴 때까지 실패한다. ARCHITECTURE.md §3의 "모든 훅은 내부 오류에서도 exit 0"이라는 요구를 그 오류 처리가 실행될 기회조차 없이 위반한다.
결정: plugin/gatekit/, plugin/gatekit/gates/, tools/는 파이썬 3.9+ 표준 라이브러리만 쓴다. pip install 없음, npm 없음, 벤더링된 서드파티 소스 없음.
대가: 일부 구현이 장황하다. spec.py의 스키마 검증은 손으로 썼고, tools/gate_skill_size.py의 프론트매터 파싱은 YAML 파서가 아니라 작은 정규식이다. 이것은 감수한 비용이지 실수가 아니다.
이득: 신선한 클론 직후 설치 단계 없이 CLI와 CI 게이트가 즉시 돈다. CI는 의존성 해석 단계도 네트워크도 필요 없다.
ADR-0003 — Claude CLI가 기본 워커, Codex는 opt-in
맥락: workers.py는 여러 백엔드를 지원한다. 사용자와 조직마다 이미 다른 도구와 라이선스 계약이 있고, gatekit의 잡·게이트 기계는 어느 CLI가 실제로 일하는지 신경 쓰지 않는다. 쓰기 범위 계약만 지키면 된다.
결정: claude 백엔드가 기본 활성이다. codex는 "enabled": false로 출하되고 명시적인 /gatekit:setup codex 단계로만 켜진다. 그 단계는 workers.check("codex")를 실행하고 사용자가 결과를 확인해야 enable이 기록된다.
이유: gatekit 자체가 Claude Code 플러그인이므로 Claude CLI는 이미 사용자가 그 안에서 돌리고 있는 도구다. 신선한 설치가 추가 설정 없이 끝까지 동작한다. 그리고 아무의 세션도 설치한 적 없고 동의한 적 없는 두 번째 CLI로 조용히 나가지 않는다.
확장: 세 번째 백엔드를 추가해도 "비활성으로 출하, enable 전에 check" 패턴을 따르면 새 ADR이 필요 없다.
아키텍처 계약의 핵심 규칙
| 규칙 | 이유 |
|---|---|
| 파이썬 3.9+ 표준 라이브러리만 | python3만 있는 신선한 머신에서 돌아야 한다 |
| 플러그인 하나, 패키지 하나 | 플러그인 간 경로가 존재하지 않는다 |
| 게이트는 훅이지 산문이 아니다 | 산문 지시는 비결정적으로 발화한다 |
| 모든 훅은 내부 오류에서도 exit 0 | 깨진 훅이 사용자 세션을 망가뜨리면 안 된다 |
| 판정 어휘는 정확히 4개 | "검사 안 함"이 통과나 실패로 반올림되면 안 된다 |
| 절대 개인 경로 금지 | 작성자의 로컬 레이아웃이 새고 다른 기여자에게서 깨진다 |
SKILL.md 40줄 상한 | 스킬이 커맨드와 경쟁하는 두 번째 실행 경로가 되는 것을 막는다 |
| 커맨드 파일이 실행 지시서 | 스킬은 트리거 shim일 뿐이다 |
| 데이터는 JSON·마크다운 파일에 | 프롬프트를 작게, 데이터를 diff 가능하게 유지 |
| 1MB 초과 파일 금지 | 커밋된 코퍼스 없음 |
| 한국어는 기본값이 아님 | 오픈소스 자세 |
CI 게이트 7종
각 게이트는 특정한 실패 유형을 막는다. 파이썬 3.9와 3.12에서 실행된다.
| 게이트 | 무엇을 막는가 |
|---|---|
gate_no_abs_paths.py | 커밋된 파일에 /Users/<이름> 같은 개인 홈 경로가 들어가 다른 기여자에게서 깨지는 것 |
gate_blob_size.py | 1MB 초과 파일. 데이터셋·미디어·벤더링 아카이브가 모든 클론을 부풀리는 것 |
gate_skill_size.py | SKILL.md가 40줄, 커맨드가 160줄을 넘어 커맨드/스킬 분리가 중복 산출물 두 개로 되돌아가는 것. 프론트매터의 allowed-tools에 AskUserQuestion이 들어가 확인 없이 자동 승인 실행되는 것. 그리고 커맨드의 스킬 심에 user-invocable: false가 빠져 / 메뉴에 같은 커맨드가 두 번 보이는 것(ADR-0026 D2) |
gate_forbidden_phrases.py | 스킬에 "Step 1:", "EXECUTE IMMEDIATELY" 같은 명령형 실행 단계가 들어가 스킬이 조용히 두 번째 실행 경로가 되는 것 |
gate_manifest.py | marketplace.json과 plugin.json이 디스크의 실제 파일과 어긋나는 것. 이름이 바뀐 훅 스크립트, CHANGELOG.md에 닿지 않은 버전 범프, plugin.json에 hooks.json을 중복 참조해 플러그인 로드가 실패하는 것 |
gate_readme_sync.py | README.md와 README.ko.md의 커맨드 목록이 plugin/commands/*.md와 어긋나, 한국어 문서를 읽는 사용자가 다른 그림을 보게 되는 것 |
gate_command_invocations.py | 커맨드·정책 파일이나 스펙 템플릿에 사용자 프로젝트 디렉터리에서 실행되지 않는 호출 형식이 들어가는 것. 모듈 실행 형식, 플러그인 루트로 cd하는 형식, 존재하지 않는 서브커맨드 이름 |
테스트 규약
cd plugin && python3 -m unittest discover -s tests -v네트워크 없이, 외부 바이너리 없이 통과해야 한다. claude나 codex 바이너리가 필요한 테스트는 임시 디렉터리에 가짜 실행 파일을 만들어 PATH 앞에 붙인다.
모든 게이트는 최소 3개의 테스트를 갖는다. 허용, 거부·차단, 그리고 내부 오류에서도 exit 0이다. 세 번째 항목이 테스트로 존재하는 이유는 그것이 hookio의 핵심 보증이기 때문이다.