Development
Hook
Hooks
What fable5-mode runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
$ npx -y skills add cozytab/fable5-mode --agent claude-codeShips with fable5-mode. Installing the plugin gets these hooks.
Where it lives
- hooks/_fable_common.pyGitHub
Read the script
"""Shared helpers for fable-mode guard hooks. Design invariants (keep these true, they are the safety contract): - FAIL-OPEN: any unexpected error must let the session proceed (exit 0). A bug in a guard must never brick the user's Claude Code session. - OPT-IN: a guard only does anything when the project has opted into fable-mode enforcement by having a `.fable/` directory somewhere from cwd up to the root. No `.fable/` dir -> guards are inert. """ import json import os import re import sys import tempfile import time def read_hook_input(): """Parse the hook JSON delivered on stdin. Returns {} on any problem.""" try: raw = sys.stdin.read() if not raw.strip(): return {} return json.loads(raw) except Exception: return {} def start_dir(data): """Best-effort project dir: hook 'cwd' field, else process cwd.""" cwd = data.get("cwd") if cwd and os.path.isdir(cwd): return cwd try: return os.getcwd() except Exception: return "." def find_fable_dir(start): """Walk up from `start` looking for a `.fable/` directory. Stops at the filesystem root or at a git repo root (whichever comes first), so a stray `.fable` far up the tree can't accidentally arm every project. Returns the absolute path to the `.fable` dir, or None. """ try: cur = os.path.abspath(start) except Exception: return None while True: cand = os.path.join(cur, ".fable") if os.path.isdir(cand): return cand # git root is a natural project boundary; don't cross it. if os.path.isdir(os.path.join(cur, ".git")): return None parent = os.path.dirname(cur) if parent == cur: # filesystem root return None cur = parent def ledger_path(fable_dir): return os.path.join(fable_dir, "LEDGER.md") # --- model-tier ranking & per-session model cache (for the model ceiling) --- TIER_ORDER = ("haiku", "sonnet", "opus", "fable") def model_tier(model_str): """Rank a model string by capability keyword; None if unrecognized.""" s = (model_str or "").lower() tiers = [i for i, k in enumerate(TIER_ORDER) if k in s] return max(tiers) if tiers else None def _sessions_dir(): d = os.path.join(tempfile.gettempdir(), "fable-mode-sessions") os.makedirs(d, exist_ok=True) return d def _safe_sid(session_id): return re.sub(r"[^A-Za-z0-9._-]", "_", str(session_id))[:120] def save_session_model(session_id, model): """Cache the session's model at SessionStart so PreToolUse guards (which never receive `model`) can enforce the ceiling. Best-effort, fail-open.""" if not session_id or not model: return try: d = _sessions_dir() now = time.time() for f in os.listdir(d): # opportunistic self-cleanup, no SessionEnd hook needed p = os.path.join(d, f) try: if now - os.path.getmtime(p) > 7 * 86400: os.remove(p) except OSError: pass with open(os.path.join(d, _safe_sid(session_id) + ".txt"), "w", encoding="utf-8") as fh: fh.write(str(model)) except Exception: pass def load_session_model(session_id): if not session_id: return None try: with open(os.path.join(_sessions_dir(), _safe_sid(session_id) + ".txt"), encoding="utf-8") as fh: return fh.read().strip() or None except Exception: return None # --- evidence-on-close convention (lever 4: report evidence, not adjectives) --- EVIDENCE_RE = re.compile(r"(evidence|verified|证据|凭证|验证)\s*[::]", re.IGNORECASE) # --- model-routing profiles (quality / balanced / frugal) --- ROUTING_PROFILES = ("quality", "balanced", "frugal") _ROUTING_RE = re.compile(r"^ROUTING\s*[::]\s*(quality|balanced|frugal)\b", re.IGNORECASE) _TIER_RE = re.compile(r"^TIER\s*[::]\s*(throughput|conservative)\b", re.IGNORECASE) def read_tier(path): """Per-round concurrency tier from a `TIER: <tier>` ledger line, or None. Same pattern as ROUTING — the user says a word, the model writes the line, the choice persists for the round and stays auditable. Fail-open. """ try: with open(path, "r", encoding="utf-8", errors="replace") as fh: for line in fh: m = _TIER_RE.match(line.strip()) if m: return m.group(1).lower() except Exception: return None return None def read_routing(path): """Per-round routing profile from a `ROUTING: <profile>` ledger line. Returns 'quality'|'balanced'|'frugal', or None when absent/unrecognized (callers fall back to the default). Fail-open on any read problem. """ try: with open(path, "r", encoding="utf-8", errors="replace") as fh: for line in fh: m = _ROUTING_RE.match(line.strip()) if m: return m.group(1).lower() except Exception: return None return None MIN_EVIDENCE_CHARS = 6 def closed_without_evidence(path): """List `- [x]` ledger lines whose evidence marker is missing OR hollow. Convention: a card may only be checked `- [x]` together with a substantive evidence note (`-- evidence: <command output / screenshot / test count>`). A marker followed by fewer than MIN_EVIDENCE_CHARS characters ("evidence: ok") is treated as missing — adjectives are not evidence. Returns [] on any read problem (fail-open). """ bad = [] try: with open(path, "r", encoding="utf-8", errors="replace") as fh: for line in fh: s = line.strip() if s[:5].lower() != "- [x]": continue m = EVIDENCE_RE.search(s) if not m or len(s[m.end():] - hooks/fable_close_guard.pyGitHub
Read the script
#!/usr/bin/env python3 """fable-mode Close Guard (Stop hook). Two duties while a `.fable/LEDGER.md` exists (both loop-safe, both off when the ledger is PAUSED): 1. Blocks ending the turn while the ledger still has open `- [ ]` items, so fable-mode can't quietly stop mid-plan (the "early-stopping" failure). Mark items `- [x]` (done+verified) or `- [~] ... -- deferred: reason`. 2. Evidence-on-close: blocks ending the turn while any `- [x]` item lacks an evidence marker (`-- evidence: ...` / `证据: ...`) — "report evidence, not adjectives" as a hard rule, not prose. Inert unless a `.fable/LEDGER.md` is found. Loop-safe via stop_hook_active. Fail-open on any error. Exit codes: 0 = allow stop; 2 = block stop (stderr shown to Claude). """ 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, closed_without_evidence, ) MAX_LIST = 12 def main(): data = read_hook_input() # Prevent an infinite stop/continue loop: if we already blocked once and # Claude is stopping again, let it through. if data.get("stop_hook_active"): return 0 fable_dir = find_fable_dir(start_dir(data)) if not fable_dir: return 0 path = ledger_path(fable_dir) if not os.path.isfile(path): return 0 # no ledger -> nothing to enforce open_items, _has_any, paused = parse_ledger(path) if paused: return 0 # round paused -> enforcement off if not open_items: # All cards closed -> enforce evidence-on-close before allowing stop. bad = closed_without_evidence(path) if bad: shown = bad[:MAX_LIST] lines = "\n".join(" " + it for it in shown) if len(bad) > len(shown): lines += "\n ... and %d more" % (len(bad) - len(shown)) sys.stderr.write( "[fable-mode] BLOCKED stop: %d checked card(s) in %s carry no " "evidence marker:\n%s\n" "A card is only done when its acceptance actually ran. Append " "`-- evidence: <what proved it>` (a command + its result, a " "test count, a screenshot path) to each `- [x]` line — or " "uncheck the card and verify it now. Adjectives are not " "evidence.\n" % (len(bad), path, lines) ) return 2 return 0 # all closed, all evidenced -> allow stop shown = open_items[:MAX_LIST] more = len(open_items) - len(shown) lines = "\n".join(" " + it for it in shown) if more > 0: lines += "\n ... and %d more" % more sys.stderr.write( "[fable-mode] BLOCKED stop: %d open ledger item(s) in %s\n%s\n" "Three legitimate exits: (1) finish each item, verify it (its " "acceptance command), mark `- [x] ... -- evidence: <proof>`; " "(2) genuinely out of scope -> `- [~] ... -- deferred: reason`; " "(3) the user is steering to unrelated work -> add a line " "`PAUSED: reason` to the ledger and continue with what they asked. " "Do NOT invent completion — pick the exit that matches reality.\n" % (len(open_items), path, lines) ) return 2 if __name__ == "__main__": try: sys.exit(main()) except Exception as e: # fail-open: never trap the user in a session sys.stderr.write("[fable-mode] close guard error (ignored): %r\n" % e) sys.exit(0) - hooks/fable_fail_streak.pyGitHub
Read the script
#!/usr/bin/env python3 """fable-mode Fail-Streak Reminder (PostToolUse hook on Bash). Grinding is the failure mode this catches: N consecutive failing commands usually means the model is patching the wrong layer. At every 3rd consecutive Bash failure it injects the attribution ladder as context: harness -> deployment -> product (1) suspect the test/driver itself, (2) prove the new code is actually running (cache/build/restart), (3) only then debug the product — and fix the class via an invariant, not the instance. Advisory only — never blocks (exit 0 always). Armed per project by `.fable/`; off while the ledger is PAUSED. Streak state lives beside the model cache in $TMPDIR/fable-mode-sessions/<sid>.fails and self-resets on the next success. Fail-open on any error. """ import json import os import re 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, load_fail_streak, save_fail_streak, ) REMIND_EVERY = 3 _EXIT_CODE_RE = re.compile(r"[Ee]xit code[: ]+([0-9]+)") LADDER = ( "[fable-mode] %d consecutive failing commands — before the next fix, walk " "the attribution ladder (cheapest layer first): (1) HARNESS: the test/" "driver/acceptance script is code too — falsify the test before the " "tested; (2) DEPLOYMENT: prove the code you just changed is actually " "running (behavior signature, cache-bust, rebuild/restart) — 'fix had no " "effect' is often 'fix never ran'; (3) PRODUCT: only now debug, and fix " "the class via an invariant, not the one observed symptom. If the same " "command keeps failing verbatim, stop retrying it." ) def command_failed(tool_response): """Best-effort failure detection; uncertain -> treated as success.""" r = tool_response if isinstance(r, str): return bool(_EXIT_CODE_RE.search(r) and _EXIT_CODE_RE.search(r).group(1) != "0") if not isinstance(r, dict): return False for key in ("exitCode", "exit_code", "code", "returncode"): v = r.get(key) if isinstance(v, int): return v != 0 for key in ("is_error", "isError"): if r.get(key) is True: return True text = " ".join(str(r.get(k, "")) for k in ("stdout", "stderr", "output")) m = _EXIT_CODE_RE.search(text) return bool(m and m.group(1) != "0") def main(): data = read_hook_input() sid = data.get("session_id") if not sid: return 0 fable_dir = find_fable_dir(start_dir(data)) if not fable_dir: return 0 # not opted in -> inert _open, _has, paused = parse_ledger(ledger_path(fable_dir)) if paused: return 0 if not command_failed(data.get("tool_response")): if load_fail_streak(sid): save_fail_streak(sid, 0) return 0 streak = load_fail_streak(sid) + 1 save_fail_streak(sid, streak) if streak >= REMIND_EVERY and streak % REMIND_EVERY == 0: print(json.dumps({ "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": LADDER % streak, } }, ensure_ascii=False)) return 0 if __name__ == "__main__": try: sys.exit(main()) except Exception as e: # advisory hook: never disturb the session sys.stderr.write("[fable-mode] fail-streak error (ignored): %r\n" % e) sys.exit(0) - hooks/fable_lint.pyGitHub
Read the script
#!/usr/bin/env python3 """fable-mode lint — one-shot discipline check for a fable-mode project. Not a hook: a CLI the model (or CI) runs at wrap-up, and the machine-checkable acceptance for the discipline itself: python3 ~/.claude/skills/fable-mode/hooks/fable_lint.py [project_dir] Checks (each finding names the file and the rule): 1. docs/SPEC.md exists next to the `.fable/` project root. 2. SPEC.md decisions carry source tags — at least one of [measured]/[inferred]/[not-shown]/[design-gap] (or the Chinese set [实测]/[读数]/[推断]/[未展示]/[设计补全]). Untagged specs hide guesses. 3. Every open `- [ ]` ledger card hints at its acceptance (mentions a test/ acceptance/验收, or carries a `backtick command`). 4. Every closed `- [x]` ledger card carries an evidence marker (`-- evidence:` / `证据:`). 5. Every deferred `- [~]` card carries a reason (`-- deferred: why` / `搁置:`/`推迟:`) — deferring everything is the cheap way out of a round; reasons keep it auditable. Exit 0 = clean, exit 1 = findings (printed one per line), exit 0 with a note if the directory isn't a fable-mode project at all. """ import os import re import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _fable_common import ( # noqa: E402 find_fable_dir, ledger_path, closed_without_evidence, ) SOURCE_TAG_RE = re.compile( r"\[(measured|inferred|not-shown|design-gap|实测|读数|推断|未展示|设计补全)\]", re.IGNORECASE) ACCEPTANCE_HINT_RE = re.compile(r"(accept|验收|test|测试|`[^`]+`)", re.IGNORECASE) DEFER_REASON_RE = re.compile(r"(deferred|搁置|推迟)\s*[::]\s*\S{2,}", re.IGNORECASE) def lint(project_dir): findings = [] fable_dir = find_fable_dir(project_dir) if not fable_dir: print("fable-lint: no .fable/ found from %s — not a fable-mode " "project, nothing to lint" % project_dir) return 0 root = os.path.dirname(fable_dir) spec = os.path.join(root, "docs", "SPEC.md") if not os.path.isfile(spec): findings.append("%s: missing (plan gate: requirements + approach + " "task cards live here)" % spec) else: try: with open(spec, encoding="utf-8", errors="replace") as fh: if not SOURCE_TAG_RE.search(fh.read()): findings.append( "%s: no source tags — tag design decisions " "[measured]/[inferred]/[not-shown] so reviewers know " "which foundations are guesses" % spec) except Exception: pass lp = ledger_path(fable_dir) if os.path.isfile(lp): try: with open(lp, encoding="utf-8", errors="replace") as fh: for i, line in enumerate(fh, 1): s = line.strip() if s[:5].lower() == "- [ ]" and not ACCEPTANCE_HINT_RE.search(s): findings.append( "%s:%d: open card with no acceptance hint " "(name the test/command that will prove it): %s" % (lp, i, s)) elif s[:5].lower() == "- [~]" and not DEFER_REASON_RE.search(s): findings.append( "%s:%d: deferred card without a reason " "(`-- deferred: why` keeps skips auditable): %s" % (lp, i, s)) except Exception: pass for s in closed_without_evidence(lp): findings.append( "%s: checked card without `-- evidence:` marker: %s" % (lp, s)) else: findings.append("%s: missing (no task cards -> no round structure)" % lp) for f in findings: print("FINDING " + f) print("fable-lint: %d finding(s)" % len(findings)) return 1 if findings else 0 if __name__ == "__main__": target = sys.argv[1] if len(sys.argv) > 1 else os.getcwd() try: sys.exit(lint(os.path.abspath(target))) except Exception as e: print("fable-lint: error (%r)" % e) sys.exit(0) - hooks/fable_profile_inject.pyGitHub
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, - hooks/fable_spawn_guard.pyGitHub
Read the script
#!/usr/bin/env python3 """fable-mode Spawn Guard (PreToolUse on Agent | Task | Workflow). Two enforcement duties when the project has opted in (`.fable/` dir present): 1. Design gate: block a *detailed* delegation while the ledger has no OPEN task cards (write the SPEC + a live round's cards before fanning out; closed cards from a finished round don't unlock new fan-out). Small spawns and forks are exempt. 2. Model ceiling: block any spawn that requests a model STRONGER than the session's (haiku < sonnet < opus < fable) — fable-mode exists to get Fable-5-grade results without reaching upward. The session model comes from the Profile Injector's per-session cache; unknown either side -> fail-open. FABLE_ESCALATION=on disables the ceiling. Inert unless a `.fable/` dir is found. Fail-open on any error. Exit codes: 0 = allow the tool call; 2 = block (stderr shown to Claude). Env: FABLE_SPAWN_MIN_CHARS minimum payload length to be considered "detailed" (default 1500). Below this, the design gate passes. FABLE_ESCALATION "on" allows spawning above the session model. """ import os import re 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, model_tier, load_session_model, ) def payload_len(tool_name, tool_input): if not isinstance(tool_input, dict): return 0 parts = [] for key in ("prompt", "script", "description"): v = tool_input.get(key) if isinstance(v, str): parts.append(v) return len("\n".join(parts)) def is_fork(tool_input): if not isinstance(tool_input, dict): return False for key in ("subagent_type", "agentType", "agent_type"): v = tool_input.get(key) if isinstance(v, str) and "fork" in v.lower(): return True return False # Key-prefixed on purpose: matches model: 'fable' / model="claude-fable-5" # inside a Workflow script, but can NOT false-positive on prose like # "fable-mode" (no model key in front of it). _SCRIPT_MODEL_RE = re.compile(r"""model\s*[:=]\s*['"]([^'"]+)['"]""") def requested_tier(tool_name, tool_input): """Strongest model tier explicitly requested by this spawn, or None.""" if not isinstance(tool_input, dict): return None tiers = [] v = tool_input.get("model") if isinstance(v, str): t = model_tier(v) if t is not None: tiers.append((t, v)) if tool_name == "Workflow": s = tool_input.get("script") if isinstance(s, str): for match in _SCRIPT_MODEL_RE.finditer(s): t = model_tier(match.group(1)) if t is not None: tiers.append((t, match.group(1))) return max(tiers) if tiers else None def main(): data = read_hook_input() tool_name = data.get("tool_name", "") tool_input = data.get("tool_input", {}) or {} fable_dir = find_fable_dir(start_dir(data)) if not fable_dir: return 0 # project not opted in -> inert # Model ceiling — checked before all exemptions: even a small spawn or a # fork must not request a model stronger than the session's. if os.environ.get("FABLE_ESCALATION", "auto").lower() != "on": sess_model = load_session_model(data.get("session_id")) sess_tier = model_tier(sess_model) req = requested_tier(tool_name, tool_input) if sess_tier is not None and req is not None and req[0] > sess_tier: sys.stderr.write( "[fable-mode] BLOCKED: this spawn requests model '%s', which is " "STRONGER than the session model '%s'. fable-mode's purpose is " "Fable-5-grade results WITHOUT reaching upward — the escalation " "ceiling is the session model. Omit the model param to inherit, " "or pull the card back inline. Set FABLE_ESCALATION=on only if " "upward deferral is genuinely intended.\n" % (req[1], sess_model) ) return 2 if is_fork(tool_input): return 0 # forks inherit full context; exempt from the spec tax try: threshold = int(os.environ.get("FABLE_SPAWN_MIN_CHARS", "1500")) except ValueError: threshold = 1500 if payload_len(tool_name, tool_input) < threshold: return 0 # small delegation -> exempt _open, has_any, paused = parse_ledger(ledger_path(fable_dir)) if paused: return 0 # round paused -> design gate off (ceiling stayed active above) if _open: return 0 # a live round with open cards -> allowed # No OPEN cards: either no ledger at all, or only closed cards from a # finished round. Stale closed cards must not unlock new fan-out — a # detailed delegation is round work, so it needs a live card first. sys.stderr.write( "[fable-mode] BLOCKED: this project is in fable-mode (.fable/ present) " "but .fable/LEDGER.md has no OPEN task cards for a current round" " (closed cards from a finished round don't count).\n" "Before fanning out a detailed subagent/Workflow, write the design gate:\n" " 1. docs/SPEC.md -- requirements + approach + task-card list\n" " 2. .fable/LEDGER.md -- one checkbox per card:\n" " - [ ] 1. <card> (each card needs a machine-checkable acceptance test)\n" "Close the load-bearing unknowns with targeted probes first and tag SPEC " "decisions [measured]/[inferred]/[not-shown]; cards close only with an " "`-- evidence:` note.\n" "Starter skeletons: <skill-dir>/templates/ (SPEC/LEDGER/PROGRESS).\n" "Then retry the delegation. (Small spawns < %d chars and forks are exempt.)\n" % threshold ) return 2 if __name__ == "__main__": try: sys.exit(main()) except Exception as e: # fail-open: n
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withfable5-mode
Make Opus 4.8 — or any Claude model — work like Claude Fable 5. A Claude Code skill plus guard hooks that give a non-frontier model Fable-5-grade work discipline: plan-gate, self-verification, and sub-agent routing, enforced mechanically.
Get the whole plugin

