Composer Plugins
SkillFiles & storageUse when working on files in packages/plugins/, adding new plugins,
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 Composer Plugins skill
What this skill tells your AI
The instructions your AI receives, as published by dxos/dxos in .agents/skills/composer-plugins/SKILL.md and read by ahel’s review.
Exemplar: packages/plugins/plugin-chess. Read its source files to understand every pattern below.
Companion skills. For building plugin UI with the design system — container layout, theme tokens,
forms, toolbars, lists/stacks, reactivity, storybook — use the composer-ui skill. For authoring
new @dxos/react-ui composite primitives (Foo.Root/Foo.Content), use composite-components. This
skill owns plugin structure (capabilities, surfaces, schema, operations) and points at those two for UI.
Read MEMORY.md first (sibling of this file) for session-logged design/implementation learnings and prior corrections.
REQUIRED — keep MEMORY.md current: Whenever the user directs a correction (tells you to do something differently, rejects an approach, or specifies a pattern), record it in MEMORY.md as part of carrying out that correction — do not defer to session end. Also capture other non-obvious design/implementation details as you learn them.
Update it appropriately:
- Append to the current session's dated section, newest first:
## YYYY-MM-DD — <plugin(s)>. Create it if absent; do not start a second section for the same session. - Keep it compact and agent-directed: terse imperative bullets, one rule per bullet, name the file/symbol/idiom. No prose, no hedging, no narration of what you did.
- Update or merge an existing bullet instead of adding a near-duplicate; delete bullets proven wrong.
- Record reusable rules, not task specifics. When a rule generalizes beyond one session, promote it into the body of this
SKILL.mdand drop it fromMEMORY.md.
Discovery
Use the dxos-introspect MCP server (@dxos/introspect-mcp, served by the dx-introspect-mcp binary) as the source of truth for plugin metadata and reference examples — not directory listings.
A "plugin" is a package whose src/meta.ts exports a Plugin.Meta, so ls packages/plugins/ overcounts (e.g. plugin-generator is tooling, not a plugin).
mcp__dxos-introspect__list_plugins— enumerate plugins (filter byidsubstring; passcompact: truefor identifying fields only).mcp__dxos-introspect__get_package— package details for a given plugin.mcp__dxos-introspect__list_surfaces/list_capabilities/list_operations/list_schemas— drill into a plugin's contributions.mcp__dxos-introspect__find_symbol/get_symbol/list_symbols— locate code by symbol rather than grepping paths.mcp__dxos-introspect__list_idioms— enumerate@idiom-tagged reference examples (filter byslugsubstring orhostKind: 'symbol' | 'story' | 'test').
Reach for these first when answering questions like "how many plugins", "which plugin contributes X surface", or "where is symbol Y defined".
Reading an operation's key and input shape
list_operations does not enumerate operations — it returns one row per
Capabilities.OperationHandler contribution, i.e. where each plugin's handler file lives. The
definitions live under a <Plugin>Operation symbol — a namespace in plugin-space, a module of
top-level exports in plugin-markdown — so go through the symbol tools:
list_plugins({ id: 'space' })→ the exact plugin id, when you only have a loose name.find_symbol({ query: 'SpaceOperation' })→@dxos/plugin-space#SpaceOperation.get_symbol({ ref: '@dxos/plugin-space#SpaceOperation', include: ['source'] })→ every definition with itsmeta.key,input,outputandservices.
Read services while you are there: a definition listing Database.Service needs a spaceId at
invoke time, which is invisible from the key alone.
Search idioms before implementing
Required. Before writing or refactoring any container, capability, operation, skill, or schema, call mcp__dxos-introspect__list_idioms and scan for a slug that matches what you're about to build. An idiom is a JSDoc-tagged pinning of the canonical way to do one thing — when one exists, it is the answer, and you should get_symbol on the host artifact and follow the pattern rather than reinventing it.
Typical triggers:
- Building a toolbar → look for
org.dxos.react-ui-menu.*idioms. - Wiring
useObject/ mutating ECHO subjects → look for ECHO idioms. - Writing a surface filter, operation handler, skill, or container scaffold → search by the feature word first.
If no idiom matches, proceed using the exemplar (plugin-chess); if you find yourself writing something that other plugins will copy, consider adding a new @idiom tag (see packages/reflect/deus/docs/IDIOMS.md for the format and slug rules).
Specification
A plugin's design is captured in two artifacts across its lifecycle — a
superpowers design doc during the initial build, then a durable
PLUGIN.mdl that outlives the first session.
Package docs go in docs/, never the package root
Every markdown file a plugin owns other than README.md lives under
packages/plugins/plugin-<name>/docs/ — docs/DESIGN.md, docs/AUDIT.md,
docs/TESTING.md, and so on (see plugin-assistant/docs/, plugin-inbox/docs/).
The package root holds only README.md, PLUGIN.mdl, and build config; a
DESIGN.md sitting beside package.json is a mistake to move, not a variant to
match. README.md links into docs/ rather than restating it.
This is about the package root staying scannable — a reader opening the plugin
should see config and src/, with prose one directory away.
Initial plugin creation (first session)
When creating a brand-new plugin, do NOT start with PLUGIN.mdl. Instead:
- Run the
superpowers:brainstormingflow and write the approved design topackages/plugins/plugin-<name>/docs/DESIGN.md, then add a short stub atagents/superpowers/specs/YYYY-MM-DD-<name>-design.mdthat links to it. The doc ships with the package it describes and the specs index still finds it; the stub carries a link and nothing else, so there is one source of truth. (agents/superpowers/specs/is the DXOS override of the superpowers defaultdocs/superpowers/…path; a design doc that belongs to no package is written there directly.) - The user approves that design doc before any code is written.
- Implement Phase 1 against the design doc.
- At the end of Phase 1, before opening the PR, author
packages/plugins/plugin-<name>/PLUGIN.mdlfrom the design doc and the as-built plugin. This is a required pre-PR step — the design doc drove the build;PLUGIN.mdlis the hand-off spec that subsequent sessions consume.
PLUGIN.mdl — the durable spec
PLUGIN.mdl is written in the MDL (.mdl) language defined by @dxos/deus.
The authoritative references live under packages/reflect/deus/:
docs/DESIGN.md— language specification.docs/IDIOMS.md— idiom format and@idiomJSDoc-tag conventions.lang/core.mdl— core dialect.lang/PLUGIN-.template.mdl— the plugin template.src/extension/mdl.grammar— Lezer grammar (use only when chasing syntax questions).
Use the template as the starting structure and packages/plugins/plugin-chess/PLUGIN.mdl
as a reference. PLUGIN.mdl is a record of what has been built — not a
working document. Design exploration for new features (in any session) happens
in a design doc under the plugin's docs/ (indexed from agents/superpowers/specs/);
PLUGIN.mdl is updated only after the design AND implementation have settled. It must be:
- Present before a new plugin's first PR merges — created at the close of Phase 1 as described above; never omitted.
- Updated after the work settles — when features are added or changed,
brainstorm and implement against a design doc, then bring
PLUGIN.mdlin line with the as-built plugin before the PR (never edit it speculatively up front). - Used for testing — derive user feature tests and acceptance criteria from
the spec's
feat,req, andtestblocks.
Every new plugin ships a QA flow and a demo video
Two artifacts, both authored at the close of Phase 1 alongside PLUGIN.mdl and
both required before the plugin's first PR merges:
- A
## QAsection inPLUGIN.mdlholding at least oneflow QA-nblock in the QA dialect (lang/qa.mdl;plugin-chess/PLUGIN.mdlis the reference). One flow covering the plugin's primary user journey end to end is the minimum. Its execution rules are not style advice — read them before authoring, especially Rule 5 (assertions must be falsifiable against a dirty fixture) and Rule 7 (before/test/after). - A recorded demo of that flow against the running app, per the
recording-demosskill: drive the flow'sdo:steps one gesture at a time, caption each step with itsdo:text verbatim, and judgeexpect:from the screen. Attach the.webmto the conversation and commit a contact sheet or stills for the PR body — never the video.
Write the flow first, then record it. A demo improvised against the app
proves the app runs; a demo that executes a written flow proves the spec and the
app agree, and the recording is what sets the flow's status:. Where they
disagree, that is a finding — report it, and fix whichever is wrong.
Workflow
- Use
/superpowers:writing-plans(Subagent-Driven) for non-trivial plugin work. - Show the change running, in the PR. A plugin PR is a change to what the app renders, so a
reviewer should not have to build it to see it. Record the flow or take the stills with
recording-demos, then publish them per hosting-artifacts
(
.agents/skills/hosting-artifacts/SKILL.md) and link them from the PR body — never commit a video or a screenshot to make it visible. For a fix to rendered output, a before/after pair from one build (see composer-ui) beats a clip.
Creating a New Plugin
When asked to create a new plugin, first produce the superpowers design doc (see
Specification above), then start with a minimal skeleton before adding features.
PLUGIN.mdl is NOT part of the initial skeleton — it is authored at the end of
Phase 1, before the PR. The skeleton should include:
README.md— brief description of the plugin's purpose, linking todocs/DESIGN.md.dx.config.ts—Config2.make({ plugin: { … } })with key, name, author, description, icon, and a quality tier tag (see below).package.json— with"private": true,#pluginimport alias,./pluginexport subpath, and minimal dependencies.moon.yml— withcompileentry points for bothsrc/index.tsandsrc/plugin.ts.src/meta.ts— plugin metadata (id, name, description, icon, iconHue).src/translations.ts— initial translation resources.src/FooPlugin.tsx— minimalPlugin.define(meta).pipe()with surface and translations modules, plusexport default FooPlugin.src/plugin.ts— lazy wrapper:export const FooPlugin = Plugin.lazy(meta, () => import('#plugin')). Re-export anyOperationHandlerSethere too.src/index.ts— exports onlymetaand types/operations. Never exports the plugin instance.src/types/— one schema type withmake()factory.src/capabilities/index.ts— singleAppCapability.surface()for ReactSurface (declare itsroles).src/capabilities/react-surface.tsx— one surface for thearticlerole.src/containers/— one container (e.g.,FooArticle) with lazy export and basic storybook.src/components/— empty barrel, ready for primitives.
Build and lint the skeleton before adding features.
Add capabilities incrementally as needed (operations, skills, settings, etc.).
Register the plugin with composer-app: FooPlugin.make() in getPlugins, and its key in the isDev block of getDefaults unless the plugin hits a permission-gated API on activation (rule 5 under Activation waves).
Once the plugin contributes a navtree section, apply both rules under App graph below — gate the section on a non-empty query, and default the create-object targetNodeId to the node that lists the objects.
Quality tiers
Every plugin MUST declare exactly one quality tier as the FIRST entry of
plugin.tags in dx.config.ts. A new plugin defaults to labs — promotion is a
deliberate, separate decision, never the scaffold's default.
| Tier | Meaning |
|---|---|
system | Core infrastructure. Force-enabled and not user-toggleable; derived in plugin-manager.ts from tags.includes('system'). Also omit the key from getDefaults in composer-app/src/plugin-defs.tsx — redundant once core. |
beta | Stable enough to lead with. Shown in the registry's Recommended category. |
alpha | A real feature, still moving. Also shown in Recommended. |
labs | Experimental, thin, dev-only, or platform-gated. The default for a new plugin. |
tags: ['labs'],
Secondary tags (connector, game, assistant, travel) follow the tier:
tags: ['labs', 'connector']. Add alpha/beta/labs/system to
RegistryTagType in plugin-registry/src/types.ts — a new secondary tag needs
no change there, but an unlisted tag renders without a hue.
Do NOT leave a plugin untagged. getCategoryPredicate in
plugin-registry/src/categories.ts selects Recommended by an explicit
beta/alpha allowlist, so an untagged plugin silently appears in no category
but bundled.
Directory Structure
plugin-foo/
package.json
moon.yml
dx.config.ts # Plugin manifest; carries the quality tier in `plugin.tags`.
PLUGIN.mdl
README.md # The only markdown at the root; links into docs/.
docs/ # Everything else the package documents.
DESIGN.md
src/
index.ts # Root entrypoint; exports only meta and types/operations — never the plugin instance.
plugin.ts # Plugin.lazy() wrapper; consumed via @dxos/plugin-foo/plugin.
meta.ts # Plugin.Meta (id, name, description, icon, iconHue).
translations.ts # i18n resources keyed by typename and meta.id.
paths.ts # Canonical qualified graph paths (only if the plugin owns navtree nodes).
FooPlugin.tsx # Plugin definition via Plugin.define(meta).pipe().
skills/ # AI skill definitions.
index.ts
capabilities/ # Lazy capability modules (one file each).
index.ts # Barrel of maker / Capability.lazyModule() exports.
react-surface.tsx
operation-handler.ts
skill-definition.ts
app-graph-builder.ts # Navtree sections, child nodes, actions.
create-object.ts # SpaceCapabilities.CreateObjectEntry per type.
components/ # Primitive UI components (no app-framework deps).
index.ts
MyComponent/
index.ts
MyComponent.tsx
MyComponent.stories.tsx
containers/ # Surface components (lazy-loaded, use capabilities).
index.ts # lazy(() => import('./X')) exports.
FooArticle/
index.ts # Bridges named -> default export.
FooArticle.tsx
FooArticle.stories.tsx
operations/ # Operation definitions and handlers.
index.ts
definitions.ts
types/ # ECHO schema definitions.
index.ts # Namespace re-export: export * as Foo from './Foo';
Foo.ts
Concepts
Component (src/components/)
Low-level UI (plugin/src/components, react-ui-*). Must NOT depend on @dxos/app-framework or @dxos/app-toolkit.
Each component lives in its own subdirectory with an index.ts barrel.
Use named exports; no default exports. Create a basic storybook for each.
Prefer composable Radix-style namespaces for non-trivial components. Mirror the Foo.Root / Foo.Toolbar / Foo.Content / Foo.Viewport pattern used by Panel.*, Card.*, Masonry.*, and ScrollArea.* in @dxos/react-ui and @dxos/react-ui-masonry. The Root provides shared context (data, callbacks, Tile component); subcomponents read it and slot into the outer Panel/ScrollArea structure. This lets containers plug in their own toolbar contents (e.g. MenuBuilder buttons) without forking the component, and keeps the component fully presentation-only.
// Pure component namespace — no app-framework deps.
export const FooMasonry = { Root: Root, Toolbar: Toolbar, Content: Content, Viewport: Viewport };
// Container composes:
<FooMasonry.Root items={items} onDelete={handleDelete}>
<FooMasonry.Toolbar>
<Menu.Root {...menuActions} attendableId={attendableId}>
<Menu.Toolbar />
</Menu.Root>
</FooMasonry.Toolbar>
<FooMasonry.Content>
<FooMasonry.Viewport />
</FooMasonry.Content>
</FooMasonry.Root>;
Sketch the namespace export first when designing a new component; only collapse to a single component if the surface really has no slots.
See: plugin-chess/src/components/Chessboard/, packages/ui/react-ui-masonry/src/Masonry.tsx
Container (src/containers/)
High-level surface component. Uses capabilities and is referenced by react-surface.
Each container lives in its own subdirectory. The subdirectory index.ts bridges named to default export (for React.lazy).
The top-level containers/index.ts uses lazy(() => import('./X')) with : ComponentType<any> annotation.
Surface components use suffixes matching their role: Article, Card, Dialog, Popover, Settings.
Create a basic storybook for each.
If a "component" needs useCapability/useCapabilities/useAppGraph/useOperationInvoker, it belongs in containers/. Storybooks won't have a PluginManager — calling capability hooks under components/ throws. Refactor: take the resolved value (URL, callback, Tile component) as a prop and move the hook one level up.
UI: forms, theming, toolbars, cards, layout
The detailed rules for building plugin UI with the design system live in the composer-ui skill
(.agents/skills/composer-ui/SKILL.md). Consult it whenever you write a container/component, reach for a
Tailwind color class, build a toolbar, edit an object with a form, or add a story. It covers: the
@dxos/react-ui* packages, verified theme tokens (never invent bg-input/text-primary), the standard
Panel + ScrollArea container layout (no wrapper divs), MenuBuilder + useMenuActions + Menu.Root
toolbar wiring (threading attendableId), schema-driven Form editing (no native inputs), the Card
3-slot subgrid, icons, attention/density, reactivity (useObject for ECHO objects passed into
components), translations, and storybook setup. For authoring brand-new @dxos/react-ui primitives, see
the composite-components skill.
Before committing UI, grep the diff for dead classes. The tailwindcss-logical dialect
(pis-*, pbs-*, pli-*, mis-*, is-*, bs-*, min-bs-*, …) was dropped in the Tailwind v4
migration and now compiles to nothing — silently, so nothing errors and nothing lints. It is the
highest-frequency UI regression in this repo, and worst when the dead class was load-bearing (a
min-bs-* height floor, a min-is-0 letting a grid child shrink), because the failure surfaces far
from its cause. Replacement table and the grep are in composer-ui § "Sizing vs logical utilities".
Capability (src/capabilities/)
Plugin modules that contribute functionality to the framework. Each is a single file with a default export using Capability.makeModule(). The barrel index.ts uses only makers (AppCapability.*) or Capability.lazyModule() exports. Do NOT add non-lazy exports.
See: plugin-chess/src/capabilities/
Cross-plugin capabilities (src/types/XCapabilities.ts)
Some plugins expose capability keys for other plugins to implement — a decoupled provider/extension
contract. See packages/plugins/AUDIT.md for the current registry.
Naming convention — use one of four suffixes depending on the role:
| Suffix | Use when | Example |
|---|---|---|
Provider | The contributor supplies data, a factory, or an array of extensions | MapCapabilities.MarkerProvider, GameCapabilities.VariantProvider, MarkdownCapabilities.ExtensionProvider |
Service | The contributor performs active async work (search, routing, …) | TripCapabilities.BookingService, TripCapabilities.RoutingService |
EventHandler | The contributor registers callbacks for host-plugin lifecycle events | CallsCapabilities.EventHandler |
Config | The contributor supplies a declarative config object keyed by typename | AppCapabilities.CommentConfig (consumed by plugin-comments) |
When the contract is app-wide rather than owned by one plugin (e.g. comment support), the capability
key lives in AppCapabilities (@dxos/app-toolkit) instead of a plugin's src/types/XCapabilities.ts;
plugin-comments re-exports AppCapabilities.CommentConfig as CommentCapabilities.CommentConfig.
Where to define — add the Capability.make<T>() call in the defining plugin's
src/types/XCapabilities.ts, namespace-exported from src/types/index.ts:
// packages/plugins/plugin-foo/src/types/FooCapabilities.ts
export const BarProvider = Capability.make<BarProvider>(`${meta.id}.capability.bar-provider`);
Expose it via a ./types subpath in package.json (see plugin-game/package.json as a reference).
The --entryPoint=src/types/index.ts entry in moon.yml is typically already present.
Where to implement — the donor plugin places its contribution in a dedicated file in
src/capabilities/, named after the capability it implements (e.g. routing-service.ts,
markdown-extension.ts). Wire it via Capability.lazy in src/capabilities/index.ts.
How to import the key — use the /types subpath, not the root entrypoint:
// ✓
import { FooCapabilities } from '@dxos/plugin-foo/types';
// ✗ — pulls in the full barrel (meta, hooks, operations, …)
import { FooCapabilities } from '@dxos/plugin-foo';
Reference implementations:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 520
- Forks
- 49
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
composer-plugins- Source
- github.com/dxos/dxos