fable-ish · 증거 원장(ledger) 상태 영속성

한 줄 요약

fable-ish의 후크들은 매번 따로 실행돼 서로 기억을 못 하므로, “이번 작업이 어떤 모드이고 무엇을 고쳤고 검증했는지”를 세션별 JSON 공책 하나에 계속 적어두는 단기 기억 장치가 증거 원장(ledger)이다. 왜 배우나: 종료 게이트가 “검증했나?”를 판단할 때 읽는 유일한 사실 출처라서, 이걸 모르면 fable-ish가 왜 멈추고 왜 통과시키는지 설명할 수 없다.

그림

flowchart TD
  A["사용자 프롬프트 도착<br/>UserPromptSubmit"] -->|classify_prompt| B["원장 리셋·초기화<br/>모드·위험·목표 기록"]
  B -->|이번 작업 규칙 주입| M((Claude 모델))
  M -->|"Bash/Edit/Write 실행"| C["도구 실행 직후<br/>PostToolUse"]
  C -->|parse_tool_result| D["원장에 증거 누적<br/>변경경로·검증결과·실패"]
  D -->|실패 감지 시 경고 주입| M
  M -->|턴 종료 시도| E["종료 게이트<br/>Stop gate"]
  E -->|"load_ledger + should_block_stop"| F{"검증 충분?"}
  F -- 아니오 --> G["종료 차단 + 사유 안내<br/>stop_blocks++ 저장"]
  G --> M
  F -- 예 / 2회 초과 --> H[종료 허용]

한 가지만 기억하세요

원장은 세 시점(프롬프트 수신 → 도구 실행 후 → 종료 시도)에서 읽고-고치고-다시 저장되는 작은 JSON 파일 하나입니다. 화살표는 “기록 → 누적 → 판단”의 한 사이클입니다.

쉽게 풀기

비유 — 교대 근무하는 경비원들의 공동 일지

후크 세 명은 손님이 들어올 때(프롬프트), 작업이 일어날 때(도구 실행), 문을 닫으려 할 때(종료) 각각 근무합니다. 문제는 셋이 서로 만나지 못한다는 점입니다 — 별개의 파이썬 프로세스라 머릿속 기억을 공유할 수 없습니다. 그래서 데스크에 공동 일지(원장) 한 권을 둡니다. 다음 경비원이 일지만 읽고 상황을 이어받고, 마지막 경비원(종료 게이트)은 “점검 기록이 없네 → 아직 문 닫으면 안 돼”라고 판단합니다.

flowchart LR
  P1["경비원1<br/>프롬프트 수신"] -->|기록| L["(공동 일지<br/>원장 JSON)"]
  P2["경비원2<br/>도구 실행"] -->|누적| L
  L -->|읽고 판단| P3["경비원3<br/>종료 게이트"]
  P3 -.점검 없으면 차단.-> P2

단계별 핵심

  1. 공책 위치 — 세션×작업폴더마다 다른 공책. 둘을 이어 붙여 SHA-256 해시를 만들고 앞 24자를 파일 이름으로 삼는다. 같은 채팅이라도 cwd가 다르면 일지가 분리된다.
  2. 적는 내용 — 작업 모드(quick/normal/deep/blocked), 목표 한 줄, 위험 태그, 고친 파일, 검증 명령과 결과, 실패 기록.
  3. 비밀 가리기 — API 키·비밀번호는 적기 전에 [REDACTED]로 마스킹하고 한 줄로 평탄화.
  4. 안전 저장 — 임시 페이지에 다 쓴 뒤 한 번에 갈아끼운다(원자적 교체). 일지가 깨졌으면 멈추지 말고 새 빈 공책으로 시작(fail-open).
  5. 길이 자르기 — 목록·기록에 상한을 둬 오래된 것 또는 머리를 잘라낸다.

핵심: 원장은 “AI가 정말 검증했는지”를 증명하는 누적 증거 공책이고, 후크들은 이 공책에만 사실을 적고 이 공책만 보고 판단한다.

핵심 정리

원장이 사는 곳과 이름 규칙:

