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/>팩 파일을 직접 읽음"]
쉽게 풀기
라우터는 “안내 데스크 직원”이다. 손님(프롬프트)이 들어올 때마다 직원이 용건을 슬쩍 듣고, “아, 고장 수리 오셨군요? 그럼 이 매뉴얼대로 하세요”라고 적절한 안내문을 건네주는 역할이다. 용건이 평범하면 아무 안내문도 주지 않고 그냥 통과시킨다.
단계별로 풀어 보면 이렇다.
-
항상 대기한다 — 사용자가 메시지를 보낼 때마다(
UserPromptSubmit이라는 시점) 라우터가 자동으로 깨어난다. 손님이 들어오면 무조건 직원이 한 번 쳐다보는 것과 같다. -
용건을 엿듣는다 — 프롬프트 글자를 받아서 전부 소문자로 바꾼다. “Bug”든 “BUG”든 “bug”든 똑같이 알아듣기 위해서다.
-
신호를 찾는다 — 글 안에 약속된 키워드가 들어 있는지 본다. 예를 들어 “debug, error, crash” 같은 단어가 보이면 “아, 디버깅 작업이구나” 하고 알아챈다. “html, render, chart” 같은 단어가 보이면 “화면에 뭔가 그리는 작업이구나” 하고 알아챈다.
-
안내문을 건넨다 — 신호를 찾으면 해당 작업에 맞는 검증된 절차의 위치와 요약을 AI에게 추가로 알려준다. 중요한 건 매뉴얼 전체를 복사해서 주는 게 아니라 “매뉴얼은 여기 있고, 핵심은 이거예요”라고 위치만 알려준다는 점이다(필요할 때 AI가 직접 찾아 읽음 = 토큰 절약).
-
신호가 없으면 가만히 둔다 — 평범한 프롬프트면 아무것도 끼워 넣지 않는다. AI가 평소처럼(baseline) 동작하도록 방해하지 않는 게 원칙이다.
핵심 설계 철학은 세 가지다. 꼭 맞는 가장 작은 팩만 주기, 진짜로 두 종류 작업이 섞였을 때만 두 개를 동시에 주기, 그리고 fablize 자신이 그동안 눈으로 확인한 좋은 행동만 AI에게 흉내 내게 하기.
핵심 정리
| 신호 키워드(소문자 부분일치) | 주입 팩 | 주입 태그 |
|---|---|---|
| debug, bug, error, traceback, stack trace, crash, failing, not working | investigation-protocol.txt | [fablize:investigation] |
| html, svg, game, canvas, chart, render, website, webpage | verification-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줄 요약:
- 라우터는 모든 프롬프트에서 깨어나 작업 신호 키워드를 찾고, 맞으면 검증된 팩의 위치+요약을 AI에게 추가 컨텍스트로 끼워 넣는다.
- 팩 전문이 아니라 경로+요약만 주는 lazy load 방식이고, 신호가 없으면 아무것도 안 해서 평소 동작을 방해하지 않는다.
- 실패해도 항상 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.