에이전트에게 일을 시키면 어디까지 하고 멈춰야 하는가.

안 A   고친다 → 커밋한다 → 결과를 보여준다
안 B   고친다 → 멈춘다 → 결과를 보여준다 → 커밋하라면 커밋한다

한 글자 차이 같지만 되돌리는 비용이 다르다. 안 A에서 리뷰에 걸리면 --amend나 추가 커밋이 필요하고, 무엇이 바뀌었는지 보려면 커밋 범위를 짚어야 한다. 안 B는 git diff 하나로 끝난다.

그런데 이것만이 이유였다면 “작업 브랜치에서는 amend가 싸니 커밋하고 고치면 되지 않나” 로 뒤집힌다. 실제로 뒤집히지 않는 이유는 따로 있고, 이 글은 그걸 적는다.


먼저: 여기서는 에이전트가 여럿이고 병렬로 돈다

이 프로젝트에는 Claude, Codex, Copilot이 같은 저장소에서 돈다. 하루에도 서로 다른 세션이 각자 작업을 밀어넣는다.

그래서 “지금 무엇이 어디까지 됐는가”가 사람 머릿속에만 있으면 안 된다. 세션이 끊기면 그 상태도 같이 사라지고, 다음 요청이 어긋난 전제 위에서 시작된다.

같은 사실이 이 저장소의 표준을 바꾼 게 이번이 처음이 아니다. 스키마 마이그레이션 표준이 파일 이름을 일련번호에서 타임스탬프로 바꾼 이유가 그것이었다.

마이그레이션을 만드는 주체가 사람만이 아니었다 — AI 에이전트가 병렬로 돈다.

두 번째로 같은 조건이 판단을 바꿨다. 이번엔 파일 이름이 아니라 작업 절차 쪽이다.

그리고 이 조건이 이 표준이 서는 자리다. 에이전트가 없으면 커밋 직전에 멈출 이유가 대부분 사라진다 — 자기가 무엇을 고쳤는지 사람은 이미 알고 있다. 사람만 쓰는 팀에서는 원칙 1부터 4까지가 과하다. 남는 것은 브랜치와 되돌리기 쪽인데, 그건 이 조건과 무관하게 성립한다.


원칙 1. 커밋 직전에 멈춘다

정해야 했던 건 이거였다 — 에이전트가 커밋까지 하는가, 그 앞에서 멈추는가.

안 A — 커밋하고 나서 보여준다

얻는 것은 왕복이 한 번 줄어든다는 것이다. “커밋해”라고 다시 말할 필요가 없다.

버린 이유는 리뷰에서 고칠 게 나오면 히스토리를 손봐야 한다는 것이다. --amend로 덮거나 추가 커밋을 얹게 되고, 확인 비용도 커진다. 커밋 전이면 git diff 하나면 되는데 커밋 뒤에는 범위를 짚어야 한다.

안 B — 커밋하고, 문제가 있으면 amendreset으로 고친다

작업 브랜치에서는 히스토리를 고치는 값이 싸다. 아직 아무도 안 본 커밋이니 덮어도 된다. 얻는 것은 안 A와 같으면서 히스토리 오염 걱정이 없다는 것이다.

버린 이유는 여기서 갈린다. 커밋해버리면 멈출 이유가 사라진다.

안 C — 멈추지 않고 커밋 메시지에 요약을 충분히 담는다

얻는 것은 기록이 히스토리에 남는다는 것이다. 나중에 git log로 되짚을 수 있다.

버린 이유는 읽는 시점이 이미 커밋 뒤라는 것이다. 요약이 아무리 좋아도 그건 보고가 아니라 사후 기록이다.

골랐다 — 커밋 직전에 멈춘다. 안 B와 안 C가 못 지켜주는 게 하나 있다.

대가는 왕복이 한 번 는다는 것이다. 그 대가를 무엇과 바꿨는지가 다음 절이다.


원칙 1-1. 멈추는 이유는 둘이고, 각각이 다른 안을 죽인다

이 표준이 처음 만들어질 때 적힌 이유는 하나였다 — 품질 검토. 잘못된 결과물이 히스토리에 남기 전에 잡는다는 것.

그것만으로는 안 B가 안 죽는다. 작업 브랜치에서 amend는 싸니까.

두 번째 이유가 있어야 죽는다 — 흐름 파악. 사용자가 에이전트들이 업무를 어디까지 진행했는지 알아야 하고, 그게 없으면 프로젝트가 산으로 갈 가능성이 높다.

커밋은 에이전트가 자기가 한 일을 강제로 드러내야 하는 유일한 자리다.
거기서 안 멈추면 요약을 내놓을 계기가 아예 없다.
품질 검토만 이유라면 흐름 파악까지 이유라면
안 B 커밋하고 amend 통할 수도 있다 커밋하면 멈출 계기가 사라진다
안 C 메시지에 요약 부분적으로 답한다 읽는 시점이 이미 커밋 뒤다

이유를 하나만 적어두면 6개월 뒤에 안 B가 다시 살아난다. 그래서 둘을 함께 적는다.


원칙 2. git add도 하지 않는다

멈추기로 했으면 어디서 멈출지가 남는다. 스테이징 직전이다.

# 스테이징하면 이게 비어 보인다
git diff

# 스테이징한 것까지 보려면 명령이 하나 더 필요하다
git diff --cached

