fix(setup): stop baking a version-pinned path into CLAUDE.md - #3
Merged
Conversation
setup.sh substituted __PLUGIN_ROOT__ with $CLAUDE_PLUGIN_ROOT, which is
.../cache/<marketplace>/<plugin>/<version>. After a plugin upgrade that
directory is gone and every path in the injected block dangles, with
nothing to notice or repair it — setup does not re-run on its own.
Observed live: after updating 2.1.1 -> 2.1.2 the block still pointed at
.../fablize/2.1.1/scripts/goals.py and .../2.1.1/packs/*.txt. It only
kept working because the old version directory happened to still be on
disk; removing it would have broken every routing instruction silently.
setup.sh now copies scripts/goals.py and packs/*.txt to ~/.fablize/lib/
and injects that stable path, so the block survives upgrades. The hooks
were never affected — hooks.json resolves ${CLAUDE_PLUGIN_ROOT} at load
time, so it is always the running version.
Also: the version written to progress.json was a string literal that
silently disagreed with the installed plugin after every release. It is
now read from .claude-plugin/plugin.json.
That field was written and never read. gate_prompt.stale_setup_notice()
now compares it against the running plugin's manifest and prepends one
line when they differ, pointing at /fablize:setup — the only thing that
can refresh the copies and the block. Silent when they match, and silent
on any error, so it can never block a prompt.
tests/test_setup_paths.py — 17 checks driving the real setup.sh against a
temp HOME and a fake versioned plugin root: no version in the injected
block, every referenced asset exists, assets refresh on re-run after an
upgrade, the block is not duplicated, and the notice fires only on a
mismatch. The subprocess call pins encoding=utf-8 because the console
codepage here (cp949) cannot decode the script's own output.
test_shadow_m4.py's deep->effort wiring guard caught the first draft of
the docstring, which used the word "best-effort" in a gate decision file.
Reworded.
tests/test_setup_paths.py all 17 checks match
tests/test_gate_classification.py PASS
tests/test_gate_false_positive.py PASS
tests/test_gate.py PASS
tests/test_gate_robustness.py PASS
tests/test_recovery.py PASS
tests/test_shadow.py PASS
tests/test_shadow_m3.py PASS
tests/test_shadow_m4.py PASS
Release 2.1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Finding fivetaku#13 from the full-repo review, and it fired for real during this upgrade.
Problem
setup.shsubstituted__PLUGIN_ROOT__with$CLAUDE_PLUGIN_ROOT, which is.../cache/<marketplace>/<plugin>/<version>. After a plugin upgrade that directory is gone and every path in the injected CLAUDE.md block dangles — and setup never re-runs on its own to repair it.Observed live: after updating
2.1.1→2.1.2the block still readIt only kept working because the old version directory happened to still be on disk. Deleting it would have broken every routing instruction in the block, silently.
The hooks were never affected —
hooks.jsonresolves${CLAUDE_PLUGIN_ROOT}at load time, so it always points at the running version. Only the injected text was pinned.Change
setup.shcopiesscripts/goals.pyandpacks/*.txtto~/.fablize/lib/and injects that stable path. Re-running setup refreshes the copies in place.progress.jsonis read from.claude-plugin/plugin.jsoninstead of a string literal that silently disagreed with the installed plugin after every release.gate_prompt.stale_setup_notice()— thatversionfield was written and never read. It now compares against the running plugin's manifest and prepends one line when they differ, pointing at/fablize:setup, which is the only thing that can refresh the copies and the block. Silent when they match, and silent on any error, so it can never block a prompt.Tests
tests/test_setup_paths.py— 17 checks driving the realsetup.shagainst a tempHOMEand a fake versioned plugin root:cache/<mkt>/<plugin>/<version>path survives in the injected block9.9.9→9.9.10)progress.jsonis unreadableThe subprocess call pins
encoding="utf-8"because the console codepage here (cp949) cannot decode the script's own✓output — the default would raise instead of running.test_shadow_m4.py's deep→effort wiring guard caught the first draft of the docstring, which used the word "best-effort" in a gate decision file. Reworded — the guard did its job.Note for existing installs
An install that already has a pinned block needs
/fablize:setupre-run once to switch to the stable path. The new notice makes that visible on the next prompt after an upgrade.Release 2.1.3.