fable-ish · 완료 검증 게이트 (Stop 차단 메커니즘)

한 줄 요약

AI가 “다 했습니다” 하고 멈추려는 순간, 고친 건 있는데 검증한 증거가 없으면 멈춤을 거부하고 “검증 명령 하나 돌리고 와”라며 다시 일하게 만드는 마지막 검문소다. 왜 배우나 — “거짓 완료 선언”을 막는 안전벨트이자, 무한 잔소리에 빠지지 않는 절제된 게이트 설계의 표준 예시다.

그림

flowchart TD
  A["UserPromptSubmit 훅<br/>작업 등급 정하고 장부 리셋"] --> B["모델 작업 + 도구 호출"]
  B --> C["PostToolUse 훅<br/>변경/검증 증거를 장부에 기록"]
  C --> B
  B --> D{"Stop 이벤트<br/>멈추려는 순간"}
  D --> E{"게이트가 나 때문에<br/>다시 불렸나?"}
  E -- 예 --> P1["통과: 무한 핑퐁 차단"]
  E -- 아니오 --> F{"말만 하고<br/>손은 안 댔나?"}
  F -- 예 --> BL1["차단: 지금 도구로 실행하라"]
  F -- 아니오 --> G[장부 펴서 모드별 판정]
  G -- 차단 --> BL2["차단 횟수 +1 저장<br/>모드별 지시문 전달"]
  G -- 통과 --> H{"두 번 막았는데<br/>아직 미검증?"}
  H -- 예 --> P2["통과 + 솔직히 보고하라 경고"]
  H -- 아니오 --> P3["통과: 빈 객체"]
  BL1 --> D
  BL2 --> D

쉽게 풀기

공항 출국 검색대를 떠올리면 쉽다. 승객(AI)이 “수속 끝, 나갑니다” 하고 나가려 할 때 검색대(게이트)가 마지막으로 한 번 더 들여다본다.

  • 장부 기록 — 일하는 동안 다른 훅들이 작은 장부(ledger)에 “파일 고쳤나?”, “테스트를 실제로 돌려 통과를 봤나?”를 적어 둔다. 게이트는 장부만 펴 볼 뿐 직접 코드를 분석하지 않는다(영수증만 모아 두는 가계부).
  • 멈출 때 검사 — 턴을 끝내려 하면 게이트가 자동으로 켜져 단 하나를 묻는다: “변경은 했는데 검증 증거가 없는가?” 그렇다면 강제로 다시 깨운다. AI는 새 지시를 받은 것처럼 느껴 자연히 검증을 실행한다.
  • 함정 회피 — 같은 잔소리로 영원히 붙잡지 않도록 최대 2번만 막고(MAX_STOP_BLOCKS), 게이트가 자기 자신 때문에 다시 호출된 경우(stop_hook_active)는 즉시 통과시켜 게이트↔모델 무한 핑퐁을 끊는다.
  • “말만 하고 손 안 댐” — “이제 구현하겠습니다”라고 선언만 하고 도구를 한 번도 안 쓴 턴은 정규식으로 잡아 다시 일하게 한다. 단 “이 방식으로 할까요?”처럼 사용자에게 결정을 넘기는 질문으로 끝났으면 멈춰도 봐준다.
flowchart LR
  U["UserPromptSubmit<br/>등급 결정·장부 리셋"] -->|채움| L["(장부 ledger)"]
  P["PostToolUse<br/>변경·검증 증거"] -->|채움| L
  L -->|읽기 전용| S["Stop 게이트<br/>판정만"]

비유 한 줄

장부 = 영수증 모음, 게이트 = 출국 검색대, MAX_STOP_BLOCKS = “두 번까지만 검사하고 보낸다”, stop_hook_active = “검색대 자기 줄에 또 선 사람”을 그냥 통과시키는 예외.

핵심 정리

게이트의 멈춤 차단 규칙은 위에서부터 순서대로 평가된다. 13번(통과 우선)이 47번(모드별 차단)보다 먼저 걸러지는 게 핵심이다.

flowchart TD
  A{"1~3 통과 우선<br/>2회초과·quick·docs_only"} -- 해당 --> PASS[통과]
  A -- 아님 --> B{"4 blocked 모드?"}
  B -- 예 --> BLK[차단]
  B -- 아니오 --> C{"5~7 변경했는데<br/>미검증?"}
  C -- 예 --> BLK
  C -- 아니오 --> PASS