작업 트리에 그대로 두면 git diff 하나로 전부 보인다. 리뷰하는 쪽이 명령 두 개를 기억해야 하는 상태를 만들지 않는다.

대신 새로 만든 파일은 git diff에 안 잡힌다. 추적되지 않기 때문이다. 그래서 멈출 때 새로 만든 파일은 새로 만들었다고 따로 표시한다. 이걸 빠뜨리면 리뷰하는 쪽은 있는 줄도 모르는 파일을 그대로 커밋하게 된다.


원칙 3. 멈출 때 무엇을 내놓는지 고정한다

"커밋해도 되겠습니까"만 묻고 넘어가면 멈춘 값어치가 없다. 멈춤은 승인을 받는 자리가 아니라 상태를 드러내는 자리다.

내놓는 것을 목록으로 고정한다.

  • 판 브랜치 이름과 딴 자리
  • 바꾼 파일 목록 — 새로 만든 파일은 그렇다고 표시
  • 파일마다 무엇을 왜 바꿨는지 한 줄
  • 확인용 명령 (git status, git diff)
  • 커밋 메시지 초안
  • 작업 중에 임의로 판단한 것과 확신이 없는 것

마지막 항목이 핵심이다. 요청에 안 적혀 있어서 에이전트가 스스로 정한 것들이 여기서 드러난다. 안 적으면 그 판단은 코드에만 남고 사람은 모른 채 지나간다.


원칙 4. 커밋·푸시·PR을 각각 따로 요청받는다

“커밋해”를 “커밋하고 푸시하고 PR 열어”로 읽지 않는다.

이유는 원칙 1과 같다. 단계마다 멈추는 자리가 있어야 흐름이 보인다. 한 번에 끝까지 가면 중간 상태를 볼 지점이 사라진다.

그리고 되돌리는 비용이 단계마다 다르다.

커밋만 했다      로컬에서 얼마든지 고친다
푸시까지 했다     다른 세션이 이미 당겨갔을 수 있다
PR 까지 열었다    닫는 흔적이 남는다

뒤로 갈수록 비싸지는데 앞에서 한꺼번에 승인할 이유가 없다.

열린 PR도 에이전트가 스스로 머지하지 않는다. 머지 여부는 사람이 정한다.

다만 이건 손을 누가 움직이나가 아니라 누가 정하나의 규칙이다. 사용자가 「머지하자」고 하면 에이전트가 머지한다 — 커밋도 푸시도 그렇게 한다. 금지하는 것은 묻지 않고 머지하는 것이다.


원칙 4-1. 이슈 전환은 PR 요청에 포함된다

원칙 4가 커밋·푸시·PR을 갈라놨다. 이슈를 완료로 바꾸는 것은 거기 끼지 않는다.

한 세션에서 같은 모양이 두 번 났다.

커밋 요청 → 「PR도 해줘」 → PR 생성 → 「방금 완료한 것도 지라에서 완료로 변경해줘」
커밋 요청 → 「PR 요청 해줘」 → PR 생성 → 「어 완료로 넘겨줘」

원칙 4가 단계를 가른 근거는 되돌리는 비용이 단계마다 다르다였다. 이슈 상태는 그 축에 없다 — 되돌리는 데 클릭 하나면 된다. 가를 축이 없는데 요청만 둘로 갈려 있었다.

정해야 했던 건 시점이다.

안 A — PR을 열기 직전에 바꾼다. 얻는 것은 요청 하나로 끝나는 것이다. 걸리는 것은 머지 전에 완료가 되는 것이다 — 리뷰에서 반려되면 상태가 사실과 어긋난 채 남는다.

안 B — PR이 머지되면 바꾼다. 얻는 것은 상태가 항상 사실과 맞는 것이다. 버린 이유는 그 시점에 에이전트가 없다는 것이다. 원칙 4가 「열린 PR을 머지하지 않는다」를, 에이전트 운영 표준이 「PR 머지를 기다리지 않는다」를 이미 정했다. 기다리게 하려면 그 둘을 먼저 뒤집어야 하고, 안 기다리면 전환은 사람이 나중에 손으로 하게 된다. 그게 지금 문제였던 그 모양이다.

골랐다 — 안 A. 안 B가 지키려던 정확성은 에이전트가 지킬 수 있는 종류가 아니다. 대신 머지 전에 완료가 되는 것을 감수한다. 이 어긋남을 없애려면 표준이 아니라 트래커 자동화가 받아야 한다(원칙 4-2).

순서는 닫고 나서 연다. 반대로 하면 「PR은 열렸는데 이슈는 그대로」가 되는데 그게 실제로 두 번 일어난 모양이다. 닫고 열면 반대쪽 어긋남(이슈는 닫혔는데 PR이 없다)이 생기지만 그건 눈에 띈다 — 완료된 이슈에 PR 번호가 없다. 조용한 어긋남과 눈에 띄는 어긋남 중 뒤를 고른다.

그리고 그 PR이 이슈의 마지막 작업일 때만이다. 에이전트 운영 표준은 한 이슈를 기획 → 개발 → 테스트로 도는데 개발 PR은 이슈의 중간이다. 거기서 닫으면 테스트가 닫힌 이슈에 붙는다. 그래서 기준을 「PR을 연다」가 아니라 「그 이슈에 남은 작업이 없다」 로 적는다. 원칙 7-1과 같은 수술이다 — 명령이 아니라 의도 높이로 적는다.


원칙 4-2. 트래커가 자동으로 전환하면 에이전트는 손대지 않는다

