Skip to content

feat(plugin): implement plugin discovery and loader in lib/plugin-loader.cjs#207

Closed
snipcodeit wants to merge 2 commits intomainfrom
issue/192-implement-plugin-discovery-and-loader-in-lib-plugin-loader-cjs
Closed

feat(plugin): implement plugin discovery and loader in lib/plugin-loader.cjs#207
snipcodeit wants to merge 2 commits intomainfrom
issue/192-implement-plugin-discovery-and-loader-in-lib-plugin-loader-cjs

Conversation

@snipcodeit
Copy link
Owner

Summary

  • Implements the complete plugin runtime subsystem for MGW v6 in lib/plugin-loader.cjs
  • Adds buildRegistry(plugins) for O(1) plugin lookup by "name:type" key after discovery
  • Adds getDefaultPluginDirs(repoRoot) returning the standard [.mgw/plugins, ~/.mgw/plugins] discovery paths
  • Non-fatal design: invalid plugins emit stderr warnings and are skipped — MGW startup never crashes on bad plugin configs

Closes #192

Milestone Context

Changes

lib/plugin-loader.cjs — Enhanced

New exports added to the existing implementation:

Export Signature Description
buildRegistry (plugins) => Map<"name:type", entry> Creates O(1) lookup registry from discoverPlugins() output. Last-found wins for duplicate keys (project-local overrides user-global).
getDefaultPluginDirs (repoRoot) => string[] Returns [<repoRoot>/.mgw/plugins, ~/.mgw/plugins]. Neither path must exist — discoverPlugins skips missing dirs.

Existing exports unchanged:

Export Description
validateManifest(manifest) Ajv schema validation with structural fallback
loadPlugin(pluginDir) Reads manifest, validates, path-containment security check, requires entrypoint
discoverPlugins(pluginDirs) Scans dirs, non-fatal warnings on invalid plugins
SCHEMA_PATH Absolute path to templates/mgw-plugin-schema.json