순서조건결과
1이미 2번 막음 (stop_blocks >= 2)통과
2가벼운 작업 (mode == "quick")통과
3문서만 고침 (docs_only)통과
4위험 경계 작업 (mode == "blocked")항상 차단
5deep, 변경했는데 미검증차단
6deep, 변경 없고 검증기록 전무차단
7normal, 변경했는데 미검증차단
8그 외 전부통과

판정의 두 함정

  • “검증됨” 기준: verification_results 안에 success is True인 원소가 하나라도 있어야 인정(부분 통과 허용). 명령 실행만으로는 부족.
  • 우선순위: quick / docs_only / 2회 초과는 어떤 모드 규칙보다 먼저 평가되어 통과한다.

실제 예시

핵심은 세 조각이다 — 재진입 가드 → “말만함” 검사 → 모드 게이트 순서로 흐른다.

flowchart TD
  IN[stdin JSON] --> R{"stop_hook_active?"}
  R -- 예 --> OUT1["통과: 핑퐁 방지"]
  R -- 아니오 --> ST{"말만 하고 끝?"}
  ST -- 예 --> OUT2["block: 지금 실행하라"]
  ST -- 아니오 --> MG["should_block_stop<br/>모드별 판정"]
  MG --> OUT3[block 또는 통과]

AI에 어떻게 연결되나

  • 트리거: Stop 이벤트 훅으로 등록, 턴 마치려는 순간 자동 실행(timeout: 10초). 입력은 stdin JSON 한 덩어리(stop_hook_active, transcript_path, session_id, cwd).
  • 소비: stdout에 {"decision":"block","reason":...}를 내면 멈춤을 취소하고 reason을 모델에게 다시 보여 턴을 재개. 아니면 빈 {}systemMessage만 내 멈춤 허용.
  • 장부 출처: 게이트는 코드를 직접 분석하지 않는다. UserPromptSubmit 훅이 task_mode를 정하고 장부를 리셋(stop_blocks=0), PostToolUse 훅이 변경/검증 증거를 채운다.
  • fail-open: 모든 훅의 __main__은 예외를 잡아 systemMessage만 내고 SystemExit(0)로 종료 — 게이트가 깨져도 모델을 영구히 가두지 않는다.

요약 & 셀프체크

3줄 요약

  1. 멈추려는 순간 장부를 펴서 “변경했는데 미검증”이면 차단하고, 모델을 다시 깨워 검증 명령을 돌리게 한다.
  2. 무한 잔소리를 막으려 최대 2번만 막고(MAX_STOP_BLOCKS), 자기 재호출은 stop_hook_active로 즉시 통과, 깨져도 fail-open으로 멈춤 허용.
  3. “말만 하고 손 안 댄” 턴은 정규식으로 잡되, 사용자에게 결정을 넘기는 질문으로 끝나면 봐준다.

스스로 답해 보기

  • Q1. quick 모드에서 코드를 고치고 검증을 안 했다면 게이트는 막을까? (힌트: 규칙 평가 순서)
  • Q2. 검증 명령을 5번 실행했지만 전부 success: False였다면 “검증됨”으로 인정될까?
  • Q3. 재진입 가드(stop_hook_active)를 빼면 어떤 사고가 나는가?

Codex 교차검증 (보존)

  • “검증됨”은 verification_resultssuccess is True 원소가 하나라도 있으면 참 — 부분 통과 허용(전건 통과 요구 아님).
  • 통과 우선 규칙(stop_blocks>=2 / quick / docs_only)이 모드별 차단보다 먼저 평가되는 순서가 동작의 핵심이며, 순서가 바뀌면 quick 작업도 붙잡히는 회귀가 난다.
  • fail-open(SystemExit(0))과 재진입 가드는 “게이트가 모델을 영구히 가두지 않는다”는 불변식을 떠받치는 두 축이다.

연결

FB_개요 · _분석축_루브릭 · FB_50_evidence-ledger-state · FB_60_evidence-extraction-from-tools · FB_40_task-classification-engine · FB_20_hook-event-loop