원칙 4-1은 연동이 없을 때의 규칙이다. 있으면 에이전트가 할 일이 없다.

GitHub Issues   PR 본문의 Closes #142 가 머지 때 닫는다
Jira            아무 일도 안 일어난다 — 연동이 기본값이 아니다

같은 「이슈」인데 한쪽은 이미 자동이고 한쪽은 아니다. 규칙을 「에이전트가 닫는다」로만 적으면 GitHub 쪽에서 에이전트와 Closes #142가 같은 일을 두 번 한다.

Jira도 자동으로 만들 수 있다. Jira Cloud에 Atlassian 공식 앱 GitHub for Jira를 붙이고 자동화 규칙을 하나 두면 된다.

트리거   Pull request merged
액션     Transition issue → 완료

이 표준의 브랜치 규칙이 그대로 열쇠가 된다. 이름을 feat/PROJ-123-order-cancel로 강제하고 있으므로 이슈 키를 따로 심을 필요가 없다. 다만 이슈의 현재 상태에서 완료로 가는 전환이 워크플로에 없으면 조용히 아무 일도 안 일어난다. 화면에 오류도 안 뜬다.

그래서 규칙을 「연동이 없으면」으로 시작한다. 「Jira면 에이전트가 닫는다」로 적으면 자동화를 붙인 날 표준이 낡는다. 조건을 트래커 이름이 아니라 사실로 적으면 사실이 바뀔 때 규칙이 따라간다.

연동 여부는 프로젝트마다 다르므로 표준이 값을 들고 있지 않는다. 프로젝트가 적어둔 곳에서 읽고, 없으면 묻는다.

적어둘 파일을 이 표준이 지목하지는 않는다. 처음엔 .coding-standards/project.yml이라고 썼는데, 그 파일은 에이전트 운영 표준이 소유한다. 운영 표준을 설치하지 않은 프로젝트에서는 가리킬 곳 없는 죽은 참조가 된다. 원칙 7-1에서 버린 안 A가 정확히 그 모양이었다 — 표준 둘이 서로를 참조하기 시작하면 한쪽을 안 깐 프로젝트에서 규칙이 끊긴다. 여기서도 같은 수술을 한다. 파일 이름 대신 「확인하고 나서 움직인다」로 적으면, 운영 표준이 켜진 프로젝트는 자연히 project.yml이 그 자리가 되고 아닌 프로젝트는 사람에게 묻는 자리가 된다.


원칙 4-3. 이슈를 언제 닫을지는 「누가 머지했나」가 정한다

원칙 4-1이 안 B(「PR이 머지되면 바꾼다」)를 버린 이유는 하나였다.

버린 이유는 그 시점에 에이전트가 없다는 것이다.

그 전제가 무너졌다. 멀티 에이전트 위상 표준이 마스터를 통합 브랜치에 상주시켰다. 마스터가 리뷰하고 머지하면 그 시점에 에이전트가 있다.

2026-08-05   커밋 대기 표준 원칙 4-1   마스터가 없던 시절의 판단
2026-08-26   멀티 에이전트 위상 표준   마스터가 통합 브랜치에 상주

실제로 마스터가 PR 넷을 리뷰·머지하고 그 뒤에 이슈를 닫아 봤다. 어긋남이 한 건도 안 생겼다 — 완료가 항상 머지 뒤였다. 4-1이 감수하기로 했던 「머지 전에 완료가 되는 것」이 그 경로에서는 아예 일어나지 않는다.

그렇다고 규칙을 안 B로 갈아엎지는 않는다. 이 표준은 마스터가 없는 프로젝트에도 깔린다. 거기서는 4-1의 근거가 그대로 살아 있다.

안 — 프로젝트 설정에서 「마스터가 있는가」를 읽는다. 정확하다. 버린 이유는 그 자리가 아직 없다는 것이다. 열린 항목 33번이 「프로젝트마다 다른 값을 적는 공통 자리가 없다」로 그것을 이미 열어두었다. 여기서 지목하면 그 안건을 통째로 끌고 들어온다.

골랐다 — 「누가 머지했나」로 가른다.

에이전트가 머지했다   그 자리에 있으므로 머지 직후에 닫는다
사람이 머지한다       그 자리에 없으므로 PR 을 열기 전에 닫는다 (원칙 4-1 그대로)

설정을 안 읽는 이유는 에이전트가 자기가 방금 머지했는지 알기 때문이다. 판정할 것이 자기 행동이라 어디서도 조회하지 않고, 그래서 33번을 안 건드린다.

대가는 규칙이 하나에서 둘이 된 것이다. 그리고 사람이 머지하는 쪽은 4-1이 감수한 어긋남을 그대로 안는다 — 없앤 것이 아니라 한쪽에서만 없앴다. 그쪽을 마저 없애려면 4-2의 트래커 자동화가 받아야 한다.


원칙 5. 문서만으로는 안 지켜져서 세 층으로 못 박는다

규칙을 스킬에 적는 것만으로는 안 됐다. 스킬은 description이 걸릴 때만 로드된다. 로드가 안 되면 에이전트는 그냥 커밋한다.

3단 구조의 판단 그대로 층을 나눴다.

무엇을 담나 언제 들어오나
1단 CLAUDE.md 세 줄로 못 박은 규칙 판단 없이 매 요청에
2단 훅 통합 브랜치 커밋 차단 git commit·git push 직전
3단 스킬 절차 전문 필요할 때

