표준 동기화 표준에서 배포 스크립트에 검사 넷을 붙였다. 밀어내기 직전에 서서, 어긋난 것이 나가지 못하게 막는 문이었다.
그 스크립트를 GitHub Actions로 바꿨다. 손으로 -Version을 주고 돌리던 일을 푸시 한 번으로 대신한다.
전 coding-standards/ 를 고친다 → 커밋 → publish-standards.ps1 -Version v1.5.0
├─ 검사 넷
└─ subtree push + 태그
후 coding-standards/ 를 고친다 → 커밋 → 푸시
└─ GitHub Actions 가 rsync + 태그 + 릴리스
편해졌다. 그런데 옮겨간 것은 「밀어내는 일」뿐이고 「검사」는 스크립트와 함께 지워졌다.
rsync는 아무것도 안 본다. 그래서 .claude/skills만 고치고 .agents/skills를 빼먹은 커밋도, 짝이 되는 글이 없는 규칙도 그대로 나간다. 실제로 자동 배포를 붙인 날 하루에 세 번(v1.12.0 → v1.14.0) 나갔고 검사는 한 번도 안 돌았다.
이 글은 그 문을 어디로 옮겼는지, 그리고 옮기다가 문 하나는 아예 필요 없게 만든 기록이다.
먼저: 이미 정해진 것
앞선 표준들이 정해둔 것 넷을 전제로 깐다. 이 글은 그 위에서만 성립한다.
- 원본은 한 곳이다.
coding-standards/를 여기서 고치고 공개 저장소로 내보낸다. 공개 저장소와 설치본은 사본이라 거기서 고치면 다음 배포·업데이트가 덮어쓴다 - 「항상 적용」 세 벌은
CLAUDE.md하나에서 생성한다. 손으로 맞추다 자바 규칙이 14 / 13 / 12줄로 갈라진 적이 있다 - 규칙과 글은 이름으로 짝을 짓는다.
ruby tools/standards-pairing.rb가 슬러그가 어긋나면 멈춘다 - 커밋은 알리고 배포는 막는다.
pre-commit훅은 경고만 찍고, 실제로 멈추는 것은 배포 쪽이다
마지막 줄이 이 글의 출발점이다. 「배포 쪽」이 스크립트에서 워크플로로 바뀌었는데, 멈추는 장치는 스크립트에 있었다.
이 표준이 서는 자리 — 넷을 떠받치는 조건이 하나 더 있다. 규칙을 읽는 것이 사람만이 아니다. 배포할 대상(「항상 적용」 세 벌과 스킬 두 벌)이 도구마다 다른 파일을 읽는다는 데서 나왔다. 사람만 읽는 팀이면 밀어낼 것이 없고 이 글도 없다.
원칙 1. 검사는 배포 직전이 아니라 워크플로 맨 앞에 둔다
정해야 했던 건 이거였다 — 검사를 어디서 돌릴 것인가.
안 A — pre-commit 훅에서만 잡는다
커밋 직전에 내 PC에서 돈다. main이 더러워지기 전에 잡으니 고치기가 제일 싸다.
얻는 것은 가장 이른 시점이다. 커밋 전이면 git diff 하나로 확인하고 고치면 끝이다.
버린 이유는 훅이 클론마다 꺼져 있다는 것이다. core.hooksPath는 로컬 설정이라 새 클론에서는 안 돈다. 이 글을 쓰는 시점에 이 PC에서도 꺼져 있었다. 즉 「검사가 있다」고 적어두고 실제로는 아무 데서도 안 돌던 상태였다. 조용히 통과하는 문은 문이 아니다.
안 B — 워크플로 안에서만 잡는다
푸시된 뒤 GitHub Actions가 검사한다. 클론 설정과 무관하게 항상 돈다.
얻는 것은 끌 수 없다는 것이다. 배포 경로에 있으므로 우회할 방법이 없다.
버린 이유는 아니고, 약점만 적는다 — 발견 시점이 늦다. main에는 이미 어긋난 커밋이 들어가 있고, 고치려면 커밋을 하나 더 쌓아야 한다.
골랐다 — 둘 다. 훅이 알리고 워크플로가 막는다. 안 A는 확실성이 없고 안 B는 시점이 늦은데, 둘의 약점이 서로 겹치지 않는다. 훅이 켜져 있으면 커밋 전에 알고, 꺼져 있어도 배포가 막는다.
이건 새 구조가 아니다. pre-commit(경고) + publish-standards.ps1(정지)이 이미 그 모양이었다. 자동 배포로 옮기면서 그 모양을 그대로 옮긴 것이다.
대신 같은 검사를 두 곳에서 부르게 되므로 목록이 갈라질 위험을 진다. 그 대가를 어떻게 갚는지가 다음 원칙이다.
원칙 2. 검사 목록은 파일 하나에 모으고 부르는 곳만 둘로 둔다
정해야 했던 건 이거였다 — 검사 목록을 어디에 적을 것인가.
안 A — 워크플로 YAML에 줄줄이 적는다
- name: Check standards
run: |
ruby tools/agents-skills.rb --check
ruby tools/always-on.rb --check
ruby tools/standards-pairing.rb
얻는 것은 한 파일만 보면 배포가 무엇을 검사하는지 다 보인다는 것이다.
버린 이유는 훅이 그걸 부를 방법이 없다는 것이다. 훅은 YAML을 실행하지 못하므로 같은 세 줄을 pre-commit에도 적게 된다. 그러면 검사를 하나 추가할 때 한쪽만 늘어난다. 이 저장소가 반복해서 당한 실패 모양이 정확히 그거다 — 자바 규칙이 세 벌로 갈라진 것도, .claude와 .agents가 어긋난 것도 전부 「같은 것을 두 곳에 적었다」였다.
골랐다 — tools/check-standards.rb 하나로 모으고 양쪽이 그걸 부른다.
tools/check-standards.rb 목록은 여기에만 있다
├─ tools/hooks/pre-commit --warn 으로 (경고)
└─ .github/workflows/publish-coding-standards 그냥 (정지)
안 A가 못 지켜주는 건 목록이 한 벌이라는 보장이다. 검사를 추가할 곳이 한 군데뿐이면 한쪽만 늘어날 수가 없다.
대신 경고와 정지를 스위치 하나로 가르게 된다. --warn이면 결과를 찍고 항상 exit 0, 없으면 exit 1이다. 이 한 줄이 「커밋은 알리고 배포는 막는다」 전체를 지탱하므로, 여길 잘못 건드리면 두 문이 동시에 열린다.
원칙 3. 어긋남은 검사로 잡지 말고 생성으로 없앤다
정해야 했던 건 이거였다 — .claude/skills와 .agents/skills 두 벌을 어떻게 같게 유지할 것인가.
Claude와 Codex는 같은 규칙을 읽어야 한다. 스킬이 들어오는 조건이 둘 다 같기 때문이다 — 모델이 이름과 설명을 보고 부를지 정한다. 그런데 손으로 둘을 맞추다 4개가 어긋난 채 배포됐다.
안 A — 검사기가 바이트를 비교한다
지금까지의 방식이다. publish-standards.ps1이 인덱스 blob 해시로 두 트리를 비교했다.
얻는 것은 나중에 Codex와 Claude의 규칙을 갈라놓을 여지다. 두 벌이 각자 파일이므로 언젠가 다르게 쓸 수 있다.
버린 이유는 검사가 잡는 시점이 늘 사후라는 것이다. 어긋난 것을 만들고 나서 알려준다. 그리고 그 여지는 실제로 한 번도 쓰지 않았다 — 쓸 일이 생기면 그때 구조를 다시 열면 된다. 안 쓰는 여지를 위해 매번 어긋날 위험을 지고 있었다.
골랐다 — .claude/skills를 원본으로 두고 .agents/skills를 생성한다.
coding-standards/.claude/skills/<slug>/SKILL.md 손으로 고치는 유일한 곳
│ ruby tools/agents-skills.rb
└─→ coding-standards/.agents/skills/<slug>/SKILL.md 생성물
안 A가 못 지켜주는 건 어긋날 수 없다는 보장이다. 원본이 하나면 검사는 「같은가」가 아니라 「최신인가」 만 보면 되고, 그건 다시 뽑으면 항상 통과한다.
「항상 적용」 세 벌과 같은 해법이고, 같은 사고에서 나왔다. 원본을 .claude 쪽으로 정한 이유도 같다 — 표준을 쓰는 도구가 Claude이므로, 글을 쓰면서 고치는 곳이 곧 원본이어야 한 번에 끝난다.
대신 두 도구의 규칙이 영원히 같아야 한다. 나중에 Codex에만 다른 문장이 필요해지면 이 구조를 다시 열어야 한다.
원칙 4. 생성물에는 표시를 박되 프런트매터 뒤에 넣는다
생성기가 있어도 생성물인 줄 모르면 손으로 고친다. 그래서 파일 머리에 표시를 박는다.
AGENTS.md는 첫 줄 아래에 그냥 넣으면 됐는데, SKILL.md는 프런트매터로 시작한다.
---
name: agent-operations-standard
description: 에이전트가 이슈 하나로 ...
---
<!-- 생성물 — coding-standards/.claude/skills 에서 tools/agents-skills.rb 가 만든다. 여기서 고치지 않는다. -->
# 에이전트 운영 표준
프런트매터 사이에 끼우면 안 된다. name과 description은 도구가 파싱하는 값이라 그 블록이 깨지면 스킬 자체가 안 불린다. 그래서 표시는 프런트매터가 닫힌 다음에 온다.
생성기가 표시를 매번 새로 끼우므로, 사람이 지워도 다음 생성에서 돌아온다.
원칙 5. 없어진 전제에 딸린 검사는 옮기지 말고 지운다
옛 검사 넷 중 하나는 옮길 자리가 없었다.
1. coding-standards/ 에 커밋 안 된 변경이 있는가 ← 이것
2. .claude/skills 와 .agents/skills 가 같은가
3. 「항상 적용」 세 벌이 최신 생성물인가
4. 규칙에 짝이 되는 글과 인덱스 항목이 있는가
1번이 있던 이유는 git subtree가 커밋만 밀어내기 때문이었다. 작업 트리에만 있는 수정은 배포에 안 실리는데 스크립트는 성공으로 끝난다. 그래서 밀어내기 전에 멈춰 세웠다.
자동 배포에는 그 전제가 없다. 워크플로는 체크아웃한 커밋만 본다. 커밋 안 된 변경은 애초에 거기까지 못 간다.
그러니 이 검사는 옮길 게 아니라 사라진 것이다. 습관적으로 CI에 git status 검사를 넣었으면 항상 통과하는 줄이 하나 남았을 것이고, 그건 지키는 게 없으면서 읽는 사람에게 「무언가 지켜지고 있다」고 믿게 만든다.
검사를 옮길 때는 그 검사가 무엇을 전제로 붙었는지 먼저 본다. 전제가 없어졌으면 검사도 없어진 것이다.
원칙 6. 뒤처진 설치본은 묻지 않고 올린다
여기서부터는 배포의 반대편이다. 밀어내는 쪽이 아니라 받는 쪽이다.
표준 동기화 표준의 원칙 6은 이렇게 적혀 있었다.
작업을 시작하기 전에
manifest.json의version과 표준 저장소의 최신 태그를 비교한다. 뒤처져 있으면 먼저 올린다.
「비교한다」의 실행을 아무도 안 잡는다. 규칙 한 줄일 뿐이라 안 하면 그만이고, 안 해도 아무 일도 안 일어난다. 자동 배포가 붙으면서 이 틈이 더 벌어졌다 — 태그가 하루에 세 번 올라가는 동안 받는 쪽은 그대로였다.
정해야 했던 건 이거였다 — 뒤처진 것을 무엇이 어떻게 알릴 것인가.
안 A — 훅이 하루 한 번 확인하고 알린다
세션이 열릴 때 git ls-remote로 최신 태그를 보고, 뒤처졌으면 한 줄 알린다. 마지막으로 본 시각을 파일에 적어 하루에 한 번만 망을 탄다.
얻는 것은 사람이 안 봐도 돌아간다는 것이다.
버린 이유는 알림은 결국 「아니오」로 쌓인다는 것이다. 지금 하던 일이 있으면 미루고, 미룬 것은 다음에도 미룬다. 뒤처진 상태로 도는 프로젝트를 만드는 건 「모르는 것」이 아니라 「알고도 안 하는 것」이다.
안 C — 표준 저장소가 프로젝트마다 update PR을 열어준다
Dependabot과 같은 모양이다. 받는 쪽은 PR을 머지하기만 하면 된다.
얻는 것은 받는 쪽에 아무 절차도 안 늘린다는 것이다.
버린 이유는 보낼 곳 목록을 어딘가에 손으로 적어야 한다는 것이다. 이 저장소가 반복해서 당한 실패가 정확히 「짝을 손으로 적는 곳」이었다. 프로젝트가 하나 늘 때 그 줄을 빼먹으면 그 프로젝트만 조용히 뒤처진다.
골랐다 — 세션 시작에 묻지 않고 올린다. 안 A는 사람의 「예」를 기다리다 실패하고 안 C는 목록을 손으로 관리하다 실패하는데, 자동 실행은 둘 다 안 거친다.
대신 작업 시작 시점에 규칙 파일이 말없이 바뀐다. 방금 읽은 규칙과 다음에 읽는 규칙이 다를 수 있다는 뜻이다. 세션 시작에만 돌게 묶어 그 창을 최소로 줄였지만, 창이 0이 되지는 않는다.
원칙 7. 업데이트는 통합 브랜치에 커밋하고 밀어서 모든 작업 브랜치가 같은 규칙 위에 서게 한다
원칙 6을 붙이자마자 걸린 게 있다. update는 파일을 복사할 뿐 커밋하지 않는다.
세션 시작 → update → 규칙 파일 수십 개가 바뀐 채로 작업 트리에 남는다
→ 작업 브랜치를 판다 → 기능을 고친다 → 커밋한다
→ 규칙 파일이 기능 커밋에 딸려 들어간다
리뷰하는 쪽은 그 PR에서 무엇이 이 기능의 변경인지 못 가른다.
정해야 했던 건 이거였다 — 바뀐 규칙 파일을 어디에 커밋할 것인가.
안 A — 작업 브랜치에서 첫 커밋으로 분리한다
작업 브랜치를 먼저 파고, 그 안에서 업데이트만 담은 커밋을 하나 만든다. 통합 브랜치를 안 건드려도 된다.
얻는 것은 통합 브랜치를 건드리지 않는다는 것이다. 기존 규칙에 예외를 안 만든다.
버린 이유는 에이전트마다 같은 커밋을 각자 만든다는 것이다. 셋이 동시에 일하면 같은 내용의 커밋이 세 개 생기고 머지할 때마다 충돌한다. 그리고 PR마다 규칙 파일 수십 개가 계속 따라붙는다.
안 B — 규칙 파일을 추적하지 않는다
.gitignore에 넣고 각자 클론에서 update로 받는다.
얻는 것은 커밋에 섞일 일 자체가 없어진다는 것이다.
버린 이유는 그 프로젝트가 어느 버전 규칙으로 짰는지가 히스토리에 안 남는다는 것이다. 6개월 뒤에 「이 코드는 왜 이 모양이지」를 물을 때 그때의 규칙을 되살릴 수 없다. 그리고 CI와 새 클론은 매번 받아야 한다.
골랐다 — 통합 브랜치에서 커밋하고 밀고, 그 위에서 작업 브랜치를 딴다. 안 A는 같은 커밋을 여러 벌 만들고 안 B는 히스토리를 잃는데, 통합 브랜치에 한 번만 담으면 그 뒤에 갈라지는 모든 브랜치가 같은 규칙 위에 선다.
푸시까지 하는 이유는 안 밀면 다음이 막히기 때문이다. 로컬 통합 브랜치가 원격보다 앞선 채로 남으면, 다음에 sync가 --ff-only라 거기서 멈춘다. 커밋과 푸시는 한 몸이다.
대신 두 규칙에 예외를 만든다 — 「통합 브랜치에서 파일을 고치지 않는다」와 「커밋·푸시는 따로 요청받는다」. 예외로 두는 근거는 담는 것이 기능이 아니라 규칙이라는 것 하나뿐이다. 이 근거가 흐려지면 예외가 번진다.
원칙 8. 자기 자신을 덮어쓰는 스크립트는 복사본으로 옮겨 붙는다
원칙 7을 구현하고 시험하다 원래 있던 버그가 나왔다.
$ sh .coding-standards/standards-update.sh
표준이 뒤처졌다. v1.0.0 -> v2.0.0
Coding standards v2.0.0 installed in ...
.coding-standards/update.sh: line 40: unexpected EOF while looking for matching `"'
update.sh는 마지막에 install.sh를 부르는데, install.sh가 관리 경로를 덮어쓰면서 실행 중인 update.sh 자신을 갈아엎는다. bash는 스크립트를 한 번에 읽지 않고 조금씩 읽으므로, 파일이 바뀌면 남은 부분을 엉뚱한 위치에서 읽고 죽는다.
설치는 이미 끝난 뒤라 결과물은 멀쩡한데 종료 코드만 1이 된다. 그래서 사람이 직접 돌릴 때는 “설치됐다는데 왜 빨간 줄이 뜨지” 하고 넘어갔고, 아무도 안 고쳤다. 부르는 쪽이 생기고 나서야 문제가 됐다 — set -e가 걸린 스크립트는 그 종료 코드를 실패로 알고 커밋 단계까지 못 갔다.
고치는 방법은 자기를 임시 파일로 복사해 거기서 다시 붙는 것이다.
if [ -z "${STANDARDS_UPDATE_REEXEC:-}" ]; then
SELF_COPY=$(mktemp "${TMPDIR:-/tmp}/standards-update.XXXXXX")
cat "$0" > "$SELF_COPY"
STANDARDS_UPDATE_REEXEC=1 sh "$SELF_COPY" "$@"
...
fi
PowerShell 쪽은 이 문제가 없다. 파일을 통째로 읽고 실행하기 때문이다. 같은 일을 하는 두 스크립트가 셸 때문에 다른 모양이 되는 자리이고, 그래서 주석으로 이유를 박아뒀다.
원칙 9. 받는 쪽 관리 디렉토리는 패키지 전용으로 못 박고, 지운 뒤 다시 깐다
install.sh는 지금까지 복사만 했다.
cp -a "$source_path/." "$destination/"
소스에 있는 파일을 덮어쓸 뿐, 소스에서 사라진 파일을 지우는 코드가 없다. 그래서 스킬 4개를 리네임했을 때 새 이름 파일은 설치본에 생겼지만 옛 이름 파일은 그대로 남았다. 프로젝트마다 규칙이 두 벌로 쌓인다.
정해야 했던 건 이거였다 — 소스에서 지워진 파일을 설치본에서 무엇이 지우는가.
안 A — 통째로 지우고 재설치한다
rm -rf "$destination"
cp -a "$source_path/." "$destination/"
얻는 것은 구현이 제일 단순하고, 소스에서 사라진 파일이 예외 없이 지워진다는 것이다.
처음엔 이걸로 버렸다. .claude/skills/에 프로젝트가 직접 만든 스킬을 넣어뒀다면 그것까지 지운다.
안 B — 매니페스트 기반 diff 삭제
설치할 때마다 “이번 버전이 실제로 심은 파일 목록”을 manifest.json에 적어두고, 다음 업데이트에서 “이전 목록에는 있었는데 이번 소스에는 없는 파일”만 지운다.
얻는 것은 프로젝트가 추가한 파일(목록에 없는 파일)은 절대 건드리지 않는다는 것이다.
버린 이유는 구현이 무겁다는 것이다. 설치마다 파일 목록을 걷어야 하고, 매니페스트가 유실되거나 프로젝트가 손으로 고치면 판단이 통째로 틀어진다.
안 C — 파일마다 소유권 마커
심는 파일 맨 위에 <!-- managed-by: coding-standards --> 같은 표시를 넣고, 그 표시가 있는 파일만 지운다.
얻는 것은 매니페스트 없이도 파일 자체가 자기 소속을 증언한다는 것이다.
버린 이유는 포맷마다 규칙이 갈린다는 것이다. SKILL.md는 주석을 넣을 수 있지만 JSON 설정 파일은 표준 주석 문법이 없어 마커를 못 넣는 파일이 생긴다.
골랐다 — 안 A. 대신 안 A가 버려졌던 이유 자체를 없앴다. managed_paths(.claude·.agents·.codex·.github/instructions)는 표준 동기화 표준에서 이미 “프로젝트가 못 고치는 곳”이라고 정해져 있었다. 프로젝트 커스텀이 필요하면 .coding-standards/project/에 둔다 — project.yml·decisions.md가 이미 서 있던, 업데이트가 절대 건드리지 않는 자리다.
안 B와 안 C는 둘 다 “패키지 파일과 프로젝트 파일이 한 디렉토리에 섞여 있다”는 전제 위에서 그 둘을 구분하는 방법이었다. 그 전제 자체가 이미 규칙 위반이었다. 관리 디렉토리에는 애초에 프로젝트 파일이 살면 안 되니, 구분할 필요 없이 통째로 지워도 된다 — 문제를 검사로 잡는 대신 설계로 없앤 것이다. 원칙 3에서 .agents/skills를 생성물로 만들어 “같은가” 검사를 없앤 것과 같은 모양이다.
for relative_path in "${managed_paths[@]}"; do
destination="$target_root/$relative_path"
...
if [[ -d "$source_path" ]]; then
rm -rf "$destination"
mkdir -p "$destination"
cp -a "$source_path/." "$destination/"
else
cp -a "$source_path" "$destination"
fi
done
mkdir -p "$managed_root/project"
대신 .claude/skills/에 직접 넣어둔 파일은 다음 업데이트에서 조용히 지워진다. 이건 새로 생긴 위험이 아니라 “여기서 못 고친다”는 기존 규칙을 이제 실제로 강제하는 것뿐이다. 그 규칙을 지키지 않고 있었다면, 지금까지는 파일이 우연히 안 지워졌을 뿐이다.
원칙 10. 프로젝트별 값은 project.yml 한 곳에 두고, 키의 주인은 그 키를 쓰는 표준이다
원칙 9가 관리 디렉토리를 「지우고 다시 깐다」로 못 박았다. 그래서 프로젝트가 적은 값은 거기 둘 수 없다. 설치기는 그것을 밖에 만든다.
if [[ ! -e "$managed_root/project.yml" ]]; then # 없을 때만
이 자리를 표준이 지목하지 못하고 있었다. 커밋 대기 표준 원칙 4-2가 「연동 여부는 프로젝트가 적어둔 곳에서 읽는다」로 끝났고, 원칙 4-3은 아예 설정을 안 읽는 길로 우회했다. 두 번 다 같은 이유였다 — 그 파일이 에이전트 운영 표준의 소유라, 지목하면 그 표준을 안 깐 프로젝트에서 죽은 참조가 된다고 봤다.
그 전제가 틀렸다. 설치기가 project.yml을 항상 만든다. 표준 하나만 깔아도 생기므로 안 깐 프로젝트가 없다. 「운영 표준의 소유」는 파일 첫 줄 주석 한 줄이었을 뿐이다.
그래서 파일은 공통 자리로 못 박는다. 남는 질문은 키를 누가 정의하는가다.
안 A — 한 표준이 파일 전체를 소유한다. 지금 모양이다. 소유가 하나라 「누구에게 물을지」가 명확하다. 버린 이유는 개념을 버린 표준이 자기 키를 못 지운다는 것이다. 커밋 대기 표준이 대기 브랜치를 폐기했는데 standby: []가 설치기 템플릿에 그대로 남았다 — 폐기한 쪽이 손댈 자리가 아니어서다. 아무도 안 보는 사이에 템플릿과 표준이 갈렸다.
골랐다 — 키마다 주인을 따로 둔다.
파일이 있다는 것 설치기가 보장한다 (이 표준)
tracker · layers · labels 에이전트 운영 표준
branches.integration 커밋 대기 표준
안 A가 못 지켜주는 것은 개념과 키가 같이 사라지는 것이다. 키의 주인이 그 개념의 주인과 같으면, 개념을 버릴 때 키도 함께 지운다.
새 키가 필요한 표준은 자기 키를 자기가 정의하고 설치기 템플릿에 더한다. 다른 표준에 부탁하지 않는다.
대가는 한 파일을 여러 표준이 나눠 가지는 것이다. 「이 키가 왜 있나」를 물으면 파일 하나가 아니라 그 키를 쓰는 표준을 찾아야 한다. 그래서 템플릿의 키마다 주석으로 주인을 적는다 — 파일만 열어도 어디를 볼지 알게 한다.
판단 기준 정리
| 질문 | 답 | 결론 |
|---|---|---|
| 검사를 어디서 돌리나 | 훅은 꺼질 수 있고 워크플로는 늦다 | 둘 다. 훅이 알리고 워크플로가 막는다 |
| 검사 목록을 어디에 적나 | 두 곳에 적으면 한쪽만 늘어난다 | check-standards.rb 하나. --warn이 경고와 정지를 가른다 |
| 두 벌을 같게 유지하는 법 | 검사는 늘 사후에 잡는다 | 원본 하나를 두고 생성한다 |
| 생성물 표시를 어디에 | 프런트매터는 도구가 파싱한다 | 프런트매터가 닫힌 다음 |
| 옛 검사를 어떻게 하나 | 전제가 없어졌으면 검사도 없어졌다 | 옮기지 말고 지운다 |
| 뒤처진 설치본을 어떻게 | 알림은 「아니오」로 쌓인다 | 세션 시작에 묻지 않고 올린다 |
| 업데이트를 어디에 커밋하나 | 작업 브랜치면 기능 커밋에 섞인다 | 통합 브랜치에 커밋하고 민다 |
| 스크립트가 자기를 덮어쓰면 | sh는 파일을 조금씩 읽는다 |
복사본으로 옮겨 붙는다 |
| 지워진 소스 파일을 설치본에서 어떻게 지우나 | 매니페스트·마커는 “패키지와 프로젝트가 섞여 있다”는 전제 위에서만 필요하다 | 관리 디렉토리를 프로젝트 전용(project/)과 분리해 통째로 지우고 재설치한다 |
| 프로젝트별 값을 어디 적나 | .coding-standards/project.yml |
설치기가 항상 만든다. 관리 경로 밖이라 안 덮인다 |
| 그 키는 누가 정의하나 | 그 키를 쓰는 표준 | 개념을 버릴 때 키도 같이 지운다 |
이 표준을 정하기까지
시작은 편의였다. publish-standards.ps1을 매번 손으로 돌리는 게 번거로워 GitHub Actions로 옮겼다. 워크플로는 rsync로 밀고 minor를 하나 올려 태그와 릴리스까지 만든다. 잘 돌았다.
문제를 발견한 건 그날 저녁이었다. 배포 이력을 보다가 하루에 세 번 나간 걸 알았고, 그제야 검사가 한 번도 안 돌았다는 게 보였다. 스크립트를 지운 게 아니라 안 부르게 된 것인데, 안 부르면 없는 것과 같다.
「검사를 워크플로에 넣자」가 첫 결론이었고, 바로 갈라졌다. 훅에도 넣을 것인가. 훅은 커밋 전이라 고치기 싸지만 core.hooksPath가 클론마다 꺼져 있다. 확인해보니 이 PC에서도 꺼져 있었다. 「검사가 있다」고 적어두고 아무 데서도 안 돌던 상태였다는 뜻이다. 그래서 둘 다 두되 역할을 갈랐다.
목록을 어디 적을지가 다음 갈림길이었다. YAML에 세 줄 적는 게 제일 간단한데, 그러면 훅에도 같은 세 줄을 적어야 한다. 같은 것을 두 곳에 적는 것 — 이 저장소가 반복해서 당한 실패가 전부 이 모양이었다. 자바 규칙 세 벌도, .claude와 .agents도. 그래서 파일 하나로 모았다.
여기서 예상 못 한 방향으로 꺾였다. 검사 목록을 옮겨 적다가 「.claude와 .agents가 같은가」를 보고 물었다 — 이걸 왜 검사로 잡고 있지? 「항상 적용」 세 벌은 이미 생성으로 풀었는데, 바로 아래층은 여전히 손으로 맞추고 검사로 잡고 있었다. 두 벌을 남겨둔 이유는 「나중에 Codex와 Claude를 갈라놓을 수 있게」였는데, 그 여지를 한 번도 쓴 적이 없었다. 안 쓰는 여지 때문에 매번 어긋날 위험을 지고 있었던 것이다.
검사 넷 중 하나는 옮길 자리가 없다는 것도 그때 알았다. 「커밋 안 된 변경이 있는가」는 git subtree가 커밋만 밀어서 붙은 검사였다. 워크플로는 체크아웃한 커밋만 보므로 그 전제가 통째로 사라졌다. 습관적으로 CI에 넣었으면 항상 통과하는 줄이 하나 남았을 것이고, 그게 더 나쁘다 — 지키는 게 없으면서 지켜진다고 믿게 만든다.
미는 쪽을 닫고 나니 받는 쪽이 그대로라는 게 보였다. 태그는 하루 세 번 올라가는데 설치본은 그대로다. 규칙에는 「비교한다」가 있었지만 그 실행을 아무도 안 잡았다. 알림으로 갈지 자동으로 갈지 놓고, 알림은 결국 「아니오」로 쌓인다는 쪽으로 정리됐다. 지금 하던 일이 있으면 미루고, 미룬 건 다음에도 미룬다.
자동으로 올리기로 하자마자 새 문제가 튀어나왔다. update는 파일을 복사할 뿐 커밋하지 않는다. 그대로 두면 규칙 파일 수십 개가 기능 커밋에 섞인다. 작업 브랜치에서 분리 커밋하는 안은 에이전트마다 같은 커밋을 만들어 충돌하고, 추적을 끊는 안은 히스토리를 잃는다. 통합 브랜치에 한 번만 담으면 그 뒤로 갈라지는 모든 브랜치가 같은 규칙 위에 선다 — 이게 남은 하나였다.
푸시까지 묶은 건 다른 표준이 강제했다. 커밋만 하고 안 밀면 로컬 통합 브랜치가 원격보다 앞서고, 다음에 sync가 --ff-only라 거기서 멈춘다. 한쪽 표준의 선택이 다른 쪽의 범위를 정해버린 자리다.
마지막은 시험하다 나온 버그였다. update.sh가 실행 중인 자기 자신을 덮어써서 죽고 있었다. 오래전부터 있었는데 아무도 안 고쳤다 — 설치는 끝난 뒤라 결과물이 멀쩡했고, 사람이 직접 돌릴 땐 빨간 줄을 무시하면 그만이었다. 부르는 쪽이 생기고 나서야 문제가 됐다. 자동화가 새 버그를 만든 게 아니라, 사람이 덮고 있던 버그를 드러낸 것이다.
받는 쪽 정리는 별도로 열려 있던 안건에서 왔다. 스킬 4개를 리네임한 뒤 열린 항목 장부에 “설치본에서 사라진 규칙 파일을 무엇이 지우는가”를 적어뒀었다. 처음 검토는 매니페스트 diff(안 B)나 소유권 마커(안 C)처럼 패키지 파일과 프로젝트 파일을 구분하는 쪽으로 흘렀다. 그런데 둘 다 “관리 디렉토리에 프로젝트 파일이 섞여 있을 수 있다”는 전제 위에 서 있었고, 그 전제가 표준 동기화 표준을 어기고 있었다. 관리 디렉토리는 이미 프로젝트가 못 고치는 곳으로 정해져 있었으니, 구분하는 도구를 만드는 대신 프로젝트 커스텀이 살 자리(project/)를 하나 못 박고 나머지는 통째로 지우면 됐다. 검사나 마커로 잡을 문제를 설계로 없앤 것이고, 원칙 3의 사고와 같은 결이었다.
정리
- 손으로 하던 것을 자동으로 바꾸면 「하는 일」만 옮겨간다. 그 앞을 지키던 검사는 같이 안 간다. 옮기지 않으면 사라진 것이다
- 검사를 옮길 때는 그 검사가 무엇을 전제로 붙었는지 먼저 본다. 전제가 없어졌으면 검사도 없어진 것이고, 습관으로 남기면 항상 통과하는 줄이 된다
- 어긋남은 검사로 잡기 전에 생성으로 없앨 수 있는지 먼저 본다. 검사는 늘 사후에 잡는다
- 두 벌을 남겨두는 이유가 「나중에 다르게 쓸 수 있어서」라면, 실제로 쓴 적이 있는지 센다. 안 쓰는 여지의 값은 매번 어긋날 위험이다
- 알림은 「아니오」로 쌓인다. 사람의 승인을 기다리는 자동화는 안 하는 것과 수렴한다
- 자동화는 새 버그를 만들기보다 사람이 덮고 있던 버그를 드러낸다. 사람은 빨간 줄을 무시할 수 있지만
set -e는 못 한다 - 파일 두 종류를 구분하는 도구를 만들기 전에, 애초에 섞이면 안 되는 건 아닌지 먼저 본다. 관리 디렉토리를 프로젝트 전용 공간과 분리하니 구분 자체가 필요 없어졌다
AI 코드 어시스턴트에 바로 적용하기
이 글은 자기 스킬을 갖지 않는다. 여기서 정한 것의 대부분은 표준 저장소 안쪽 일이라 설치본을 쓰는 프로젝트가 할 일이 아니다. 검사기도 생성기도 그 프로젝트에는 없다.
설치본 쪽이 실제로 바뀐 건 둘이다.
- 세션을 시작하면
sync다음에standards-update를 돌린다 — 배포본CLAUDE.md(1단)에 들어간다 - 사본에서 규칙을 고치지 않고 이슈로 올린다 — 표준 동기화 표준의 스킬이 이미 들고 있고, 이 글의 결정에 맞춰 고쳤다
- 프로젝트별 값은
project.yml한 곳에 두고, 키의 주인은 그 키를 쓰는 표준이다. 파일이 있다는 것만 설치기가 보장한다.
규칙 전문은 그 글에 있다. 같은 파일을 두 글이 나눠 실으면 한쪽만 낡는다 — 이 글이 내내 말한 그 실패다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기