Package authoring
This guide covers everything an external author needs to ship a Muoto extension package without reading kernel source. It follows the same public contract the official packages use.
What kind of package are you writing?
Section titled “What kind of package are you writing?”| Kind | Purpose | Official example |
|---|---|---|
| Integration | Mechanics of one external system (API clients, projections of external state) | @muoto/github, @muoto/gitlab |
| Reusable workflow | Workflow, lane, rule, check, gate, reaction definitions | @muoto/issue-to-pr |
| Preset | Opinionated composition: bundles + config + workspace/execution strategies | @muoto/solo-github |
| Utility | Plain library code without extension declarations | a GitHubClient class |
All of them are ordinary npm/TypeScript packages. There is no Muoto-specific distribution protocol: users install them with their normal package manager, and the AppRevision records the resolved dependency lock.
Package anatomy
Section titled “Package anatomy”A minimal integration package (this is the actual shape of the official packages):
import type { ExtensionBundle, PackageMetadata,} from "@muoto/sdk/extensions.ts";import type { CapabilityDeclaration } from "@muoto/sdk/sdk.ts";
const CAPABILITIES: readonly CapabilityDeclaration[] = [ { operation: "network.github", scope: "api.github.com" },];
export const acmeGitHubBundle: ExtensionBundle = { id: "@acme/github", version: "1.0.0", packageMetadata: { id: "@acme/github", version: "1.0.0", sdkCompatibility: "1.x", capabilities: CAPABILITIES, migrations: [], dependencies: [ { name: "@muoto/sdk", source: "registry", requested: "^1.0.0" }, ], }, requiredCapabilities: CAPABILITIES, projections: [ { kind: "projection", id: "acme.github.records", reduce: (state, event) => state, }, ],};{ "name": "@acme/github", "version": "1.0.0", "type": "module", "exports": { ".": "./src/mod.ts" }, "types": "./src/mod.ts", "repository": { "type": "git", "url": "git+https://example.com/acme/github.git" }, "homepage": "https://example.com/acme/github", "bugs": "https://example.com/acme/github/issues", "files": ["src"], "peerDependencies": { "@muoto/sdk": "^1.0.0" }}The @muoto/sdk import specifiers resolve against the SDK source checkout (sdk/ + the
repository deno.json import map) during development; at runtime the running Muoto install injects
the versioned SDK modules into the AppHost, so consumers never install the SDK themselves. The
peerDependencies entry declares the SDK generation the package is written against.
Contract rules:
- Bundle
id, bundleversion, andpackageMetadataidentity must agree, and the published npm version must equal them. - Bundle IDs use npm conventions (
@scope/nameorname); extension IDs are lowercase with dashes and dots, never slashes. - Declare
repository,homepage, andbugsso the catalog can link back to source. - The bundle declares what the application consumes:
workflows,lanes,rules,checks,gates,reactions,policies,agents,workspaces,executions,projections,frontends,migrations.
Compatibility metadata
Section titled “Compatibility metadata”packageMetadata.sdkCompatibility declares which SDK major versions the package supports, using
range tokens (1, 1.x, >=1 <2, ^1). The kernel rejects a candidate whose packages are
incompatible with the revision’s SDK version.
packageMetadata.dependencies lists package-level inputs (workspace, registry, or Git sources);
packageLock entries in the AppRevision resolve them to immutable versions, and package revisions
require a bundled deno.lock so the AppHost boots frozen. Commit lock files.
Capabilities and escalation
Section titled “Capabilities and escalation”Declare every capability your extensions need in requiredCapabilities and mirror it in
packageMetadata.capabilities. Adding capabilities is an escalation: the candidate is still
packaged, but activation requires explicit approval before the revision pointer moves. Removing a
capability never escalates. The conformance kit rejects unknown capability operations and empty
scopes.
Runnable escalation flow (same path the kernel exercises in its package lifecycle tests):
muoto mold validate <session> # compatibility checks passmuoto mold capability-diff <session> # shows the added capability grantmuoto mold candidate <session> # kernel mandatory CI runsmuoto mold activate <session> # requires the escalation to be approvedState and migrations
Section titled “State and migrations”Extensions get namespaced persistent state through the AppHost context (state.get / state.put
with a schema version). Migrations are declared per package with fromVersion/toVersion; the
kernel enforces that migration targets advance and that a migration is owned by its declaring
extension.
The conformance kit
Section titled “The conformance kit”Run the same checks the official packages run, in your own CI:
import { assertExtensionBundleConforms } from "@muoto/sdk/conformance.ts";import { acmeGitHubBundle } from "./src/mod.ts";
assertExtensionBundleConforms(acmeGitHubBundle, { sdkVersion: 1, existingExtensions: [], // extension IDs already in the revision availableServices: [], // service contracts other packages provide});The kit validates bundle identity, semver, SDK compatibility, capabilities, migrations, extension conflicts, command conflicts, service dependency graphs, and idempotency. It throws a structured report on any violation, so CI fails deterministically for broken cleanup, invalid dependency graphs, and side-effect ownership violations.
Service dependencies and lifecycle
Section titled “Service dependencies and lifecycle”Reusable extensions compose through abstract service contracts, not concrete packages. A provider
declares services.provides, a consumer declares services.requires (optionally optional: true),
and the AppHost reconciles activation: providers activate before dependents, dependents deactivate
through their lifecycle scopes when a provider is lost, and recovery reactivates exactly once.
Every extension’s register hook receives a LifecycleScope: own reversible AppHost-local
effects (subscriptions, timers, watchers) and provide service instances on it. The scope is
disposed in reverse registration order on deactivation.
Read Side-effect ownership before shipping: kernel resources (workspaces, executions, ACP sessions) go through kernel RPC, and externally visible operations (messages, pushes, merges) are proposed as idempotent, capability-checked commands with audit provenance — never represented as undo.
Publishing
Section titled “Publishing”The official workspace (packages/) publishes with npm provenance:
- Release tags:
packages/<package-name>@<version>. - Each package owns a
CHANGELOG.md; entries describe contract, SDK compatibility, capability, and migration changes. - CI runs
npm run check --workspaces --include-workspace-root,npm test, andnode packages/validate-release.mjsbefore any publish job. - Separate repositories may host packages, but they must run the same conformance kit and publish the same metadata fields.
A forkable preset
Section titled “A forkable preset”A preset composes bundles and defaults into one opinionated application. Mirror the
@muoto/solo-github shape: export a config, a bundles list, a packageLock, and default
workspace/execution strategy definitions. Consumers fork it, adjust the pieces they disagree with,
and register it through defineApplication({ ...preset }) — the same mechanism official packages
use.
See also
Section titled “See also”- SDK API reference for the exact contracts.
- Source of truth for what is generated versus authored.