1단에 올리는 것은 문서에 적는 것과 다르다. 스킬은 모델이 부를지 판단하지만 1단은 판단이 없다. 이 표준은 “말 안 하면 기본값으로 어기는” 규칙의 전형이라 1단 자리가 맞다.

그 위에 훅을 얹은 이유는 1단도 읽는 것과 실행하는 것이 다르기 때문이다. 읽고도 커밋할 수 있다.


원칙 6. 훅은 통합 브랜치만 막고 작업 브랜치는 막지 않는다

훅을 어디까지 막게 할지가 갈림길이었다.

안 — 모든 커밋을 막는다. 그러면 “커밋해”라고 해도 안 된다. 뚫리지 않는 훅은 규칙이 아니라 고장이다.

골랐다 — 통합 브랜치 위의 git commit·git push와, 통합 브랜치를 대상으로 지목한 git push만 막는다.

main 위에서 git commit                    차단
main 위에서 git push                      차단
작업 브랜치에서 git push origin HEAD:main  차단
작업 브랜치에서 git commit                 통과
main 위에서 git status / git diff          통과

작업 브랜치의 커밋은 문서가 맡고 훅은 안 맡는다. 거기서 멈추는 건 원칙 1의 규칙이지 기계가 강제할 일이 아니다 — “커밋해”라고 했을 때 뚫려야 하니까.

반대로 통합 브랜치 직커밋은 사람이 허락해도 잘못이라 예외를 둘 자리가 없다. 그래서 그쪽만 기계로 옮겼다.

임시 저장소에서 PowerShell과 bash 두 스크립트에 같은 입력을 넣고 종료 코드를 비교해 11개 케이스 전부 통과를 확인했다. 그 과정에서 둘이 잡혔다.

  • 한글이 깨졌다. PowerShell 5.1은 BOM 없는 .ps1을 ANSI로 읽고 출력도 CP949로 낸다
  • .shgit commit을 못 잡았다. git[[:space:]]가 공백을 먹어 뒤의 매칭이 실패했는데, git add . && git commit&& 분기로 우연히 통과해 겉보기엔 멀쩡했다

원칙 7. dev가 없는 저장소는 main에서 딴다

처음 규칙은 dev에서 딴다. dev가 없으면 어디서 딸지 사용자에게 묻는다. main에서 바로 따지 않는다” 였다.

이게 실제로 막혔다. 이 블로그 저장소에는 dev가 없는데, 그러면 어디로도 PR을 열 수 없는 상태가 된다. 묻는다고 답이 생기지도 않는다 — 통합 브랜치가 하나뿐이라 답은 이미 정해져 있다.

그래서 dev가 없으면 main에서 딴다. 물을 것이 없다.

그리고 금지 대상을 옮겼다.

before   main 으로 직접 PR 을 열지 않는다      ← dev 없으면 막다른 길
after    작업 브랜치를 통합 브랜치에
         로컬에서 직접 머지하지 않는다          ← 진짜 막아야 할 것

진짜 문제는 PR을 어디로 여느냐가 아니라 PR 없이 머지하는 쪽이었다. 실제로 로컬 main에 머지했더니 훅이 main 푸시를 막아서 올라가지도 못하고 히스토리만 갈렸다.


원칙 7-1. 따는 자리와 PR 대상은 이름이 아니라 출발 내용으로 적는다

원칙 7과 같은 수술을 한 번 더 했다. 그때 금지 대상은 옮겼는데 긍정 규칙 쪽에 잔재가 남아 있었다.

걸린 자리는 에이전트 운영 표준이다. 여러 에이전트가 워크트리로 도는데, 워크트리는 .git 하나를 공유하고 한 브랜치는 한 워크트리에서만 체크아웃된다. 워커 A가 dev에 서 있으면 워커 B는 dev로 이동조차 못 한다. 그래서 에이전트마다 대기 브랜치를 두고 거기서 작업 브랜치를 판다.

그러면 이 표준의 두 문장과 정면으로 부딪힌다.

따는 자리는 dev 다           vs   dev 에서 따지 않는다
PR 대상은 딴 자리와 같다      vs   딴 자리는 대기 브랜치지만 PR 은 dev 로

부딪히는 건 문장이지 의도가 아니었다. 대기 브랜치는 작업 브랜치를 따기 직전에 origin/dev로 덮어쓰므로 내용이 dev와 같다.dev에서 딴다”로 지키려던 것 — 작업 브랜치의 출발 내용이 통합 브랜치 최신일 것 — 은 하나도 안 깨진다.

두 문장 다 명령을 규칙으로 적어둔 것이었다. dev는 지켜야 할 것이 아니라 그것을 지키는 흔한 방법이다.

before   따는 자리는 dev 다
after    작업 브랜치는 통합 브랜치의 최신 내용에서 판다

before   PR 대상은 브랜치를 딴 자리와 같다
after    PR 대상은 그 내용이 출발한 통합 브랜치다

안 A — 예외 절을 세 곳에 단다. “운영 표준을 함께 쓰면 대기 브랜치에서 딴다”를 붙인다. 얻는 것은 원래 문장의 구체성이다 — dev라고 딱 적혀 있어야 어긴 것을 한 줄로 안다. 버린 이유는 표준 둘이 서로를 참조하게 되는 것이다. 운영 표준을 설치하지 않은 프로젝트에서 그 절은 가리킬 곳이 없는 죽은 참조가 되고, 다음 예외가 나오면 또 세 곳을 고친다.

