Read the script
#!/usr/bin/env python3
"""fable-mode Profile Injector (SessionStart hook).
When a project has opted into fable-mode (`.fable/` dir present), inject the
discipline at session start — sized to the ledger state, so a big project's
small tasks aren't taxed:
starting (.fable/ but no task cards) -> full injection (guide the round)
active (ledger has open `- [ ]`) -> full injection + context recovery
idle (all cards closed) -> minimal one-liner; quick work flows
paused (a `PAUSED` line in ledger) -> one-liner; enforcement off except
the model ceiling
Also caches {session_id -> model} so the spawn guard can enforce the model
ceiling (PreToolUse hooks never receive `model`).
Inert unless `.fable/` is found. Fail-open: any error -> exit 0, no output.
Tier: model contains "fable" -> throughput, else conservative
(env FABLE_MODE_PROFILE=auto|conservative|throughput overrides).
FABLE_ESCALATION=on marks a stronger tier as genuinely available.
"""
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _fable_common import ( # noqa: E402
read_hook_input, start_dir, find_fable_dir, ledger_path, parse_ledger,
save_session_model, read_routing, ROUTING_PROFILES, read_tier,
)
# resolved from this file's real location, correct wherever the skill is cloned
SKILL = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "SKILL.md")
MAX_LIST = 12
def choose_profile(model, lp):
"""Concurrency tier: env FABLE_MODE_PROFILE > ledger `TIER:` > by model."""
env = os.environ.get("FABLE_MODE_PROFILE", "auto").lower()
if env in ("conservative", "throughput"):
return env
ledger_tier = read_tier(lp)
if ledger_tier:
return ledger_tier
return "throughput" if "fable" in (model or "").lower() else "conservative"
def choose_routing(lp):
"""Routing profile: env FABLE_ROUTING > ledger `ROUTING:` line > balanced."""
env = os.environ.get("FABLE_ROUTING", "").lower()
if env in ROUTING_PROFILES:
return env
return read_routing(lp) or "balanced"
def routing_text(routing):
"""Per-profile routing guidance. The two iron rules and the safety net are
identical in every profile — profiles only tune safe downgrading."""
if routing == "quality":
head = (
"Model routing [QUALITY — user-selected]: every card runs on this "
"session's model, no downgrades at all; effort still routed "
"(max = verify/judge, high = implement, low = gather)."
)
elif routing == "frugal":
head = (
"Model routing [FRUGAL — user-selected]: implementation cards "
"default ONE tier down (machine-checkable acceptance required; "
"tricky or vaguely-specified cards stay inherited); mechanical "
"gather/format/search goes to the cheapest tier at low effort."
)
else:
head = (
"Model routing [BALANCED]: inherit by default; a tightly-specified "
"implementation card (machine-checkable acceptance) may drop one "
"tier; mechanical work goes cheap at low effort."
)
return head + (
" In EVERY profile: task decomposition, orchestration, design, "
"debugging and all verification stay on this session's model — never "
"downgraded. Two failed acceptances -> escalate, CAPPED AT this "
"session's model (top of the ladder is pulling the card back inline — "
"never spawn above the session model); the verifier must be at least "
"as strong as the implementer. When unsure, inherit the session model. "
"Switch profiles only when the user asks (write `ROUTING: <profile>` "
"into the ledger), never silently."
)
def build_context(profile, model, ledger_state, open_items, routing):
m = model or "unknown"
if ledger_state == "paused":
return (
"[fable-mode] Round PAUSED (.fable/LEDGER.md has a PAUSED line): "
"enforcement is off except the model ceiling (never spawn above "
"this session's model). Remove the PAUSED line to resume the round."
)
if ledger_state == "idle":
return (
"[fable-mode] Project armed, ledger idle — quick tasks flow "
"freely, no process tax. For the next substantial task, follow "
"%s: SPEC + task cards in .fable/LEDGER.md first (a detailed "
"fan-out needs a live open card). Always: audit every progress "
"claim against a tool result; don't end the turn on an actionable "
"promise; lead with the outcome. Never spawn a subagent above "
"this session's model." % SKILL
)
# starting / active -> full injection
if profile == "throughput":
tier = (
"Current model %s -> THROUGHPUT tier: delegate parallel subagents "
"aggressively, communicate async (don't block on each return); "
"still enforce ledger-before-delegation and staged fresh-eyes "
"verification. Cost (~15x tokens / rate limits) is accepted." % m
)
else:
tier = (
"Current model %s -> CONSERVATIVE tier: cap concurrency at 5. "
"Quality-critical, tightly-coupled implementation stays inline "
"(if unsure delegation preserves quality, do it yourself) — but "
"MULTITASK within the cap: batch independent tool calls in one "
"message, and run independent side-tasks (searches, verification "
"runs, bulk mechanical work) as background subagents while you "
"keep working. Never sit idle waiting for a result you don't "
"need yet." % m
)
lines = [
"[fable-mode] This project has fable-mode enabled (.fable/ detected). "
"Follow the six levers in %s." % SKILL,
tier,