Skip to content

wedge stage 09: finish plugin wiring (tools/hooks/skills)#185

Merged
gnanam1990 merged 4 commits into
mainfrom
feat/wedge09-finish-plugins
Jun 13, 2026
Merged

wedge stage 09: finish plugin wiring (tools/hooks/skills)#185
gnanam1990 merged 4 commits into
mainfrom
feat/wedge09-finish-plugins

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Finishes the previously-stubbed internal/plugins so loaded plugin manifests actually take effect at runtime. Before this change, plugin tools/hooks/skills were parsed for discovery only (the zero plugins listing) and never registered — plugins loaded but did nothing. This stage turns each plugin's declared tools, hooks, and skills into live registrations.

What was built

internal/plugins/activate.go (new)Activate(registry, plugins, options):

  • Tools → registry: a pluginTool adapter implements the tools.Tool interface by invoking the plugin's declared command. Tool arguments are passed as JSON on stdin; stdout/exit map onto a tools.Result; ${AGENT_PLUGIN_ROOT} placeholders in the command/args are expanded to the plugin dir, which is also exported as the AGENT_PLUGIN_ROOT env var. The runner is injectable for tests and defaults to a bounded subprocess. Each tool is registered via registry.Register.
  • Permission mapping: ToolPermission (allow/prompt/deny) maps onto tools.Safety (always shell side-effect + the carried permission). allow only survives manifest parsing when AllowManifestToolAutoApproval was set, so mutating plugin tools are not auto-approved by default; deny tools register but are never advertised or executed.
  • Hooks → dispatcher: builds an internal/hooks.Definition per hook extension (mapping the plugin HookEvent to the hooks Event, carrying command/args, ${AGENT_PLUGIN_ROOT}-expanded), with plugin-namespaced ids.
  • Skills → loader: exposes each plugin skill's search root (the directory internal/skills.Load scans) and provides a plugin-aware skill tool that merges the default skills dir with plugin roots, deduplicating by name and recording collisions (preserving skills.Duplicates behaviour, never crashing).
  • Isolation: a malformed plugin/extension is skipped with a recorded warning; a single bad plugin never aborts activation. Activation order is deterministic (plugins sorted by id). Provenance (originating plugin id) is recorded for tools/hooks/skills and tagged in tool-result Meta.

internal/cli/plugin_activate.go (new) + bootstrap wiringactivatePlugins(...) loads the workspace's plugins and runs activation. It is invoked in both bootstrap paths:

  • interactive TUI: internal/cli/app.go in runInteractiveTUIWithSetup, right after specialist + MCP tool registration (so plugin tools are part of the deferral count).
  • one-shot exec: internal/cli/exec.go in runExec, after MCP registration and before --list-tools/filter validation (so plugin tools are listable and filter-validatable).

newHookDispatcherWithExtra (in internal/cli/hook_dispatch.go) folds the plugin hook definitions into the dispatcher's active hook set; the plugin-aware skill tool is registered when a plugin declares skills. Activation fails open: a load error or malformed plugin is surfaced as a [zero] WARNING on stderr and skipped, never wedging startup.

internal/plugins/plugins.go — comment-only update replacing the stale "not yet registered" note (no behavioural change).

Where activation is invoked

  • internal/cli/app.go: activatePlugins(workspaceRoot, registry, deps, stderr) in runInteractiveTUIWithSetup, and newHookDispatcherWithExtra(workspaceRoot, pluginActivation.hooks) in the TUI agent.Options.
  • internal/cli/exec.go: activatePlugins(...) in runExec, and newHookDispatcherWithExtra(...) in the exec agent.Options.

Testing

  • internal/plugins/activate_test.go: tool registration + command invocation + stdout→Result mapping (stubbed runner); non-zero exit → error; permission mapping (prompt requires grant, allow runs, deny never executes); hook Definition build + event mapping + dispatcher Select; skill search-root derivation + loader discovery; merged skill list + duplicate recording; malformed-plugin skip-with-warning; disabled-plugin skip; deterministic ordering.
  • internal/plugins/activate_unix_test.go: real-subprocess end-to-end (JSON args on stdin and $AGENT_PLUGIN_ROOT round-trip; missing binary → tool error).
  • internal/cli/plugin_activate_test.go: bootstrap helper registers the plugin tool, collects namespaced hooks, re-registers the plugin-aware skill tool, fails open on load error, warns on malformed plugin; newHookDispatcherWithExtra runs a folded plugin hook.
  • Gate: gofmt -l clean on changed dirs, go build ./... and go test ./... both green.

