FBZ_10 — 훅 라우터 (작업별 팩 주입)

한 줄 요약

사용자가 프롬프트를 보낼 때마다 자동으로 작동해, 프롬프트 속 작업 신호(디버깅·렌더 등)를 키워드로 감지하면 그에 맞는 검증된 절차를 AI에게 슬쩍 귀띔해 주는 자동 라우터다. 왜 배우나 — fablize가 “알아서 똑똑하게 동작하는” 핵심 장치가 바로 이 라우터라서, 이걸 알면 fablize의 자동화가 어떻게 시작되는지가 한눈에 들어온다.

그림

flowchart TD
    A[사용자가 메시지 제출] --> B{UserPromptSubmit 훅 발화}
    B --> C["router.sh 실행<br/>프롬프트 JSON을 입력으로 받음"]
    C --> D[프롬프트를 소문자로 정규화]
    D --> E{"작업 신호 키워드가<br/>들어 있나?"}
    E -->|디버깅 신호| F["조사 프로토콜<br/>경로+요약 주입"]
    E -->|렌더 신호| G["검증 접지 팩<br/>경로+요약 주입"]
    E -->|둘 다| H[둘 다 주입]
    E -->|신호 없음| I["아무것도 주입 안 함<br/>그냥 통과 = baseline 유지"]
    F --> J[AI 컨텍스트에 추가됨]
    G --> J
    H --> J
    J --> K["AI가 필요할 때<br/>팩 파일을 직접 읽음"]

쉽게 풀기

라우터는 “안내 데스크 직원”이다. 손님(프롬프트)이 들어올 때마다 직원이 용건을 슬쩍 듣고, “아, 고장 수리 오셨군요? 그럼 이 매뉴얼대로 하세요”라고 적절한 안내문을 건네주는 역할이다. 용건이 평범하면 아무 안내문도 주지 않고 그냥 통과시킨다.

단계별로 풀어 보면 이렇다.

  1. 항상 대기한다 — 사용자가 메시지를 보낼 때마다(UserPromptSubmit이라는 시점) 라우터가 자동으로 깨어난다. 손님이 들어오면 무조건 직원이 한 번 쳐다보는 것과 같다.

  2. 용건을 엿듣는다 — 프롬프트 글자를 받아서 전부 소문자로 바꾼다. “Bug”든 “BUG”든 “bug”든 똑같이 알아듣기 위해서다.

  3. 신호를 찾는다 — 글 안에 약속된 키워드가 들어 있는지 본다. 예를 들어 “debug, error, crash” 같은 단어가 보이면 “아, 디버깅 작업이구나” 하고 알아챈다. “html, render, chart” 같은 단어가 보이면 “화면에 뭔가 그리는 작업이구나” 하고 알아챈다.

  4. 안내문을 건넨다 — 신호를 찾으면 해당 작업에 맞는 검증된 절차의 위치와 요약을 AI에게 추가로 알려준다. 중요한 건 매뉴얼 전체를 복사해서 주는 게 아니라 “매뉴얼은 여기 있고, 핵심은 이거예요”라고 위치만 알려준다는 점이다(필요할 때 AI가 직접 찾아 읽음 = 토큰 절약).

  5. 신호가 없으면 가만히 둔다 — 평범한 프롬프트면 아무것도 끼워 넣지 않는다. AI가 평소처럼(baseline) 동작하도록 방해하지 않는 게 원칙이다.

핵심 설계 철학은 세 가지다. 꼭 맞는 가장 작은 팩만 주기, 진짜로 두 종류 작업이 섞였을 때만 두 개를 동시에 주기, 그리고 fablize 자신이 그동안 눈으로 확인한 좋은 행동만 AI에게 흉내 내게 하기.

핵심 정리

신호 키워드(소문자 부분일치)주입 팩주입 태그
debug, bug, error, traceback, stack trace, crash, failing, not workinginvestigation-protocol.txt[fablize:investigation]
html, svg, game, canvas, chart, render, website, webpageverification-grounding-pack.txt[fablize:grounding]

설계 원칙 3가지 (router.sh 주석에 명시)

  • 최소 매칭 팩만 (smallest matching pack only)
  • 진짜 다중 카테고리일 때만 중첩 (overlap only when genuinely multi-category)
  • 관측 가능한 행동만 모방 (mimic observable behavior only)

기억할 점

  • 두 신호가 동시에 잡히면 둘 다 주입한다(genuinely multi-category).
  • 멀티스토리·증거게이트(goals.py)는 라우터가 아니라 스킬/CLAUDE.md 블록을 통해 발동한다. 라우터가 모든 자동화를 다 하는 건 아니다.
  • 라우터는 팩 전문을 붙이지 않고 경로 + 요약만 준다(lazy load).

