feat: add the webgrip bundle plugin so the estate can be namespaced #2

Merged
ryangr0 merged 1 commit from feat/bundle-plugin into main 2026-08-04 14:00:01 +00:00
Owner

PR

What / why

The catalog offers per-skill plugins only, so every skill loads flat. That makes this estate un-installable next to another estate shipping the same names — and one exists: Code 14's ai-skills collides on eight of our ten (adr-writer, domain-language, guard-secrets, harvest-knowledge, protocol-fit-research, skill-usage, skillsmith, worktree-herd-dev). Installed flat side by side, whichever lands last silently wins, and the agent has no way to address the other. Same-named skills across estates encode different org policy, so that's a real loss, not a cosmetic one.

This makes the repo root a plugin. /plugin install webgrip@ai-skills loads all ten skills prefixed webgrip:<skill>, so the two estates coexist and stay individually addressable. Per-skill plugins are untouched and still work for anyone wanting a minimal surface.

What's in it

  • .claude-plugin/plugin.json — the bundle manifest, name webgrip (deliberately not a skills/ dir name, or the catalog would carry two entries with that name).
  • hooks/hooks.json — re-declares the guard-secrets PreToolUse and skill-usage PostToolUse hooks with skills/<name>/ prefixes. This one is load-bearing: a skill's own hooks.json resolves ${CLAUDE_PLUGIN_ROOT} to that skill's directory, which under the bundle is the repo root, so ${CLAUDE_PLUGIN_ROOT}/scripts/guard_secrets.py points at a path that doesn't exist. Claude Code loads that without complaint — the bundle would ship both hooks silently dead.
  • check_manifests.py — validates the bundle manifest, rejects a bundle name colliding with a skills/ dir, and fails if a skill grows a hook the bundle doesn't mirror. That last check is what stops the trap above from reappearing; verified against a negative case (removed the PreToolUse entry → fails, naming the exact command it wants).
  • sync_marketplace.py — emits the bundle entry (source: "./") ahead of the per-skill entries. Catalog stays generated, never hand-edited.
  • release_skills.py — bumps the bundle version whenever any skill releases, at the strongest level in the run. Consumers only pull an update when the catalog version moves, so a release leaving it untouched would never reach anyone installed on the bundle. Versioned, deliberately never tagged<skill>-v<X.Y.Z> stays the only release tag and unit of pinning.

Verified in a live session: claude plugin details webgrip reports Skills (10), Hooks (2) PreToolUse + PostToolUse.

Notes for review

  • The hook duplication is deliberate. The same hook exists twice — rooted at the skill dir for the per-skill plugin, at the repo for the bundle — because ${CLAUDE_PLUGIN_ROOT} differs between the two install shapes. The new check is what keeps the pair honest instead of a comment asking people to remember.
  • Initial version is hand-written as 1.0.0. check_plugin_commits.py only inspects four-part skills|plugins/<name>/.claude-plugin/plugin.json paths, so a root manifest isn't covered by the no-hand-bumps guard. Worth deciding whether that guard should grow to cover the root manifest now that one exists.
  • Code 14's estate hit exactly this bug and fixed it the same way; this port keeps the two repos' packaging behaviour aligned.

Checklist (author)

  • npm run check && npm test pass locally
  • Conventional commits; release-triggering type (feat)
  • No hand-edits to generated files — marketplace.json regenerated via sync_marketplace.py; no skill version touched
  • No secrets, customer data, or hardcoded instance facts
  • N/A — no SKILL.md, description, evals/, or bundled skill script changed; this is packaging only

Checklist (reviewer)

  • Confirm the bundle hook paths match where the scripts actually live (skills/guard-secrets/scripts/guard_secrets.py, skills/skill-usage/scripts/log_usage.py)
  • Read the hook_commands() / mirror check in check_manifests.py as application code
  • Sanity-check the rewrite rule: it assumes every skill hook command contains ${CLAUDE_PLUGIN_ROOT}/. A future hook without that prefix would pass trivially — acceptable, or should it fail loudly?
  • Decide whether vikunja-product-owner's .mcp.json also needs bundle-level re-declaration, or whether MCP config resolves differently from hooks under a bundle install