templates/mgw-plugin-schema.json — Included (cherry-picked from #191)

JSON Schema draft-07 for mgw-plugin.json manifests. Covers: name (kebab-case), version (semver), type (agent-template|hook|validator), entrypoint, supported_stages, hooks (lifecycle event bindings), config_schema, requires_mgw_version.

Note on cherry-pick: PR #206 (#191) is still open. This PR cherry-picks that commit as a foundation. If #206 merges first, git will fast-forward cleanly since it's the same content — no conflict.

Phase Context

Planning & execution context from GitHub issue comments

Phase 41 of v6 milestone. This is the runtime plugin loading infrastructure that future issues in the milestone (integration into validate_and_load, agent-template plugin type, hook dispatching, etc.) will build on.

PR #206 (issue #191) designed the manifest schema and initial loader. This PR completes Phase 41 by adding the registry and standard discovery directory helper, giving the pipeline everything it needs to load and look up plugins at startup.

Test Plan

  • node -e "const pl = require('./lib/plugin-loader.cjs'); console.log(typeof pl.buildRegistry, typeof pl.getDefaultPluginDirs)" outputs function function
  • buildRegistry([]) returns an empty Map (14/14 verification checks pass)
  • discoverPlugins(['/nonexistent']) returns [] without throwing
  • validateManifest({ name: 'my-plugin', version: '1.0.0', type: 'hook', entrypoint: 'index.cjs' }) returns { valid: true, errors: null }
  • buildRegistry last-wins: second entry with same name:type key overwrites first
  • getDefaultPluginDirs('/tmp/proj') returns ['/tmp/proj/.mgw/plugins', '<homedir>/.mgw/plugins']
  • Integration test: discoverPlugins(getDefaultPluginDirs(process.cwd())) exits 0 with 0 plugins (no plugins installed)

Cross-References

Stephen Miller and others added 2 commits March 5, 2026 21:45
Add templates/mgw-plugin-schema.json defining the JSON Schema (draft-07)
for mgw-plugin.json manifests. Schema covers plugin metadata (name, version,
author), plugin type (agent-template, hook, validator), entry point,
supported pipeline stages, hook definitions, and optional config schema.

Add lib/plugin-loader.cjs with validateManifest(), loadPlugin(), and
discoverPlugins() functions. Uses Ajv for schema validation with graceful
degradation to structural checks when Ajv is unavailable.

Closes #191

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…ader

Add buildRegistry(plugins) that takes the discoverPlugins() output and
returns a Map<'name:type', pluginEntry> for O(1) lookup by extension point.
Last-found wins on duplicate keys so project-local plugins (discovered
last when dirs are ordered project-first) override user-global plugins.

Add getDefaultPluginDirs(repoRoot) that returns the standard two-path
discovery array: <repoRoot>/.mgw/plugins (project-local, priority) and
~/.mgw/plugins (user-global). Neither path must exist — discoverPlugins
silently skips missing directories.

Also add os module import and expand the module docblock to document
the registry and standard discovery directory pattern.

All smoke tests pass. No breaking changes to existing exports.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@github-actions github-actions bot added core Changes to core library templates Changes to templates labels Mar 6, 2026
@snipcodeit
Copy link
Owner Author

Testing Procedures

Quick Smoke Test (copy-paste ready)

# From the repository root after checking out this branch:
node -e "
const pl = require('./lib/plugin-loader.cjs');

// Verify full API surface
const fns = ['validateManifest', 'loadPlugin', 'discoverPlugins', 'buildRegistry', 'getDefaultPluginDirs'];
fns.forEach(fn => console.assert(typeof pl[fn] === 'function', fn + ' exported'));
console.assert(typeof pl.SCHEMA_PATH === 'string', 'SCHEMA_PATH exported');

// validateManifest — valid input
const v = pl.validateManifest({ name: 'my-plugin', version: '1.0.0', type: 'hook', entrypoint: 'index.cjs' });
console.assert(v.valid === true && v.errors === null, 'valid manifest accepted');

// validateManifest — invalid input  
const inv = pl.validateManifest({ name: 'BAD_NAME', version: '1.0.0', type: 'hook', entrypoint: 'index.cjs' });
console.assert(inv.valid === false && Array.isArray(inv.errors), 'invalid manifest rejected');

// discoverPlugins — missing dirs
const none = pl.discoverPlugins(['/tmp/does-not-exist-xyz']);
console.assert(Array.isArray(none) && none.length === 0, 'missing dir handled');

// buildRegistry
const reg = pl.buildRegistry([
  { manifest: { name: 'my-hook', type: 'hook', version: '1.0.0', entrypoint: 'i.cjs' }, plugin: {}, dir: '/a' }
]);
console.assert(reg instanceof Map && reg.has('my-hook:hook'), 'registry works');

// getDefaultPluginDirs
const dirs = pl.getDefaultPluginDirs(process.cwd());
console.assert(dirs.length === 2, 'getDefaultPluginDirs returns 2 paths');

// Integration
const found = pl.discoverPlugins(dirs);
console.log('Integration: discovered', found.length, 'plugins (0 expected — none installed)');

console.log('All tests passed');
"

Manual Integration Test

# Create a minimal test plugin
mkdir -p /tmp/test-mgw-plugin/my-test-plugin
cat > /tmp/test-mgw-plugin/my-test-plugin/mgw-plugin.json << 'EOF'
{
  "name": "my-test-plugin",
  "version": "1.0.0",
  "type": "hook",
  "entrypoint": "index.cjs",
  "description": "Test plugin"
}
EOF
cat > /tmp/test-mgw-plugin/my-test-plugin/index.cjs << 'EOF'
module.exports = { afterTriage: () => console.log('hook called') };
EOF

# Test discovery + loading + registry
node -e "
const { discoverPlugins, buildRegistry } = require('./lib/plugin-loader.cjs');
const plugins = discoverPlugins(['/tmp/test-mgw-plugin']);
console.log('Found:', plugins.length, 'plugin(s)');
console.log('Name:', plugins[0].manifest.name);
const reg = buildRegistry(plugins);
console.log('Registry has my-test-plugin:hook:', reg.has('my-test-plugin:hook'));
"

# Cleanup
rm -rf /tmp/test-mgw-plugin

Test Invalid Plugin (non-fatal warning)

mkdir -p /tmp/bad-plugin/broken-plugin
echo '{"name": "INVALID NAME"}' > /tmp/bad-plugin/broken-plugin/mgw-plugin.json

node -e "
const { discoverPlugins } = require('./lib/plugin-loader.cjs');
const plugins = discoverPlugins(['/tmp/bad-plugin']);
console.log('Result:', plugins.length, 'plugins (0 expected)');
console.log('Test passed — invalid plugin was skipped non-fatally');
" 2>&1

rm -rf /tmp/bad-plugin

Expected: stderr shows WARNING: Skipping plugin in /tmp/bad-plugin/broken-plugin: ..., stdout shows 0 plugins.

@snipcodeit
Copy link
Owner Author

Resetting milestone for rerun

@snipcodeit snipcodeit closed this Mar 6, 2026
@snipcodeit snipcodeit deleted the issue/192-implement-plugin-discovery-and-loader-in-lib-plugin-loader-cjs branch March 6, 2026 04:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Changes to core library templates Changes to templates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement plugin discovery and loader in lib/plugin-loader.cjs

1 participant