안 B — 운영 표준이 브랜치·커밋·PR 절차를 통째로 가져간다. 운영 표준이 켜진 프로젝트는 한 자리만 읽으면 되니 충돌이 원천적으로 없다. 버린 이유는 커밋 대기의 나머지(멈추는 자리, 내놓는 것, 훅, 리뷰 처리)까지 복사해야 하는 것이다. 그 순간 같은 규칙이 두 벌이 되는데, 이 저장소는 「항상 적용」 세 벌이 어긋난 채 배포된 뒤에 always-on.rb로 생성으로 바꾼 적이 있다.

골랐다 — 안 C, 의도 높이로 다시 쓴다. 안 A와 안 B는 둘 다 “표준이 둘이니 어떻게든 이어 붙이자”인데, 안 C는 애초에 갈라지지 않게 쓴다. 예외를 하나도 안 달고 둘 다 성립하며, 다음에 또 다른 환경이 나와도 규칙이 그대로 받는다.

대신 판정이 물러진다.dev에서 땄나”는 한 줄로 확인되지만 “통합 브랜치 최신 내용인가”는 그렇지 않다. 그래서 맞춘 직후에만 딴다를 함께 못 박았다 — 이게 없으면 몇 주 묵은 대기 브랜치에서 따고도 규칙을 지켰다고 말할 수 있다.


원칙 8. 브랜치는 「내가 아닌 누군가가 본다」가 있을 때만 판다

이 원칙은 적용 범위를 가른다.

멈추는 것과 브랜치를 파는 것은 다른 이유로 있는 장치다.

멈춘다      품질 검토 + 흐름 파악      ← 혼자여도 필요하다
브랜치·PR   내가 아닌 누군가가 본다     ← 그 누군가가 없으면 빈 절차다

이 블로그 저장소는 혼자 쓰는 기록 공간이다. 실제로 이 표준대로 브랜치를 파고 PR을 열어봤더니, 내가 열고 내가 머지하는 왕복만 남고 얻은 것이 하나도 없었다.

그래서 이 저장소는 멈추는 것만 지키고 브랜치는 안 판다. main에서 바로 고치고 바로 푸시한다. 배포본(coding-standards/)은 그대로 브랜치·PR을 요구한다 — 여럿이 쓰는 프로젝트를 전제하기 때문이다.

main 커밋을 막는 훅도 배포본에만 걸어둔다.

대가는 다른 세션과 부딪히는 것이다. 브랜치가 없으면 충돌을 막을 게 없으므로 푸시 전에 git pull --ff-only로 당기는 것을 규칙에 넣었다.

둘은 같아야 하는 규칙이 아니라 전제가 다른 규칙이다.


원칙 9. 되돌리는 명령이 겨냥한 것은 명령이 아니라 아직 사람이 안 본 변경이다

이 규칙은 원래 한 줄이었다.

git reset --hard, git checkout ., git clean, force push 를
확인 없이 실행하지 않는다

그리고 이 글 어디에도 근거가 없었다. 원칙 1부터 8까지, 판단 기준 정리, 「정하기까지」 — 전부 찾아봐도 없다. 규칙만 SKILL.md에 있고 왜가 없는 항목이었다. 이 저장소가 막으려는 바로 그 상태를 이 표준 자신이 갖고 있었다.

드러난 계기는 운영 표준이다. 에이전트가 일감을 받을 때마다 git reset --hard origin/dev로 대기 브랜치를 맞춘다. 한쪽은 확인 없이 치지 말라 하고 다른 쪽은 매번 치라 한다.

근거 문장은 없었지만 자리와 짝은 남아 있었다. 그 줄을 처음 넣은 커밋(5908d12)에서 「하지 않는 것」의 이웃이 이랬다.

리뷰 전에 만든 커밋을 --amend 로 덮지 않는다
reset --hard · checkout . · git clean · force push 를 확인 없이 실행하지 않는다
--no-verify 로 커밋 훅을 건너뛰지 않는다

셋이 전부 리뷰 게이트를 무력화하는 경로다. --amend는 사람이 본 것을 덮고, --no-verify는 훅을 건너뛰고, 되돌리는 명령은 리뷰 대상 자체를 지운다. 같은 파일 위쪽에 짝도 있다 — 「되돌리라고 하면 무엇을 버리는지 파일 단위로 확인받은 뒤에 되돌린다」. 이 금지는 그 줄의 부정형이었다.

그래서 겨냥한 것은 명령이 아니라 「아직 사람이 안 본 변경」이다. 이걸 세우고 나면 대기 브랜치 덮어쓰기는 처음부터 사정권 밖이다 — 담는 것이 없고 트리도 깨끗하니 안 본 변경이 아예 없다.

다만 이것으로 끝나지 않는다. 무엇이 「안 본 변경」인지를 누가 판정하느냐가 남는데, 그건 이 줄에도 이웃에도 안 적혀 있다. 다음 절이 그걸 정한다.


원칙 9-1. 「안 본 변경인가」의 판정을 에이전트에게 넘기지 않는다

원칙 9가 겨냥할 대상을 정했으니 남은 것은 판정 주체다. 후보는 둘이었다.

안 A — 에이전트가 그때그때 판정한다. “이건 안 본 변경이 아니다” 싶으면 친다. 규칙이 한 줄로 끝나고 앞의 두 수술과 모양도 같아진다.