안전장치 체크리스트:

  • timeout 10 — 라우터가 10초 안에 못 끝내면 강제 종료(프롬프트가 멈추지 않게)
  • 항상 exit 0 — 라우터가 실패해도 프롬프트 흐름을 절대 막지 않음
  • set -uo pipefail — 스크립트 안전 모드

실제 예시

훅 등록 (hooks/hooks.json)

// hooks/hooks.json — 플러그인 설치 시 자동 등록되는 훅 정의
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/hooks/router.sh",
            "timeout": 10 }
        ]
      }
    ]
  }
}

matcher: "" = 모든 프롬프트에 대해 실행. ${CLAUDE_PLUGIN_ROOT}는 런타임이 주입하는 플러그인 루트 경로.

라우팅 로직 (hooks/router.sh)

# hooks/router.sh — stdin: JSON {"prompt":"..."} / stdout: 매칭 시 추가 컨텍스트 / 항상 exit 0
set -uo pipefail
ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"   # 런타임 변수 우선, 없으면 스크립트 위치로 폴백
PACKS="$ROOT/packs"
 
prompt="$(python3 -c 'import sys,json
try: print(json.load(sys.stdin).get("prompt",""))
except Exception: pass' 2>/dev/null || true)"
[ -z "${prompt:-}" ] && exit 0
low="$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')"   # 소문자 정규화
 
emit=""
add() { emit="${emit:+$emit
}$1"; }
 
# 디버깅/근본원인 → 조사 프로토콜
case "$low" in
  *debug*|*bug*|*error*|*traceback*|*"stack trace"*|*crash*|*failing*|*"not working"*)
    add "[fablize:investigation] Debugging/root-cause signal — follow $PACKS/investigation-protocol.txt: reproduce first, form 3+ competing hypotheses, ..." ;;
esac
# 렌더/실행 산출물 → 검증 접지
case "$low" in
  *html*|*svg*|*game*|*canvas*|*chart*|*render*|*website*|*webpage*)
    add "[fablize:grounding] Render/executable artifact signal — follow $PACKS/verification-grounding-pack.txt grounding loop: run it in the real renderer, ..." ;;
esac
 
[ -n "$emit" ] && printf '%s\n' "$emit"
exit 0

직접 만들 라우터 템플릿

#!/usr/bin/env bash
# my-router.sh — UserPromptSubmit 라우터 스켈레톤
set -uo pipefail
ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
prompt="$(python3 -c 'import sys,json;print(json.load(sys.stdin).get("prompt",""))' 2>/dev/null||true)"
[ -z "${prompt:-}" ] && exit 0
low="$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')"
emit=""; add(){ emit="${emit:+$emit
}$1"; }
case "$low" in
  *test*|*spec*) add "[myrouter:test] 테스트 신호 — 먼저 실패를 재현하고 ..." ;;
esac
[ -n "$emit" ] && printf '%s\n' "$emit"; exit 0

직접 만들 때 지킬 체크리스트: ①신호 없으면 무주입(baseline 보존) ②경로+요약만(전문 금지, lazy) ③항상 exit 0 ④최소 매칭 원칙 ⑤소문자 정규화로 대소문자 무시.

요약 & 셀프체크

3줄 요약:

  1. 라우터는 모든 프롬프트에서 깨어나 작업 신호 키워드를 찾고, 맞으면 검증된 팩의 위치+요약을 AI에게 추가 컨텍스트로 끼워 넣는다.
  2. 팩 전문이 아니라 경로+요약만 주는 lazy load 방식이고, 신호가 없으면 아무것도 안 해서 평소 동작을 방해하지 않는다.
  3. 실패해도 항상 exit 0으로 프롬프트 흐름을 막지 않으며, 10초 타임아웃과 소문자 정규화 등의 안전장치를 갖췄다.

스스로 답해 보기:

  • 프롬프트에 “Render the CHART”라고 썼을 때 라우터는 왜 신호를 잡아낼 수 있을까? (힌트: 소문자 정규화)
  • 라우터가 팩 전문을 통째로 붙이지 않고 “경로+요약”만 주는 이유는?
  • “debug the HTML chart” 처럼 두 신호가 동시에 있으면 라우터는 어떻게 동작할까?

연결

FBZ_개요 · _분석축_루브릭 · FBZ_30_검증접지-팩 · FBZ_40_조사-프로토콜 · FBZ_60_스킬-운영블록

Codex 교차검증 보존

멀티스토리·증거게이트(goals.py)는 라우터가 아니라 스킬/CLAUDE.md 블록을 통해 발동한다는 점, 그리고 현재 자동 라우팅되는 검증 팩은 investigation-protocol.txt와 verification-grounding-pack.txt 2종이라는 점을 소스(/home/seunghyeong/harness-fablize/hooks/router.sh, hooks.json)와 대조해 확인함. 근거 파일: hooks/hooks.json, hooks/router.sh, packs/investigation-protocol.txt, packs/verification-grounding-pack.txt.