AI

매일 밤 돌리자 구현이 아니라 운영이 깨졌다

권한 우회의 대가, base 브랜치를 안 갈라 날린 482줄, 측정되지 않는 지표가 사람 검문을 없애라고 권한 일

한 줄로

루프를 하룻밤 돌리는 것과 매일 밤 돌리는 것은 다른 일이다. 코드보다 운영이 문제가 된다. 권한 우회의 대가를 무엇이 받치는지, 작업 트리만 갈라서 482줄을 날린 일, 그리고 측정할 데이터가 없는 지표가 매일 아침 “사람 검문을 없애도 된다”고 권했던 일을 적는다.

멈춤 조건과 한도는 이미 쓴 글이 있다

사용량 한도, 머지 잠금, 멈춤 조건 다섯 가지는 밤새 도는 에이전트 루프는 멈추는 법이 절반이다에서 다뤘다. 겹치는 설명은 그 글로 넘긴다.

이 글에는 그 글에 없는 것만 적는다 — 권한 우회의 대가, base 브랜치 분리, 사람 개입의 정규 경로, 지표 사고.

권한: 승인해 줄 사람이 없다

무인 세션은 권한을 승인받을 길이 없다. 아무것도 안 주면 첫 도구 호출에서 승인 대기에 걸려 사이클 시간을 통째로 태운다.

선택 대가
허용 목록으로 좁힌다 안전하다. 대신 빠진 도구 하나가 매 사이클을 조용히 멈춘다
권한 검사를 우회한다 확실히 돈다. 대신 세션이 작업 트리 안에서 사실상 무엇이든 한다

우리는 우회를 골랐다. 그 대가는 전용 작업 트리, 파일 범위, 검문, 그리고 3편의 “상태와 판정 코드 대조” 가 받친다.

어느 쪽이든 정답은 없고, 팀이 보안 판단으로 고를 일이다.

병렬: 작업 트리만 갈라서는 부족하다

루프 셋을 동시에 띄우면서 작업 트리는 셋으로 갈랐지만 base 브랜치는 하나였다.

세 루프가 한 브랜치에 커밋을 쌓았고, 한 루프의 커밋이 다른 루프의 산출물 482줄을 지웠다.

루프마다 작업 트리와 base 브랜치를 둘 다 따로 판다. 작업 트리만 가르면 충돌이 파일 수준에서는 안 보이고 브랜치 수준에서만 터진다.

한 루프 안에서 태스크를 병렬로 돌릴 때는 태스크마다 작업 트리를 따로 두고, 머지는 잠금을 잡고 하나씩 한다.

잠금에는 “이만큼 오래됐으면 죽은 것”이라는 나이 기준을 두고, 대기 시간은 그 나이보다 길게 잡는다. 반대로 잡으면 정당하게 회수할 수 있는 잠금 앞에서 먼저 포기한다.

사람의 개입도 정규 경로로

막힌 태스크를 사람이 급한 마음에 base 브랜치에 직접 고쳐 넣었다. 그러자 그 태스크는 다음 사이클에서 “바뀐 것 없음”이 되어 정규 경로로는 영영 끝낼 수 없었다.

  • 사람은 원인을 고친다 — 테스트, 계약, 픽스처, 공유 파일. 그다음 보류를 풀고 구현은 루프가 다시 하게 한다
  • 보류를 푸는 명령을 처음부터 둔다. 없으면 사람이 상태 파일을 손으로 고치게 되고, 그것이 또 다른 사고가 된다
  • 루프를 멈춘 뒤에 개입한다. 드라이버를 멈추면 돌던 세션도 함께 멈춰야 한다. 그렇지 않으면 남은 세션이 계속 커밋한다

멈추는 이유는 하나 더 있다. 다음 태스크를 잡는 명령이 작업 트리를 깨끗이 되돌리므로, 사람이 고치던 미커밋 변경이 지워진다.

구현을 꼭 손으로 끝내야 한다면 상태를 done 으로 바꾸고 그 사실을 커밋 메시지에 남긴다. 정규 경로를 거친 것처럼 보이게 두지 않는다.

사람이 고친 것도 실제로 돌려서 확인한다

한 번은 사람이 보류를 풀며 경로 계산을 머릿속으로 하고 “확인 완료”라 적었는데, 틀려서 사이클 하나를 잃었다.

정본 문서를 다시 열지 않고 수정 방향을 정했다가, 그 수정을 적용한 뒤 형제 테스트 9개가 깨져 판정을 다시 내린 일도 있었다.

돌려 볼 곳은 루프의 것이 아닌 임시 작업 트리다. 운영 루프는 실패한 시도의 코드를 별도 브랜치로 남겨 두므로, 그것을 꺼내 돌려 볼 수도 있다.

근거 없는 숫자는 근거 없다고 적는다

“사람 검문을 풀어도 되나”를 재는 지표가 있었다. 그런데 그 지표가 세려던 사건을 기록하는 곳이 코드 어디에도 없어서 값이 언제나 0% 였다.