요소
데이터 루트$CLAUDE_PLUGIN_DATA/$PLUGIN_DATA, 없으면 /tmp/fable-ish (.resolve()로 절대화)
파일 경로<루트>/ledgers/<ledger_key>.json (세션×cwd당 1개)
ledger_key`sha256(“session_id

격리 키의 핵심

session_id 와 cwd 를 |로 이어 SHA-256 → 앞 24자. 같은 세션이라도 작업 디렉터리가 다르면 원장이 자동으로 분리됩니다.

원장의 주요 필드(전부 필수, 분류기·후크가 채움). 연관도(coverage_relation) 등급은 none < uncertain < generic < direct(오른쪽일수록 “이 검증이 바로 그 변경을 확인했다”에 가까움):

필드기본값
task_modequick/normal/deep/blocked"quick"
goal목표 한 줄(180자, redact)""
risk_flags위험 태그[]
changed_paths변경 경로(고유, 상한 40)[]
verification_results검증 레코드 배열(최근 40)[]
coverage_relation검증↔변경 연관 최고값"none"
failures실패 레코드(최근 40)[]
stop_blocks종료 막은 횟수(최대 2)0

실제 예시

핵심 골격은 세 가지뿐: 세션×cwd 해시 파일명 → tmp.replace 원자 교체 → 읽기 실패 시 빈 원장 fail-open. 저장 전 모든 문자열은 redact를 거쳐 (1)개행 제거 (2)비밀 4종 마스킹 (3)길이 절단됩니다.

flowchart LR
  IN[입력 문자열] -->|redact| R["비밀 마스킹·평탄화·절단"]
  R -->|update_ledger| T[trim 상한 적용]
  T -->|tmp.write_text| TMP[.tmp 임시파일]
  TMP -->|tmp.replace| FIN["최종 .json<br/>원자적 교체"]
  RD[load_ledger] -->|JSON 깨짐| FO["빈 원장 + failures 기록<br/>fail-open"]

원장이 AI 행동에 들어가는 경로

원장 자체는 LLM 프롬프트에 통째로 주입되지 않습니다. 원장은 후크가 읽고 쓰는 내부 상태이고, 모델은 후크가 원장을 근거로 만든 **짧은 자연어 메시지(additionalContext / block reason)**만 소비합니다.

flowchart TD
  U[UserPromptSubmit] -->|classify_prompt| RST["원장 리셋·초기화"]
  RST -->|context_for_mode| MSG1["additionalContext<br/>모드·위험·검증규칙"]
  PT["PostToolUse<br/>Bash·Edit·Write"] -->|parse_tool_result| ACC[update_ledger 누적]
  ACC -->|도구 실패 시| MSG2[고쳐라 경고 주입]
  SG[Stop gate] -->|load_ledger| JDG{should_block_stop}
  JDG -->|미검증| MSG3["block reason<br/>stop_blocks++"]
  MSG1 & MSG2 & MSG3 --> MODEL((Claude 모델))
  1. UserPromptSubmit (user_prompt_submit.py) — classify_prompt로 모드·위험·목표 산출 → 원장을 리셋 후 새 작업으로 초기화(changed/verification/failures/stop_blocks 비움) → context_for_modeadditionalContext로 주입. 단, 프롬프트가 fable-ish: run/add/resolve로 시작하는 종료게이트 재투입이면 리셋하지 않고 현재 모드만 재안내.
  2. PostToolUse (post_tool_use.py, 매처 ^(Bash|Edit|Write|MultiEdit|NotebookEdit)$) — 도구 입출력을 parse_tool_result.py로 파싱 → 변경 경로·종류·검증 레코드·실패 추출 → update_ledger로 누적(append + coverage_relation 최고값 갱신). 도구 실패 시 “완료 보고 말고 고쳐라” 주입.
  3. Stop (stop_gate.py) — 종료 시도 시 load_ledger로 읽어 should_block_stop 판정. 미검증/위험 미해결이면 {"decision":"block","reason":...}종료를 막고 stop_blocks++(최대 2회). “말만 하고 안 한”(stated_but_unstarted) 응답도 transcript 역방향 스캔으로 차단.

요약 & 셀프체크

3줄 요약:

  1. 증거 원장은 세션×cwd 해시를 파일명으로 한 단일 JSON 공책으로, 후크들이 공유하는 단기 기억이다.
  2. 쓰기는 항상 redact → trim → tmp.replace 원자 저장, 읽기 실패는 빈 원장으로 fail-open한다.
  3. 종료 게이트가 이 원장을 읽어 “검증 충분?”을 판단하므로, 원장은 검증 게이트의 유일한 사실 출처다.

스스로 답해보기:

  • 같은 채팅 세션인데 작업 폴더(cwd)를 옮기면 원장은 같은 파일을 쓸까, 다른 파일을 쓸까? 그 이유는?
  • 일지(원장) 파일이 깨져 있으면 fable-ish는 멈출까, 계속 갈까? 그 동작을 뭐라 부르나?
  • verification_resultsverification_commands는 왜 머리가 아니라 꼬리에서 잘릴까?

근거 파일

연결

FB_개요 · _분석축_루브릭 · FB_40_task-classification-engine · FB_60_evidence-extraction-from-tools · FB_70_stop-completion-gate