Skip to content

feat!: rename Claude plugin to planbridge@contextbridge; refresh plugins on update#52

Merged
blimmer merged 4 commits into
mainfrom
feat/rename-claude-plugin-to-planbridge
May 8, 2026
Merged

feat!: rename Claude plugin to planbridge@contextbridge; refresh plugins on update#52
blimmer merged 4 commits into
mainfrom
feat/rename-claude-plugin-to-planbridge

Conversation

@blimmer
Copy link
Copy Markdown
Contributor

@blimmer blimmer commented May 7, 2026

Summary

Move PlanBridge's Claude Code plugin assets (plugin.json, hooks.json) from contextbridge/claude-plugin into this repo at harnessIntegrations/claude/, rename the plugin id from cli@contextbridge to planbridge@contextbridge, and couple binary + plugin updates so contextbridge update re-runs install for harnesses that already have PlanBridge wired up. Closes #49.

Review focus

  • runUpdate post-swap refresh in packages/cli/src/commands/update.ts walks ALL_INSTALLERS, skips harnesses with status.managed.length === 0, and spawns <process.execPath> install <harness-id> per harness with state. Refresh failure logs a warning and never blocks update success. Spawning process.execPath (not the old runUpdate's in-process logic) is deliberate — the child runs the new binary's installer code, which knows about the rename and updates/migrates Claude plugin state automatically.
  • ClaudeInstaller.status() now surfaces both planbridge@contextbridge and cli@contextbridge entries in managed[], but installed stays gated on the new id only. This is what makes the refresh filter naturally migrate legacy users without a separate "do I have legacy state?" abstraction.
  • runInstall/runUninstall legacy cleanup at the target scope only — see ClaudeInstaller.ts. Tests cover the cross-scope edge case (legacy at project, install at user → leaves legacy alone). Existing planbridge@contextbridge installs now use claude plugin update so contextbridge update pulls the latest plugin version.
  • release-please-config.json extra-files keeps harnessIntegrations/claude/.claude-plugin/plugin.json's version in sync with binary releases.

Sequencing: depends on contextbridge/claude-plugin#1 merging first so the new plugin id resolves in the marketplace catalog. Then this PR; then the release-please release PR.

Commits

  • b2807dd — package the Claude Code plugin integration assets in this repo
  • a382919 — rename the Claude plugin to planbridge@contextbridge
  • 9494b1f — refresh managed harness integrations after contextbridge update

@@ -0,0 +1,12 @@
{
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This moves from github.com/contextbridge/claude-plugin (see contextbridge/claude-plugin#1). That repo is now just a catalog of all ContextBridge claude plugins (right now, just PlanBridge, but could expand to other products in the future).

I'm doing this migration now, which will make #51 easier to implement in a single place.

]);
expect(pluginInstall).toHaveLength(1);
expect(pluginInstall[0]?.args).toEqual(['plugin', 'install', 'cli@contextbridge', '--scope', 'user']);
expect(pluginInstall[0]?.args).toEqual(['plugin', 'install', 'planbridge@contextbridge', '--scope', 'user']);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm renaming this to match up. If we don't feel strongly, we can leave it as cli - feedback welcome.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think planbridge will make more sense given the marketing site and product name

expect(spawnCall?.args).toEqual(['install', 'claude', '--scope', 'user']);
});

