SDK Lifecycle Policy
Tracked by pm-z5pmf8, pm-xm0id4, and pm-2ew0w3.
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.
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.
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.