feat: package bough as a Claude Code plugin#103
Conversation
Ship the repo as a Claude Code plugin so bough can be driven from inside a
session (/bough:* slash commands) instead of only from a shell, closing the
one packaging gap versus a slash-command/skill continuous-learning plugin.
- .claude-plugin/{marketplace,plugin}.json: a single `bough` plugin whose
source is the repo root; version omitted so it tracks the git commit SHA.
- commands/*.md: 11 user-facing verbs (create/remove/list/status/verify/
doctor/instinct-status/instinct-list/instinct-promote/evolve/config-validate),
each a thin wrapper that shells out to `bough` on PATH.
- hooks/hooks.json: mirrors, verbatim, the `bough hook handle --event <E>`
dispatcher `bough hook install` writes for all 8 events, so installing the
plugin auto-wires the observe->instinct->evolve->preserve + WorktreeCreate/
Remove loop without editing settings.json.
- skills/using-bough/SKILL.md: model-invoked orchestration + `command -v bough`
PATH preflight.
- internal/hooks/plugin_sync_test.go: fails if hooks.json ever drifts from
hooks.CanonicalCommand / hooks.AllEvents (the manifest is hand-authored).
- bough doctor: warns when bough hooks live in settings.json, since a
settings.json copy plus the installed plugin double-fires every event.
The plugin ships markdown + JSON only; the binary still comes from the existing
release channels (tarball / nix / go install). Validated with
`claude plugin validate .`.
| // plugin from here (its registry is Claude Code's, not bough's), | ||
| // so this is a conditional heads-up, not an assertion the plugin | ||
| // is present. | ||
| fmt.Fprintln(w, " note: these bough entries live in settings.json. If you ALSO installed the") |
There was a problem hiding this comment.
This note renders whenever hasBoughSettingsHooks() is true - i.e. whenever bough hooks are in settings.json at all, not just when the plugin is ALSO installed. Since bough cannot see the plugin from here, this fires for essentially every existing bough hook install user (the standard, pre-plugin workflow), not just the double-wire case the wording implies.
Rewording so it explicitly says this is unconditional guidance (not a diagnosed problem) rather than adding detection logic that would depend on Claude Code's undocumented internal plugin registry. Pushing that change now.
| } | ||
|
|
||
| // loadPluginHooks reads + parses the committed plugin hooks.json. | ||
| func loadPluginHooks(t *testing.T) map[HookEvent][]HookGroup { |
There was a problem hiding this comment.
This reinvents settings.json hook parsing (pluginHooksFile / hand-rolled unmarshal) instead of reusing decodeHookSet, which already exists in this package and is what bough hook install/List/Doctor actually decode with. Since this test never exercises decodeHookSet, a bug in the production decode path wouldn't be caught here, and any future change to its semantics would need to be hand-duplicated to keep this drift guard in sync - exactly the kind of drift this file exists to prevent elsewhere.
Switching loadPluginHooks to parse via decodeHookSet directly. Pushing that change now.
…uses decodeHookSet - hooks.go: the plugin double-wire note fired whenever bough hooks were in settings.json at all, not just when the Claude Code plugin was ALSO installed -- bough cannot detect the plugin from here, so this rendered for essentially every existing `bough hook install` user (the standard, non-plugin workflow). Reworded to say explicitly that this is unconditional guidance, not a diagnosed problem, rather than adding detection logic that would depend on Claude Code's undocumented internal plugin registry. - plugin_sync_test.go: loadPluginHooks reinvented settings.json hook parsing instead of reusing decodeHookSet, so the drift guard never exercised the actual production decode path and would need manual updates if decodeHookSet's semantics ever changed. Now parses via decodeHookSet directly. Found by code review on PR #103.
Why
bough could only be driven from a shell (
bough <verb>). This packages the repoas a Claude Code plugin so it can be driven from inside a session
(
/bough:*slash commands), and installing it auto-wires the continuous-learningcontinuous-learning plugin. The binary still comes from the existing release
channels; the plugin is markdown + JSON only.
Install
What's in it
.claude-plugin/{marketplace,plugin}.jsonboughplugin, source./;versionomitted → tracks git SHAcommands/*.md(11)bough <verb>wrappershooks/hooks.jsonbough hook handle --event <E>, verbatim mirror ofbough hook install; installing auto-wires observe→instinct→evolve→preserve + WorktreeCreate/Removeskills/using-bough/SKILL.mdcommand -v boughPATH preflightinternal/hooks/plugin_sync_test.gohooks.jsondrifts fromCanonicalCommand/AllEventsbough doctorsettings.json(asettings.jsoncopy + the plugin double-fires every event)Don't double-wire: use the plugin or
bough hook install, not both.Verification
claude plugin validate .passes; an isolated (CLAUDE_CONFIG_DIR) installsmoke shows
plugin list=bough@boughenabled andplugin details= 11commands +
using-bough+ 8 hooks recognized, without touching the real~/.claude.go test ./internal/...,go test ./conformance/hooks/, andgolangci-lintall green.