6. fable-ish

한 줄 요약

LLM이 검증도 안 하고 “다 됐습니다”라며 끝내려는 순간에 **마찰(friction)**을 걸어, “정말 검증했냐”를 따지고 안 했으면 종료를 막는 경량 Claude Code 플러그인이다.

왜 배우나 — ‘똑똑한 모델’이 아니라 ‘검증 루프’로 품질을 만드는 Fable의 핵심 발상을, 훅 3개 + 스킬 1개라는 최소 부품으로 어떻게 코드화하는지 보여주는 가장 작고 선명한 교과서이기 때문이다.

그림

flowchart TD
    A["사용자 프롬프트 입력"] -->|UserPromptSubmit 훅| B["작업 분류<br/>quick / normal / deep / blocked"]
    B --> C["에이전트 작업 수행<br/>Bash · Edit · Write …"]
    C -->|PostToolUse 훅| D["증거 수집<br/>검증명령·성공·실패·변경 기록"]
    D --> E["증거 장부(JSON)<br/>세션ID×cwd 격리"]
    C --> F["에이전트가 종료 시도"]
    F -->|Stop 훅| G{"종료 게이트<br/>변경했는데 미검증?"}
    E --> G
    G -->|통과| H["완료 허용"]
    G -->|"차단 (최대 2회)"| I["검증하라고 되돌림"]
    I --> C

쉽게 풀기

상황을 비유로 — 신입 인턴이 “보고서 다 썼습니다!”라고 말하는데, 정작 맞춤법 검사도 안 돌리고 숫자도 안 맞춰봤다고 해보자. 똑똑한 인턴이라도 검증을 건너뛰는 습관은 못 고친다. fable-ish는 이 인턴 옆에 붙은 깐깐한 검수관이다. 모델을 더 똑똑하게 만들려 하지 않고, “끝났다”고 선언하는 순간 검수관이 장부를 펴서 “실제로 테스트 돌린 기록이 있냐”를 확인한다.

작동을 4단계로 풀면:

  1. 입구에서 난이도 분류 — 일이 들어오면 먼저 “이건 가벼운 일(quick)이냐, 보통(normal)이냐, 신중해야 할 일(deep)이냐, 위험한 일(blocked)이냐”를 정규식으로 가른다. 난이도에 따라 요구하는 검증 깊이가 달라진다.
  2. 작업하는 동안 어깨너머로 기록 — 인턴이 명령을 실행할 때마다(Bash, Edit, Write 등) 검수관이 옆에서 본다. “테스트를 돌렸네, 성공했네, 이 파일을 바꿨네”를 증거 장부에 적어둔다.
  3. 장부는 한 권만 — 훅들은 서로 다른 프로세스라 기억을 공유 못 한다. 그래서 디스크에 JSON 장부 하나를 두고 모두가 같은 장부를 읽고 쓴다. 세션마다 따로 격리되어 섞이지 않는다.
  4. 나갈 때 검문 — 인턴이 “끝!”이라며 나가려 하면(Stop) 검수관이 장부를 본다. “파일은 바꿨는데 검증 기록이 없네?” → 종료를 막고 다시 일하라고 돌려보낸다. 단, 무한 반복은 곤란하니 최대 2회까지만 막고, 그 뒤엔 통과시킨다.

여기에 fable-ish만의 영리한 한 수가 있다. 인턴이 “이제 구현하겠습니다”라고 말만 하고 아무것도 안 한 채 끝내려 하면 그것도 잡아낸다. 마지막 발언을 거꾸로 훑어, ‘하겠다 선언’은 있는데 실제 행동(도구 호출)이 없으면 “말만 하지 말고 진짜 해”라며 막는다.

검수관은 절대 일을 막지 않는다

검수관(훅) 자신이 고장 나도 워크플로는 멈추지 않는다. 모든 훅은 예외가 나면 **fail-open(exit 0)**으로 조용히 빠져, “검증 장치 때문에 작업이 막히는” 부작용을 원천 차단한다.

핵심 정리

부품역할한 줄 비유
3개 훅 (hooks.json)라이프사이클 3지점에 검수관 배치입구·작업현장·출구의 CCTV
스킬 1개 (SKILL.md)모델이 읽는 검증 규율 매뉴얼인턴 행동수칙집
scripts/ 4모듈실제 판단 로직검수관의 두뇌
증거 장부 (JSON)훅 간 공유되는 유일 사실한 권뿐인 작업일지

얇은 훅 + 두꺼운 로직