리포트는 매일 아침 “해제 조건 충족”을 찍었다. 0 일 수밖에 없는 숫자로 유일한 사람 검문을 없애라고 권한 셈이다.

분모 쪽 사고도 있었다. 게이트 실패율을 재는데 통과 이벤트를 남기는 곳이 없어, 실패율이 0% 아니면 100% 두 값만 나왔다. 태스크 20개 중 하나가 한 번만 실패해도 100% 였다.

그 뒤로 두 가지를 지킨다.

  • 데이터를 만드는 곳이 없는 지표는 숫자를 내지 말고 “측정되지 않는다” 고 말한다
  • 시도 횟수 3 같은 임계값은 “근거 없이 잡았다” 고 적고 일지를 보며 조정한다

다시 띄우거나 늘릴 때 나는 사고

  • 이전 인스턴스를 확인하지 않고 백그라운드로 다시 띄웠더니 같은 작업 트리에 루프가 넷 쌓였다. 부모만 죽이면 자식 세션은 고아로 남는다 → 띄우기 전에 남은 프로세스를 찾고, 셸에서 떼어 내어 띄운다
  • 환경변수 하나(base 브랜치)를 빠뜨리고 재개했더니, 잘못된 base 로 도는 루프와 올바른 루프가 동시에 돌았다
  • 비대화형 셸에서 AI CLI 가 PATH 에 없으면 루프는 idle 로 조용히 끝난다 → 멈춘 이유 한 줄로는 원인이 안 보이니 드라이버 로그를 연다
  • 도는 중에 판정 코드나 프롬프트를 고치지 않는다. 옛 프롬프트로 도는 사이클이 새 CLI 와 맞지 않아 한 사이클을 잃는다(판정 코드를 고치면 3편의 지문 비교에 걸려 아예 멈춘다)
  • 세션에 주는 셸 명령은 bash 를 가정한다. zsh 에서는 맞는 파일이 없는 글롭이 오류가 되어 && 체인이 끊긴다
  • 상태를 바꾸지 않는 단계를 병렬로 돌리면 같은 태스크가 여러 번 나간다. 구체화 단계를 넷으로 돌렸더니 넷 다 같은 태스크를 받았다 → 그 단계는 하나만 돌린다

언제 무엇을 붙이나

1편의 다섯째 원칙대로, 장치는 그 사고가 내 프로젝트에서도 날 수 있을 때 붙인다.

이런 일이 생기면 붙일 장치
테스트는 통과하는데 사람이 보기에 틀린 구현이 나온다 별도 리뷰 에이전트의 두 번째 검문 (5편)
루프가 계약에 없는 것을 기본값으로 골라 엉뚱하게 만든다 계약 필수 항목 검사 (4편)
태스크는 다 초록인데 메인 브랜치가 빨갛다 모듈 끝 전체 테스트 태스크 (5편)
배포 전 보안 점검이 필수다 보안 점검 + 처방 반영 태스크 (7편)
태스크가 많고 서로 독립이다 태스크별 작업 트리 + 머지 잠금 (이 편)
재시도가 같은 실수를 처음부터 반복한다 앞 시도의 작업 이어받기
코딩 루프가 안정됐다 기획·계획까지 루프로 (7편)

최소 뼈대가 못 하는 것

  • 재시도는 base 에서 브랜치를 새로 판다 → 앞 시도의 코드는 버리고 실패 사유 한 문단만 넘긴다
  • .gitignore 된 파일은 시도 사이에 남는다 → 손으로 만든 빌드 산출물이 테스트를 통과시킬 수 있다
  • 테스트는 태스크 브랜치에서 돈다 → 머지된 결과는 다시 돌리지 않으므로, 그사이 base 가 움직였으면 둘이 다를 수 있다
  • 상태 파일에 잠금이 없다 → 루프 하나만 돈다고 가정한다

남는 교훈

매일 밤 돌리기 시작하면 고장은 코드가 아니라 운영에서 난다.

  • 작업 트리를 갈라도 base 브랜치를 안 가르면 충돌이 브랜치 수준에서만 터진다. 둘 다 가른다
  • 사람의 급한 개입이 태스크를 정규 경로로 끝낼 수 없게 만든다. 사람은 원인을 고치고 구현은 루프에 돌려준다
  • 데이터를 만드는 곳이 없는 지표는 0% 를 낸다. 그리고 그 0% 가 사람 검문을 없애라고 권한다. 측정 경로 없는 지표는 숫자를 내지 않는다
  • 사람이 머릿속으로 한 계산도 틀린다. 보류를 풀 때도 실제로 돌려서 확인한다

남겨둔 것 — 상태 파일에 잠금이 없어서 루프 하나만 돈다고 가정하고 있다. 여러 루프가 한 상태 파일을 보게 할 계획은 없다. 대신 루프마다 상태 파일을 따로 두는 쪽을 택했다.