Authoring APM Packages

A package is a named, independently versioned bundle of one or more primitives. Packages are how consumers depend on a curated set of primitives as a single unit (the way eslint-config-airbnb bundles ESLint rules).

Source of truth: schemas/apm-package.schema.json.


When to create a package vs. a primitive

Create a primitiveCreate a package
You are shipping one self-contained capability.You are shipping a curated set of primitives that is meaningful as a unit.
Consumers will install it on its own.Consumers benefit from declaring one dependency instead of several.
Versioning is independent of any other content.The set evolves together — a primitive bump is also a package bump (semver-wise).

If a package only ever has one primitive, ship the primitive directly.


1. Pick a directory

packages/<name>/

<name> is kebab-case (^[a-z][a-z0-9-]+$) and matches the name field in the manifest.

2. Write apm-package.json

Required fields:

FieldTypeNotes
idstringReverse-domain id matching ^[a-z][a-z0-9]*\.package\.[a-z][a-z0-9-]+$. Use calab.package.<name>.
namestringkebab-case, matches the directory.
versionstringSemver MAJOR.MINOR.PATCH. Drives refs/tags/packages/<name>/v<version>.
descriptionstringOne-sentence catalog description.
primitivesarrayAt least one. Each item is { "ref": "primitives/<type>/<name>" }. Order matters for human-readable output; uniqueness is enforced semantically.

Optional fields:

FieldUse it for
maintainers["calab-ai"] or specific GitHub handles/teams.
tagsMarketplace search.
compatibility.toolsSubset of copilot, claude, codex, opencode, any.
compatibility.min_catalog_versionSet when relying on newer catalog features.
filesOptional [{ "src": "...", "dest": "..." }] for package-level files alongside the composed primitives.

The GA calab-workspace-base package is the canonical example:

{
  "$schema": "../../schemas/apm-package.schema.json",
  "id": "calab.package.workspace-base",
  "name": "calab-workspace-base",
  "version": "1.0.0",
  "description": "Base workspace package for Calab AI consumer repositories. Installs the core build and plan agents, shared coding-standards and security instructions, and essential CLI skills.",
  "maintainers": ["calab-ai"],
  "tags": ["workspace", "agents", "instructions", "skills", "base"],
  "compatibility": {
    "tools": ["copilot", "claude", "codex", "opencode", "any"]
  },
  "primitives": [
    { "ref": "primitives/agents/build" },
    { "ref": "primitives/agents/plan" },
    { "ref": "primitives/instructions/coding-standards" },
    { "ref": "primitives/instructions/security" },
    { "ref": "primitives/skills/az-cli" },
    { "ref": "primitives/skills/gh-cli" },
    { "ref": "primitives/skills/summarise" }
  ]
}

The smaller calab-org-agents package shows the minimal shape (two agents, no extra files):

{
  "$schema": "../../schemas/apm-package.schema.json",
  "id": "calab.package.org-agents",
  "name": "calab-org-agents",
  "version": "1.0.0",
  "description": "Org-level Copilot agent package. Composes the build and plan agents for distribution to calab-ai/.github-private via the APM release pipeline.",
  "maintainers": ["calab-ai"],
  "tags": ["agents", "org", "copilot", "github-private"],
  "compatibility": { "tools": ["copilot"] },
  "primitives": [
    { "ref": "primitives/agents/build" },
    { "ref": "primitives/agents/plan" }
  ]
}

Composition rules

The schema and the publish pipeline together enforce these rules — break them at your peril.

  1. Refs are repository-relative paths, not ids. They match ^primitives/(agents|instructions|prompts|skills|hooks|init)/[a-z][a-z0-9-]+$. The composed primitive must already exist in the same registry release.
  2. Each ref is unique within a package. JSON Schema only catches identical objects; the publish validator catches duplicate ref values.
  3. Optional primitives ({ "ref": "...", "optional": true }) are included if present at install time but do not block resolution if missing — use sparingly, only when a primitive is platform-conditional.
  4. Package-level files copy additional assets next to the composed primitives. src is package-relative; dest is consumer-install-target relative. Both follow the same relative-path rules as primitives (no leading slash, no .., no backslashes).
  5. Compatibility narrows. compatibility.tools on a package should be the intersection of the tools each composed primitive supports. The catalog generator currently warns on mismatch; the publish step will reject it once strict mode lands.

Versioning

Packages follow strict semver:

  • PATCH — no behavior change, no primitive set change (e.g. description tweak).
  • MINOR — additive primitive (new ref) or non-breaking metadata change.
  • MAJOR — primitive removed, primitive replaced with a different one, or any composed primitive made a major-version jump that breaks consumers.

A primitive’s minor/patch bump does not require a package bump unless the package explicitly pins to a specific primitive version (which today’s schema does not support — primitives are always pulled at the catalog version).

Validate locally

The primitive validator is live today:

pip install 'jsonschema==4.*'
python scripts/validate_primitive_manifests.py

A dedicated package equivalent (scripts/validate_package_manifests.py) is not yet shipped in calab-ai/apm-registry — it is planned and will follow the same pattern (jsonschema==4.* against apm-package.schema.json). In the meantime, package manifests are validated in CI as part of the catalog generation step on PR merge; for local pre-flight, validate by hand against the schema with any JSON Schema tool, or wait for the script to land.

Ready to release?

Continue to Publishing.