한 줄로
무인 루프가 낼 수 있는 최악의 출력은 실패가 아니라 거짓 완료다. 머지되지 않았는데 done 이 찍혔고, 그 거짓말은 루프의 유일한 기억에 조용히 남는다. 이 사고가 다섯 번 났고 원인은 다섯 번 다 달랐다. 그래서 처방이 “가장 좋은 검사 하나”가 아니라 겹친 검사가 됐다.
문제: 다섯 번의 원인이 다섯 층에 흩어져 있었다
| # | 무엇이 일어났나 | 어느 층 |
|---|---|---|
| 1 | 잠금 해제 명령의 종료 코드가 머지 실패의 종료 코드를 덮었다 | 셸 |
| 2 | 프롬프트의 분기가 갈라져 머지 실패 경로를 지나쳤다 | 프롬프트 |
| 3 | 같은 ref 를 태스크 브랜치와 base 브랜치 자리에 두 번 넘겼다 | 호출 |
| 4 | 구현이 파일을 쓰고 커밋하지 않았다 | 세션 |
| 5 | 실패하는 pre-commit 훅이 git commit 을 조용한 무동작으로 만들었다 |
저장소 설정 |
같은 증상에 원인이 다섯 층으로 흩어져 있다. 공통 원인이 없다는 것이 이 사고의 성질이다.
5번이 특히 나빴다 — 진단을 엉뚱한 곳으로 돌렸다
실행 중 모든 태스크가 똑같이 막혔는데, git log -1 이 base 커밋을 보여 주어 커밋이 된 것처럼 보였다. 진단은 엉뚱한 머지를 탓했다.
훅이 실패하면 git commit 은 아무것도 하지 않고 조용히 끝난다. 그러면 git log -1 은 직전 커밋, 즉 base 커밋을 보여준다. 커밋이 생긴 것과 구분되지 않는다.
그래서 훅 정책을 정했다. 먼저 평범하게 커밋하고, 훅에 막히면 우회 옵션(--no-verify)으로 다시 커밋한 뒤 우회했다는 사실을 일지에 남긴다.
다만 이 우회 비율은 실제보다 낮게 나온다. 기록하지 않은 우회는 안 잡히고, 커밋도 못 하고 보류된 시도는 분모에만 섞인다. 낮게 나와도 안심하지 않는다.
“가장 좋은 검사 하나”가 아니라 겹친 검사
다섯 번 다 원인이 달랐다는 것은, 다음 여섯 번째도 다를 거라는 뜻이다. 그래서 처방은 무언가 하나를 완벽하게 만드는 것이 아니라 서로 독립인 검사를 겹치는 것이었다.
- 게이트 1 이 미커밋 변경을 거부한다 → 4번을 발생 지점에서 막는다
-
complete는 세션이 게이트를 돌렸다고 말해도 다시 돈다. 머지 뒤에는 git 에 직접 묻는다 — 커밋이 있나, 두 ref 가 다른가, 실제로 머지됐나 - 머지 자체를 CLI 가 한다 → 2번이 나던 무대(프롬프트 분기)를 없앤다
1번은 머지 잠금이 없는 단일 루프라서 사라진 것이다. 6편처럼 잠금을 다시 붙이면 다시 날 수 있다.
검사의 검사 — 발동할 수 없는 방어가 있었다
겹친 방어에는 함정이 있다. 한때 “네 번째 검사”가 있었는데, 다른 검사가 이미 덮고 있어서 절대 발동할 수 없었다.
죽은 검사는 방어의 두께를 과대평가하게 만든다. 지웠다.
그 뒤로 규칙을 하나 두었다.
검사마다, 그 검사 하나만 빼면 깨지는 테스트를 둔다.
그러면 어느 검사가 실제로 일하고 있는지 언제든 확인된다.
세션이 상태 파일을 직접 고친다면
권한 우회로 도는 세션은 tasks.yaml 을 직접 done 으로 바꿀 수도, 판정 코드를 고칠 수도 있다.
그래서 드라이버가 사이클마다 done 태스크를 git 과 대조하고, 판정 코드의 지문을 비교해 바뀌면 멈춘다. 막는 것이 아니라 잡는 것이지만, “조용한 거짓말”을 “시끄러운 정지”로 바꾼다.
직접 만든 뼈대에도 구멍이 있었다
이 연재의 뼈대를 일반화한 키트로 다시 짜면서 실제 git 저장소 위에서 도는 테스트를 붙였다. 그다음 작성자가 아닌 별도 리뷰 에이전트에게 코드를 보였고, 리뷰어가 다섯을 찾았다.
-
보호 파일을 옮기면 보호 검사를 빠져나갔다. git 의 이름 바꿈 감지가 새 경로만 보여 주기 때문이다 → 바뀐 파일을 셀 때 이름 바꿈 감지를 끈다(
--no-renames) - 머지 직후, 상태 저장 직전에 세션이 죽으면 재시도가 “커밋이 없다”로 시도를 태우고 보류됐다 → 태스크를 다시 잡을 때 이미 머지됐는지 git 에 묻고 done 으로 복구한다
- 시도를 다 써서 보류된 태스크를 풀어도 시도 횟수가 남아 영영 선택되지 않았다 → 보류를 풀 때 시도 횟수도 되돌린다
- 세션이 상태 파일이나 판정 코드를 고칠 수 있었다 → 위 절의 대조와 지문 비교가 여기서 나왔다
- 드라이버를 멈춰도 세션이 계속 돌며 커밋했다 → 드라이버가 멈출 때 세션의 프로세스 묶음을 함께 죽인다
자기가 쓴 코드를 자기가 승인하지 않는다는 규칙이 여기서도 값을 했다.
남는 교훈
원인이 매번 다른 사고에는 “더 좋은 검사”가 답이 아니다. 서로 독립인 검사를 겹치는 것이 답이다.
- 다섯 번의 원인이 다섯 층(셸·프롬프트·호출·세션·저장소 설정)에 흩어져 있었다. 하나를 완벽하게 만들어도 나머지 네 층은 그대로다
- 겹친 방어는 두께를 과대평가하게 만든다. 그래서 검사마다 “그 검사만 빼면 깨지는 테스트”를 둔다
- 고칠 때마다 “그 고침을 되돌리면 빨갛게 되는” 테스트를 함께 둔다
지금은 거짓 완료가 의심되면 세션의 보고를 읽지 않고 git log 와 머지 여부를 직접 본다. 말이 아니라 저장소 상태를 본다.
남겨둔 것 — 우회 비율은 여전히 실제보다 낮게 나온다. 분모를 정확히 세려면 커밋 실패로 보류된 시도를 따로 세야 하는데, 아직 안 했다.
다음 편은 이 모든 검사를 통과한 결함이 어디서 왔는지다 — 태스크 계약에는 금지가 아니라 값을 쓴다.