훅 파일(hooks/*.py)은 stdin 읽기 → 로직 호출 → JSON 출력만 하는 얇은 껍데기다. 진짜 판단은 모두 scripts/로 분리되어 있다.

  • classify_task.py — 정규식 휴리스틱으로 quick/normal/deep/blocked 4모드 분류 + 리스크 플래그
  • ledger.py — 세션ID×cwd 해시 키로 격리된 JSON 장부 (원자적 쓰기·손상 복구·시크릿 레닥션·trim)
  • parse_tool_result.py — 툴 입출력에서 검증명령/성공/실패/커버리지 추출
  • verify_state.py — 종료 차단 판정 + “말만 하고 안 한” 의도 탐지

훅 3개의 실행 순서 (hooks/hooks.json)

  • UserPromptSubmit → 작업 분류
  • PostToolUse (matcher: Bash|Edit|Write|MultiEdit|NotebookEdit) → 증거 수집
  • Stop → 종료 게이트

종합 점수: 33 / 50 (Claude 초안 38 → Codex 보정 후 33)

강한 축은 철학·아키텍처(각 5) — 단일 목적에 집중한 마이크로 아키텍처와 “관찰하지 않은 검증을 주장하지 마라”는 선명한 포지셔닝. 약한 축은 툴/오케스트레이션·자기개선(각 2) — 신규 도구·멀티에이전트·세션 간 학습이 없는 단조 루프. 중간 축(각 3~4)은 검증루프·상태/영속성·컨텍스트·가드레일·배포.

독창적 아이디어 (가장 저평가된 한 수 )

  • ‘말만 하고 안 한’ 의도 탐지 — Stop 훅이 마지막 어시스턴트 턴을 역스캔해, “이제 구현하겠습니다”류 선언만 있고 tool_use가 없으면 종료를 막아 실제 실행을 강제(verify_state.py:97, 한/영 양쪽 패턴). “착수 선언 후 종료”라는 근본적 LLM 실패 모드를 정조준.
  • 커버리지 관계 추적 — 검증명령이 ‘바뀐 그 파일’을 건드렸는지 direct/generic/uncertain/none 4단계 판정(parse_tool_result.py:122). 단, 이 값은 기록만 되고 종료 판정엔 쓰이지 않음(아래 약점 참고).
  • 안전기능을 의도적으로 빼는 역발상 — 정규식 명령차단이 실측 16개 중 4개만 막는다는 근거로, 하드 차단은 네이티브 permissions.deny에 위임하고 훅은 검증에만 집중.
  • 위험비례 동적 탈출조건 — 고정 체크리스트 대신 모드별로 요구 검증 깊이를 차등화.
  • fail-open 설계 — 모든 훅이 예외 시 exit 0 + systemMessage로 워크플로를 절대 막지 않음.

핵심 약점 (Codex 교차검증 반영)

  • 모든 판단이 정규식 휴리스틱 — 명령차단에서 비판한 ‘정규식 한계’가 VERIFY_RE/FAILURE_RE/coverage에도 그대로. 성공 메시지에 ‘failed’가 우연히 있으면 오탐.
  • Bash 변경 감지가 좁다sed -i·리다이렉션(>)·임의 스크립트 쓰기는 MUTATING_BASH_RE(parse_tool_result.py:27)에 없어 놓침 → 변경을 못 본 채 종료 통과 가능.
  • coverage_relation이 게이트에 미반영 — 기록만 되고 Stop 판정은 success is True 존재 여부만 봄. uncertain 성공도 통과.
  • 최대 2회 차단 뒤 무조건 허용(verify_state.py:48) — deep 작업도 2회 후엔 미검증 통과 가능.
  • 자기개선/학습/세션 간 회고 부재 — 매 프롬프트가 상태 리셋.
  • 문서·실체 불일치 — README는 /fable-ish 슬래시 커맨드를 약속하지만 매니페스트(plugin.json)엔 없음.

실제 예시

훅 배선의 핵심은 단 하나의 JSON 파일이다. 어떤 라이프사이클 지점에, 어떤 도구에 반응할지를 선언한다.

// /mnt/d/6study/_소스레포/fable-ish/hooks/hooks.json
{
  "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "user_prompt_submit.py" }] }],
  "PostToolUse": [{
    "matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit",  // 이 5개 도구만 감시
    "hooks": [{ "type": "command", "command": "post_tool_use.py" }]
  }],
  "Stop": [{ "hooks": [{ "type": "command", "command": "stop_gate.py" }] }]
}

종료 차단 판정의 핵심 로직 — 모드 × 변경 × 검증 조합으로 결정한다.

# /mnt/d/6study/_소스레포/fable-ish/scripts/verify_state.py (요지)
def should_block_stop(mode, changed, verified):
    # docs-only / quick 은 항상 통과
    # deep + 미검증  → 차단
    # normal + 변경 + 미검증 → 차단
    # verified 판정은 'success is True' 검증의 '존재 여부'만 본다
    #   ↳ coverage_relation(direct/uncertain…)은 기록만 될 뿐 여기서 보지 않음(verify_state.py:29,61)
    ...

“말만 하고 안 한” 의도 탐지는 transcript의 마지막 어시스턴트 턴을 역스캔한다.

# /mnt/d/6study/_소스레포/fable-ish/scripts/verify_state.py:97  stated_but_unstarted
# 닫는 문장이 다음 행동을 선언("이제 구현하겠습니다")했지만
# tool_use 도 없고 사용자 질문도 아니면 → 종료를 막고 실제 실행을 강제 (한/영 양쪽 패턴)

요약 & 셀프체크

3줄 요약

  1. fable-ish는 “검증 안 하고 끝내기”라는 LLM 습관에 마찰을 거는, 훅 3개(분류→증거→종료게이트) + 스킬 1개의 경량 플러그인이다.
  2. 핵심은 종료 게이트가 “변경했는데 미검증이면 종료 차단”을 코드로 강제하는 것, 그리고 “말만 하고 안 한 턴”까지 잡아내는 것이다.
  3. 단, 모든 판단이 정규식 휴리스틱이고 coverage는 게이트에 미반영, 최대 2회 뒤엔 통과되며 세션 간 학습은 없다 — ‘완벽 재현 아닌 흉내(-ish)‘를 정직하게 표방한다.

스스로 답해보기

  • 검수관(훅)이 코드 오류로 죽으면 내 작업은 어떻게 되나? (힌트: fail-open)
  • 테스트를 돌려 성공했지만 ‘바뀐 그 파일’은 안 건드렸을 때, 종료 게이트는 막을까 통과시킬까? (힌트: coverage_relation의 위치)
  • “이제 구현하겠습니다”라고만 쓰고 끝내면 무슨 일이 일어나는가?

연결

기능별 분해 (하위 노트)

핵심 파일

  • /mnt/d/6study/_소스레포/fable-ish/hooks/hooks.json — 3개 라이프사이클 지점 바인딩
  • /mnt/d/6study/_소스레포/fable-ish/hooks/user_prompt_submit.py — 분류 훅(얇은 어댑터)
  • /mnt/d/6study/_소스레포/fable-ish/hooks/post_tool_use.py — 증거 수집 훅
  • /mnt/d/6study/_소스레포/fable-ish/hooks/stop_gate.py — 종료 게이트 훅
  • /mnt/d/6study/_소스레포/fable-ish/scripts/classify_task.py — 4모드 분류 + 리스크 플래그
  • /mnt/d/6study/_소스레포/fable-ish/scripts/ledger.py — 세션 격리 영속 장부
  • /mnt/d/6study/_소스레포/fable-ish/scripts/parse_tool_result.py — 검증/실패/커버리지 추출
  • /mnt/d/6study/_소스레포/fable-ish/scripts/verify_state.py — 종료 차단 판정 + 의도 탐지
  • /mnt/d/6study/_소스레포/fable-ish/skills/fable-ish/SKILL.md — 워크플로 지침 레이어
  • /mnt/d/6study/_소스레포/fable-ish/skills/fable-ish/references/verification.md — 검증 사다리
  • /mnt/d/6study/_소스레포/fable-ish/tests/test_hooks.py — 11개 계약 테스트
  • /mnt/d/6study/_소스레포/fable-ish/.claude-plugin/plugin.json — 최소 매니페스트(skills+hooks)

기준 노트 _분석축_루브릭 · HOME · _비교매트릭스

Claude ↔ Codex 교차검증 (메타 학습 포인트)

두 모델의 불일치 자체가 배울 거리다. Codex가 파일을 직접 대조해 잡은 것:

  • 사실 오류 3건 — ① 참조 파일은 4개가 아니라 3개(SKILL.md:28-30). ② “blocked는 조언만”은 부정확, Stop 단계에서 blocked는 실제 종료를 막음(verify_state.py:54). ③ “커버리지까지 보고 완료 강제”는 과장, coverage는 기록만 되고 판정은 success is True 존재만 봄(verify_state.py:29,61).
  • Claude가 빠뜨린 포인트 — Bash 변경 감지 구멍(parse_tool_result.py:27,117), ‘검증 불가 사유’를 게이트가 파싱·승인하지 않고 2회 뒤 허용(verify_state.py:48), README↔매니페스트 슬래시 커맨드 불일치.
  • 점수 보정 — 컨텍스트엔지니어링 4→3, 가드레일/안전 4→3, 검증루프 5→4, 자기개선/반복 3→2, 배포/DX 4→3 (아키텍처·툴·오케·상태·철학은 동일 유지). 종합 38→33.
  • Codex가 꼽은 가장 저평가된 핵심말만 하고 안 한 일 탐지(verify_state.py:97, stop_gate.py:30).
  • 참고python3 tests/test_hooks.py는 Codex 샌드박스에 쓰기 가능한 임시 디렉터리가 없어 setUp()에서 11개 모두 실패. 코드 동작 실패가 아니라 환경 제약으로 판단함.