Cross-platform install + activation that actually fires: auto-mode, /fable, --ultra, fable doctor#3
Open
denfry wants to merge 13 commits into
Open
Cross-platform install + activation that actually fires: auto-mode, /fable, --ultra, fable doctor#3denfry wants to merge 13 commits into
denfry wants to merge 13 commits into
Conversation
- test-after-edit: FABLE_TEST_HOOK_ALLOW trust allowlist and a per-project .fable-test override so the hook only auto-runs commands in repos you trust (documented in SECURITY.md, covered by new tests) - merge_settings: write the pristine settings.json.bak only when absent so re-installs can't destroy the original restore point - close leaked file handles in merge_settings, uninstall and the test hook - launcher defaults to --effort xhigh; ultracode becomes an explicit opt-in
… injection The playbook injector had never fired: the effort path needs effort in the hook payload or CLAUDE_EFFORT in the hook env (absent before Claude Code 2.1.199), and the trigger phrases were undiscoverable. - fable-trigger.py is dual-event: SessionStart injects when the launcher declares FABLE_MODE=1 (startup/clear/compact always, resume respects the session marker); UserPromptSubmit keeps phrase/effort paths and gains a FABLE_MODE once-per-session fallback - launcher pins --model claude-opus-4-8 and appends the new fable-code.md, an original Claude Code-native distillation of Fable's terminal behavior (final-message contract, readable-over-concise, tool discipline, autonomy); the consumer prompt stays bundled for reference - new /fable skill for explicit mid-session activation - SessionStart registration in merge_settings + settings.fragment.json; uninstall strips it; round-trip uninstall test; playbook voice layer gains the Claude Code corrections
- fable-trigger.py scores prompt complexity (ru+en task verbs, code fences, file paths, multi-step markers, length; >= 2 points) and loads the playbook by itself for task-shaped prompts, once per session, in any session; opt out with FABLE_AUTO=0 - fable --ultra / -u launches with ultracode auto-orchestration; the playbook gains an Orchestration section (fan-out, adversarial verification via grounding-verifier, plan-gating, calibration) - fable doctor (hooks/fable-doctor.py) verifies the whole chain: files, registered hooks, interpreter paths, Claude Code version, a live-fire injection test, transcript evidence; FABLE_DOCTOR_SKIP_CLI=1 for offline runs - the /fable skill triggers proactively on non-trivial tasks and skips the file reads when the playbook is already in context
Windows PowerShell 5.1 strips embedded quotes when building the native
command line, so the inline --settings JSON reached claude as
{ultracode: true} and failed validation. Reproduced end to end by driving
fable.ps1 through powershell.exe with an argv-capturing fake claude.
Pass a settings file instead of inline JSON: ship
shell/ultracode.settings.json, install it to ~/.claude, and point both
launchers --ultra path at it — a file path survives quoting on every
shell and PowerShell version. The doctor now checks the file, and
tests/test_launchers.py guards the regression (static no-inline-JSON
check plus a live PowerShell 5.1 argv test).
Fixes HalalifyMusic#2
GitHub's macOS runners ship a 'powershell' binary that is just pwsh, so the which() guard alone let the test run on macOS, where the cmd-based fake claude can't execute. Windows PowerShell 5.1 only exists on Windows; gate the test on os.name too.
Without the launcher, SessionStart now re-injects the playbook on source=compact whenever the session marker proves the mode was active (the auto/effort paths previously lost it forever), and drops the marker on source=clear so the once-per-session guard re-arms. The heavy path scans the transcript for a prior "Fable mode active" line, so a session already activated via the /fable skill doesn't get a second copy. Also: more ru/en task verbs in the auto heuristic (update/deploy/обнови/ запусти etc.) and week-old marker GC for %TEMP%.
A failed Edit changes nothing on disk, so running the suite would only report a stale result - bail out when tool_response.success is false. Debounce stamps older than a week are pruned; Windows never clears %TEMP%, so they used to accumulate forever.
The shell profile used to source the launcher straight from the clone: deleting or moving the checkout broke every new shell, and a re-install from a new path silently kept the stale line. install.py now copies fable.ps1/fable.zsh into ~/.claude/shell, sources the stable copy from the profile (both PowerShell 7 and 5.1 when present), and replaces stale launcher lines instead of keeping them. Skills are installed with a .fable-mode-bundled marker; a same-named directory without the marker is the user's own work and is preserved under ~/.claude/backups/skills instead of being rmtree'd. uninstall.py removes only marked directories, strips the launcher from every profile, and deletes the ~/.claude/shell copies. fable.ps1 doctor now falls back across python/py/python3 instead of hardcoding python.
The most fragile link was unchecked: a profile line sourcing a launcher file that no longer exists breaks every new shell - that is now a hard failure, while no launcher line at all is only a warning. Hooks registered in settings.local.json count as registered. The CLI probe also greps claude --help for --effort / --append-system-prompt-file (understanding the --append-system-prompt[-file] bracket shorthand) and the installed ~/.claude/shell launcher copies joined CORE_FILES.
FABLE_PLAYBOOK.md is injected verbatim into sessions, yet it instructed the model to read files that only ever existed on the author's machine (~/Downloads/Fable_Mindset_public.md, ~/compare_models.py, ~/fable_dataset_delta.py, reference/llm-bias-awareness.md) - every user session risked a failing Read. They are provenance notes now, and stop-slop//code-review are no longer described as bundled. The /fable skill's fallback pointed at "this skill's repository", which does not exist next to the installed SKILL.md; it now points at fable doctor / re-running the installer. tests/test_content.py guards both.
npm installs the claude CLI as a .cmd shim on Windows, and CreateProcess cannot spawn those by bare name or path - the probe failed with WinError 2 on such installs (and on CI, where the fake test CLI is a .cmd on purpose). The probe now resolves the executable via shutil.which and routes .cmd/.bat through cmd.exe. The fake CLI in the test also reports a deliberately unreal version (2.1.777) so a pass proves the fake answered, not a claude that happens to be installed on the host.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch does two things: makes the install work the same on every OS, and makes the mode itself actually fire — which, as it turns out, it never did.
The headline fix
The playbook injector (
fable-trigger.py) had never fired in a real session. Its effort path needseffortin the hook payload orCLAUDE_EFFORTin the hook environment — Claude Code only started providing those in 2.1.199 — and nobody types "use fable" unprompted. Checked the transcripts: zero injections, ever. The core promise of the repo was silently a no-op.Activation is now redundant by design:
fablelauncher declares the mode withFABLE_MODE=1, andfable-trigger.pybecame dual-event — SessionStart injection works on every Claude Code version (startup/clear/compact always inject; resume respects the session marker);FABLE_AUTO=0opts out;/fableskill activates it explicitly mid-session, and the old phrase/effort paths still work.The launcher also pins
--model claude-opus-4-8(the README's promise, now true regardless of your default model) and appends the newfable-code.md— an original Claude-Code-native distillation of Fable's terminal behavior (final-message contract, readable-over-concise, tool discipline, autonomy rules) instead of the 1,600-line consumer prompt, which stays bundled for reference.Also here
fable doctor— one command that checks the whole chain: files, registered hooks, interpreter paths, Claude Code version, a live-fire injection test, and transcript evidence of past activations. It would have caught the silent no-op above in a second.fable --ultra— ultracode auto-orchestration behind a flag, plus an Orchestration section in the playbook (fan-out, adversarial verification viagrounding-verifier, plan-gating, calibration).install.py, with.sh/.ps1wrappers), an uninstaller that surgically reverses it, settings merge with absolute interpreter/hook paths, line-ending normalization.FABLE_TEST_HOOK_ALLOWtrust allowlist and a per-project.fable-testcommand override (documented in SECURITY.md).Testing
32 pytest tests (trigger, doctor, installer, uninstaller round-trip, test-after-edit), green on every commit of this branch; CI covers ubuntu/macos/windows × Python 3.9/3.12. Verified end to end on a live install:
fable doctorreports healthy with 0 warnings, and a launcher-equivalent session shows the playbook injected at SessionStart.Update: hardening pass (six follow-up commits)
A full review of the activation chain surfaced four real gaps plus a second tier of robustness issues; all fixed, each commit green in isolation:
~/.claude/shell/and the profile sources that stable copy; a stale line pointing at an old clone path is replaced on re-install instead of silently kept. On Windows both PowerShell 7 and Windows PowerShell 5.1 profiles get the launcher, andfable doctorfalls back acrosspython/py/python3.compacteven withoutFABLE_MODE, and drops the marker onclearso the once-per-session guard re-arms. A session already activated via the/fableskill is detected in the transcript and never double-injected..fable-mode-bundledmarker; a same-named directory the user owns is preserved under~/.claude/backups/skills/instead of being rmtree'd, and uninstall removes only marked directories.fable doctorchecks the most fragile link. Launcher lines in shell profiles (a line sourcing a missing file is a hard failure), hooks registered insettings.local.json, and aclaude --helpprobe for--effort/--append-system-prompt-file(understanding the[-file]bracket shorthand).~/Downloads/..., measurement scripts) — provenance notes now, guarded bytests/test_content.py— and the/fableskill's fallback points atfable doctorinstead of a nonexistent "skill's repository".test-after-edit.pyskips the run when the edit itself failed and GCs its%TEMP%markers; the auto-heuristic learned more everyday ru/en task verbs.Testing is now 57 pytest tests (up from 32); the six new commits were each verified green in isolation via git worktrees, and a live Windows re-install ends with
fable doctor: healthy, 0 warnings.Fixes #2.