버린 이유는 판정이 틀릴 수 있고 틀리면 복구가 안 되는 것이다. 빌드를 돌려 추적 안 되는 파일이 쌓였을 때 에이전트는 “다 산출물이니 git clean -fd“라고 판단한다. 그런데 추적 안 되는 파일이 바로 리뷰 대상일 수 있다 — 원칙 2가 “새로 만든 파일은 새로 만들었다고 표시한다, 추적되지 않아 git diff에 안 잡힌다”고 적어둔 것이 정확히 그 얘기다. 사용자가 손으로 둔 파일이 섞여 있었으면 git이 되살리지 못한다. 커밋된 적이 없어서다.

안 B — 명령 자체를 에이전트에게서 떼어놓는다. 상황과 무관하게 항상 사람을 거치니 위 사고가 원천적으로 안 난다. 버린 이유는 운영 표준이 줄이려던 사람 개입을 티켓마다 하나씩 되돌리는 것이다. 처리량 상한이 사람이 앉아 있는 시간인 구조에서 그 한 번이 그대로 비용이다.

골랐다 — 안 C, 자리로 가른다. 안 A가 못 지켜주는 것은 판정의 정확도이고 안 B가 못 지켜주는 것은 에이전트가 혼자 도는 구간이다. 안 C는 판정을 매번 시키지 않고 표준이 미리 하나만 열어둔다.

허용   담는 것이 없다고 표준이 선언한 브랜치를
       통합 브랜치 최신으로 맞출 때 + 작업 트리가 깨끗할 때
금지   그 외 전부 — 무엇을 버리는지 파일 단위로 확인받는다
금지   force push 는 자리와 무관하게 항상

에이전트는 무엇이 안 본 변경인지 생각하지 않는다. 조건 둘에 맞는지만 본다. 빌드 찌꺼기 상황은 이 자리가 아니므로 그대로 물어본다.

이건 원칙 6이 이미 그은 선이다. 거기서 “통합 브랜치 직커밋은 사람이 허락해도 잘못이라 예외를 둘 자리가 없다”며 그쪽만 기계로 옮기고 나머지는 문서에 남겼다. 판정이 명확한 것만 자동으로 넘긴다 — 같은 기준이다.

대가는 둘이다. 규칙이 한 줄에서 네 줄로 늘었고, 「담는 것이 없다」는 선언을 누가 하느냐가 새로 필요해졌다. 지금은 운영 표준이 대기 브랜치에 대해 한다. 그 선언 없이 이 예외를 자기에게 적용하는 브랜치가 생기면 규칙이 뚫린다.


원칙 9-2. force push만 자리와 무관하게 항상 확인받는다

원칙 9-1의 예외가 앞의 셋에는 열리는데 force push에는 안 열린다. 네 명령을 한 줄에 묶어뒀던 것이 여기서 갈린다.

reset --hard · checkout . · git clean   내 컴퓨터 안의 것만 지운다
force push                              이미 올라간 것을 지운다

force push가 지우는 것은 남들이 이미 받아간 히스토리줄에 붙은 PR 리뷰 코멘트다. 그래서 원칙 9의 조건을 적용할 수가 없다 — 남의 클론은 보이지 않으므로 「잃을 것이 없다」를 확인할 방법 자체가 없다.

확인할 수 없는 것은 항상 묻는 쪽이 맞다. 원칙 9-1이 예외를 연 근거가 “조건 둘을 보면 안전이 확정된다”였는데, 여기는 확정할 수 있는 조건 자체가 없다.

대가는 리뷰 반영분을 정리하는 흔한 작업이 매번 사람을 거치는 것이다. 작업 브랜치는 워커마다 달라 실제로 남이 안 받아간 경우가 대부분인데도 그렇다. 그걸 감수하는 이유는 “대부분”을 에이전트가 판정하게 두는 순간 원칙 9-1의 안 A로 되돌아가기 때문이다.


판단 기준 정리

질문
어디까지 하고 멈추나 커밋 직전 커밋하면 보고할 계기가 사라진다
amend로 고치면 안 되나 품질은 되고 흐름이 안 된다 이유가 둘이라 안 하나로는 못 죽인다
git add는 하나 안 한다 git diff 하나로 다 보여야 한다
커밋·푸시·PR을 묶어도 되나 각각 따로 되돌리는 값이 단계마다 다르다
이슈 닫기도 따로 요청받나 PR 요청에 포함된다 되돌리는 값이 없어 가를 축이 없다
이슈를 언제 닫나 누가 머지했나로 가른다 에이전트가 머지했으면 직후, 사람이 머지하면 PR 전
왜 설정을 안 읽나 자기가 방금 머지했는지는 안다 조회할 자리가 없다(열린 항목 33)
어느 PR에서 닫나 그 이슈의 마지막 작업일 때만 개발 PR은 이슈의 중간일 수 있다
트래커가 자동으로 닫으면 손대지 않는다 같은 일을 두 번 하고 표준이 낡는다
훅이 어디까지 막나 통합 브랜치만 뚫리지 않는 훅은 고장이다
dev가 없으면 main에서 딴다 통합 브랜치가 하나뿐이면 물을 게 없다
따는 자리를 무엇으로 적나 이름이 아니라 출발 내용 dev는 지킬 것이 아니라 지키는 방법이다
dev를 체크아웃 못 하면 dev와 같게 맞춘 브랜치에서 딴다 내용이 같으면 의도가 안 깨진다
되돌리기 금지가 겨냥한 것 아직 사람이 안 본 변경 리뷰 게이트를 무력화하는 세 경로 중 하나였다
되돌리는 명령을 언제 치나 표준이 열어둔 자리에서만 판정을 맡기면 추적 안 되는 리뷰 대상을 지운다
force push는 예외 없이 확인받는다 남의 클론이 안 보여 확인이 불가능하다
브랜치를 꼭 파나 볼 사람이 있을 때만 혼자면 PR이 빈 절차가 된다