Deferred / out of scope

  • Plugin prompts remain discovery-only (the spec scoped this stage to tools/hooks/skills).
  • No new config surface for manifest tool auto-approval was added; AllowManifestToolAutoApproval stays off by default at the bootstrap, so plugin allow tools are clamped to prompt unless a caller opts in.
  • Plugin tools are advertised eagerly (they do not opt into the deferred-tool/tool_search mechanism).

Summary by CodeRabbit

  • New Features

    • Plugins now activate automatically during CLI interactive and execution runs; plugin-provided hooks are merged into the CLI hook dispatcher while preserving workspace hook config.
    • Plugin tools and skills are dynamically registered and made available at runtime; activation emits warnings for load/activation issues without blocking runs.
  • Tests

    • Comprehensive unit and end-to-end tests added to validate plugin activation, tool execution, hook wiring, skill merging, and subprocess behavior.
  • Documentation

    • Clarified manifest parsing docs to distinguish discovery vs activation consumers.

Activate resolved plugin manifests so their declared tools, hooks, and
skills take effect at runtime instead of being parsed-but-unconsumed.

- internal/plugins/activate.go: Activate() registers each plugin tool into
  the tools.Registry via a command-runner adapter (JSON args on stdin,
  stdout/exit -> tools.Result, ${AGENT_PLUGIN_ROOT} expansion + env),
  builds hooks.Definition values for plugin hooks, and exposes plugin
  skill search roots. Permission maps allow/prompt/deny -> tools.Safety;
  allow only survives load-time clamping when manifest auto-approval is
  enabled, so mutating plugin tools are not auto-approved by default. A
  plugin-aware skill tool merges the default skills dir with plugin roots
  (recording duplicate names, never crashing). Malformed plugins/extensions
  are skipped with a warning; activation order is deterministic.
- internal/cli: activatePlugins() invokes activation in both bootstrap
  paths (interactive TUI in app.go and one-shot exec in exec.go), after
  core/specialist/MCP registration. newHookDispatcherWithExtra folds plugin
  hooks into the dispatcher's hook set. Fails open: a bad plugin warns and
  is skipped, never wedging startup.
- internal/plugins/plugins.go: update the stale 'not yet registered' note.
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: cc8da8f7a67b
Changed files (9): internal/cli/app.go, internal/cli/exec.go, internal/cli/hook_dispatch.go, internal/cli/plugin_activate.go, internal/cli/plugin_activate_test.go, internal/plugins/activate.go, internal/plugins/activate_test.go, internal/plugins/activate_unix_test.go, internal/plugins/plugins.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eec80130-29e1-442c-9e66-126ced1a373c

📥 Commits

Reviewing files that changed from the base of the PR and between 4321b09 and cc8da8f.

📒 Files selected for processing (1)
  • internal/cli/plugin_activate.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/cli/plugin_activate.go

Walkthrough

Adds a plugin activation layer and integrates it into CLI bootstrap: registers plugin tools, collects plugin hooks and skill roots, folds plugin hooks into the workspace hook dispatcher, re-registers a merged skill tool when plugin skills exist, and wires activation into interactive and exec entry points with tests.

Changes

Plugin Activation and Integration

