Boring Plugin Build
SkillFiles & storageBuild or shape a boring-ui plugin for a shipped app or playground. Covers choosing runtime vs app/internal plugin shape, where files live, how to register plugins, and when to use static composition vs defaultPluginPackages. Use when the user asks for plugin shape, new panels/tabs/catalogs/tools, or app-specific workspace extensions.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Boring Plugin Build skill
What this skill tells your AI
The instructions your AI receives, as published by hachej/boring-ui in .agents/skills/boring-plugin-build/SKILL.md and read by ahel’s review.
Companion files in this skill bundle
Read these companion docs when needed:
DECISION_TREE.md— choose the plugin shape firstREGISTRATION_MATRIX.md— map plugin surfaces to the right load pathPROGRESS_DISCLOSURE.md— how to report plugin progress clearlyCHECKLISTS.md— shape, registration, and verification checklistsSNIPPETS.md— copy-paste commands for runtime plugins, app plugins, core boot, and Vercel entry wiring
The one rule
Decide plugin trust level before you write files.
There are two valid plugin shapes in this repo:
- runtime/generated plugin under
.pi/extensions/<name>/ - app/internal trusted plugin package declared through
package.json#boring.defaultPluginPackages
If the plugin is part of a real shipped app, default to app/internal trusted package. If the plugin is for fast local iteration or agent-authored experimentation, default to runtime/generated plugin.
Read these first
packages/pi/skills/boring-plugin-authoring/SKILL.mdpackages/workspace/docs/PLUGIN_STRUCTURE.mdpackages/workspace/docs/PLUGIN_SYSTEM.mdpackages/core/docs/PLUGIN_INTEGRATION.mdwhen the plugin is for a core-based shipped appapps/workspace-playground/README.mdapps/workspace-playground/src/front/App.tsxapps/workspace-playground/package.jsonapps/workspace-playground/src/plugins/playgroundDataCatalog/package.json
Treat packages/pi/skills/boring-plugin-authoring/SKILL.md as the deep canonical authoring manual. This local skill is the repo-specific dispatcher that tells you which path to use.
Also follow PROGRESS_DISCLOSURE.md while implementing so the user always knows the current plugin shape, load path, and restart/reload requirement.
Use DECISION_TREE.md before writing files.
Decision table
| Need | Choose |
|---|---|
live local experimentation, /reload, no trusted backend routes | .pi/extensions/<name>/ |
| app-owned domain logic, trusted routes/tools, production app package | app/internal plugin package |
| provider/binding plugin that wraps React tree | app/internal plugin package + static front composition |
| panel/command/catalog/surface-resolver only | either, but shipped apps usually use app/internal packages |
Runtime/generated plugin path
Use when:
- iterating inside a workspace
- authoring with the
boring-ui-pluginCLI - relying on
/reloadfor front/Pi resources - avoiding custom backend routes
Workflow:
- run the scaffold command from the canonical skill
- edit the generated files in place
- run
boring-ui-plugin verify <name> "$BORING_AGENT_WORKSPACE_ROOT" - tell the user to run
/reload
Use SNIPPETS.md if you want copy-paste commands instead of rebuilding them by hand.
Read and follow:
packages/pi/skills/boring-plugin-authoring/SKILL.md
Do not invent a custom layout for this path.
App/internal trusted plugin path
Use when:
- the plugin ships with the app
- you need trusted server routes or agent tools
- the plugin is part of the app’s deployable identity
- you want the plugin discovered at boot from the app manifest
Two common homes:
apps/<app>/src/plugins/<name>/package.jsonfor app-local direct-source pluginsplugins/<name>/for shared publishable repo plugins
Concrete in-repo app-local example:
apps/workspace-playground/src/plugins/playgroundDataCatalog/package.json
Important:
plugins/<name>/is the repo-level packaged plugin home; create it withboring-ui-plugin create <name> --path pluginsapps/<app>/src/plugins/<name>/follows the direct-source app-local pattern shown in the playground example, not the builtdist/*CLI template shape
Use the same manifest principles as the canonical skill:
{
"name": "my-plugin",
"version": "0.0.0",
"private": true,
"boring": {
"label": "My Plugin",
"front": "front/index.tsx",
"server": "server/index.ts"
},
"pi": {
"systemPrompt": "Short agent guidance for when this plugin matters."
}
}
Registration rules
Use REGISTRATION_MATRIX.md when in doubt about how a plugin surface should load.
Package-discovered defaults
For package plugins, register through the app’s package.json:
{
"boring": {
"defaultPluginPackages": [
"@hachej/boring-ask-user",
"./src/plugins/my-plugin"
]
}
}
Use this for server/Pi/static package discovery and, outside core, some package-driven plugin loading paths.
For core-based shipped apps, make sure the server boot path actually passes
appPackageJsonPath or defaultPluginPackages; otherwise the manifest entry can
exist and nothing will load. See packages/core/docs/PLUGIN_INTEGRATION.md and
.agents/skills/boring-app-setup/SKILL.md.
Also: for core-based shipped apps, do not assume defaultPluginPackages
alone makes front panel/command/catalog/surface-resolver UI appear. Front plugin
surfaces should be statically composed in the app shell when the shipped UI must
render them.
Static front composition still needed for providers/bindings
If the plugin contributes providers or bindings, compose it explicitly in the front app shell:
<WorkspaceAgentFront plugins={[myProviderPlugin]} ... />
Do not assume provider/binding plugins can be safely hot-mounted through the dynamic package path.
For shipped apps, prefer this build order
- prove the UX in
apps/workspace-playground - harden the plugin into an app/internal package
- register it with
defaultPluginPackages - for core-based shipped apps, statically compose front plugin surfaces in the app shell when the shipped UI must render them
- restart/redeploy when
boring.serverchanges
This is the shortest path from idea to production-safe plugin.
Guardrails
- use
definePlugin({ ... }), not legacy API names - use
defineServerPlugin({ ... })for trusted server-side contributions - keep
src/shared/**browser-safe if you add shared code - do not add
boring.serverto runtime.pi/extensionsplugins - do not promise
/reloadwill apply trusted server-route changes - do not deep-import undocumented workspace internals
Verification
For runtime plugins:
boring-ui-plugin verify <name> "$BORING_AGENT_WORKSPACE_ROOT"
For app/internal plugins:
pnpm typecheck
pnpm lint:invariants
Also verify the integration point:
- manifest entry present in
package.json#boring.defaultPluginPackages - front plugin composed where needed
- server restart/redeploy done when
boring.serverchanged
When to stop and ask
Stop and ask if any of these are unclear:
- should this plugin be runtime or app/internal?
- does it need trusted backend routes/tools?
- is it part of one app only or a shared plugin?
- does it need a permanent left tab, or just a panel/command?
- does it need provider/binding behavior?
Do not pick silently. Plugin shape drives everything else.
Slash commands that open a panel (UI actions)
Runtime plugins can register slash commands that open their panel (or trigger any in-process action) directly from the Pi composer. Two steps:
1. Declare in package.json
{
"pi": {
"extensions": ["agent/index.ts"],
"slashCommands": [
{ "name": "open-<kebab-name>", "description": "Open the <Label> panel" }
]
}
}
name— command name without a leading slash (e.g."open-counter", not"/counter").description— one-line hint shown in the picker.- No
command,title,action, orhandlerkeys — those are not part of the schema and will be ignored.
2. Register the handler in agent/index.ts
import { openPanel, notify } from '@hachej/boring-workspace/plugin'
export default function (pi: PiApi) {
pi.registerCommand('open-<kebab-name>', async () => {
await openPanel({ pluginName: '<kebab-name>' })
await notify({ message: '<Label> opened' })
})
}
openPanel and notify are imported from @hachej/boring-workspace/plugin.
Key rules:
- The
nameinpi.slashCommandsmust exactly match the command name passed topi.registerCommand. - The declaration in
package.jsonis for picker discoverability; the actual behavior lives inregisterCommand. openPanel({ pluginName })uses thepluginNamematchingpackage.json#boring.label(or the inferred kebab name).notifyis optional — use it to give the user feedback after the action.
References
DECISION_TREE.mdREGISTRATION_MATRIX.mdPROGRESS_DISCLOSURE.mdCHECKLISTS.mdSNIPPETS.mdpackages/pi/skills/boring-plugin-authoring/SKILL.mdpackages/workspace/docs/PLUGIN_STRUCTURE.mdpackages/workspace/docs/PLUGIN_SYSTEM.mdpackages/core/docs/PLUGIN_INTEGRATION.mdapps/workspace-playground/README.mdapps/workspace-playground/src/front/App.tsxapps/workspace-playground/package.jsonapps/workspace-playground/src/plugins/playgroundDataCatalog/package.json
Signals
- GitHub stars
- 46
- Forks
- 2
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
boring-plugin-build- Source
- github.com/hachej/boring-ui