You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Plugin version (pact-plugin/.claude-plugin/plugin.json's version field) is the marketplace cache key. Installations are keyed on version string — two distinct merges into the same version label become indistinguishable to users who already have that version cached. Hook/skill/command/agent changes that ship without a version bump are silently buried in the cache for every existing installation until the next bump.
Live instance: PR #683 (commit 1f20c886, "Decouple secretary self-completion carve-out from spawn name to agent type") merged the RESERVED_NAMES relax into main but did not bump plugin.json from 4.1.4. Users running the cached 4.1.4 (populated at the prior #664 bump) ran the pre-#682dispatch_gate.py for an unbounded interval — until #689's republish to 4.1.5 invalidated the cache. The session that merged #682 itself caught this: bootstrap ritual called Agent(name="secretary", ...) and the cached gate refused with reserved-token set despite source on disk explicitly relaxing the set.
The pinned memory "Tag + GitHub release after every plugin-version bump" documents the post-merge release discipline, but there is no enforcement that the bump itself happened pre-merge.
Why this is uniquely dangerous
A typical bug missed in PR review fails for one PR. Patch-version reuse fails for N future PRs that all merge into the same unbumped version label — every fix from those PRs is invisible to existing installations until someone notices and bumps. The asymmetry between cost (trivial 4-file edit) and silent-bury risk (unbounded latency to user-visible) is what makes this gate-worthy rather than checklist-worthy.
Affected change types
A bump is required when any of these change in a PR (because users running the old cache won't pick the change up):
pact-plugin/hooks/**/*.py — hook code (load-bearing, runs at tool-use boundaries)
pact-plugin/skills/** — skill bodies that the orchestrator/teammates read at runtime
pact-plugin/commands/** — slash-command bodies
pact-plugin/agents/** — agent persona files
pact-plugin/protocols/** — protocol references resolved via Read(...) from agent bodies
The exclusion list (changes that do NOT require a bump):
pact-plugin/tests/** — CI-only, never reaches user installs via cache
docs/** — gitignored per project policy; never installed
Top-level README.md (when the change is editorial / unrelated to versioned content)
.github/** — repo automation, not packaged
Proposed approaches (sketch — pick or combine)
Option A — PreToolUse hook on gh pr merge (defense-in-depth)
A pre-merge hook (run at gh pr merge time, similar pattern to merge_guard_pre.py) that checks the PR's diff for changes to the affected paths. If any are present AND plugin.json's version matches main's version, refuse the merge with a directive to bump first.
Pro: ironclad, gate-enforced.
Con: another merge_guard-family hook; pairs with the existing #665 regex bug and #677 field-mismatch bug as a third thing to maintain.
Option B — GitHub Actions check on PR
A CI check that runs on every PR with paths: filter for the affected directories, comparing plugin.json's version against main's.
# .github/workflows/version-bump-gate.yml (sketch)on:
pull_request:
paths:
- 'pact-plugin/hooks/**'
- 'pact-plugin/skills/**'
- 'pact-plugin/commands/**'
- 'pact-plugin/agents/**'
- 'pact-plugin/protocols/**'
- 'pact-plugin/templates/**'
- 'pact-plugin/.claude-plugin/plugin.json'jobs:
require-bump:
runs-on: ubuntu-lateststeps:
- uses: actions/checkout@v4with:
fetch-depth: 0
- name: Compare plugin.json version against mainrun: | MAIN_VER=$(git show origin/main:pact-plugin/.claude-plugin/plugin.json | jq -r .version) PR_VER=$(jq -r .version pact-plugin/.claude-plugin/plugin.json) if [ "$MAIN_VER" = "$PR_VER" ]; then echo "::error::PR touches packaged paths but plugin.json version is unchanged ($MAIN_VER). Bump version (4-file dance) before merge." exit 1 fi
Pro: standard GitHub Actions discipline; no hook surface to maintain; visible on PR page.
Con: bypassable via admin merge or by stripping the check; relies on branch protection to enforce.
Option C — Pre-commit hook in the repo
A .pre-commit-config.yaml entry or a pre-commit git hook that fires on git commit when staged changes touch the affected paths.
Pro: catches the issue at commit-time, before the PR exists.
Con: pre-commit hooks aren't enforced for contributors who don't install the hook locally; weakest of the three options.
Recommendation
Start with Option B (GitHub Actions). Lowest maintenance cost, visible on every PR, and the failure mode (admin bypass) is explicit and accountable rather than silent. Layer Option A in later if Option B's bypassability becomes a real problem in practice.
Adjacent considerations
4-file dance enforcement
A bump touches 4 files (pact-plugin/.claude-plugin/plugin.json, .claude-plugin/marketplace.json, README.md, pact-plugin/README.md). If only 1 or 2 are bumped, the cache invalidates but the docs lie. The version-bump gate could check all four are in sync on merge:
# Cross-file consistency check (Option B addendum)
PLUGIN_VER=$(jq -r .version pact-plugin/.claude-plugin/plugin.json)
MARKET_VER=$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json)
ROOT_README_VER=$(grep -oE '4\.[0-9]+\.[0-9]+' README.md | head -1)
PLUGIN_README_VER=$(grep -oE '> \*\*Version\*\*: \K[0-9.]+' pact-plugin/README.md)if [ "$PLUGIN_VER"!="$MARKET_VER" ] || [ "$PLUGIN_VER"!="$ROOT_README_VER" ] || [ "$PLUGIN_VER"!="$PLUGIN_README_VER" ];thenecho"::error::Version mismatch across canonical 4-file set. Bump all four to the same version."exit 1
fi
Tag + release verification
Pinned memory says "Tag + GitHub release after every plugin-version bump". A second CI step on main could detect a fresh version commit and post a comment if the matching v{N} tag is missing > 24h after merge. Out of scope for this issue but worth tracking as a follow-up to a follow-up.
Calibration corpus reference
This is the second failure of plugin-version discipline in the post-v4 era:
If a third instance appears, the case for Option A (hook-enforced) over Option B (CI-enforced) gets stronger — same-user admin bypass becomes empirically common rather than theoretical.
Acceptance criteria
A merged PR that touches pact-plugin/hooks/** or pact-plugin/skills/** or pact-plugin/commands/** etc. without a version bump is rejected before merge.
The 4-file dance consistency is checked: all four files report the same version on main.
Documentation updated: pinned memory in CLAUDE.md extended to reference the gate (rather than relying on memory alone).
Test: a deliberately-malformed PR (touches a hook, leaves version untouched) demonstrably fails the gate.
Out of scope
Automatic version-bump suggestion (semver heuristic) — the developer should still consciously choose patch / minor / major; the gate just enforces "some bump happened".
Problem
Plugin version (
pact-plugin/.claude-plugin/plugin.json'sversionfield) is the marketplace cache key. Installations are keyed on version string — two distinct merges into the same version label become indistinguishable to users who already have that version cached. Hook/skill/command/agent changes that ship without a version bump are silently buried in the cache for every existing installation until the next bump.Live instance: PR #683 (commit
1f20c886, "Decouple secretary self-completion carve-out from spawn name to agent type") merged theRESERVED_NAMESrelax intomainbut did not bumpplugin.jsonfrom4.1.4. Users running the cached4.1.4(populated at the prior #664 bump) ran the pre-#682dispatch_gate.pyfor an unbounded interval — until #689's republish to4.1.5invalidated the cache. The session that merged #682 itself caught this: bootstrap ritual calledAgent(name="secretary", ...)and the cached gate refused withreserved-token setdespite source on disk explicitly relaxing the set.The pinned memory "Tag + GitHub release after every plugin-version bump" documents the post-merge release discipline, but there is no enforcement that the bump itself happened pre-merge.
Why this is uniquely dangerous
A typical bug missed in PR review fails for one PR. Patch-version reuse fails for N future PRs that all merge into the same unbumped version label — every fix from those PRs is invisible to existing installations until someone notices and bumps. The asymmetry between cost (trivial 4-file edit) and silent-bury risk (unbounded latency to user-visible) is what makes this gate-worthy rather than checklist-worthy.
Affected change types
A bump is required when any of these change in a PR (because users running the old cache won't pick the change up):
pact-plugin/hooks/**/*.py— hook code (load-bearing, runs at tool-use boundaries)pact-plugin/skills/**— skill bodies that the orchestrator/teammates read at runtimepact-plugin/commands/**— slash-command bodiespact-plugin/agents/**— agent persona filespact-plugin/protocols/**— protocol references resolved viaRead(...)from agent bodiespact-plugin/templates/**— output templatespact-plugin/.claude-plugin/plugin.json's non-version fields (e.g.,commands[],agents[],skillslist)The exclusion list (changes that do NOT require a bump):
pact-plugin/tests/**— CI-only, never reaches user installs via cachedocs/**— gitignored per project policy; never installedREADME.md(when the change is editorial / unrelated to versioned content).github/**— repo automation, not packagedProposed approaches (sketch — pick or combine)
Option A — PreToolUse hook on
gh pr merge(defense-in-depth)A pre-merge hook (run at
gh pr mergetime, similar pattern tomerge_guard_pre.py) that checks the PR's diff for changes to the affected paths. If any are present ANDplugin.json'sversionmatches main'sversion, refuse the merge with a directive to bump first.Pro: ironclad, gate-enforced.
Con: another
merge_guard-family hook; pairs with the existing#665regex bug and#677field-mismatch bug as a third thing to maintain.Option B — GitHub Actions check on PR
A CI check that runs on every PR with
paths:filter for the affected directories, comparingplugin.json's version againstmain's.Pro: standard GitHub Actions discipline; no hook surface to maintain; visible on PR page.
Con: bypassable via admin merge or by stripping the check; relies on branch protection to enforce.
Option C — Pre-commit hook in the repo
A
.pre-commit-config.yamlentry or apre-commitgit hook that fires ongit commitwhen staged changes touch the affected paths.Pro: catches the issue at commit-time, before the PR exists.
Con: pre-commit hooks aren't enforced for contributors who don't install the hook locally; weakest of the three options.
Recommendation
Start with Option B (GitHub Actions). Lowest maintenance cost, visible on every PR, and the failure mode (admin bypass) is explicit and accountable rather than silent. Layer Option A in later if Option B's bypassability becomes a real problem in practice.
Adjacent considerations
4-file dance enforcement
A bump touches 4 files (
pact-plugin/.claude-plugin/plugin.json,.claude-plugin/marketplace.json,README.md,pact-plugin/README.md). If only 1 or 2 are bumped, the cache invalidates but the docs lie. The version-bump gate could check all four are in sync on merge:Tag + release verification
Pinned memory says "Tag + GitHub release after every plugin-version bump". A second CI step on
maincould detect a fresh version commit and post a comment if the matchingv{N}tag is missing > 24h after merge. Out of scope for this issue but worth tracking as a follow-up to a follow-up.Calibration corpus reference
This is the second failure of plugin-version discipline in the post-v4 era:
merge_guard_pre._GH_PR_NUMBER_REgreedy-quantifier bug (merge_guard_pre _GH_PR_NUMBER_RE matches wrong digit on heredoc bodies and stderr redirects #665), which is a separate issue but lives in the samemerge_guard-family neighborhood as Option A above.If a third instance appears, the case for Option A (hook-enforced) over Option B (CI-enforced) gets stronger — same-user admin bypass becomes empirically common rather than theoretical.
Acceptance criteria
pact-plugin/hooks/**orpact-plugin/skills/**orpact-plugin/commands/**etc. without a version bump is rejected before merge.main.CLAUDE.mdextended to reference the gate (rather than relying on memory alone).Out of scope
Refs
1f20c886— the original miss.--body-fileforgh pr merge, never--bodyheredoc (merge_guard_pre _GH_PR_NUMBER_RE matches wrong digit on heredoc bodies and stderr redirects #665)" — adjacent merge-guard family.