Layer / File(s) Summary
Plugin Activation Core Infrastructure
internal/plugins/activate.go
Defines activation types and options, registers validated plugin tools, builds namespaced hook definitions, collects/deduplicates skill roots, provides merged-skill helpers and a plugin-aware skill tool, expands ${AGENT_PLUGIN_ROOT} in commands/args, injects env, and executes plugin tools via a subprocess runner with timeout, redaction, and exit-code mapping.
Plugin Activation Testing
internal/plugins/activate_test.go, internal/plugins/activate_unix_test.go
Unit and end-to-end tests covering tool registration/execution, ${AGENT_PLUGIN_ROOT} expansion, env injection, permission-safety mapping, non-zero exit handling, hook definition/event mapping, skill discovery and duplicates, deterministic ordering, malformed/disabled plugin handling, and real subprocess behavior.
CLI Plugin Activation Wrapper
internal/cli/plugin_activate.go, internal/cli/plugin_activate_test.go
CLI-level wrapper that loads/activates workspace plugins, writes standardized stderr warnings for load/diagnostics/failures, conditionally re-registers the skill tool with merged plugin roots, and tests activation wiring and failure-open behavior.
Hook Dispatcher Plugin Integration
internal/cli/hook_dispatch.go
Centralizes dispatcher construction via newHookDispatcherWithExtra and merges plugin-provided hooks into workspace hook config without replacing existing hook IDs.
Interactive and Execution Entry Points
internal/cli/app.go, internal/cli/exec.go
runInteractiveTUIWithSetup and runExec now call activatePlugins during bootstrap and pass plugin hooks into newHookDispatcherWithExtra(..., pluginActivation.hooks); updates an inline registry comment to reflect plugin inclusion.
Documentation Clarification
internal/plugins/plugins.go
Updates ParseManifest comment to state extension fields are consumed by both DISCOVERY and ACTIVATION.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI
  participant ACT as Activate
  participant REG as tools.Registry
  participant DISP as hooks.Dispatcher
  participant PROC as Subprocess
  CLI->>ACT: load workspace plugins
  ACT->>REG: register plugin tools
  ACT->>DISP: return hook definitions
  REG->>PROC: run plugin tool (JSON stdin, env AGENT_PLUGIN_ROOT)
  PROC->>REG: stdout, stderr, exit code
  REG->>CLI: mapped tools.Result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • Vasanthdev2004
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: completing plugin integration wiring for tools, hooks, and skills activation at runtime.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wedge09-finish-plugins

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/plugin_activate.go`:
- Around line 30-39: The loadPlugins call can return per-plugin diagnostics that
are currently ignored; after calling deps.loadPlugins(plugins.LoadOptions{Cwd:
workspaceRoot}) and before invoking plugins.Activate, iterate over the
diagnostics on the returned loaded (e.g., loaded.Diagnostics or whichever field
holds per-plugin diagnostics in the LoadResult) and call
writePluginActivationWarning(stderr, diag) for each diagnostic string; keep the
existing top-level error handling intact and then proceed to call
plugins.Activate(registry, loaded.Plugins, plugins.ActivateOptions{Cwd:
workspaceRoot}) as before so activation continues but all load diagnostics are
surfaced.

In `@internal/plugins/activate_test.go`:
- Around line 234-275: Add a regression case to
TestActivateExposesSkillDirInLoaderRoots that covers manifest-relative skill
paths: create a LoadedPlugin whose Skills contains a PathExtension with a
relative Path like "skills/ts-review/SKILL.md" (and a variant using
"${AGENT_PLUGIN_ROOT}/skills/ts-review/SKILL.md"), call Activate(registry,
[]LoadedPlugin{plugin}, ActivateOptions{}) and assert that result.SkillRoots
contains the full pluginDir-based root (filepath.Join(pluginDir,"skills"))
rather than the bare "skills"; reference LoadedPlugin, PathExtension, Activate,
ActivateOptions and TestActivateExposesSkillDirInLoaderRoots when adding the new
assertions.

In `@internal/plugins/activate.go`:
- Around line 221-229: The code passes ext.Path verbatim into skillSearchRoot
causing relative manifest paths like "skills/foo/SKILL.md" to yield incorrect
roots; resolve ext.Path against the plugin root first (e.g. compute resolved :=
filepath.Join(plugin.Root, ext.Path) or use filepath.Clean/Abs as appropriate)
and then call skillSearchRoot(resolved) so SkillRoots contains the real
filesystem directory (update references to ext.Path -> resolved when computing
root and keep the seenRoot/SkillRoots logic unchanged). Ensure you
import/path-utilize filepath and preserve deduplication via seenRoot.
- Around line 184-186: Before calling registry.Register(tool), check whether a
tool with the same name already exists and fail the activation instead of
overwriting: use the registry lookup/has function to detect an existing entry
for ext.Name and if found return an error from the activation routine (include
ext.Name and plugin.ID in the message); only call registry.Register(tool) and
append to result.Tools (ToolProvenance{ToolName: ext.Name, PluginID: plugin.ID})
when no collision is detected. Ensure you do not append to result.Tools on
collision.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 86dbf486-1586-43d7-be39-2c6e965bce54

📥 Commits

Reviewing files that changed from the base of the PR and between 59c3326 and 29ff473.

📒 Files selected for processing (9)
  • internal/cli/app.go
  • internal/cli/exec.go
  • internal/cli/hook_dispatch.go
  • internal/cli/plugin_activate.go
  • internal/cli/plugin_activate_test.go
  • internal/plugins/activate.go
  • internal/plugins/activate_test.go
  • internal/plugins/activate_unix_test.go
  • internal/plugins/plugins.go

Comment thread internal/cli/plugin_activate.go
Comment thread internal/plugins/activate_test.go
Comment thread internal/plugins/activate.go
Comment thread internal/plugins/activate.go
expandPluginRoot kept the manifest's forward slashes, so a ${AGENT_PLUGIN_ROOT}/bin/x command became a mixed-separator path on Windows (C:\...\001/bin/x), failing the path tests. Added expandPluginRootPath (filepath.FromSlash on the placeholder-expanded executable path only; args keep their slashes since they may be URLs/flags) and used it for the tool and hook command paths.
- surface per-plugin load diagnostics on stderr (not just top-level errors)
- skip plugin tools whose name collides with an already-registered tool
- resolve manifest-relative plugin skill paths against the plugin dir
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Addressed the CodeRabbit review (pushed 4321b09):

Fixed

  • Surface load diagnostics (internal/cli/plugin_activate.go): activatePlugins now forwards each LoadResult.Diagnostics entry to stderr, so a plugin dropped for a malformed manifest is no longer silent (only the top-level error was reported before). Added TestActivatePluginsSurfacesLoadDiagnostics.
  • Reject colliding tool names (internal/plugins/activate.go): activateTools checks the registry before registering and skips a plugin tool whose name is already taken (e.g. a core tool), recording a warning instead of silently overwriting via last-wins Register. Kept the file's isolation-first warn-and-skip behavior rather than aborting activation. Added TestActivateSkipsToolWhoseNameCollidesWithRegisteredTool.
  • Resolve skill paths against the plugin dir (internal/plugins/activate.go): activateSkills now expands ${AGENT_PLUGIN_ROOT} and anchors a still-relative path under PluginDir before deriving the search root, so a relative skills/foo/SKILL.md yields <pluginDir>/skills instead of a bare skills. Added TestActivateResolvesManifestRelativeSkillRoot (relative + placeholder cases).

Note: the production load path (ParseManifest -> ResolvePluginPath) already resolves skill paths to absolute, so this primarily hardens direct Activate callers; the guard is a no-op for already-absolute paths.

Gate: gofmt -l clean on changed dirs, go build ./... ok, go test ./... green for the touched packages.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/plugin_activate.go`:
- Around line 63-68: formatLoadDiagnostic currently only appends
diagnostic.ManifestPath, leaving output lacking context when it's empty; update
formatLoadDiagnostic to fall back to diagnostic.PluginPath and, if that is also
empty, diagnostic.PluginID when ManifestPath is empty, so the message becomes
"[Kind] Message (path-or-id)". Use the existing symbols Diagnostic.ManifestPath,
Diagnostic.PluginPath, Diagnostic.PluginID, and the function
formatLoadDiagnostic to locate and implement the fallback chain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 83b3715d-cf0b-45e1-875d-f7762b5d94f7

📥 Commits

Reviewing files that changed from the base of the PR and between 9fb3357 and 4321b09.

📒 Files selected for processing (4)
  • internal/cli/plugin_activate.go
  • internal/cli/plugin_activate_test.go
  • internal/plugins/activate.go
  • internal/plugins/activate_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/cli/plugin_activate_test.go
  • internal/plugins/activate_test.go
  • internal/plugins/activate.go

Comment thread internal/cli/plugin_activate.go

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Approved

Reviewed the plugin activation/runtime wiring. No blockers found.

What I checked:

  • Plugin tools register through the normal tool registry and keep tools.Safety permission metadata; no raw execution bypass is introduced.
  • Tool name collisions are skipped with warnings instead of replacing core tools or earlier plugin tools.
  • Plugin command output is redacted before display, commands are timeout-bounded, and AGENT_PLUGIN_ROOT is passed explicitly.
  • Plugin hooks are namespaced, appended deterministically, and still respect hooks being disabled overall.
  • Plugin skills merge with default skills while default/user skills win name collisions.

Validation run locally on the PR head:

  • git diff --check origin/main...HEAD
  • go test ./...
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke

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.

2 participants