AI

무인 루프의 최소 뼈대는 세 조각이다

드라이버 · 판정 CLI · 게이트 1 — 이 셋만으로 하룻밤을 돌릴 수 있다

한 줄로

무인 루프의 최소 뼈대는 세 조각이다. 세션을 띄우는 드라이버, 상태를 바꾸는 유일한 입구인 판정 CLI, 기계 검사인 게이트 1. 1편의 원칙을 코드로 옮긴 결과이고, 이 셋만으로 하룻밤을 돌릴 수 있다.

판정 권한이 세션 밖에 있다

세션은 구현과 커밋만 한다. 태스크를 잡는 일과 done 을 찍는 일은 양쪽 끝에서 CLI 가 맡는다.

판정 권한은 세션 밖에 있다 CLI 가 한다 next 태스크를 고르고 브랜치를 판다 complete 게이트 재검증 · 머지 git 에 물어 done 을 찍는다 세션이 한다 구현 코드를 쓰고 커밋한다 게이트 1 요청 스스로 확인할 때 통과하지 못하면 fail 시도를 하나 태운다 세 번이면 보류 block 시도를 안 태운다 사람에게 넘긴다 세션은 구현과 커밋만 한다. 태스크를 잡는 일과 done 을 찍는 일은 양쪽 끝에서 CLI 가 맡는다 게이트 1 은 구조 검사(브랜치·미커밋·보호 파일·범위)를 먼저 보고, 걸리면 테스트를 돌리지 않는다

이 배치가 1편 2번 원칙(“세션은 일하고, 판정은 프로그램이 한다”)의 전부다. 세션이 “게이트를 돌렸고 통과했다”고 말해도 complete 가 다시 돌린다.

태스크 하나의 모양

- id: t-0001
  title: 가격 계산 함수
  done_when: calc_total(items) 가 수량×단가의 합을 반환하고 빈 목록이면 0 을 반환한다.
  tests: python3 -m pytest tests/acceptance/test_total.py -q
  files_owned: [src/pricing.py]
  depends_on: []

끝난 상태(done_when)는 한 문장, 판정은 명령 하나(tests), 고쳐도 되는 파일은 목록(files_owned)으로 적는다.

인수 테스트 파일은 보호 파일로 묶어 어떤 태스크도 못 고치게 한다. 테스트를 고쳐서 통과시키는 길을 막기 위해서다.

상태 파일 둘이 유일한 기억

  • tasks.yaml — 태스크와 상태(todo · doing · done · blocked), 시도 횟수, 마지막 실패 사유
  • journal.jsonl — 일어난 일을 덧붙이기만 하는 일지

세션은 매번 새로 뜨므로 이 두 파일 밖의 기억은 없다. 재시도하는 세션은 앞 시도가 남긴 실패 사유 한 문단을 읽고 시작한다.

판정 CLI — 여섯 동사

명령 하는 일
next 다음 태스크를 고르고 전용 작업 트리에 task/<id> 브랜치를 판다
gate1 기계 검사를 돌린다 (세션이 스스로 확인할 때)
complete 게이트를 다시 돌리고, 머지하고, git 에 물어 확인한 뒤 done 을 찍는다
fail 구현이 모자랐다. 시도를 하나 태우고, 세 번이면 보류
block 구현으로 풀 수 없다. 시도를 안 태우고 사람에게 넘긴다
unblock 사람이 원인을 고친 뒤 보류를 푼다. 이것만 세션이 아니라 사람이 부른다

태스크 선택 순서에는 이유가 있다

다음 태스크는 “다른 태스크를 많이 풀어 주는 것 → 덜 실패한 것 → id 순” 으로 고른다.

실제 운영 루프에서는 선행이 막히면 그것에 기대는 후속이 전부 따라 막힌다(전염 보류). 한 실행에서 보류 기록 14건 중 10건이 이런 전염이었고, 원인 3건이 풀리자 함께 풀렸다.

