지식위키

. fable-ish

6. fable-ish

한 줄 요약

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

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

그림

FB_개요-diagram.svg

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

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

핵심 정리

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

[!note] 얇은 훅 + 두꺼운 로직 훅 파일(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 → 종료 게이트

[!note] 종합 점수: 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로 워크플로를 절대 막지 않음.

[!warning] 핵심 약점 (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 · _비교매트릭스

[!tip] 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개 모두 실패. 코드 동작 실패가 아니라 환경 제약으로 판단함.