Composer Plugins

SkillFiles & storage

Use when working on files in packages/plugins/, adding new plugins,

Available today. Use it from your connected AI after setup.

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.md and drop it from MEMORY.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 by id substring; pass compact: true for 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 by slug substring or hostKind: '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:

  1. list_plugins({ id: 'space' }) → the exact plugin id, when you only have a loose name.
  2. find_symbol({ query: 'SpaceOperation' })@dxos/plugin-space#SpaceOperation.
  3. get_symbol({ ref: '@dxos/plugin-space#SpaceOperation', include: ['source'] }) → every definition with its meta.key, input, output and services.

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:

  1. Run the superpowers:brainstorming flow and write the approved design to packages/plugins/plugin-<name>/docs/DESIGN.md, then add a short stub at agents/superpowers/specs/YYYY-MM-DD-<name>-design.md that 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 default docs/superpowers/… path; a design doc that belongs to no package is written there directly.)
  2. The user approves that design doc before any code is written.
  3. Implement Phase 1 against the design doc.
  4. At the end of Phase 1, before opening the PR, author packages/plugins/plugin-<name>/PLUGIN.mdl from the design doc and the as-built plugin. This is a required pre-PR step — the design doc drove the build; PLUGIN.mdl is 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/:

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.mdl in 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, and test blocks.

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:

  1. A ## QA section in PLUGIN.mdl holding at least one flow QA-n block in the QA dialect (lang/qa.mdl; plugin-chess/PLUGIN.mdl is 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).
  2. A recorded demo of that flow against the running app, per the recording-demos skill: drive the flow's do: steps one gesture at a time, caption each step with its do: text verbatim, and judge expect: from the screen. Attach the .webm to 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:

  1. README.md — brief description of the plugin's purpose, linking to docs/DESIGN.md.
  2. dx.config.tsConfig2.make({ plugin: { … } }) with key, name, author, description, icon, and a quality tier tag (see below).
  3. package.json — with "private": true, #plugin import alias, ./plugin export subpath, and minimal dependencies.
  4. moon.yml — with compile entry points for both src/index.ts and src/plugin.ts.
  5. src/meta.ts — plugin metadata (id, name, description, icon, iconHue).
  6. src/translations.ts — initial translation resources.
  7. src/FooPlugin.tsx — minimal Plugin.define(meta).pipe() with surface and translations modules, plus export default FooPlugin.
  8. src/plugin.ts — lazy wrapper: export const FooPlugin = Plugin.lazy(meta, () => import('#plugin')). Re-export any OperationHandlerSet here too.
  9. src/index.ts — exports only meta and types/operations. Never exports the plugin instance.
  10. src/types/ — one schema type with make() factory.
  11. src/capabilities/index.ts — single AppCapability.surface() for ReactSurface (declare its roles).
  12. src/capabilities/react-surface.tsx — one surface for the article role.
  13. src/containers/ — one container (e.g., FooArticle) with lazy export and basic storybook.
  14. 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.

TierMeaning
systemCore 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.
betaStable enough to lead with. Shown in the registry's Recommended category.
alphaA real feature, still moving. Also shown in Recommended.
labsExperimental, 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:

SuffixUse whenExample
ProviderThe contributor supplies data, a factory, or an array of extensionsMapCapabilities.MarkerProvider, GameCapabilities.VariantProvider, MarkdownCapabilities.ExtensionProvider
ServiceThe contributor performs active async work (search, routing, …)TripCapabilities.BookingService, TripCapabilities.RoutingService
EventHandlerThe contributor registers callbacks for host-plugin lifecycle eventsCallsCapabilities.EventHandler
ConfigThe contributor supplies a declarative config object keyed by typenameAppCapabilities.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