이 표준을 정하기까지

이 글은 표준보다 늦게 쓰였다. 규칙과 훅이 먼저 만들어졌고 글이 없었다 — 이 저장소 규칙(표준을 만들면 다섯 파일을 같은 커밋에)을 어긴 상태로 배포까지 나갔다. 그래서 「정하기까지」가 두 세션에 걸쳐 있다.

처음 이유는 하나였다. 커밋한 뒤에 보여주면 리뷰에서 고칠 게 나왔을 때 --amend나 추가 커밋이 필요하고 확인 비용도 커진다는 것. 커밋 전이면 git diff 하나로 끝난다.

스킬만으로는 안 지켜진다는 걸 만들면서 알았다. 스킬은 description이 걸릴 때만 로드되고, 로드가 안 되면 에이전트는 그냥 커밋한다. 그래서 1단 세 줄과 훅이 얹혔다. 훅을 어디까지 막게 할지에서 선이 그어졌다 — 작업 브랜치 커밋까지 막으면 “커밋해”가 안 되니, 통합 브랜치만 막고 나머지는 문서가 맡는다.

훅을 검증하다 두 가지가 잡혔다. PowerShell 5.1의 BOM·CP949 문제와, bash 정규식이 git commit을 못 잡는데 && 분기 덕에 겉보기엔 통과하던 것. 두 번째는 테스트가 없었으면 못 봤을 종류다.

그리고 이 표준을 실제로 쓰다가 세 가지가 드러났다. 여기서부터가 다음 세션이다.

첫째, dev가 없으면 막다른 길이었다. 이 저장소가 그렇다. “묻는다”는 규칙이 있었지만 통합 브랜치가 하나뿐이면 물을 것이 없다. main에서 딴다로 바꿨다.

둘째, 금지 대상이 틀린 자리에 있었다. 로컬 main에 머지했다가 훅이 푸시를 막아 되돌리면서 알았다. 막아야 할 것은 PR을 어디로 여느냐가 아니라 PR 없이 머지하는 쪽이었다.

셋째, 이유가 하나가 아니었다. 내가 “품질 검토가 아니라 흐름 파악이 이유”라고 좁혔더니 정정이 왔다 — 둘 다라고. 그게 중요한 이유는 각각이 다른 안을 죽이기 때문이다. 품질만으로는 “커밋하고 amend” 안이 살아남고, 흐름까지 있어야 죽는다. 이유를 하나로 좁히면 6개월 뒤에 버린 안이 되살아난다.

마지막으로 적용 범위가 갈렸다. 이 저장소에서 브랜치와 PR을 실제로 돌려보니 내가 열고 내가 머지하는 빈 절차였다. 멈추는 것과 브랜치가 다른 이유로 있는 장치라 하나만 뗄 수 있다는 걸 여기서 알았다. 혼자 쓰는 기록 공간은 멈추는 것만 지키고, 배포본은 둘 다 지킨다.

그리고 다음 표준이 이 표준을 시험했다. 여기서부터가 세 번째 세션이다. 에이전트 운영 표준을 만든 뒤 둘을 나란히 놓았더니 세 군데가 부딪혔다 — 따는 자리, PR 대상, 그리고 reset --hard.

셋을 놓고 보니 하나였다. 세 곳 모두 이 표준이 지키려는 것이 아니라 명령을 규칙으로 적어둔 자리였다. 운영 표준은 의도를 하나도 안 어겼고 자구만 어겼다. 그래서 질문이 “예외를 몇 개 달까”에서 “규칙을 어느 높이에 적을까” 로 바뀌었다. 이 전환이 없었으면 예외 절 세 개를 붙이고 끝냈을 것이다.

그건 원칙 7이 이미 한 수술이었다.main으로 PR을 열지 않는다”를 “로컬에서 직접 머지하지 않는다”로 옮긴 것. 그때 금지 대상은 옮겼는데 긍정 규칙 쪽에 잔재가 남아 있었다PR 대상은 딴 자리와 같다가 그대로 서 있었고 이번에 걸린 게 정확히 그 문장이다. 한 번 옮길 때 같은 모양을 전부 훑지 않으면 남는다.

reset --hard에서는 고칠 근거가 없다는 걸 발견했다. 글 전체를 뒤져도 그 금지의 이유가 없었다. 규칙만 있고 왜가 없는 항목을, 하필 이 저장소 규칙을 만든 표준이 스스로 갖고 있었다.

근거는 글이 아니라 히스토리에서 나왔다. 처음에는 이유를 새로 세우려 했는데, 그 줄을 넣은 커밋을 열어보니 자리와 짝이 그대로 남아 있었다. 「하지 않는 것」의 이웃 셋이 전부 리뷰 게이트를 무력화하는 경로였고, 위쪽 「되돌리라고 하면 파일 단위로 확인받는다」의 부정형이 이 금지였다. 겨냥한 것은 명령이 아니라 「아직 사람이 안 본 변경」이었다. 근거 문장이 없다고 해서 근거가 없는 것은 아니었다 — 구조에 남아 있었다.

