| name | boring-plugin-build |
| description | Build 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. |
Boring Plugin Build
Companion files in this skill bundle
Read these companion docs when needed:
DECISION_TREE.md — choose the plugin shape first
REGISTRATION_MATRIX.md — map plugin surfaces to the right load path
PROGRESS_DISCLOSURE.md — how to report plugin progress clearly
CHECKLISTS.md — shape, registration, and verification checklists
SNIPPETS.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.md
packages/workspace/docs/PLUGIN_STRUCTURE.md
packages/workspace/docs/PLUGIN_SYSTEM.md
packages/core/docs/PLUGIN_INTEGRATION.md when the plugin is for a core-based shipped app
apps/workspace-playground/README.md
apps/workspace-playground/src/front/App.tsx
apps/workspace-playground/package.json
apps/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-plugin CLI
- relying on
/reload for 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.json for app-local direct-source plugins
plugins/<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 with boring-ui-plugin create <name> --path plugins
apps/<app>/src/plugins/<name>/ follows the direct-source app-local pattern shown in the playground example, not the built dist/* 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.server changes
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.server to runtime .pi/extensions plugins
- do not promise
/reload will 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.server changed
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, or handler keys — 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
name in pi.slashCommands must exactly match the command name passed to pi.registerCommand.
- The declaration in
package.json is for picker discoverability; the actual behavior lives in registerCommand.
openPanel({ pluginName }) uses the pluginName matching package.json#boring.label (or the inferred kebab name).
notify is optional — use it to give the user feedback after the action.
References
DECISION_TREE.md
REGISTRATION_MATRIX.md
PROGRESS_DISCLOSURE.md
CHECKLISTS.md
SNIPPETS.md
packages/pi/skills/boring-plugin-authoring/SKILL.md
packages/workspace/docs/PLUGIN_STRUCTURE.md
packages/workspace/docs/PLUGIN_SYSTEM.md
packages/core/docs/PLUGIN_INTEGRATION.md
apps/workspace-playground/README.md
apps/workspace-playground/src/front/App.tsx
apps/workspace-playground/package.json
apps/workspace-playground/src/plugins/playgroundDataCatalog/package.json