Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
9 changes: 8 additions & 1 deletion plugins/claude-code/hooks/pre_compact.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
#!/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",
# # 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",
Comment thread
phernandez marked this conversation as resolved.
# ]
# ///
"""PreCompact hook — the entire hook. All logic (settings resolution, the
extractive checkpoint note, lifecycle-envelope capture) lives in the released
Expand Down
9 changes: 8 additions & 1 deletion plugins/claude-code/hooks/session_start.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
#!/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",
# # 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
Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-code/hooks/test_claude_pre_compact.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__]

Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-code/hooks/test_claude_session_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__]

Expand Down
4 changes: 4 additions & 0 deletions plugins/codex/hooks/pre_compact.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
# 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",
# ]
# ///
"""PreCompact hook launcher backed by a pinned Basic Memory revision.
Expand Down
4 changes: 4 additions & 0 deletions plugins/codex/hooks/session_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
# 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",
# ]
# ///
"""SessionStart hook launcher backed by a pinned Basic Memory revision.
Expand Down
4 changes: 2 additions & 2 deletions scripts/update_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
24 changes: 23 additions & 1 deletion scripts/validate_claude_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -145,9 +159,17 @@ 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}")
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"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_claude_plugin_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
18 changes: 18 additions & 0 deletions tests/test_codex_plugin_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion tests/test_update_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading