From 533c414956087a7bb47fd8998cbcafd0f60561c8 Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 23 Aug 2026 21:34:27 -0500 Subject: [PATCH 1/5] fix(plugins): ship the claude-code hook version bumps with releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/update_versions.py bumps the claude-code hook shims' basic-memory dependency floor, but the release recipe's git add list only staged the codex copies — the v0.23.0 release left both files dirty in the working tree instead of on the bump commit. Stage them in the recipe and land the stranded v0.23.0 bumps. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9 Signed-off-by: phernandez --- plugins/claude-code/hooks/pre_compact.py | 2 +- plugins/claude-code/hooks/session_start.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/claude-code/hooks/pre_compact.py b/plugins/claude-code/hooks/pre_compact.py index bdb002fd4..d0662f866 100755 --- a/plugins/claude-code/hooks/pre_compact.py +++ b/plugins/claude-code/hooks/pre_compact.py @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --quiet --script # /// script # requires-python = ">=3.12" -# dependencies = ["basic-memory>=0.22.1"] +# dependencies = ["basic-memory>=0.23.0"] # /// """PreCompact hook — the entire hook. All logic (settings resolution, the extractive checkpoint note, lifecycle-envelope capture) lives in the released diff --git a/plugins/claude-code/hooks/session_start.py b/plugins/claude-code/hooks/session_start.py index 2dc789223..694d88376 100755 --- a/plugins/claude-code/hooks/session_start.py +++ b/plugins/claude-code/hooks/session_start.py @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --quiet --script # /// script # requires-python = ">=3.12" -# dependencies = ["basic-memory>=0.22.1"] +# dependencies = ["basic-memory>=0.23.0"] # /// """SessionStart hook — the entire hook. All logic (settings resolution, the context brief, lifecycle-envelope capture) lives in the released basic-memory From f9a45cc437b950a3ddeac8bcf2e5ca2485da1d9f Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 23 Aug 2026 21:34:51 -0500 Subject: [PATCH 2/5] fix(plugins): stage claude-code hooks in the release and beta recipes Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9 Signed-off-by: phernandez --- justfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/justfile b/justfile index 13842d03d..da1da1c12 100644 --- a/justfile +++ b/justfile @@ -706,6 +706,8 @@ release version: .claude-plugin/marketplace.json \ plugins/claude-code/.claude-plugin/plugin.json \ plugins/claude-code/.claude-plugin/marketplace.json \ + plugins/claude-code/hooks/session_start.py \ + plugins/claude-code/hooks/pre_compact.py \ plugins/codex/.codex-plugin/plugin.json \ plugins/codex/hooks/session_start.py \ plugins/codex/hooks/pre_compact.py \ @@ -826,6 +828,8 @@ beta version: .claude-plugin/marketplace.json \ plugins/claude-code/.claude-plugin/plugin.json \ plugins/claude-code/.claude-plugin/marketplace.json \ + plugins/claude-code/hooks/session_start.py \ + plugins/claude-code/hooks/pre_compact.py \ plugins/codex/.codex-plugin/plugin.json \ plugins/codex/hooks/session_start.py \ plugins/codex/hooks/pre_compact.py \ From 285467e7e9ce0d07d3714be10d79b4b0bcdda30d Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 23 Aug 2026 21:40:28 -0500 Subject: [PATCH 3/5] fix(plugins): allow pre-release resolution in hook shims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit basic-memory 0.23.0 depends on fastmcp==4.0.0b1, and older uv versions (e.g. 0.8.4) refuse pre-release transitive dependencies: plain 'uv pip install basic-memory' silently resolves 0.22.1 and the hook shims fail resolution entirely, which the fail-open contract turns into the #1210 silent no-op. Current uv (0.12.x) resolves fine. A [tool.uv] prerelease = "allow" block in each shim's script metadata makes resolution work on both: verified live under uv 0.8.4 — the SessionStart shim resolved PyPI 0.23.0 and printed the full brief. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9 Signed-off-by: phernandez --- plugins/claude-code/hooks/pre_compact.py | 3 +++ plugins/claude-code/hooks/session_start.py | 3 +++ plugins/codex/hooks/pre_compact.py | 3 +++ plugins/codex/hooks/session_start.py | 3 +++ 4 files changed, 12 insertions(+) diff --git a/plugins/claude-code/hooks/pre_compact.py b/plugins/claude-code/hooks/pre_compact.py index d0662f866..8d565b5ef 100755 --- a/plugins/claude-code/hooks/pre_compact.py +++ b/plugins/claude-code/hooks/pre_compact.py @@ -2,6 +2,9 @@ # /// script # requires-python = ">=3.12" # dependencies = ["basic-memory>=0.23.0"] +# +# [tool.uv] +# prerelease = "allow" # /// """PreCompact hook — the entire hook. All logic (settings resolution, the extractive checkpoint note, lifecycle-envelope capture) lives in the released diff --git a/plugins/claude-code/hooks/session_start.py b/plugins/claude-code/hooks/session_start.py index 694d88376..66674eaff 100755 --- a/plugins/claude-code/hooks/session_start.py +++ b/plugins/claude-code/hooks/session_start.py @@ -2,6 +2,9 @@ # /// script # requires-python = ">=3.12" # dependencies = ["basic-memory>=0.23.0"] +# +# [tool.uv] +# prerelease = "allow" # /// """SessionStart hook — the entire hook. All logic (settings resolution, the context brief, lifecycle-envelope capture) lives in the released basic-memory diff --git a/plugins/codex/hooks/pre_compact.py b/plugins/codex/hooks/pre_compact.py index 06a1f490d..71be4778a 100755 --- a/plugins/codex/hooks/pre_compact.py +++ b/plugins/codex/hooks/pre_compact.py @@ -4,6 +4,9 @@ # dependencies = [ # "basic-memory @ git+https://github.com/basicmachines-co/basic-memory@v0.23.0", # ] +# +# [tool.uv] +# prerelease = "allow" # /// """PreCompact hook launcher backed by a pinned Basic Memory revision. diff --git a/plugins/codex/hooks/session_start.py b/plugins/codex/hooks/session_start.py index ac7d94c9c..2007210a0 100755 --- a/plugins/codex/hooks/session_start.py +++ b/plugins/codex/hooks/session_start.py @@ -4,6 +4,9 @@ # dependencies = [ # "basic-memory @ git+https://github.com/basicmachines-co/basic-memory@v0.23.0", # ] +# +# [tool.uv] +# prerelease = "allow" # /// """SessionStart hook launcher backed by a pinned Basic Memory revision. From 30875d7ccf4a4ba75ace91508744bee0a7c41dbb Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 23 Aug 2026 22:13:48 -0500 Subject: [PATCH 4/5] fix(plugins): pin fastmcp explicitly instead of allowing all pre-releases A blanket [tool.uv] prerelease = 'allow' would let the stable shims resolve a future basic-memory beta. Declaring the exact fastmcp beta as a direct dependency scopes the pre-release opt-in to the one package that needs it: old uv resolves basic-memory 0.23.0 (verified live under uv 0.8.4), and basic-memory itself stays stable-only. The version updater, validator, and floor tests follow the new multi-line form. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9 Signed-off-by: phernandez --- plugins/claude-code/hooks/pre_compact.py | 12 ++++++++---- plugins/claude-code/hooks/session_start.py | 12 ++++++++---- plugins/claude-code/hooks/test_claude_pre_compact.py | 2 +- .../claude-code/hooks/test_claude_session_start.py | 2 +- plugins/codex/hooks/pre_compact.py | 7 ++++--- plugins/codex/hooks/session_start.py | 7 ++++--- scripts/update_versions.py | 4 ++-- scripts/validate_claude_plugin.py | 2 +- tests/test_claude_plugin_hooks.py | 2 +- tests/test_update_versions.py | 5 ++++- 10 files changed, 34 insertions(+), 21 deletions(-) diff --git a/plugins/claude-code/hooks/pre_compact.py b/plugins/claude-code/hooks/pre_compact.py index 8d565b5ef..69c550cea 100755 --- a/plugins/claude-code/hooks/pre_compact.py +++ b/plugins/claude-code/hooks/pre_compact.py @@ -1,10 +1,14 @@ #!/usr/bin/env -S uv run --quiet --script # /// script # requires-python = ">=3.12" -# dependencies = ["basic-memory>=0.23.0"] -# -# [tool.uv] -# prerelease = "allow" +# dependencies = [ +# "basic-memory>=0.23.0", +# # Direct pre-release pin so old uv resolves basic-memory (which +# # requires this exact beta transitively) without enabling +# # pre-releases for basic-memory itself. Keep in lockstep with +# # core pyproject's fastmcp pin. +# "fastmcp==4.0.0b1", +# ] # /// """PreCompact hook — the entire hook. All logic (settings resolution, the extractive checkpoint note, lifecycle-envelope capture) lives in the released diff --git a/plugins/claude-code/hooks/session_start.py b/plugins/claude-code/hooks/session_start.py index 66674eaff..d3c9203fc 100755 --- a/plugins/claude-code/hooks/session_start.py +++ b/plugins/claude-code/hooks/session_start.py @@ -1,10 +1,14 @@ #!/usr/bin/env -S uv run --quiet --script # /// script # requires-python = ">=3.12" -# dependencies = ["basic-memory>=0.23.0"] -# -# [tool.uv] -# prerelease = "allow" +# dependencies = [ +# "basic-memory>=0.23.0", +# # Direct pre-release pin so old uv resolves basic-memory (which +# # requires this exact beta transitively) without enabling +# # pre-releases for basic-memory itself. Keep in lockstep with +# # core pyproject's fastmcp pin. +# "fastmcp==4.0.0b1", +# ] # /// """SessionStart hook — the entire hook. All logic (settings resolution, the context brief, lifecycle-envelope capture) lives in the released basic-memory diff --git a/plugins/claude-code/hooks/test_claude_pre_compact.py b/plugins/claude-code/hooks/test_claude_pre_compact.py index 6c44ceec6..ec2cb62c2 100644 --- a/plugins/claude-code/hooks/test_claude_pre_compact.py +++ b/plugins/claude-code/hooks/test_claude_pre_compact.py @@ -119,7 +119,7 @@ def test_dependency_floor_matches_package_version() -> None: # scripts/update_versions.py bumps this line at release; drift between the # script floor and the package version fails here, before a release lands. text = SCRIPT.read_text(encoding="utf-8") - floors = re.findall(r'^# dependencies = \["basic-memory>=([^"]+)"\]$', text, re.MULTILINE) + floors = re.findall(r'^# "basic-memory>=([^"]+)",$', text, re.MULTILINE) assert floors == [__version__] diff --git a/plugins/claude-code/hooks/test_claude_session_start.py b/plugins/claude-code/hooks/test_claude_session_start.py index 8d5c9110e..460c19efc 100644 --- a/plugins/claude-code/hooks/test_claude_session_start.py +++ b/plugins/claude-code/hooks/test_claude_session_start.py @@ -119,7 +119,7 @@ def test_dependency_floor_matches_package_version() -> None: # scripts/update_versions.py bumps this line at release; drift between the # script floor and the package version fails here, before a release lands. text = SCRIPT.read_text(encoding="utf-8") - floors = re.findall(r'^# dependencies = \["basic-memory>=([^"]+)"\]$', text, re.MULTILINE) + floors = re.findall(r'^# "basic-memory>=([^"]+)",$', text, re.MULTILINE) assert floors == [__version__] diff --git a/plugins/codex/hooks/pre_compact.py b/plugins/codex/hooks/pre_compact.py index 71be4778a..d4fb4de88 100755 --- a/plugins/codex/hooks/pre_compact.py +++ b/plugins/codex/hooks/pre_compact.py @@ -3,10 +3,11 @@ # requires-python = ">=3.12" # dependencies = [ # "basic-memory @ git+https://github.com/basicmachines-co/basic-memory@v0.23.0", +# # Direct pre-release pin so old uv resolves basic-memory (which +# # requires this exact beta transitively) without enabling +# # pre-releases broadly. Keep in lockstep with core pyproject. +# "fastmcp==4.0.0b1", # ] -# -# [tool.uv] -# prerelease = "allow" # /// """PreCompact hook launcher backed by a pinned Basic Memory revision. diff --git a/plugins/codex/hooks/session_start.py b/plugins/codex/hooks/session_start.py index 2007210a0..a7b0e271a 100755 --- a/plugins/codex/hooks/session_start.py +++ b/plugins/codex/hooks/session_start.py @@ -3,10 +3,11 @@ # requires-python = ">=3.12" # dependencies = [ # "basic-memory @ git+https://github.com/basicmachines-co/basic-memory@v0.23.0", +# # Direct pre-release pin so old uv resolves basic-memory (which +# # requires this exact beta transitively) without enabling +# # pre-releases broadly. Keep in lockstep with core pyproject. +# "fastmcp==4.0.0b1", # ] -# -# [tool.uv] -# prerelease = "allow" # /// """SessionStart hook launcher backed by a pinned Basic Memory revision. diff --git a/scripts/update_versions.py b/scripts/update_versions.py index e3e3d4cb0..3cff5a982 100644 --- a/scripts/update_versions.py +++ b/scripts/update_versions.py @@ -153,8 +153,8 @@ def _update_packages(version: str, *, dry_run: bool) -> None: for script in HOOK_SCRIPTS: update_text( script, - r'^# dependencies = \["basic-memory>=[^"]+"\]$', - f'# dependencies = ["basic-memory>={version}"]', + r'^# "basic-memory>=[^"]+",$', + f'# "basic-memory>={version}",', dry_run=dry_run, ) update_text( diff --git a/scripts/validate_claude_plugin.py b/scripts/validate_claude_plugin.py index e8fe10059..2f72781b4 100644 --- a/scripts/validate_claude_plugin.py +++ b/scripts/validate_claude_plugin.py @@ -145,7 +145,7 @@ def validate_claude_plugin(plugin_dir: Path) -> None: raise SystemExit(f"Hook script is not executable: {script}") text = script.read_text(encoding="utf-8") if "# /// script" not in text or not re.search( - r'^# dependencies = \["basic-memory>=[^"]+"\]$', text, re.MULTILINE + r'^# "basic-memory>=[^"]+",$', text, re.MULTILINE ): raise SystemExit(f"Hook script missing PEP 723 basic-memory floor: {script}") diff --git a/tests/test_claude_plugin_hooks.py b/tests/test_claude_plugin_hooks.py index 2c3fea411..552bd173c 100644 --- a/tests/test_claude_plugin_hooks.py +++ b/tests/test_claude_plugin_hooks.py @@ -80,7 +80,7 @@ def test_claude_script_floor_matches_released_version(event: str, script_name: s # Release drift between the PEP 723 floor and the package version fails # here (and in each script's co-located test) before a release lands. text = (REPO_ROOT / "plugins/claude-code/hooks" / script_name).read_text(encoding="utf-8") - floors = re.findall(r'^# dependencies = \["basic-memory>=([^"]+)"\]$', text, re.MULTILINE) + floors = re.findall(r'^# "basic-memory>=([^"]+)",$', text, re.MULTILINE) assert floors == [CURRENT_VERSION] diff --git a/tests/test_update_versions.py b/tests/test_update_versions.py index d0fbd07c3..06b3cbda9 100644 --- a/tests/test_update_versions.py +++ b/tests/test_update_versions.py @@ -35,7 +35,10 @@ def test_parse_version_preserves_python_prerelease_for_non_npm_manifests() -> No "#!/usr/bin/env -S uv run --quiet --script\n" "# /// script\n" '# requires-python = ">=3.12"\n' - '# dependencies = ["basic-memory>=0.0.0"]\n' + "# dependencies = [\n" + '# "basic-memory>=0.0.0",\n' + '# "fastmcp==4.0.0b1",\n' + "# ]\n" "# ///\n" # Mirrors the real scripts: the docstring mentions a launcher spelling # without a version spec, which the anchored updater pattern must skip. From 176250a9c7e17a684922d8f6282768292e5ebd64 Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 23 Aug 2026 22:40:24 -0500 Subject: [PATCH 5/5] test(plugins): enforce fastmcp pin lockstep with core pyproject The shim pins are rewritten by nothing automatic, so drift against core's fastmcp requirement would pass package checks and fail at shim resolution time. The Claude plugin validator now compares each shim pin against pyproject.toml, and a codex-side test does the same. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9 Signed-off-by: phernandez --- scripts/validate_claude_plugin.py | 22 ++++++++++++++++++++++ tests/test_codex_plugin_package.py | 18 ++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/scripts/validate_claude_plugin.py b/scripts/validate_claude_plugin.py index 2f72781b4..0af19e7ad 100644 --- a/scripts/validate_claude_plugin.py +++ b/scripts/validate_claude_plugin.py @@ -137,6 +137,20 @@ def validate_claude_plugin(plugin_dir: Path) -> None: for event in REQUIRED_HOOK_EVENTS: if event not in hooks: raise SystemExit(f"hooks/hooks.json: missing {event} hook") + # Trigger: the shims pin core's exact fastmcp beta so older uv can resolve + # basic-memory (pre-release transitives are refused there) without + # enabling pre-releases for basic-memory itself. + # Why: nothing else keeps the shim pin and core's pyproject in lockstep, + # and drift makes shim resolution conflict — which the fail-open contract + # turns into a silent hook no-op. + # Outcome: validation fails loudly when core moves its fastmcp pin without + # the shims following. + core_pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + core_match = re.search(r'^\s*"fastmcp==([^"]+)",$', core_pyproject, re.MULTILINE) + if not core_match: + raise SystemExit("pyproject.toml: no exact fastmcp pin found") + core_fastmcp_pin = core_match.group(1) + for rel in REQUIRED_HOOK_SCRIPTS: script = plugin_dir / rel if not script.exists(): @@ -148,6 +162,14 @@ def validate_claude_plugin(plugin_dir: Path) -> None: r'^# "basic-memory>=[^"]+",$', text, re.MULTILINE ): raise SystemExit(f"Hook script missing PEP 723 basic-memory floor: {script}") + shim_pins = re.findall(r'^# "fastmcp==([^"]+)",$', text, re.MULTILINE) + if len(shim_pins) != 1: + raise SystemExit(f"Hook script missing exact fastmcp pin: {script}") + if shim_pins[0] != core_fastmcp_pin: + raise SystemExit( + f"{script}: fastmcp pin {shim_pins[0]} does not match " + f"core pyproject pin {core_fastmcp_pin}" + ) # --- Output style --- output_style = plugin_dir / "output-styles/basic-memory.md" diff --git a/tests/test_codex_plugin_package.py b/tests/test_codex_plugin_package.py index 8e2eb3d82..4c9f5b32c 100644 --- a/tests/test_codex_plugin_package.py +++ b/tests/test_codex_plugin_package.py @@ -386,3 +386,21 @@ def test_pr_create_skill_delegates_to_current_pr_workflow() -> None: assert "never merges" in skill assert "Do not enable auto-merge" in skill assert "current-head gate" in skill + + +def test_codex_hook_fastmcp_pins_match_core_pyproject() -> None: + """The shims pin core's exact fastmcp beta; drift breaks shim resolution. + + Old uv refuses pre-release transitives, so each shim carries the pin as a + direct dependency. Nothing rewrites it automatically — this test is the + lockstep enforcement when core's pyproject moves its fastmcp pin. + """ + repo_root = Path(__file__).resolve().parents[1] + core = re.search( + r'^\s*"fastmcp==([^"]+)",$', (repo_root / "pyproject.toml").read_text(), re.MULTILINE + ) + assert core is not None + for name in ("session_start.py", "pre_compact.py"): + shim = (repo_root / "plugins/codex/hooks" / name).read_text() + pins = re.findall(r'^# "fastmcp==([^"]+)",$', shim, re.MULTILINE) + assert pins == [core.group(1)], name