그리고 복원해도 질문이 하나 남았다. 무엇이 안 본 변경인지를 누가 판정하느냐는 그 줄에도 이웃에도 없었다. 거기서 안 A(에이전트가 매번 판정)가 깨지는 자리가 드러났다 — 추적 안 되는 파일이 바로 리뷰 대상일 수 있다고 원칙 2가 이미 적어뒀다. 원칙 6이 그은 선 — 판정이 명확한 것만 자동으로 넘긴다 — 을 따라 자리로 갈랐다.

같은 실수가 한 번 더 났다는 것도 여기서 봤다. 이 절 첫 문단에 “규칙과 훅이 먼저 만들어졌고 글이 없었다”고 적어뒀는데, 에이전트 운영 표준이 똑같이 나갔다 — SKILL.md 두 벌과 instructions.md만 배포되고 글·인덱스·장부가 없었다. 그 글은 이 자리에서 같이 썼지만, 사람이 알아채서 메운 것이라 다음을 막지는 못한다. 적어두는 것만으로는 안 막힌다 — 무엇이 막을지는 장부에 열어뒀다.

네 번째 세션은 이슈 트래커에서 왔다. 다른 프로젝트에서 일하다 “PR을 열면 지라도 완료로 바꿔달라”는 요청이 두 번 연달아 올라왔다. 이 표준이 이슈 번호를 브랜치 이름과 Closes #142에만 쓰고 트래커 상태는 아예 모른다는 것이 그때 드러났다.

처음엔 「PR을 열면 닫는다」 한 줄이면 될 줄 알았다. 그런데 에이전트 운영 표준을 나란히 놓으니 정면으로 부딪혔다 — 거기는 「테스트까지 끝나야 이슈를 닫는다」다. 개발 PR은 이슈의 끝이 아니라 중간이었다. 원칙 7-1에서 한 수술을 그대로 한 번 더 했다. 기준을 「PR을 연다」에서 「그 이슈에 남은 작업이 없다」 로 올리니 둘 다 성립했다.

전환 시점은 안 B가 먼저 옳아 보였는데 뒤집혔다. 「머지되면 완료」가 상태와 사실을 항상 맞추니까. 그런데 그 시점에 에이전트가 없다. 원칙 4와 운영 표준이 이미 「머지를 기다리지 않는다」를 정해뒀고, 안 기다리면 전환은 사람이 나중에 손으로 하게 된다 — 그게 애초에 문제였던 그 모양이다. 정확성을 지킬 주체가 없으면 그 정확성은 규칙에 적을 수 없다.

그리고 트래커마다 이미 답이 다르다는 걸 봤다. GitHub Issues는 Closes #142가 벌써 자동으로 닫는데 Jira는 아무 일도 안 일어난다. 「에이전트가 닫는다」로만 적으면 GitHub 쪽에서 같은 일을 두 번 한다. 조건을 트래커 이름이 아니라 「연동이 있는가」라는 사실로 적었다 — Jira에도 자동화를 붙일 수 있으므로 이름으로 적으면 붙인 날 표준이 낡는다.


정리

  • 커밋 직전에 멈춘다. 커밋은 에이전트가 자기 일을 드러내야 하는 유일한 자리다
  • 이유는 둘이다 — 품질 검토와 흐름 파악. 하나로 좁히면 “커밋하고 amend” 안이 되살아난다
  • git add도 하지 않는다. git diff 하나로 전부 보여야 한다. 새로 만든 파일은 따로 표시한다
  • 커밋·푸시·PR을 각각 따로 요청받는다. 되돌리는 값이 단계마다 다르다
  • 이슈 전환은 거기 안 낀다. 되돌리는 값이 없어 가를 축이 없다. PR 요청 하나가 거기까지다
  • 닫고 나서 PR을 연다. 머지 시점엔 에이전트가 없고, 순서를 뒤집으면 조용히 안 닫힌 이슈가 남는다
  • 연동이 있으면 손대지 않는다. 조건을 트래커 이름이 아니라 사실로 적어야 자동화를 붙인 날 낡지 않는다
  • 문서만으로는 안 지켜진다. 스킬은 로드될 때만 걸리므로 1단과 훅을 얹는다
  • 훅은 통합 브랜치만 막는다. 뚫리지 않는 훅은 규칙이 아니라 고장이다
  • dev가 없으면 main에서 딴다. 금지 대상은 PR 대상이 아니라 로컬 직머지다
  • 따는 자리와 PR 대상은 이름이 아니라 출발 내용으로 적는다. dev는 지킬 것이 아니라 지키는 흔한 방법이다
  • 되돌리기 금지가 겨냥한 것은 「아직 사람이 안 본 변경」이다. 근거 문장은 없었지만 자리와 짝이 히스토리에 남아 있었다
  • 되돌리는 명령은 표준이 열어둔 자리에서만 친다. 무엇이 안 본 변경인지를 에이전트가 매번 판정하게 두지 않는다
  • force push는 예외가 없다. 남의 클론이 안 보여 「잃을 것이 없다」를 확인할 수 없다
  • 브랜치는 볼 사람이 있을 때만 판다. 멈추는 것과 다른 이유로 있는 장치라 하나만 뗄 수 있다

자신만의 철학을 만들어가는 중입니다.
최상단으로 이동했습니다!
확대 이미지

댓글남기기