SDK Lifecycle Policy
Tracked by pm-z5pmf8, pm-xm0id4, pm-2ew0w3, and pm-scbr0s.
The public SDK owns lifecycle transactions and policy. CLI and MCP operations
delegate to SDK modules, so an item does not gain different storage,
governance, history, or graph semantics depending on the presentation surface
that invoked it. SDK-owned operations include create, append, update, bulk
update, claim/release, close, bulk close, copy, delete, restore, focus, get,
context, next, activity, calendar, aggregate, history, planning, annotations,
runtime contracts, guide/completion, reindex, and upgrade. Their historical
src/cli/commands/* modules are compatibility exports only.
The ownership boundary is architectural, not cosmetic:
sdk/lifecycle/deleteowns tombstone retention and dry-run outcomes.sdk/lifecycle/restoreowns history-target replay, rollback, locking, and derived-index refresh.sdk/lifecycle/copyowns similarity governance and atomic item/history construction.sdk/lifecycle/focusowns durable focus state.sdk/lifecycle/create,update, andplanown item construction, mutation, transition validation, planning transactions, and their shared parsers.sdk/lifecycle/append,claim,close-many, andupdate-manyown the remaining single- and bulk-item mutation transactions.sdk/query/getowns bounded current and point-in-time item reads.sdk/query/context,next,list,search,aggregate,activity,calendar, andhistoryown every built-in read primitive.
The mandatory static boundary walks TypeScript imports and re-exports. Any
module below src/sdk that reaches into src/cli fails quality:static,
including type-only edges. The SDK is therefore the substrate rather than a
facade over command implementations.
Package authors can import these operations from @unbrained/pm-cli/sdk
without importing CLI modules or spawning the executable. Existing CLI import
paths remain source-compatible while integrations migrate.
See Concurrent Workspace Mutations for settings snapshot reconciliation, schema publication, session locks, and checkpoint collision semantics shared by these operations.
Relationship Identity
Tracked by pm-olcoon.
normalizeItemReference(reference, prefix, sourceKind?) is the shared public
SDK primitive for parent and dependency identities. Local shorthand, optional
leading #, and case variants normalize to the workspace's canonical prefix.
Explicit URL or provider locators (github:, gitlab:, jira:, linear:)
retain their spelling and case; externally sourced dependency IDs do too.
Only surrounding whitespace is trimmed from remote identities.
Create, update, and plan materialization persist the canonical local parent, so lookup success cannot leave an unresolved shorthand in the stored graph. Explicit remote parents remain external references without a local missing-parent warning. Local missing parents still follow workspace policy, and self-parent and hierarchy-cycle checks remain enforced. A foreign-looking bare string is treated as local unless external provenance is supplied; the SDK does not guess a remote workspace from punctuation.
import { normalizeItemReference } from "@unbrained/pm-cli/sdk";
normalizeItemReference("#ABC", "team-"); // team-abc
normalizeItemReference("github:Org/Repo#42", "team-"); // unchanged
normalizeItemReference("OTHER-Case", "team-", "external"); // unchanged
Reason Contract
resolveTerminalReason is the pure precedence primitive. It chooses the first
non-blank author-controlled value in this order:
- explicit reason;
- duplicate target (
Duplicate of <id>); - structured resolution;
- history message.
requireTerminalReason adds governance. When a reason is required and no
author-controlled source exists, it throws close_reason_required. It never
invents a placeholder for immutable history. Direct close,
update --status <terminal>, and direct terminal create therefore share the
same refusal contract.
Ordering Contract
The default orderingEdges: "preserve" policy clears transient
blocked_by/blocked_reason scalars when an item closes but retains
dependencies[].kind = "blocked_by" rows. Those rows are predecessor facts:
they remain useful to historical graph traversal, planning analytics, and
context reconstruction after both endpoints are terminal.
An embedded SDK consumer can explicitly choose orderingEdges: "remove" for
a domain where predecessor facts are intentionally ephemeral. The CLI and MCP
do not expose that override and use the durable default.
Explicit removal returns closed_removed_predecessors:<item>:<ids> for the
complete removed row set and closed_removed_predecessors_count:<item>:<n>
for its cardinality. The count is the number of dependency rows, including
distinct provenance rows that reference the same item. Clearing a transient
blocked_by scalar is reported separately as
closed_cleared_blocked_by:<item>:<blocker> and does not inflate that count.
Both removal and its receipts use the same selected rows; other relationship
kinds survive unchanged.
import {
closeItem,
requireTerminalReason,
type TerminalTransitionPolicy,
} from "@unbrained/pm-cli/sdk";
const policy: TerminalTransitionPolicy = {
requireCloseReason: true,
orderingEdges: "preserve",
};
const reason = requireTerminalReason(
{ resolution: "Acceptance suite and package-consumer proof passed." },
policy.requireCloseReason,
);
await closeItem(
"pm-example",
reason.closeReason,
{ lifecyclePolicy: policy },
{ path: "/project/.agents/pm" },
);
The compilable example lives at
examples/sdk-lifecycle-policy/index.ts.
Compatibility
runClose, CloseCommandOptions, and CloseResult remain supported aliases.
New integrations should prefer closeItem, CloseOperationOptions, and
CloseOperationResult; the operation vocabulary remains meaningful outside a
command-line host. The additive CreateOperationOptions,
UpdateOperationOptions, PlanOperationOptions, AppendOperationOptions,
CloseManyOperationOptions, and UpdateManyOperationOptions names provide the
same presentation-neutral vocabulary without breaking integrations that still
refer to the historical *CommandOptions aliases. runCopy, runDelete,
runRestore, runFocus, and runGet retain their established result
contracts while their implementation ownership moves under the SDK.