# PR ## What / why The catalog offers per-skill plugins only, so every skill loads flat. That makes this estate **un-installable next to another estate shipping the same names** — and one exists: Code 14's `ai-skills` collides on eight of our ten (`adr-writer`, `domain-language`, `guard-secrets`, `harvest-knowledge`, `protocol-fit-research`, `skill-usage`, `skillsmith`, `worktree-herd-dev`). Installed flat side by side, whichever lands last silently wins, and the agent has no way to address the other. Same-named skills across estates encode *different org policy*, so that's a real loss, not a cosmetic one. This makes the repo root a plugin. `/plugin install webgrip@ai-skills` loads all ten skills prefixed `webgrip:<skill>`, so the two estates coexist and stay individually addressable. Per-skill plugins are untouched and still work for anyone wanting a minimal surface. ## What's in it - **`.claude-plugin/plugin.json`** — the bundle manifest, name `webgrip` (deliberately not a `skills/` dir name, or the catalog would carry two entries with that name). - **`hooks/hooks.json`** — re-declares the `guard-secrets` PreToolUse and `skill-usage` PostToolUse hooks with `skills/<name>/` prefixes. This one is load-bearing: a skill's own `hooks.json` resolves `${CLAUDE_PLUGIN_ROOT}` to that skill's directory, which under the bundle is the repo root, so `${CLAUDE_PLUGIN_ROOT}/scripts/guard_secrets.py` points at a path that doesn't exist. Claude Code loads that without complaint — the bundle would ship both hooks **silently dead**. - **`check_manifests.py`** — validates the bundle manifest, rejects a bundle name colliding with a `skills/` dir, and fails if a skill grows a hook the bundle doesn't mirror. That last check is what stops the trap above from reappearing; verified against a negative case (removed the PreToolUse entry → fails, naming the exact command it wants). - **`sync_marketplace.py`** — emits the bundle entry (`source: "./"`) ahead of the per-skill entries. Catalog stays generated, never hand-edited. - **`release_skills.py`** — bumps the bundle version whenever any skill releases, at the strongest level in the run. Consumers only pull an update when the catalog version moves, so a release leaving it untouched would never reach anyone installed on the bundle. Versioned, deliberately **never tagged** — `<skill>-v<X.Y.Z>` stays the only release tag and unit of pinning. Verified in a live session: `claude plugin details webgrip` reports Skills (10), Hooks (2) PreToolUse + PostToolUse. ## Notes for review - **The hook duplication is deliberate.** The same hook exists twice — rooted at the skill dir for the per-skill plugin, at the repo for the bundle — because `${CLAUDE_PLUGIN_ROOT}` differs between the two install shapes. The new check is what keeps the pair honest instead of a comment asking people to remember. - **Initial version is hand-written as `1.0.0`.** `check_plugin_commits.py` only inspects four-part `skills|plugins/<name>/.claude-plugin/plugin.json` paths, so a root manifest isn't covered by the no-hand-bumps guard. Worth deciding whether that guard should grow to cover the root manifest now that one exists. - Code 14's estate hit exactly this bug and fixed it the same way; this port keeps the two repos' packaging behaviour aligned. ## Checklist (author) - [x] `npm run check && npm test` pass locally - [x] Conventional commits; release-triggering type (`feat`) - [x] No hand-edits to generated files — `marketplace.json` regenerated via `sync_marketplace.py`; no skill `version` touched - [x] No secrets, customer data, or hardcoded instance facts - [ ] N/A — no SKILL.md, description, `evals/`, or bundled skill script changed; this is packaging only ## Checklist (reviewer) - [ ] Confirm the bundle hook paths match where the scripts actually live (`skills/guard-secrets/scripts/guard_secrets.py`, `skills/skill-usage/scripts/log_usage.py`) - [ ] Read the `hook_commands()` / mirror check in `check_manifests.py` as application code - [ ] Sanity-check the rewrite rule: it assumes every skill hook command contains `${CLAUDE_PLUGIN_ROOT}/`. A future hook without that prefix would pass trivially — acceptable, or should it fail loudly? - [ ] Decide whether `vikunja-product-owner`'s `.mcp.json` also needs bundle-level re-declaration, or whether MCP config resolves differently from hooks under a bundle install
feat: add the webgrip bundle plugin so the estate can be namespaced
All checks were successful
CI / Manifests, dist & plugin tests (pull_request) Successful in 14s
419fcca3d6
The catalog offered per-skill plugins only, so every skill loaded flat. That
makes the estate un-installable next to another one shipping the same names
(adr-writer, domain-language, guard-secrets, harvest-knowledge,
protocol-fit-research, skill-usage, skillsmith, worktree-herd-dev all collide
with Code 14's estate) — whichever installs last wins, silently.

Make the repo root a plugin, so `/plugin install webgrip@ai-skills` loads all
ten skills prefixed webgrip:<skill> and the two estates can coexist.

- .claude-plugin/plugin.json: the bundle manifest, name `webgrip`
- hooks/hooks.json: re-declares the guard-secrets and skill-usage hooks with
  skills/<name>/ prefixes. A skill's own hooks.json resolves
  ${CLAUDE_PLUGIN_ROOT} to the skill dir, which is the repo root under the
  bundle — without this the bundle ships both hooks silently dead
- check_manifests.py: validates the bundle manifest, rejects a bundle name
  that collides with a skills/ dir, and fails if a skill grows a hook the
  bundle doesn't mirror
- sync_marketplace.py: emits the bundle entry (source "./") ahead of the
  per-skill entries; the catalog stays generated, never hand-edited
- release_skills.py: bumps the bundle version whenever any skill releases,
  at the strongest level in the run. Consumers only pull an update when the
  catalog version moves, so a release that left it untouched would never
  reach anyone on the bundle. Versioned, never tagged
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
webgrip/ai-skills!2
No description provided.