많이 풀어 주는 것부터 끝내 두면, 막힘이 일어날 때 이미 그 아래가 풀려 있어 전염 반경이 작다. 대신 초반에 done 숫자가 느리게 오른다.

최소 뼈대는 후속을 보류로 바꾸지 않고 후보에서 빼 둘 뿐이라, 남은 것이 전부 그런 태스크면 멈춘다. 사람이 보류 원인을 고치고 unblock 하면 다시 돈다.

게이트 1 — 싼 검사부터

순서가 요점이다. 구조 검사를 먼저 보고, 하나라도 걸리면 테스트를 돌리지 않는다.

  • 올바른 브랜치인가
  • 미커밋 변경이 없는가
  • 커밋이 있는가
  • 보호 파일을 건드렸나
  • 범위 밖을 건드렸나

테스트를 고쳐서 초록을 만든 시도가 “테스트 통과”로 보이지 않게 하려는 것이다.

드라이버는 판단하지 않는다

드라이버는 세션을 띄우고, 시간 제한을 걸고, 멈출 조건만 본다. 멈춘 이유는 STOP_REASON 한 줄로 남긴다 — 다 끝났는지(finished), 너무 많이 막혔는지(halt), 여러 사이클 진전이 없는지(idle), 한 사이클이 너무 길었는지(cycle_timeout). 아침에 그 한 줄로 어느 종류의 정지인지를 안다. 원인까지 보려면 드라이버 로그를 연다.

“진전”은 일지에 남은 끝맺음(done · fail · block)의 수로 센다. 처음에는 상태 파일이 바뀌었는지로 셌는데, 태스크만 잡고 죽은 세션도 상태를 바꾸므로 진전으로 잡혔다.

드라이버에는 설정 기본값도 넣지 않는다. base 브랜치 기본값을 넣었더니 태스크 파일의 설정을 덮었다.

사람의 작업 트리를 주지 않는다

루프는 전용 작업 트리와 전용 브랜치에서 돈다. 루프는 브랜치를 갈아타고 잔해를 지우므로, 사람의 작업 트리를 주면 그 작업이 날아간다. 메인 브랜치 반영은 사람이 PR 로 한다.

처음 돌릴 때의 순서

각 단계는 확인이 통과해야 다음으로 간다.

  1. 태스크 셋을 손으로 쓴다 → 테스트 명령만으로 “끝났나”를 판정할 수 있는지 본다
  2. 계획 검사와 red 검사를 돌린다 → 계획에 결함이 없고, 모든 테스트가 지금 실패하는지 본다. 명령이 아예 안 도는 것과 테스트가 0개인 것은 실패로 치지 않는다
  3. 드라이런 → 세션 없이 배선만 확인한다
  4. 한 사이클만 지켜본다 → 로그에서 잡기 → 구현 → 검사 → 완료를 밟았고 base 에 머지 커밋이 생겼는지 본다
  5. 하룻밤 돌린다 → 아침에 멈춘 이유와 보류 사유를 읽는다

2번이 중요하다. 처음부터 통과하는 테스트는 아무것도 증명하지 못한다.

남는 교훈

최소 뼈대에서 가장 중요한 설계는 “무엇을 세션에게 주지 않을지”였다.

  • 판정 권한을 주지 않았다 → done 은 CLI 가 git 에 물어 찍는다
  • 테스트 파일을 주지 않았다 → 보호 파일로 묶어 시험 문제 조작을 막는다
  • 사람의 작업 트리를 주지 않았다 → 전용 작업 트리에서만 돈다
  • 드라이버에 판단을 주지 않았다 → 멈출 조건만 보고 STOP_REASON 한 줄을 남긴다

남겨둔 구멍도 있다. 재시도는 base 에서 브랜치를 새로 파므로 앞 시도의 코드를 버린다. 실패 사유 한 문단만 넘어간다. 같은 실수를 반복하는 것이 눈에 보이면 이어받기를 붙일 계획이지만, 아직 그 사고가 나지 않았다.

다음 편은 이 뼈대가 실제로 배신당한 이야기다 — 거짓 done 다섯 번.