Skip to content

Cross-platform install + activation that actually fires: auto-mode, /fable, --ultra, fable doctor#3

Open
denfry wants to merge 13 commits into
HalalifyMusic:mainfrom
denfry:cross-platform-install
Open

Cross-platform install + activation that actually fires: auto-mode, /fable, --ultra, fable doctor#3
denfry wants to merge 13 commits into
HalalifyMusic:mainfrom
denfry:cross-platform-install

Conversation

@denfry

@denfry denfry commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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 needs effort in the hook payload or CLAUDE_EFFORT in 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:

  • the fable launcher declares the mode with FABLE_MODE=1, and fable-trigger.py became dual-event — SessionStart injection works on every Claude Code version (startup/clear/compact always inject; resume respects the session marker);
  • a complexity heuristic (ru+en task verbs, code fences, file paths, multi-step markers, length) auto-loads the playbook for task-shaped prompts in any session, once per session — FABLE_AUTO=0 opts out;
  • a /fable skill 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 new fable-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 via grounding-verifier, plan-gating, calibration).
  • One-command cross-platform installer (install.py, with .sh/.ps1 wrappers), an uninstaller that surgically reverses it, settings merge with absolute interpreter/hook paths, line-ending normalization.
  • test-after-edit hardeningFABLE_TEST_HOOK_ALLOW trust allowlist and a per-project .fable-test command 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 doctor reports 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:

  • The clone is deletable now. Launchers are copied into ~/.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, and fable doctor falls back across python/py/python3.
  • Compaction no longer kills auto-activated sessions. The session marker proves the mode was active, so SessionStart re-injects on compact even without FABLE_MODE, and drops the marker on clear so the once-per-session guard re-arms. A session already activated via the /fable skill is detected in the transcript and never double-injected.
  • User skills survive the installer. Bundled skills carry a .fable-mode-bundled marker; 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 doctor checks the most fragile link. Launcher lines in shell profiles (a line sourcing a missing file is a hard failure), hooks registered in settings.local.json, and a claude --help probe for --effort / --append-system-prompt-file (understanding the [-file] bracket shorthand).
  • The playbook no longer points at the author's personal files (~/Downloads/..., measurement scripts) — provenance notes now, guarded by tests/test_content.py — and the /fable skill's fallback points at fable doctor instead of a nonexistent "skill's repository".
  • Smaller fixes: test-after-edit.py skips 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.

denfry added 13 commits July 3, 2026 09:35
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error: Invalid JSON provided for --settings

1 participant