Extension Author Contracts
Tracker: pm-at83
This page records runtime contracts package authors need when they build on the public SDK. For the install, manifest, lifecycle, and troubleshooting overview, see Packages and Extensions.
Flag Spelling and Output Assertions
Tracked by pm-gh1461 and
pm-gh1462.
Value grammar corrections are tracked by pm-gh1466
and pm-gh1467.
Prefer { long: "--repo", value_name: "project", value_type: "string" }.
Legacy --repo <project>, --repo=<project> and --label [name] declarations
retain one canonical identity, one value placeholder and one help listing.
Optional brackets permit presence without a value. Explicit value_name wins
over the inline label. Canonical boolean flags are switches, including when an
inline label is present. Legacy bool declarations retain their value grammar:
--enabled false, --enabled 0, and their short forms consume and coerce the
value. A bare legacy bool permits presence without a value, preserving the
published switch form too. An explicit --enabled <value> still requires its
operand; --enabled [value] permits omission. A canonical
boolean switch leaves subsequent positional values intact. Existing equals and
negated forms remain supported; loose negated flags remain in context.args.
Use -- to distinguish a positional operand immediately after an optional-valued
legacy bool, for example --enabled -- operand.
The SDK's explicit value_arity distinguishes none, required, and optional
from the scalar value_type. required describes flag presence: an optional
value remains optional even when the flag itself is required. Help, Commander,
recovery and machine contracts consume the shared SDK value declaration.
Explicit legacy value_name metadata remains discoverable on switches for
compatibility; value_arity: "none" still means that no value token is consumed.
Typed authors should use canonical value_type names (boolean, number, string).
Use decodeToon from @unbrained/pm-cli/sdk/testing to assert on host output
semantically; encodeToon and the native codec option types are available from
that subpath and /sdk. They use the host's pinned implementation, so a package
does not need a separate TOON dependency or a private dist/ import.
Telemetry Capture Level
Every pm command records a telemetry event. Extension commands use the same telemetry pipeline as core commands and inherit telemetry.capture_level from the active settings.
Allowed values:
| Value | Meaning |
|---|---|
redacted |
Default. Records sanitized command args, options, result summary, and runtime environment. |
minimal |
Records structural metadata and one-way digests only. No readable command text, args, options, result summary, or runtime environment is kept. |
max |
Same as redacted, but raises the per-string truncation cap from 512 to 2048 characters. Redaction still applies. |
All levels redact inline secrets, bearer tokens, private paths, emails, private IPv4 addresses, and object keys whose names look sensitive. minimal additionally digests arg and option values with HMAC-SHA-256 so extension authors cannot reconstruct user input.
Inspect and change the level:
pm health --json | jq '.details.capture_level'
pm config set telemetry.capture_level minimal
pm config set telemetry.capture_level redacted
pm config set telemetry.capture_level max
The setting lives in the global pm settings file (~/.pm-cli/settings.json by default), not in the project tracker.
Item Write Paths
pm has two item write paths. Hook, service, and migration authors should treat them as different contracts.
Create path:
- Used by
pm createand importer-created items. - Acquires the item lock, writes the item file atomically, appends the create history entry, fires
onWritefor the item file and history stream, records the after-command affected-item snapshot, then releases the lock. - Does not fire
onRead, because there is no existing item. - Does not call the
item_store_writeservice override. UseonWritewithop="create"if a package needs to observe new items.
mutateItem path:
- Used by
pm update,pm close,pm claim,pm release,pm restore,pm delete, and other mutations of existing items. - Locates the item, acquires the lock, reads the item through the
onReadhook, enforces ownership and history policy, applies the mutation, enters the commit fence, consultsitem_store_write, writes the item atomically, appends history, firesonWritefor the item and history stream, records the after-command affected-item snapshot, then releases the lock. item_store_writehandlers can redirect the target path, replace serialized contents, or skip the physical item write. Inside an expected-revision scope (withExpectedItemRevisions,--expect-hash) the scope then advances to the item read back from storage, not the pre-override document, so the caller's next guarded write is admitted while the item stays readable in the tracker with the id the commit kept. A handler that rewrites the embedded id leaves no revision to bind to, so that write refuses withitem_revision_conflictalthough no other writer ran. An item moved out of the tracker advances the scope to the empty tombstone, and a later guardedupdate,closeordeletecannot locate it. Contents the store cannot parse back make the next guardedupdate,closeordeletefail withitem_document_invalidbefore any revision check, and a guardedrestorerefuse withitem_revision_conflict. Anitem_store_deletehandler that skips or redirects the removal advances the scope to the item that remains.- The commit fence verifies the lock lease and the stored item revision first. When the lease was taken over (
lock_lost) or the item changed after the read (stale_revision), the mutation refuses beforeitem_store_writeoritem_store_delete(non-dry-run) runs, so a handler that persists the payload itself never commits a refused write.
Both paths guarantee:
- Atomic item writes.
- Lock coverage across item write and history append.
- History append after the item write, with rollback if history append fails.
onWritehooks only after item and history commits.- Non-fatal hook failures reported as
extension_hook_failed:<layer>:<name>:onWrite.
changed_fields sentinel values:
| Value | Meaning |
|---|---|
["imported"] |
Item came from an importer. |
["restored"] |
Item was restored from a previous state. |
["deleted"] |
Item was deleted. |
Surface matrix:
| Surface | Create path | mutateItem path |
|---|---|---|
onRead hook |
No | Yes |
item_store_write service override |
No | Yes |
history_append service override |
Yes | Yes |
onWrite item hook |
Yes, op="create" |
Yes, op=<mutation op> |
onWrite history hook |
Yes, op="create:history" |
Yes, op="<mutation op>:history" |
From docs/EXTENSION_AUTHOR_CONTRACTS.md in pm 2026.11.13. View on GitHub