it('refreshes Claude at project scope when the new plugin is installed at project scope', async () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Things could get a little bit weird if the old named plugin is installed in various projects, but that didn't feel like something I wanted to work around.

]);
await runPluginCommand(ctx, binaryName, 'install', ['install', PLUGIN_ID, '--scope', scope]);
if (hasCurrentAtScope) {
await runPluginCommand(ctx, binaryName, 'update', ['update', PLUGIN_ID, '--scope', scope]);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude will update plugins on some cadence, but we want to force it to be up-to-date (e.g., when we introduce new harness features).

{
"type": "json",
"path": "harnessIntegrations/claude/.claude-plugin/plugin.json",
"jsonpath": "$.version"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a handy feature that'll keep the claude plugin released in-step with the binary.

@blimmer blimmer marked this pull request as ready for review May 8, 2026 14:37
@blimmer blimmer requested a review from jcarver989 as a code owner May 8, 2026 14:37
"homepage": "https://plan.contextbridge.ai",
"repository": "https://github.com/contextbridge/planbridge",
"license": "MIT",
"keywords": ["contextbridge", "planbridge", "planning", "review", "hooks", "ExitPlanMode"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ExitPlanMode in the keywords intentional? Just curious since it sticks out against the others.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, just copied this over verbatim from the other repo - you're right though, not very helpful. Let me update.

]);
expect(pluginInstall).toHaveLength(1);
expect(pluginInstall[0]?.args).toEqual(['plugin', 'install', 'cli@contextbridge', '--scope', 'user']);
expect(pluginInstall[0]?.args).toEqual(['plugin', 'install', 'planbridge@contextbridge', '--scope', 'user']);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think planbridge will make more sense given the marketing site and product name

commandRunner
.on(CLAUDE_BINARY, ['plugin', 'list', '--json'])
.resolves(pluginListResult([{ id: 'cli@contextbridge', scope: 'user' }]));
.resolves(pluginListResult([{ id: 'planbridge@contextbridge', scope: 'user' }]));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we should use the constant that's defined elsewhere in this PR across these tests.

// the user never wired up; never blocks update success on a refresh failure.
async function refreshInstalledHarnesses(ctx: CliContext): Promise<void> {
const { commandRunner, logger } = ctx;
for (const installer of ALL_INSTALLERS) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably just me being me, but I'd flatMap here (using empty array in the failure case) as it'd avoid the let binding + re-assignment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - I'll take a more functional approach throughout.

Comment thread packages/cli/src/commands/update.ts Outdated
continue;
}

for (const scope of refreshScopes) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that this is sequential, assuming this is intentional since concurrent install commands might cause issues?

Copy link
Copy Markdown
Contributor Author

@blimmer blimmer May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it's intentional. claude plugin install mutates shared config under ~/.claude (and Codex hook installs touch ~/.codex/hooks.json), so parallel installs across scopes/harnesses would race on those files. Sequential is also fine performance-wise — the post-update refresh fan-out is small (one per managed scope per installer), and stdio: 'inherit' already streams progress to the user.

blimmer added 4 commits May 8, 2026 09:33
- drop the `ExitPlanMode` keyword from the Claude plugin manifest
- export CLAUDE_PLUGIN_ID / CLAUDE_LEGACY_PLUGIN_ID / CLAUDE_MARKETPLACE_*
  from ClaudeInstaller and use them in tests instead of literals
- split refreshInstalledHarnesses into helpers so the status-failure
  path returns [] rather than a let/reassign + continue
@blimmer blimmer force-pushed the feat/rename-claude-plugin-to-planbridge branch from 9494b1f to bd01fce Compare May 8, 2026 15:48
@blimmer blimmer merged commit 2794ae6 into main May 8, 2026
12 checks passed
@blimmer blimmer deleted the feat/rename-claude-plugin-to-planbridge branch May 8, 2026 15:54
blimmer added a commit that referenced this pull request May 8, 2026
## Summary

Fixes #63. After `brew upgrade --cask cli@alpha` (or `cli`), the
post-update harness refresh introduced in #52 fails silently with
`ENOENT` because `process.execPath` was captured at process start and
now points at the cellar dir brew already purged. The refresh exists to
keep harness state in sync across binary changes (renames, hook
contracts) — silent no-op defeats the entire feature for every
brew-installed user.

Resolve the binary via `commandRunner.which('contextbridge')` instead.
The PATH symlink (`/opt/homebrew/bin/contextbridge` for brew,
`~/.local/bin/contextbridge` for install-script users) survives the
upgrade. If `which` returns null, log `error` and skip — refusing to
fall back to the broken `execPath` keeps us from silently masking the
very bug we're fixing.

Also bumped refresh-failure logs from `warn` → `error` so Sentry
surfaces these in production.
blimmer added a commit that referenced this pull request May 8, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.3.0](v0.2.0...v0.3.0)
(2026-05-08)


### ⚠ BREAKING CHANGES

* rename Claude plugin to planbridge@contextbridge; refresh plugins on
update ([#52](#52))

### Features

* add automatic release changelog with release-please
([#21](#21))
([45a1bf1](45a1bf1))
* rename Claude plugin to planbridge@contextbridge; refresh plugins on
update ([#52](#52))
([2794ae6](2794ae6))
* **ui:** add GitHub link to header help menu
([#19](#19))
([c36289b](c36289b))


### Bug Fixes

* emit plan review analytics from shared runner
([#48](#48))
([5c769ff](5c769ff))
* **plan:** pre-scan src for transitive deps to stop vitest reload flake
([3b3cc0c](3b3cc0c)),
closes [#12](#12)
* **plan:** stop vitest reload flake from lazy zod optimization
([#15](#15))
([3b3cc0c](3b3cc0c))
* refresh Claude marketplace cache during install
([#62](#62))
([58be09a](58be09a))
* resolve contextbridge via PATH for post-update refresh
([#64](#64))
([f658af4](f658af4))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: contextbridge-pr-automation[bot] <259134118+contextbridge-pr-automation[bot]@users.noreply.github.com>
Co-authored-by: Ben Limmer <ben@benlimmer.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: move Claude Code plugin assets into this repo, rename to planbridge@contextbridge

2 participants