feat(plugin): implement plugin discovery and loader in lib/plugin-loader.cjs#207
Closed
snipcodeit wants to merge 2 commits intomainfrom
Closed
Conversation
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]>
Owner
Author
Testing ProceduresQuick 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-pluginTest 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-pluginExpected: stderr shows |
This was referenced Mar 6, 2026
Owner
Author
|
Resetting milestone for rerun |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
lib/plugin-loader.cjsbuildRegistry(plugins)for O(1) plugin lookup by"name:type"key after discoverygetDefaultPluginDirs(repoRoot)returning the standard[.mgw/plugins, ~/.mgw/plugins]discovery pathsCloses #192
Milestone Context
Changes
lib/plugin-loader.cjs— EnhancedNew exports added to the existing implementation:
buildRegistry(plugins) => Map<"name:type", entry>discoverPlugins()output. Last-found wins for duplicate keys (project-local overrides user-global).getDefaultPluginDirs(repoRoot) => string[][<repoRoot>/.mgw/plugins, ~/.mgw/plugins]. Neither path must exist —discoverPluginsskips missing dirs.Existing exports unchanged:
validateManifest(manifest)loadPlugin(pluginDir)discoverPlugins(pluginDirs)SCHEMA_PATHtemplates/mgw-plugin-schema.jsontemplates/mgw-plugin-schema.json— Included (cherry-picked from #191)JSON Schema draft-07 for
mgw-plugin.jsonmanifests. Covers: name (kebab-case), version (semver), type (agent-template|hook|validator), entrypoint, supported_stages, hooks (lifecycle event bindings), config_schema, requires_mgw_version.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)"outputsfunction functionbuildRegistry([])returns an emptyMap(14/14 verification checks pass)discoverPlugins(['/nonexistent'])returns[]without throwingvalidateManifest({ name: 'my-plugin', version: '1.0.0', type: 'hook', entrypoint: 'index.cjs' })returns{ valid: true, errors: null }buildRegistrylast-wins: second entry with samename:typekey overwrites firstgetDefaultPluginDirs('/tmp/proj')returns['/tmp/proj/.mgw/plugins', '<homedir>/.mgw/plugins']discoverPlugins(getDefaultPluginDirs(process.cwd()))exits 0 with 0 plugins (no plugins installed)Cross-References