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/delete owns tombstone retention and dry-run outcomes.
  • sdk/lifecycle/restore owns history-target replay, rollback, locking, and derived-index refresh.
  • sdk/lifecycle/copy owns similarity governance and atomic item/history construction.
  • sdk/lifecycle/focus owns durable focus state.
  • sdk/lifecycle/create, update, and plan own item construction, mutation, transition validation, planning transactions, and their shared parsers.
  • sdk/lifecycle/append, claim, close-many, and update-many own the remaining single- and bulk-item mutation transactions.
  • sdk/query/get owns bounded current and point-in-time item reads.
  • sdk/query/context, next, list, search, aggregate, activity, calendar, and history own 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:

  1. explicit reason;
  2. duplicate target (Duplicate of <id>);
  3. structured resolution;
  4. 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.


Sdk_lifecycle remote
Report an issue