文档
本页内容
  1. Agent Quick Context
  2. Universal read-output ceiling
  3. Command Families
  4. Bootstrap
  5. Packages
  6. Triage
  7. Incremental "what changed since" filters
  8. Keyword relevance control (GH-181)
  9. Default search mode (pm-n8a6e7)
  10. Inline filter syntax and matched-text highlighting (GH-157, pm-ldr1)
  11. Offline BM25 provider (pm-75k9)
  12. Search relevance evaluation — pm eval (pm-u8n5)
  13. Full results, totals, and bodies
  14. Output render formats (--format)
  15. Missing-metadata filters
  16. Content-field presence filters
  17. Bulk Operations
  18. Create and Update
  19. Atomic heterogeneous mutation batches
  20. Focus (session default parent)
  21. Templates
  22. Lifecycle Aliases
  23. Scheduling Shortcuts
  24. Ownership
  25. Expected revisions (--expect-hash)
  26. Logs
  27. Linked Artifacts
  28. Graph Queries
  29. Linked Tests
  30. Search Reindex and Eval
  31. Calendar and Context
  32. Validation and Maintenance
  33. Telemetry Local Analytics
  34. Self-Repair Remediation
  35. Single-commit merge settlement
  36. History and Recovery
  37. Custom Item Types
  38. Project Profiles
  39. Plan Workflow
  40. Machine Contracts
  41. Completion

Command Reference

This is a task-oriented command guide. For exact flags, use runtime help because extensions and settings can change the active surface:

Tracked implementation updates: pm-52eh, pm-mcxr, pm-qd3woa, pm-ypuc39, pm-tz2ikr, the schema-migration recovery contract pm-s79kel, the lossless mutation contracts pm-x3dq0l, pm-lppm6y, and pm-embm6t, positional action discovery pm-2tan, and the SDK-first agent grammar tranche pm-p316vn, pm-st7wgu, pm-mkinft, pm-ulqu, pm-qmjx, pm-4bzq, pm-x2vx, and pm-g543.

Workspace-integrity contracts are tracked by pm-22rzjp, pm-76fkpp, pm-igdvfq, pm-643e0k, and pm-larv4r.

Create-safe init prefix normalization is tracked by pm-gh1073.

pm <command> --help
pm <command> --help --json
pm contracts --summary --json
pm contracts --command <command> --flags-only --json

Agent Quick Context

  • Prefer pm context, pm search, and narrow list commands before mutation.
  • Prefer TOON for reading and --json for strict parsing.
  • Use the guide topic map when optional pm guide local docs routing is needed.
  • Use pm contracts --summary --json for the cheapest command map, then narrow with command-scoped contracts.
  • Every mutation writes history.

Tracked documentation work: pm-u9d0, pm-7nqo6b, and pm-j1r8gl. Universal command intent and default output-budget enforcement are tracked by pm-5t33or, pm-s2h0mq, pm-pmrae8, and pm-7i97c3.

Universal read-output ceiling

Every SDK-backed read has a format-aware default token ceiling discovered from pm contracts. Results already within that ceiling are returned unchanged, so the default adds no receipt overhead to ordinary reads. An oversized result is degraded deterministically through string compaction, row compaction, and a truthful omission envelope; automatic degradation adds read_output with budget_source: default, budget_tokens, the final estimate, and the applied compaction flags.

Use --output-budget <tokens> to request a tighter per-call ceiling. Use --output-budget unbounded only when the caller deliberately accepts the full cost. This cost opt-out is distinct from command-local row controls such as activity --unbounded, which disable a row limit but do not disable the shared token ceiling. --output-format json and global --json select the declared JSON ceiling; TOON uses the smaller default agent ceiling.

Command Families

Family Commands Purpose
Bootstrap init, config, health, telemetry create and inspect tracker setup
Lifecycle create, copy, focus, claim, update, item reopen, append, close, release, close delete mutate item state, including terminal-work recurrence without duplicate creation
Bulk item mutate, item complete, update many, close many atomically commit heterogeneous SDK mutation batches or evidence-backed completion, or apply one change across a matched, dry-run-previewed set with a rollback checkpoint
Scheduling meet, event, remind low-friction Meeting/Event/Reminder creation
Planning plan create, plan add-step, plan update-step, plan complete-step, plan link, plan approve, plan materialize agent-optimized living plans with ordered steps, evidence, decisions, validation, and materialization
Links files, docs, test, deps connect items to artifacts, tests, and relationships
Verification test, test-all, test-runs†, validate, gc run linked tests and repository checks
History history, history-compact, history-redact, history-repair, activity, restore, stats inspect, compact, redact, re-anchor, and recover item state
Schema schema add/remove-* / rename-type / rename-field / remap-status / apply-preset manage config-driven types, statuses, and fields; losslessly migrate existing items when definitions evolve; adopt presets or infer types
Profiles profile list / show / apply / lint compose item types, statuses, fields, workflows, config, templates, and recommended packages into archetype bundles (agile/ops/research); apply stages every dimension idempotently; lint reports author-time consistency findings without writing
Calendar calendar, cal project deadlines, reminders, and events
Packages install, upgrade, package, packages, extension, package/extension command groups install, upgrade, manage, and run package-backed extension commands
Machines contracts, help, optional guide/completion command contracts plus optional guide-shell docs routing and shell helpers

† test-runs subcommands are provided by the optional linked-test-adapters package (pm package install linked-test-adapters --project).

Bootstrap

pm init
pm init --defaults --with-packages
pm init --id-prefix ops --defaults
pm init --workspace ./new-project --defaults
pm init ./sandbox-tracker --defaults
pm init --agent-guidance status
pm init --agent-guidance add
pm init --no-merge-fence
pm config project list
pm health --check-only --summary --json
pm telemetry status

pm init creates .agents/pm. pm health --check-only --summary --json gives the smallest machine-readable health gate without refreshing optional search artifacts. Use --id-prefix <value> (alias --prefix) when automation should set the item ID prefix without relying on the legacy positional form. pm init ops remains compatible, and supplying both forms is accepted only when they normalize to the same prefix; conflicting values fail with init_id_prefix_conflict instead of silently choosing one. Prefixes normalize through the same primitive used by item creation: letters become lowercase, each run of whitespace or punctuation becomes one -, edge separators are removed, and exactly one trailing - is stored. For example, "Release Candidate" persists as release-candidate-; an input with no ASCII letters or digits falls back to pm-. Path-like values remain invalid tracker prefixes when passed through --id-prefix; a path-like positional value remains the advanced tracker-root target described below. To repair an older unsafe value, rerun init with a safe prefix and explicit replacement intent, for example pm init --id-prefix "release candidate" --force --defaults. Use pm init --workspace <dir> when <dir> is a project root; it creates <dir>/.agents/pm. A path-like positional remains the advanced tracker-root form and writes tracker files directly at that path. Both explicit target forms return target.mode, target.tracker_root, and tracker-scoped executable next_steps so agents can run the suggestions from any working directory. Implicit init discovery also reports whether the selected tracker is local or was found in an ancestor. If pm init <name> would change an ancestor tracker, the refusal names both directories and gives the safe current-directory retry: pm init <name> --pm-path "$PWD/.agents/pm" --defaults. pm init --agent-guidance ask is the default behavior: prompt in TTY only when AGENTS/CLAUDE guidance is missing and no decline is recorded. Fresh Git-backed trackers also install the semantic merge fence and clone-local drivers automatically. --no-merge-fence opts out; non-Git targets remain valid and return an actionable skip warning. Use --agent-guidance add to write guidance, --agent-guidance skip to persist a decline without writing, and --agent-guidance status to inspect guidance state. Use --with-packages for one-step agent setup when bundled package commands should be active immediately.

Packages

pm package                     # bare command defaults to --explore (list installed)
pm package install '*' --project
pm package catalog --project
pm package manage --project --output-format json
pm package install npm:@scope/pm-package --project
pm package describe --project   # by-name surface map of every loaded package
pm package describe my-package --markdown --output docs/my-package-reference.md
pm package doctor --project --detail summary
pm package migrate --project --dry-run --json
pm package migrate --project --json
pm package upgrade --dry-run
pm package upgrade --packages-only
pm package upgrade --cli-only --repair

pm package is the canonical package-first workflow. pm package and the hidden pm extension alias default to --explore on a bare invocation so agents can list installed packages without remembering an action flag. pm package install '*', shell-expanded pm package install *, and pm package install all install bundled first-party packages. pm install, pm upgrade, and pm extension remain executable as deprecated compatibility spellings declared in PM_COMMAND_ALIAS_CONTRACTS; they emit a migration hint and must not appear in documentation or skills as instructions. pm package catalog emits one row per package, with every resolvable bundled alias preserved in aliases; totals therefore measure packages rather than alias-index entries. pm package manage participates in the universal read output contract, so --output-include, --output-limit, --output-budget, and --output-format behave like the same controls on other SDK-backed reads. Install output includes a light verification summary with target tracker root, activation state, registered commands/actions/item types, and an ok|degraded health verdict. Runtime activation failure sets the command result and process exit status to failure; inspect activation_diagnostics and command_discovery.next_steps for the exact recovery path. Bare install names use bundled aliases before installed npm packages. Every install result reports source_resolution; when both candidates exist it marks the choice ambiguous and provides explicit bare and npm: retry commands. package migrate plans or applies active migration registrations and writes durable workspace-history receipts; a successful migration is skipped on later processes, while a failed migration remains retryable. extension migrate is the compatibility spelling. When package-owned commands are unavailable, usage guidance includes an install-ready retry (for example pm package install calendar, pm package install search-advanced, pm package install governance-audit, or pm package install guide-shell).

Triage

pm next                                 # the single highest-priority READY item + why, then the ready/blocked queues
pm context --limit 10
pm search "calendar reminder validation" --limit 10
pm get pm-a1b2                          # read one item; add --fields/--depth for lower-token projections
pm get pm-a1b2 --tree --tree-depth 2    # item plus its descendant subtree
pm get pm-a1b2 --at 7                   # verified, mutation-free historical version
pm list --status open --type Task --priority 1 --limit 20
pm list --status in_progress --limit 20
pm aggregate --group-by parent,type --status open
pm aggregate --group-by parent,type --completion --include-unparented
pm duplicates --status all --threshold 0.8

Use pm next when the only question is "what should I work on now?" It computes dependency-aware readiness, keeps dangling dependency ids blocked until repaired, and combines lifecycle-blocked plus graph-blocked work in the companion queue. The recommended projection is excluded from the ready tail; every actionable row carries its one-based rank, and the tail preserves the exact recommendation order. Foreign-owned in-progress work is summarized under held_by_others. Human-gated Decision records remain visible under decision_needed but are excluded from agent recommendations by default.

Use pm claim --next to atomically select and claim caller-available work with bounded conflict retry. It accepts the same --type, --tag, --priority, --assignee-filter, --parent, --sprint, and --release candidate filters as pm next; --max-attempts bounds the ranked race-loss walk (default 10, maximum 100). With --if-available, exhaustion is a successful empty result (available: false, item: null, skipped: true) instead of an exception, which supports contention-tolerant worker loops. Pass --include-decisions only for a coordinated maintainer lane authorized to claim human-gated decisions. Scope snapshots with --parent, cap rows with --limit/--blocked-limit, or use --ready-only for the tightest projection.

pm next                                 # recommend the next action + ready/blocked queues
pm next --parent pm-epic01              # scope readiness to one epic's subtree
pm next --ready-only --limit 3 --json   # tightest agent-loop projection

pm next has two budgets with different scopes. --token-budget <n> bounds only the selection (recommended plus ready) and reports it under truncation.ready_budget; filters.token_budget echoes that request and is null when none was made. The complete response, including decision_needed, containers, blocked and held_by_others, is bounded by the universal output budget (--output-budget, default ceiling of the read class) and disclosed by the read_output receipt, which a selection budget now always triggers so the total cost and the ceiling that bound it are visible. Use --token-accounting for per-section bytes. Companion sections are not visibility toggles: --include-decisions, --include-gates and --include-containers move those rows into the claimable ready queue.

The ready queue is ordered by a declared policy, disclosed as filters.ranking_policy. The default, priority_floor, resumes in-progress work first, then orders ready items by priority tier and lets the relevance score (recency, activity, deadlines, graph proximity) order items only within a tier, so an automated log that gains a comment every hour cannot outrank a ready P0 incident (GH-1618). weighted keeps the blended relevance order. Set it per tracker with pm config project set context_next_ranking_policy weighted (SDK callers pass rankingPolicy); a project's own context_relevance scorer keeps its order under either policy. When the policy and the score disagree, the recommendation's reasons say so (priority floor (relevance rank N) or relevance over pN ready work), and --explain-ranking still shows every item's score beside its final rank.

Use context first for a compact active-work snapshot. Use search when the request names a concept, component, or prior issue. Use duplicates --status all for a true whole-history duplicate check. The result echoes filters.statuses: null for the unrestricted corpus; named or custom statuses are normalized through the runtime status registry and an unknown status fails instead of returning a false-clean result. The default lossless prefix filter preserves every pair that meets the canonical similarity threshold. --exhaustive compares all pairs for small-corpus verification. Both modes disclose the algorithm, possible and scored pair counts, exact recall guarantee, and safety ceiling; exceeding that ceiling refuses without returning a partial result. Review clusters before closing anything: title similarity is evidence for investigation, not proof that work is interchangeable. As a read-only structured surface, duplicates accepts the universal output controls, including --output-format json, --lean, projection/amount controls, and token accounting, while its default TOON output remains bounded. Tracked by pm-gh1076. Use pm get <id> to read a single item by ID — the single-item read primitive used throughout the agent loop. It accepts --fields <list> and --depth brief|standard|deep|full for token-minimal projections, and --tree/--tree-depth <n> to include descendants. The universal --output-include selector can request stored collections directly, for example pm get <id> --output-include comments,learnings,tests; the same bare and item.<field> grammar works through CLI, SDK, and MCP transports. Standard/deep reads expose a normalized schedule facet (deadline, start_at, end_at, location, reminders, and events) when scheduling metadata exists. Standard reads of every item type avoid a workspace child scan. For container-oriented built-ins (Epic, Feature, Milestone, and Plan) and custom types, --depth deep adds type-agnostic child counts, a bounded sample, and continuation metadata. Explicit --full and --depth full reads compute that rollup for every type, including Task parents. An explicit --fields id,children requests that rollup without the other deep fields. A read without a child rollup retains a children omission hint until a rollup proves the item has no children. See bounded context reads for the cost contract. pm get <id> --json returns the body inside the item object (.item.body); see Full results, totals, and bodies. To duplicate an existing item as a starting point, pm copy <id> --title "New title" clones it into a fresh id with lifecycle fields reset.

When the strongest duplicate match is terminal because the same work recurred, reuse its lineage instead of creating or copying another item:

pm item reopen pm-a1b2 "The production failure recurred" --status in_progress

The command appends a structured reopen history event, preserves the earlier closure in immutable history, clears stale terminal metadata through the normal update path, and refuses an already-active item. See Recurrence and Executable Recovery Contracts.

Add --at <version|ISO-timestamp> for a verified point-in-time read. It replays the same hash-checked history kernel used by restore but never acquires a lock, writes the item, or appends history. The result always includes reconstructed: true, as_of_version, and as_of_timestamp. Future/out-of-range targets fail with structured valid_range metadata. --at cannot be combined with --tree; workspace-wide historical graph projections require a future indexed primitive. context standard/deep views include high-level child completion counters plus recently_created and unparented sections, so agents can spot new orphan work before creating duplicates. Use pm aggregate --completion when you need per-group open, in_progress, closed, other, and completion_pct progress context. Each aggregate row carries an explicit group_label: a blank/null group value (e.g. unassigned items under --group-by assignee) renders as (unassigned)/(untagged)/(unparented) rather than an ambiguous empty key, while the structured group value keeps the raw null for machine consumers. Multi-field grouping joins each field=value pair into the label.

--sort accepts priority|deadline|updated_at|created_at|title|parent, plus the convenience aliases updated (→ updated_at) and created (→ created_at):

pm list --all --sort updated --order desc

Brief list rows include an explicitly selected sort field (including normalized aliases such as updated_at) and declare it in projection.fields. Explicit --fields, compact and full projections retain their requested fields. This rule is shared by SDK list queries.

With the default sort, brief rows include priority. For flat lists, the compatible sorting.sort: "default" and order: "asc" fields are accompanied by ordered sorting.effective_keys tuples [field, direction]: nonterminal items before terminal items (using the active status registry), priority ascending, updated_at descending, then ID ascending. The synthetic key status_registry:nonterminal_before_terminal names both the registry source and the grouping semantics; it is not a stored item field. This policy remains visible in JSON and TOON, including status-filtered reads. Tree mode places descendants beneath parents and omits these flat-order keys because hierarchy traversal can override priority. Tracker (repository only; excluded from npm): pm-zd46m7. Brief JSON lists use compact whitespace while retaining the complete envelope and receipts, so visible ranking facts fit the existing output ceilings.

Lifecycle filters such as --all and --status all retain the canonical list command's brief default; they select rows without expanding item fields. Use --full, --compact, --brief, --fields or --include-body to choose more evidence explicitly. Raw SDK list calls keep their full default, and explicit --all --full --strict-read remains a complete corpus read. The implementation records pm-gh1481 and pm-gh1489 are repository-only tracker files, excluded from the npm package.

New dependency writes record created_at. Supplied values must be calendar-valid RFC 3339 date-times. SDK dependency reads expose this field as optional because legacy rows without a timestamp remain readable: they sort before timestamped rows, with deterministic id/kind/source tie-breakers and no invented provenance. An invalid supplied timestamp produces item_document_invalid with the item path and dependency field. pm validate --check-storage-integrity and pm health --check-only --full expose that malformed item. The implementation record pm-gh1482 is a repository-only tracker file, excluded from the npm package.

Incremental "what changed since" filters

Every list* command accepts --updated-after/--updated-before/--created-after/--created-before. pm search now supports the SAME filter surface as pm list (full parity): --status, --type, --tag, --priority, --deadline-before/--deadline-after, --updated-after/--updated-before, --created-after/--created-before, --assignee, --sprint, --release, and --parent, all with identical semantics. These keep a long-running agent's context focused on the slice it cares about instead of re-scanning the whole tracker:

# Items touched since my last context window (feed back the previous run's `now`)
pm list --all --updated-after 2026-06-04T15:18:32Z --brief

# Relative offsets are SIGNED: -2h/-7d reach into the past, +1d into the future.
# Units are h/d/w/m (m = months — there is no minutes unit).
pm list --status open --updated-after=-2h --brief
pm list --status open --created-after=-7d

# Common list windows avoid date math (`pm list --status open` keeps the view open-only).
pm list --status open --today --brief
pm list --all --recent --brief

# Search scoped to open work only (drops closed-history noise); statuses accept
# all (no lifecycle restriction), open/closed/canceled aliases, or configured
# ids, comma-separated, with a did-you-mean hint on typos.
pm search "reminder validation" --status open --limit 10

# Duplicate checks can deliberately scan every lifecycle bucket with one flag.
pm search "reminder validation" --status all --limit 10

# Full filter parity with list — scope retrieval before ranking.
pm search "calendar" --type Task --assignee alice --updated-after=-7d --parent pm-abcd

Use only one updated-at window per list call: --today, --recent, and --updated-after are mutually exclusive. --today starts at local midnight; --recent is the same as --updated-after=-7d.

pm get and pm history accept command-local --format json|toon. pm search also accepts ndjson, which emits each projected hit as one JSON object per line with no envelope or trailing summary. --format json is equivalent to the global --json flag for that command, while --format toon keeps the default agent-readable output. Do not combine global --json with --format toon.

list/search full and fields projections echo full filter metadata. Compact mode emits only active filters (plus runtime schema filters when present) and omits the default projection/sorting/now trailer keys for lower token cost.

Keyword relevance control (GH-181)

pm search ranks keyword hits by a weighted score and returns them sorted (highest first). Three controls tune matching and result volume:

  • --match-mode <and|or|exact> — or (default) matches an item if ANY query token appears, but multi-token queries get an additive ALL-TERMS ranking bonus so items covering every token outrank partial matches. and HARD-FILTERS to items where every distinct token matched some field. exact requires the full normalized query to appear as a contiguous phrase (same as --phrase-exact).
  • --min-score <float> — per-query minimum score threshold (finite, >= 0). Overrides the persistent search.score_threshold setting for this query only; the effective value is echoed in filters.score_threshold.
  • --count — return ONLY the match count (post-filter, post-threshold, pre-limit) with no hit rows. Token-efficient for "how many" questions; the response sets count_only: true and count/total to the matched total.

Keyword mode now applies the configured search.max_results default (50) when --limit is omitted, so a broad query no longer returns every hit. When the limit drops rows the result adds a top-level total (pre-limit match count).

Default search mode (pm-n8a6e7)

A bare pm search runs in the mode the SDK resolves from search.default_mode, never from a literal in the CLI. auto (the default) picks hybrid exactly when the stored settings name the active embedding provider (search.provider, the same configured verdict pm health reports) and a vector store or extension adapter resolves, or when an extension search provider is active; every other workspace runs keyword. Auto-detected Ollama/LanceDB runtime defaults do not count as configured, so an unconfigured host never pays an embedding round-trip it did not ask for. A concrete search.default_mode (keyword, semantic, hybrid) pins the answer, and --mode always wins. The result reports mode (what ran, after any runtime fallback) beside mode_source (explicit, settings, or auto), so a caller can tell a chosen mode from a derived one (compact responses carry mode_source only for settings, since explicit is the caller's own choice and auto is the documented default); when the provider is unreachable a hybrid default still degrades to keyword hits with the usual search_semantic_fallback warning.

pm search "context budget"                       # auto: hybrid here because search.provider is configured and the index resolves
pm config project set search_default_mode keyword   # pin keyword for a workspace that prefers exact-token results
pm search "context budget" --mode semantic       # explicit request, mode_source: explicit
pm search "reminder validation queue" --match-mode and          # require all three tokens
pm search "exact title phrase" --match-mode exact               # contiguous-phrase match
pm search "calendar" --min-score 5                              # this-query threshold override
pm search "reminder" --count                                    # just the number

Inline filter syntax and matched-text highlighting (GH-157, pm-ldr1)

Inline field:value tokens can be embedded directly in the query string and are parsed out as the equivalent filter. Recognized fields are tag:, status:, type:, and priority:; the value runs to the end of the token, so colon-bearing values like tag:area:search parse correctly. The remaining words drive keyword/semantic matching as usual:

pm search "auth tag:area:auth status:open"        # query "auth" + --tag area:auth --status open
pm search "ranking type:Task priority:1"          # query "ranking" + --type Task --priority 1

Precedence: an explicit --flag always wins over a conflicting inline token. When both are supplied, the flag value is used and the result carries a search_inline_filter_ignored:<field>:flag_takes_precedence warning so the override is observable. On the CLI, both unquoted inline tokens (status:all scene) and quoted multi-word queries ("status:all scene") stay query input so the search parser can remove inline filters and preserve the remaining keywords. The same parser covers the pm_search MCP tool, where the whole query arrives as a single string. A query consisting solely of inline tokens (no keyword terms) is rejected — use pm list with the equivalent --tag/--status/--type/--priority flags for pure filtering.

Pass --highlight to emit per-field matched-text snippets on each hit (off by default for token efficiency). Each matched field gets a { field, snippet } entry under highlights, with the matching token runs wrapped in «…» and a … ellipsis where the field text was windowed:

pm search "auth" --highlight                       # adds highlights:[{field,snippet}] to each hit
pm search "auth" --full --highlight                # full hit payloads + highlight snippets

Offline BM25 provider (pm-75k9)

semantic/hybrid search normally needs an embedding service (Ollama/OpenAI) plus a vector store. For air-gapped, CI, or zero-setup environments there is a built-in BM25 lexical ranker that runs entirely in-process over the item corpus — no network, no service. BM25 improves on naive keyword counting with inverse-document-frequency weighting, term-frequency saturation, and document-length normalization.

Activate it by setting the search provider:

pm config project set search_provider bm25      # always use offline BM25 for semantic/hybrid
pm config project set search_provider auto       # use BM25 only when no embedding provider is configured
pm search "connection pool leak" --semantic       # BM25-ranked; hits carry matched_fields:["bm25"]
pm search "retry backoff" --hybrid                 # blends BM25 with the keyword scorer
  • bm25 — always use BM25 for --semantic/--hybrid, even if an embedding provider is configured.
  • auto — use BM25 as the offline fallback only when no embedding provider (and no extension search provider) is available; the result carries a search_<mode>_offline_bm25:no_embedding_provider:using_lexical_bm25 warning so the offline path is observable.
  • Plain keyword mode is unchanged. BM25 quality is lexical — strong, but below true dense retrieval; configure an embedding provider when you need semantic similarity.

Tune ranking via search.bm25.k1 (term-frequency saturation, default 1.2) and search.bm25.b (length normalization in [0,1], default 0.75): pm config project set search_bm25_k1 1.5.

Search relevance evaluation — pm eval (pm-u8n5)

pm eval measures retrieval quality against a curated golden-query set so relevance regressions (from corpus, weight, or provider changes) are caught instead of guessed. Ground truth lives in a git-tracked <pmRoot>/search/eval-queries.json — an array of { query, relevant_ids, mode? } objects:

[
  {
    "query": "offline search ranking",
    "relevant_ids": ["pm-75k9"],
    "mode": "keyword"
  },
  { "query": "relevance regression gate", "relevant_ids": ["pm-u8n5"] }
]

It runs each query through the live search path and reports nDCG@k, MRR@k, precision@k, and recall@k per query plus the macro average:

pm eval --json                       # full per-query + aggregate metrics
pm eval --mode hybrid --k 10         # default mode for queries without their own; cutoff @k
pm eval --queries ./my-eval.json     # evaluate an alternate golden set
pm eval --fail-under 0.6 --json      # CI gate: exit non-zero when aggregate nDCG@k < 0.6

--fail-under <0..1> turns pm eval into a CI gate: it still prints the report (stdout) but exits non-zero when the aggregate nDCG@k falls below the threshold, so a relevance drop fails the build.

Full results, totals, and bodies

pm list selects every matched row when neither --limit nor --offset is set; the default output budget can still bound delivery. Every JSON/TOON result has the same pagination envelope: total is the pre-pagination match count, has_more and truncated are booleans, and next_cursor is either the continuation token or null. completeness reports whether the corpus was complete, partial, or unchecked (derived-index page), with unreadable item/directory counts. Use --strict-read when omissions must fail the command. The filters object omits unset values instead of emitting null placeholders, keeping long-running agent context stable and lean. Pass --no-truncate to disable the default output budget; an explicit --limit still caps rows, and an explicit --output-budget still applies. --all selects every lifecycle status; combine the flags for an unbounded all-status audit:

pm list --all --no-truncate --brief        # every matched row without the default token ceiling
pm list --status open --limit 20 --json    # stable total/has_more/truncated/next_cursor envelope
pm list --all --strict-read --json         # fail if any source item cannot be read

Continuation recovery is tracked by pm-gh1457. For list, search and context, pass a producer next_cursor to --after. When continuation_kind=output_cursor is present, consume next_cursor through --output-cursor instead. An output continuation resumes within the same producer page, so retain its original query and any producer --after value. Passing an output cursor to --after returns invalid_query_cursor with the executable flag substitution in next_steps. Changed filters or snapshots still refuse rather than silently restarting.

Compatibility note: older responses emitted total only when pagination omitted rows, omitted next_cursor on a completed page, and emitted unset verbose filter keys as null. Integrations should now read total as the unconditional pre-pagination match count, branch on has_more or next_cursor != null, and test filter-key presence rather than using "total" in result, next_cursor !== undefined, or filters.<key> === null guards.

JSON output uses brief rows by default (id/status/type/title/priority) for token efficiency. To pull item bodies in bulk in a single call — instead of one pm get per item — add --include-body, which expands each row to the full field set plus body:

pm list --status open --json --include-body # full fields + body for every returned row

pm get <id> --json returns the item's body inside the item object (i.e. .item.body), matching where list --include-body places it and the long-form description/acceptance_criteria fields — so a single read exposes every field at a consistent path. Body is included at the default standard depth and above; --depth brief omits it.

Output render formats (--format)

pm list* accepts --format <csv|table|json|ndjson|toon> to choose how rows render. csv and table are human export modes — pipe them into a spreadsheet or read them directly in a terminal — while json/toon override the machine output format the same way the global --json flag does. ndjson writes each projected item as one self-contained JSON object per line, with no wrapper or trailing summary. The rendered fields follow the active projection, so combine --format with --fields/--brief/--compact to control exactly what appears:

pm list --status open --format table                 # aligned, monospace-friendly columns
pm list --all --fields id,title,priority --format csv # spreadsheet export with chosen columns
pm list --status open --format csv > backlog.csv     # capture for reporting
pm list --all --brief --format ndjson | jq -c 'select(.status == "open")'

CSV output is RFC 4180 compliant (values with commas, quotes, or newlines are quoted; array fields such as tags join with ;). --format csv|table|ndjson cannot be combined with the legacy envelope-oriented --stream mode.

Missing-metadata filters

Every list* command also accepts metadata-presence filters for governance backfill: --filter-ac-missing (no acceptance_criteria), --filter-estimates-missing (no estimated_minutes; singular --filter-estimate-missing is an alias), --filter-resolution-missing (terminal items with no resolution), and --filter-metadata-missing (the union — missing any of those). Specific flags AND together; combine them with any other filter. They surface in the result's filters echo (filter_ac_missing etc.).

The same list* commands and pm search extend this with governance-field presence selectors — --filter-reviewer-missing, --filter-risk-missing, --filter-confidence-missing, --filter-sprint-missing, and --filter-release-missing — each selecting items where that single field is unset.

# Find open Tasks that still need acceptance criteria
pm list --status open --type Task --filter-ac-missing --brief

# Closed items that were never given a resolution
pm list --status closed --filter-resolution-missing --json

# Open items that still need a reviewer assigned
pm list --status open --filter-reviewer-missing --brief

Content-field presence filters

list* and pm search also accept paired presence/absence selectors for each content field, so you can scope to items that have a given field populated or that are missing it: --has-notes/--no-notes, --has-learnings/--no-learnings, --has-files/--no-files, --has-docs/--no-docs, --has-tests/--no-tests, --has-comments/--no-comments, --has-deps/--no-deps, --has-body/--empty-body, and --has-linked-command/--no-linked-command. Requesting both the present and absent variant for the same field is a usage error. Multiple content filters AND together and compose with any other filter.

# Closed items that shipped no documented learnings
pm list --status closed --no-learnings --brief

# Open work that has linked tests but no linked files yet
pm list --status open --has-tests --no-files --json

Bulk Operations

update many and close many apply one change across a matched set with a dry-run preview and a rollback checkpoint. Both share the --filter-* scoping family (--filter-status/-type/-tag/-priority/-sprint/-release/-parent/-assignee/-deadline-before|after/-updated-after|before/-created-after|before) plus --ids for an explicit comma-separated allowlist intersected with the other filters. update many additionally accepts the missing-metadata selectors --filter-ac-missing/--filter-estimates-missing/--filter-resolution-missing/--filter-metadata-missing for bulk metadata backfill.

Both update many and close many also accept the governance-field selectors --filter-reviewer-missing/--filter-risk-missing/--filter-confidence-missing/--filter-sprint-missing/--filter-release-missing and the content-field presence selectors under the --filter- prefix: --filter-has-notes/--filter-no-notes, --filter-has-learnings/--filter-no-learnings, --filter-has-files/--filter-no-files, --filter-has-docs/--filter-no-docs, --filter-has-tests/--filter-no-tests, --filter-has-comments/--filter-no-comments, --filter-has-deps/--filter-no-deps, --filter-has-body/--filter-empty-body, and --filter-has-linked-command/--filter-no-linked-command. These mirror the list/search presence filters and intersect with the rest of the scoping family, so you can bulk-select (for example) closed Tasks with no documented learnings before applying a change.

# Bulk metadata update by explicit id allowlist (compose with search --json | jq)
pm update many --ids pm-a,pm-b,pm-c --priority 1 --dry-run
pm update many --filter-tag wave:7 --reviewer maintainer-review

# Bulk-backfill a placeholder estimate onto open Tasks that have none
pm update many --filter-status open --filter-type Task --filter-estimates-missing --estimate 60 --dry-run

# Audited bulk close: routes EACH match through full `pm close` semantics
# (close validation, active-child orphan checks, blocked-edge cleanup).
# A shared --reason is required
# and at least one filter is required so it never matches every item.
pm close many --filter-sprint S-12 --reason "Sprint S-12 acceptance criteria met" --dry-run
pm close many --filter-sprint S-12 --reason "Sprint S-12 acceptance criteria met"
pm close many --rollback close-many-20260604-abc123   # restore the batch

close many skips already-terminal matches by default (pass --force to re-close), accepts --completed-at <timestamp> to preserve one shared actual-completion time across the batch, reports a per-item plan (close/skip, plus active_child_ids for parents that would be orphaned) under --dry-run, and writes a checkpoint by default (--no-checkpoint to disable). Checkpoints for both commands live under .agents/pm/checkpoints/<command>/ and are restored with --rollback <checkpoint-id>.

Rollback safety is tracked by pm-gh1563 and the public checkpoint option by pm-gh1564. Rollback reverses only committed effects attributed to that batch. It preserves later edits to unrelated fields, including peer comments, notes and links. If a batch-modified field has since changed, that row fails without changing any of its fields; inspect get and history and reconcile the conflict explicitly. Collections are guarded as complete fields, so a later edit to a collection the batch changed also conflicts. Other rows can recover independently.

Rows with no committed effect, and rows already rolled back, report skipped and do not append history. The result includes restored_count, skipped_count and failed_count; a repeated completed rollback returns the normal no-effect exit. Checkpoints from older versions without attributed effects refuse rather than overwrite later context. Inspect their history before choosing an explicit point-in-time restore. Rollback respects current ownership and mutation policy; it does not implicitly force an ownership or lock override. New checkpoints use schema version 2 so older CLI versions also refuse them instead of applying their former whole-item restoration behavior.

When a flag is rejected with Unknown option, the error guidance now suggests the nearest supported flag (including abbreviations like --desc → --description) and notes when the flag is valid on a different command (for example --type on test-all points to create/list). A quoted value that starts with -- and contains spaces, such as a close reason "--unset is now a list", is parsed as an unknown option. Its refusal says so, and its replay never drops the value. When only options follow the value, the replay passes it after a -- separator, for example pm close <id> -- "--unset is now a list". Otherwise the refusal explains how to pass it (pm-stq61b; repository-only tracker record excluded from npm). Unknown-command guidance ranks agent verb synonyms first, bounded edit distance second, and substring matches last; pm log therefore points to history, comments, and notes, never a catalog command.

Commands addressed to one item retain their canonical positional form and also accept --id <item-id> consistently. For example, pm get --id pm-a1, pm comments --id pm-a1 --add "...", and pm test --id pm-a1 --run are equivalent to their positional forms. Supplying both forms is a usage error. Command-scoped pm contracts --flags-only, completion, and help expose the same alias.

Create and Update

Tracked structured-input and compact-output work: pm-xm7c, pm-kipd, pm-nilh, pm-cfed, and pm-g9xk.

Shortest agent-friendly create (positional title + defaults to Task type):

pm create "Document command contracts"
pm create "Fix login bug" --type Issue --priority high

pm create defaults --type to settings.governance.create_default_type (falling back to Task). Set it with pm config project set governance-create-default-type <Type> (must resolve to a known item type). Pass --create-mode strict to require an explicit --type flag for governance-controlled flows. Type-aware help reports requirements for the selected mode and lists stricter requirements separately. A strict required repeatable collection can be deliberately empty: for example, --clear-deps satisfies a required dependency consideration while writing no relationship edge and recording explicit_unset=dependencies in history. Required runtime-schema fields are named with their metadata key and expected input format in structured recovery. pm update --status can be constrained per item type via schema.type_workflows plus pm config project set governance-workflow-enforcement <off|warn|strict> (see CONFIGURATION.md → Per-Type Workflows). Priority accepts either 0..4 or the equivalent names critical, high, medium, low, and minimal.

Minimal progressive create with explicit fields:

pm create \
  --title "Document command contracts" \
  --description "Add command contract examples for agents." \
  --type Task \
  --status open \
  --priority 1 \
  --create-mode progressive

Strict create is best when metadata is ready:

pm create \
  --title "Fix restore replay" \
  --description "Restore should replay patches through the target version." \
  --type Issue \
  --status open \
  --priority 1 \
  --tags "restore,history" \
  --ac "Restore reproduces the target state and has regression coverage." \
  --message "Create restore replay issue"

For long markdown bodies (multi-paragraph specs with code blocks or tables), load the body from a file with --body-file <path> on both pm create and pm update instead of escaping a huge inline --body string or issuing a second pm append:

pm create Feature "Search relevance overhaul" --body-file ./specs/search-relevance.md
pm update pm-a1b2 --body-file ./specs/updated-spec.md

--body-file is mutually exclusive with --body (passing both errors). Use --body - to read the body from piped stdin.

For whole-item automation, --stdin-json on create/update accepts either a direct item object or the envelope emitted by pm get <id> --json. Read-only fields are ignored, linked facets are normalized through their audited mutation inputs, and explicit CLI flags override document values:

# Create from a complete document.
printf '%s' '{"title":"Imported incident","type":"Issue","tags":["imported"]}' \
  | pm create --stdin-json --json

# Lossless get -> edit -> update round trip.
pm get pm-a1b2 --json > item.json
# edit item.json with an ordinary JSON-aware tool
pm update pm-a1b2 --stdin-json < item.json

Input is strict: a near-miss such as acceptance_criterai fails with a did-you-mean diagnostic instead of becoming an accidental custom field.

Repeated singular/plural list flags now accumulate, so --tag a --tag b is equivalent to --tags a,b (the same holds for --status, --ids, and --fields on read commands). Earlier versions silently kept only the last value. list/search also accept --tags as a never-block alias for the canonical read filter --tag.

--tags REPLACES the whole tag list. To edit tags without restating the full set, use the additive/subtractive flags on create/update/update many:

  • --add-tags <value> adds tags to the existing list without replacing it (repeatable; CSV or JSON-array values accepted).
  • --remove-tags <value> prunes the given tags from the existing list (repeatable; CSV or JSON-array). Available on update/update many only — create has no prior tags to remove.
pm update pm-abc1 --add-tags urgent,backend     # keeps existing tags, adds two
pm update pm-abc1 --remove-tags stale            # drops "stale", keeps the rest
pm create "New backend task" --add-tags backend,p1

Acceptance criteria get the same additive treatment on update/update many: --acceptance-criteria/--ac explicitly REPLACES the whole value and returns an acceptance_criteria_replaced:<before-count>:<after-count> warning when it changes existing criteria. --add-ac <text> appends one criterion (repeatable; deduped on exact text), while --remove-ac <text> removes one criterion by exact text match. Every requested removal must match: otherwise the mutation fails atomically with acceptance_criteria_remove_unmatched and reports the unmatched selectors, without applying valid removals or additions. Whole-value replacement cannot be combined with additive flags in one mutation. Criteria are stored with semicolon-space separators, so one criterion cannot contain a semicolon. Disjoint --add-ac edits from concurrent agents/branches merge cleanly instead of clobbering each other.

pm update pm-abc1 --add-ac "error path covered by a regression test"
pm update pm-abc1 --remove-ac "superseded manual QA step"

Update existing work:

pm update <id> --status in_progress --message "Start implementation"
pm update <id> --priority medium --deadline +1d --estimate 120
pm update <id> --parent <parent-id>
pm append <id> --body "Detailed implementation notes."
pm notes <id> --add-json '{"type":"step.completed","step":7,"ok":true}'
pm notes <id> --event-type step.completed --since 2026-07-31T00:00:00Z --include-meta

append mutates the scalar Markdown body and is not a concurrency-safe event log. notes --add stores union-merged text entries; notes --add-json stores a validated JSON payload plus timestamp and author in that same merge-safe collection. Structured events retain canonical JSON text for older readers and expose format: json, data, and optional event_type to SDK/CLI/MCP consumers. --since, --event-type, --limit, and --include-meta provide bounded reads without client-side free-text parsing.

pm update can make non-lifecycle metadata updates and append comments, linked files, and linked docs in one audited history entry. It is intentionally append-only for evidence: lifecycle/ownership fields, dependencies, tests, notes, learnings, reminders, events, and all clear/replace operations stay restricted or use their dedicated commands.

pm update <id> \
  --comment "Audit evidence: reproduced in staging" \
  --file "path=src/cli/commands/lifecycle/update.ts,scope=project,note=audit evidence" \
  --doc "path=docs/COMMANDS.md,scope=project,note=user-facing behavior" \
  --message "Append audit evidence"

--expected and --actual are short aliases for --expected-result and --actual-result on create/update/update many, matching the aliases pm close already accepts:

pm update <id> --expected "Retry succeeds after backoff" --actual "Retry threw on first attempt"

Repeat --ac/--acceptance-criteria to build multi-part criteria; values are stored in order joined by ; . Dependency inputs accept either a bare existing item id or the explicit id=<id>,kind=<kind> form, and malformed shorthand such as related:pm-abcd fails before it can create a dangling graph edge. New local dependency targets must already exist. Cross-workspace targets must declare source_kind=external (canonicalized to global); deliberately staged local edges require --allow-unresolved-deps and return one structured dependency_target_unresolved:<id> warning per missing target. The same validation runs during update-many --dry-run, so preview and apply cannot disagree.

Mutation commands (create/update/close/append/...) default to an agent-efficient id/status/changed_field_count envelope. Use --no-changed-fields for the full item echo without the redundant delta array, or --full-changed-fields to restore the legacy complete envelope:

pm create "Probe item"                       # id + status + changed_field_count
pm --no-changed-fields create "Probe item"   # item echo + changed_field_count
pm --full-changed-fields create "Probe item" # item echo + changed_fields array
pm --id-only create "Probe item"             # output is only id + status

For strict JSON consumers, the global --lean flag recursively omits null, undefined, empty-array, and empty-object noise while preserving meaningful false/zero/empty-string values. On refusals it keeps only the fields that change the next action: type, code, detail, exit_code, next_steps, code-specific required/examples/why, specific recovery (suggested_retry, missing, allowed_values, next_best_command) and typed evidence such as the holder fields below. The generic requirement, the pm --help examples and the invocation echo (attempted_command, normalized_args, provided_fields) are dropped, in text as well as JSON, so a lean refusal is never larger than its default rendering (GH-1610). Text refusals print a single-line message once, as the headline, and repeat it under What happened only when the headline is a classification. An already_claimed_by refusal carries the holder as typed fields in every mode: claimed_by, claim_source (claim, assignment, or unknown when history is unreadable, silent, or names a different holder) and claimed_at (the latest assignee change, or null when the origin is unknown); text output renders them as Held by: <holder> (<source> since <time>) and caps a very long holder name, which JSON keeps whole.

pm --json --lean get pm-a1b2
pm --json --lean create --unknown-option

Use pm create --allow-missing-parent --parent <id> only for deliberate imports or staged backlog reconstruction. Normal pm create --parent <id> fails fast when the parent id cannot be resolved.

Use pm close <duplicate-id> --duplicate-of <canonical-id> to close duplicates. The command validates the canonical target exists, records duplicate_of, auto-fills the close reason as Duplicate of <canonical-id> when no reason text was supplied, and fills the resolution/expected/actual closure fields when they were not provided explicitly.

pm close accepts short aliases for the common flags: -m (--message), -r (--reason), and -d (--duplicate-of). When governance.require_close_reason is enabled and no positional/--reason text is given, the close reason is derived from the next-best signal in priority order — explicit reason text, then --duplicate-of (Duplicate of <id>), then --resolution — so a single pm close <id> --resolution "<summary>" no longer hard-blocks. The resolution is still written to the item's resolution field.

Every transition into a terminal status stamps two distinct ISO timestamps: closed_at records when the tracker mutation happened, while completed_at records when the work actually finished. They initially match, but a delayed close can supply pm close <id> --completed-at <timestamp> and a terminal item can be corrected through the governed pm update <id> --completed-at <timestamp> path. Reopening clears both fields, and pm copy strips both. Time-window context, release notes, and changelog bucketing prefer completed_at; legacy items disclose their compatibility path by falling back to closed_at, then updated_at only when neither lifecycle timestamp exists.

--unset on pm create, pm update and pm update many clears scalar metadata fields by name. It is repeatable and also accepts a comma-separated list, so --unset deadline,assignee --unset release is the same as three separate flags. Each name is validated on its own: an unknown or empty part is refused by name before anything is written, including in update many dry runs. Field keys cannot contain commas, so the split is never ambiguous, and pm schema add-field refuses comma keys and flags (pm-gh1633; repository-only tracker record excluded from npm).

pm create --blocked-by <id> and pm update --blocked-by <id> keep the scalar blocked_by field as the latest/primary blocker for compatibility. When the value resolves to a real item, the command also appends a blocked_by dependency edge so pm deps and reverse dependency scans see the full blocker graph. Repeating pm update --blocked-by with a different blocker preserves the prior blocker edge and appends the new one; repeating the same blocker is deduplicated. pm update <id> --unset blocked-by (or --blocked-by none) clears the scalar and only the blocked_by rows that name its target: a row whose id is the scalar's own text, or the canonical row a --blocked-by write derived for a short scalar. An independent prerequisite keeps its own edge and keeps blocking (pm-4yxzvd; tracker record excluded from npm). To retire every blocked_by edge, use the explicit kind-wide selector pm update <id> --dep-remove kind=blocked_by. An update that removes dependency rows, by any of these flags or by --clear-deps/--replace-deps, names each removed row in a dependencies_removed:<item>:<id>/<kind>,... warning.

Unsupported pm block and pm unblock invocations return usage errors with pm update --help and these explicit mutation templates (pm-gh1607; tracker record excluded from npm):

pm update <item-id> --blocked-by <blocker-id> --blocked-reason "waiting"
pm update <item-id> --unset blocked-by --unset blocked-reason --unblock-note "ready"

Inspect the actual blockers before applying either template. The unblock recipe clears the scalar blocker, the rows naming it, and stale blocker metadata while preserving other relationship kinds; independent blocked_by edges stay and are removed with --dep-remove "id=<id>,kind=blocked_by" or --dep-remove kind=blocked_by. --unblock-note alone only records a note. The help retry and mutation examples preserve an explicit --pm-path and --no-extensions from the failed invocation (pm-lifecycle-example-scope; tracker record excluded from npm).

When closing a blocker, pm close scans reverse blocked_by edges and auto-unblocks dependent items only when every resolvable blocker is now terminal. Each unblocked item is updated through the normal audited mutation path, gets an unblock_note, and the close result reports compact auto_unblocked:<id>:resolved_blockers=<ids> warnings. Items with another active blocker remain blocked.

Over MCP, single-item mutations (pm_create, pm_update, pm_close, and pm_run append) default to the same lean result as the CLI: id, status, and changed_field_count. The MCP transport still wraps that result in structuredContent.result, as required by the protocol. Pass fullChangedFields=true to restore the full item echo and changed_fields delta, or idOnly=true for id/status output. Bulk mutations remain compact row summaries. See tracker item pm-awe3t6.

Atomic heterogeneous mutation batches

Tracked by pm-o8z748 and pm-cyn0y6.

pm item mutate is the noun-first CLI adapter over the public SDK planner (planItemMutationDocument) and commitItemMutations primitive. Pipe either the legacy non-empty JSON array, a versioned { "schema_version": 1, "mutations": [...] } document, or JSON Lines (one record per line), or read any of them from a file with --from <path>. Mix create/update/close/release operations in order. Create rows may declare a unique ref and omit id; exact @ref values work in target ids, parent, blockedBy, and dependency id fields. The resolver derives replay-stable ids before the writer lock and returns a references receipt plus an input-ordered rows receipt (index, ref, id, op, upserted). When --transaction-id is omitted the id is derived from the exact document bytes (transaction_id_source: "derived"), so identical bytes select the same journal. Replaying a committed import also requires an unchanged resolved plan:

pm item mutate \
  --transaction-id sync-2026-07-20-001 \
  --stdin-json <<'JSON'
{
  "schema_version": 1,
  "mutations": [
    {"op":"create","ref":"initiative","options":{"title":"Imported initiative","type":"Epic"}},
    {"op":"create","ref":"delivery","options":{"title":"Deliver it","type":"Feature","parent":"@initiative","dep":["id=@initiative,kind=implements"]}},
    {"op":"update","id":"@initiative","options":{"addTags":["synced"]}}
  ]
}
JSON

--from <path> selects a file and rejects an explicitly supplied --stdin-json. Default stdin input, explicit --stdin-json without a file, and --from - remain supported.

Use --dry-run to validate and preview without acquiring a writer lock. Reuse the exact transaction id and deterministic batch after interruption: the SDK journal resumes pending steps and never duplicates a successfully applied step. A different plan under a committed or resumable journal refuses with transaction_plan_mismatch; pass a new --transaction-id to commit that changed plan as its own transaction. A fully compensated attempt has nothing left to resume and may accept a new plan under the same transaction id. Formatting-only edits change a derived id; pass the original id explicitly to replay them when the resolved plan is unchanged. --create-compensation close|delete, --lock-ttl-seconds, and --lock-wait-ms expose the transaction safety controls. The equivalent MCP surface is pm_mutate.

Raw mutation options use command option types: write estimatedMinutes: "45". Item documents may instead use numeric estimated_minutes: 45. A non-string raw estimate refuses before journal or item writes with mutation_option_value_invalid (exit 2), identifying the row and option. Corrupt durable journals, including a wrong stored schema version or transaction identity, refuse with transaction_journal_invalid; preserve the journal and inspect or restore verified state before retrying. Changing the transaction id does not safely recover unknown previously applied work.

Bulk creation (pm create many route)

Tracked by pm-gh1456. pm create many is not a command; it refuses with bulk_create_route and points here. A record without op is a create document that uses the same field names as pm create flags or their JSON equivalents (title, type, description, body, tags, parent, priority, declared custom fields, ...); a create row may also carry that document under item instead of options. Records without ref or id receive row-<n> aliases so the receipt and @row-<n> references stay addressable. One process, one workspace transaction: every record is created or none.

cat > items.jsonl <<'JSONL'
{"title":"Imported A","type":"Task","external_id":"ext-1","tags":["import"]}
{"ref":"b","title":"Imported B","type":"Feature","external_id":"ext-2","parent":"@row-1"}
JSONL
pm item mutate --from items.jsonl --dry-run --json   # validate, derived transaction id
pm item mutate --from items.jsonl --json             # commit; re-running replays (recovered: true)

--upsert-key <field> makes a re-run safe for changed data: a create record whose field value matches exactly one existing item (core field such as title, or a declared custom field such as external_id) becomes an update of that item and is reported with upserted: true. Matching and lock-held guards use the field's stored metadata key; core option names such as acceptanceCriteria, declared field keys and their SDK option targets (for example externalId) resolve to that same stored key. The record keeps its ref in the receipt, and later records may still address it as @ref, which then resolves to the matched item. A record missing the key, two records sharing a key value, or a key value matching several existing items refuses before any write (upsert_key_missing, upsert_key_duplicate, upsert_key_ambiguous). Matching happens before the writer lock, so every record that matched nothing carries its key into the commit, which re-checks it under the lock: when another writer created an item with that key value after planning, the transaction refuses with upsert_key_conflict before any write, and a re-run plans the record as an update of that item. A record that did match is committed as an update that re-reads the key under the lock and refuses with upsert_match_changed when the key was changed or removed after planning. Either refusal rolls the attempt back completely, and a rolled-back attempt leaves its derived transaction id reusable, so re-running the same document simply starts a fresh attempt from the current key values.

printf '%s\n' '{"title":"Imported A (renamed)","type":"Task","external_id":"ext-1"}' \
  | pm item mutate --upsert-key external_id --json

Over MCP, pm_mutate accepts the same document rows as {ref?, item: {...}} entries, upsertKey, and an optional transactionId.

pm item complete composes evidence, governed closure, and claim release into one compensating SDK transaction. It accepts the normal repeatable evidence flags and can preview the exact ordered mutations before writing:

pm item complete pm-a1b2 "Implemented and verified" \
  --transaction-id complete-pm-a1b2-v1 \
  --file path=src/index.ts,scope=project,note=implementation \
  --doc path=docs/SDK.md,scope=project,note=contract \
  --test command="pnpm test",scope=project,timeout_seconds=240 \
  --comment "Evidence: full verification passed" \
  --validate-close warn

If any phase fails, the SDK restores the evidence, lifecycle, and prior claim. Reusing the exact transaction id and payload returns the committed result; changing a replayed payload fails against the journal plan fingerprint. --lock-ttl-seconds and --lock-wait-ms tune the same workspace transaction controls exposed by pm item mutate for slow or contended trackers.

Focus (session default parent)

pm focus sets a session "focused" item so subsequent pm create calls default their --parent to it — project management is context management, and focus keeps new work attached to the active parent without restating --parent every time.

pm focus pm-epic1     # focus an item (validates it exists)
pm focus              # show the current focus (or a no-focus hint)
pm focus --clear      # clear focus; new items stop inheriting a parent

Behavior and guarantees:

  • Focus is session-local: it is stored in .agents/pm/runtime/session.json, which is gitignored, so it never affects teammates or the tracker's git history.
  • When a focus is set and pm create or pm plan create is run without --parent, the new item inherits the focused item as its parent. With --full-changed-fields, pm create reports "parent_source": "focus"; pm plan create exposes the effective parent as plan.parent.
  • --no-inherit-focus or --parent none suppresses inheritance (pm create also accepts --unset parent). SDK and MCP creates, including plans, default to ignoring saved focus and opt in with inheritFocus: true (pm-gh1577; this tracker record is repository-only and excluded from npm).
  • An explicit --parent always overrides focus, including --parent none (create with no parent). An explicit but unresolvable focused parent produces the same missing-parent error/warning as an explicit stale --parent (it flows through the same validation.parent_reference policy).
  • pm focus <id> validates the item exists and fails fast with a not-found error otherwise. pm focus --clear <id> is a usage error (choose either set or clear).

Over MCP the equivalent tool is pm_focus (id to set, clear=true to clear, neither to show current focus).

Tracker references: pm-72xf.

Templates

After pm package install templates --project, pm templates lists both saved templates and built-in starters:

pm templates
pm templates show bug
pm create --template bug --title "Fix search regression"

Built-ins are bug, feature, spike, and chore. A saved template with the same name overrides the built-in. Template-defined custom type options accept string, number, and boolean defaults and pass through the same schema validation as explicit --type-option values. Explicit --type-option values take precedence over defaults for the same key.

Tracker reference: pm-l6rz.

Use pm close <id> "<reason>" instead of pm update --status closed. An update that still names the close status routes through pm close and reports auto_routed_from_update_to_close. When the item is already closed with the same reason (and the same --completed-at, if given), the update is not a transition: it keeps closed_at, writes no closure history, and reports terminal_state_unchanged instead, even with --force (GH-1628). Any other requested fields take the ordinary update path: updated_at moves and a history entry is written only when one of them changes a stored value, so a request whose every field already matches leaves the item untouched. A different reason is a repeat closure, which still needs --force. A blank reason while governance.require_close_reason is enabled goes to the close route, which refuses it exactly as it would on an open item. If another writer reopens the item before the other fields are written, the request is closed through pm close as usual.

Lifecycle Aliases

Lifecycle composition flags combine claim, status, and close operations:

pm claim <id> --start                       # claim + move to the configured in-progress status
pm claim --next --start                     # select the next available item, then claim and start it in one locked write
pm release <id> --pause                     # move to the configured open status + release claim
pm close <id> "<reason>" --release-assignment # close with evidence + release assignment

For lifecycle ownership commands, --assignee <agent> is accepted as an alias for --author <agent> on claim, release, start-task, pause-task, and close-task. Use pm update --assignee <person> when changing item metadata rather than active work ownership.

The old task spellings remain hidden compatibility aliases. See Lifecycle Commands and Ownership for migration, typed SDK receipts, MCP controls, and partial-failure semantics.

Tracker references: pm-eq4x, pm-ik19, pm-qfte, pm-98cz.

After pm create of a workable item type, the result includes a non-binding next_transition hint (pm claim <id> --start → in_progress) when the workflow defines a distinct in-progress status. This nudges agents to move work through in_progress instead of jumping straight from open to closed. Scheduling/reference types (Event, Meeting, Reminder, Milestone, Decision) never receive the hint. (GH-216)

Scheduling Shortcuts

Low-friction creation for the scheduling item types so time-based tracking feels native. Each command translates friendly time flags into the canonical --event/--reminder fields and delegates to pm create (parent/focus inheritance, governance, and validation all still apply). The lightweight schedule preset is applied so progressive scheduling fields are not demanded up front. (GH-217)

pm meet "Sprint Planning" --start +1h --duration 1h          # Meeting (start defaults to now, duration to 1h)
pm event "Release v2" --start 2026-07-01T10:00:00Z --duration 2h --location "Room A"
pm remind "Review PR" --at +2d                               # Reminder (--at defaults to +1d, text defaults to the title)
  • --start/--at/--end accept ISO timestamps, now, or relative tokens (+1h, +2d, +2w, +6m).
  • pm meet/pm event take --start, --duration (or --end), --location, --timezone, and --all-day; passing --end overrides --duration.
  • --duration accepts relative units (h hours, d days, w weeks, m months) plus sub-hour forms (30min, PT30M). Bare m remains months for backward compatibility (45m = 45 months).
  • pm remind takes --at and --text (text defaults to the title).
  • Common create flags also apply: --parent, --allow-missing-parent, --tags, --priority, --body, --description, --author, --message.

Ownership

pm claim <id>
pm claim --next
pm release <id>

claim is the normal start signal. --assignee is an ownership alias for --author on claim/release flows, not a metadata update. Use --force only when explicitly overriding terminal-state or lock conflicts.

Starting work respects declared blockers (pm-urzubi). pm update <id> --status in_progress, pm claim <id> --start, pm claim --next --start and the start-task action refuse with start_blocked_by_open_dependency (exit 4) when the item moves into the start status while a blocked_by target is still open. pm claim --next --start skips a ranked candidate refused this way (its blocker opened after ranking, and nothing is written to it), claims the next eligible candidate, and lists the skipped ids in start_refused. When the walk is exhausted, the no_available_next_item refusal (or the --if-available empty result) carries them too. An item already in that status is not rechecked, so claim --start on it succeeds. The check runs before any write: the explicit claim <id> --start judges it under the claim's item lock, and its status update judges again under its own lock. When the first check refuses, nothing is written. When a blocker opens between the two writes, the start refuses and, if it took the item from no owner, releases that claim. The release is fenced by assignee and claim principal, so another principal's takeover is never cleared; principal is pm's unit of ownership, so a concurrent reclaim by the same principal is indistinguishable from the original claim. If the item already had an owner, or that release cannot run, the claim stays and the refusal's why reports the partial completion. The refusal lists the open blockers in details.open_blockers and suggests the exact pm update that drops the edge. When no single command can do that safely (a row naming the blocker that clearing the scalar keeps, such as a hand-added prefixed row beside a short scalar, or a legacy row's short id, alias kind or external locator that would not match a removal selector; see pm-4yxzvd), it suggests inspecting the item instead. The check uses the effective start status, including the registered in_progress fallback when the workflow names open. A terminal blocker never blocks, and an update that removes the blocker in the same call is admitted. --force starts anyway and records start_blocker_override:<id>:<blockers>. governance.start_blocker_enforcement selects strict (default), warn (start and record start_with_open_blockers:<id>:<blockers>), or off. A blocker whose record cannot be judged (an invalid or non-UTF-8 document, an unsupported item format, or an ambiguous or conflicting identity) refuses under strict with that typed error. warn and --force still admit the start and record start_blocker_unverifiable:<id>:<code>. Filesystem failures and merge-conflict markers always propagate.

Expected revisions (--expect-hash)

Read-decide-write agents guard a write with the revision they decided on (pm-gh1576). pm get <id> --json reports item_hash. The global --expect-hash <item_hash> flag makes every locked write the command makes to that item refuse with item_revision_conflict (exit 4) if the item changed since:

H=$(pm get pm-a1b2 --json | jq -r .item_hash)
pm close pm-a1b2 "Delivered by PR 1" --expect-hash "$H"
H1=$(pm get pm-a1b2 --json | jq -r .item_hash)
H2=$(pm get pm-c3d4 --json | jq -r .item_hash)
pm update-many --ids pm-a1b2,pm-c3d4 --priority 1 \
  --expect-hash "pm-a1b2=$H1" --expect-hash "pm-c3d4=$H2"

A bare hash binds to the command's <id> argument (for plan, the id after the subcommand; plan create has none). <id>=<item_hash> (repeatable or comma-separated) names targets explicitly and is required for commands without one item argument. The refusal reports expected_revision, current_revision, and details.changes_since (the newest 10 history entries since the declared revision, with their top-level fields). Its required, examples and nextSteps are read-only re-reads (get and history) rendered for the refused invocation (pm-gh1677): they keep its tracker selection (--pm-path/--path, or PM_PATH when that tracker is not the cwd default), --no-extensions and output encoding, so replaying them reads the refused item and its history from the same tracker. Those reads are live, not pinned to current_revision: a write after the refusal shows up in them, so decide against the item_hash they report. The guard never retries the write or replaces the expected hash itself. The refused write is suppressed; writes the same command already committed (for example the claim of a composed claim --start) stay durable, and the message then says so ("Earlier writes by this same invocation already committed"). Read commands refuse the flag with expect_hash_requires_mutation, and an empty value (such as an unset $H) refuses with expect_hash_invalid instead of running unguarded. Bulk update-many/close-many fail only stale rows, forward and --rollback alike, each carrying error_code, expected_revision, current_revision and details.changes_since. When close auto-unblocks a guarded dependent whose revision moved, the close stays committed and the command refuses with that dependent's item_revision_conflict once the fan-out finishes. side_effects names every write that committed first: the close and each dependent already unblocked. Do not retry the close; it is already terminal. required and nextSteps name only the remaining step for each refused guarded dependent (details.pending_unblock): re-read it, confirm every blocker it declares is resolved, then run the scoped unblock nextSteps lists (pm update <dependent> --status <open-status> --unset blocked_by --unset blocked_reason --expect-hash <dependent>=<item_hash>, carrying the configured open status and tracker scope). --unset blocked_by removes only the scalar and the rows naming its target, so edges to the other resolved blockers stay as resolved history and a blocker a concurrent change added keeps its edge; skipping the check can still reopen a still-blocked item, which validate then reports. Dependents whose unblock failed for another reason are in details.auto_unblock_failed with that reason; fix the cause first. Each list names at most 10 ids and counts the rest in *_omitted, keeping the details within the error-details size limit. When ids were omitted, or the steps exceed the 20-step cap (ten conflicts plus one other failure make 21), the last step lists every item in a blocked-role status (pm list --status <blocked-statuses> --fields id,title,blocked_by,dependencies, over the statuses the fan-out drew from), so a dependent whose step was cut is still found; it returns 200 rows per page, so follow --after <next_cursor> while has_more: true. close-many reports such a row as closed (update-many closing through --status, as updated), with the dependent's conflict under dependent_revision_conflict. A guarded restore refuses to recover over a live file that cannot be read as a document; an unguarded restore still recovers it. deps only reads, so it refuses the flag. History maintenance (history-redact, history-repair including --salvage-tail, history-compact) is checked under the stream lock. Its bulk forms (history-compact --ids, history-repair --all) carry the same typed conflict fields on refused rows. An item file whose embedded id differs from its physical file name (even by case on a case-sensitive filesystem) binds no revision: reading or declaring one refuses with item_identity_conflict until the identity is repaired. validate --auto-fix/--prune-missing refuses typed once every fix was attempted. When an extension parser or preflight override rewrites the invocation's tracker or arguments, a guarded invocation refuses with expect_hash_invocation_rewritten, since which item it writes cannot be proved. The same refusal covers a parser pass during final dispatch, which must also keep the guard admissible (adding --background late refuses with expect_hash_background_unsupported). Inventory commands (package inventory, extension inventory) never write items, so they validate the flag and then refuse with expect_hash_requires_mutation. An item the command never wrote produces the warning expect_hash_unchecked:<id>:no_locked_write_reached_this_item. MCP tools accept the same tokens as expectHash; the SDK exposes withExpectedItemRevisions (see SDK).

Logs

pm comments <id> "Implemented command parsing fix."
pm comments <id> "Implemented command parsing fix." --author agent-a --if-absent
printf '%s\n' '## Verification summary' '- Linux pass' '- macOS pass' | pm comments <id> --stdin
pm comments <id> --file docs/release-evidence.md
pm comments <id> --edit 2 "Corrected: the regression was in the parser, not the renderer."
pm comments <id> --delete 3
pm comments <id> --edit 263 --expect-at 2026-11-06T06:02:01.768Z "Corrected the newest comment."
pm notes <id> --add "Keep renderer changes isolated to TOON output." --if-absent
pm learnings <id> --add "Use runtime contracts instead of duplicating flag lists." --if-absent

Use comments for progress and evidence, notes for implementation context, and learnings for durable future guidance. All three accept --text as a hidden compatibility alias for canonical --add; comments also retain --body/--comment, and notes retain --note. Choose exactly one input source ([text], an add alias, --stdin, or --file) per invocation. Conflicting alias values fail before mutation. An add alias that took an option as its value (pm comments <id> --add --file note.md) refuses with annotation_flag_like_value before any source conflict is reported: the refusal names the option's own form (--file <path> or --stdin works without --add) and, when dropping --add leaves exactly one source, recovery.suggested_retry_args holds your command without it. Use --add text=--file for literal dash-leading text. For retrying agents, --if-absent makes any annotation append idempotent by the resolved author plus exact normalized stored text: the first call returns changed: true and mutation_receipt.changed_count: 1; an exact retry returns changed: false, the existing entry position, and changed_count: 0 without writing item or history state. Different authors remain distinct, and omitting --if-absent deliberately preserves duplicate-appending compatibility. The flag is append-only and is rejected for list, edit, and delete operations. To clean up obsolete orchestration notes, --edit <index> rewrites the comment at a 1-based index and --delete <index> removes it; both record history and honor ownership rules.

Reading annotations (tracked by pm-8s53lj and pm-hka7ep; tracker records excluded from npm): rows stay in stored order, oldest first. Every row leads with index, its absolute 1-based position in the stored collection, so the index a row reports is the one --edit and --delete accept on any page, limit or pm notes event filter. When a bare pm comments, pm notes or pm learnings read must be bounded by its output budget, it keeps the newest entries, the same tail --limit <n> and --output-limit <n> select, so .comments[-1] is always the newest comment. The page then carries count (rows delivered) beside total_count (entries stored), and next_cursor continues backwards: --output-cursor <cursor> returns the next older page until no older rows remain. A page's count is not an entry address. To check that an index still holds an entry with the timestamp you read, pass that row's created_at as --expect-at <ts> with --edit or --delete (MCP and SDK: expectAt). An entry at that index with a different created_at is refused with exit 4 and annotation_entry_mismatch before anything is written. On that refusal, details.matching_indexes lists every index whose entry has the expected created_at. The flag is refused without --edit or --delete (annotation_expectation_without_target). Without --expect-at, --edit <index> and --delete <index> behave as before and address the absolute index unguarded. created_at is a timestamp, not a unique identity: entries imported or written in the same millisecond can share one, and when such entries shift, the guard accepts any entry with that timestamp at the index without a refusal. A stable per-entry identity is owned by pm-gh1381 (tracker record excluded from npm).

Linked Artifacts

pm files <id> --add path=src/cli/main.ts,note="command wiring"
pm files <id> --add src/cli/main.ts --note "command wiring"
pm files <id> --add-glob "src/cli/**/*.ts"
pm docs <id> --add path=docs/COMMANDS.md,note="public command docs"
pm docs <id> --add docs/COMMANDS.md --note "public command docs"
pm deps <id> --format tree
pm deps <id> --format context --direction both --kind blocked_by,parent --token-budget 800

Linked files and docs keep reviews reproducible. deps is read-only and projects item relationships. Tree and graph formats are bounded by depth, node, edge, and estimated-token budgets (safe defaults: depth 32, 200 nodes, 400 edges, 16,000 tokens); pass --max-depth, --node-limit, --edge-limit, or --token-budget to tighten them. A bounded result carries deterministic truncation.reasons, effective limits, full unique-node/edge totals, and omitted counts. The estimate is calculated from the emitted JSON or TOON representation, including its accounting envelope. --format context returns one bounded, explainable relationship packet: a counts-first summary, per-node role/via/reasons, root evidence pointers, enumerated missing_references with active-versus-legacy classification, meta.completeness, exact rendered meta.usedTokens, and cursor continuation; --direction, repeatable or comma-separated --kind, and --cursor further control that traversal (see Relationship Graph). --edge-limit caps both returned graph edges and enumerated missing_references; missing_reference_count retains the total when rows are omitted. The standalone --note <text> flag annotates every link added by --add/--add-glob in the same invocation (a per-entry embedded note= wins); --note without an add is a usage error.

A files, docs or test write (--add, --add-json, --add-glob, --remove, --remove-index, --migrate, or a linked-test --update/--update-index edit) returns only the entries it added or rewrote, so its reply no longer grows with the stored list (pm-gh1700; tracker record excluded from npm). count is the number of entries returned and total_count the number stored. mutation_receipt reports changed_count, removed_count and full_history_included, and omission_receipt names the full_history selector when stored entries were withheld, exactly as for comments. A retried identical add, or an --add-glob that matches nothing, returns no entries and changed_count: 0; the receipt reports entries as storage saved them (normalized paths included). Pass --full-history (SDK fullHistory, MCP full) to receive the whole collection after a write. The implicit post-write path check validates the returned links; --validate-paths validates every stored path. failure_categories appears only when pm test ran linked tests.

When note prose in an --add entry is misread as a new key (note=see, host:port opens a key host after the comma), the refusal is linked_artifact_note_key_misread. It names the segment and key in the message and in details (misread_segment, misread_key, note, the underlying cause), and recovery.suggested_retry_args replays the whole invocation (every entry, --add-glob, --remove, --migrate and control) with that entry's note quoted as note="…", so its commas stay prose; passing the prose through the standalone --note flag in a single-entry call works too (pm-pwqiao; tracker record excluded from npm). Every other refusal, including a typo key with no note, keeps its own message.

Structured key/value forms reject unrecognized keys with an Allowed keys: … error (matching test --add), so a typoed key (lable= instead of label=) fails fast instead of being silently dropped: --add/--file/--doc accept path,scope,note; --add-glob accepts pattern,glob,path,scope,note; --remove accepts path; --migrate accepts from,to; create/update --dep accepts id,kind,type,author,created_at,source_kind; --reminder accepts at,date,text,title; --event accepts start,date,end,duration,title,description,location,timezone,all_day and the recur_* recurrence keys. A key set twice in one entry (path=a.txt,path=b.txt, or note prose such as note=moved here, path: b.txt) is refused with structured_entry_key_repeated instead of silently keeping the last value (pm-oglm0t; tracker record excluded from npm); the comment, note and learning seed fallbacks store such prose verbatim. Set source_kind=global for a dependency owned by another workspace: pm preserves its id verbatim, materializes it as an external graph endpoint, and excludes it from dangling-local-reference findings. Explicit remote locators also retain their spelling; other dependency and parent references share local id_prefix normalization, including uppercase and # shorthand. Bare values (--add src/cli/main.ts) skip key validation.

Context meta.usedTokens measures the selected built-in JSON or TOON emission, including its newline, alias counts, omission receipt and optional discovery or token-accounting metadata. If the required rendered packet cannot fit in the requested token budget, pm item deps returns usage exit code 2 with invalid_argument_value. Structured recovery.token_budget reports the requested budget and a verified feasible budget. Its minimum_status is exact only after every smaller positive budget is checked; bounded searches can instead report feasible_only or unavailable with no invented minimum. When a feasible budget is known, suggested_retry_args preserves the tracker, query and output options in an executable context retry with that --token-budget. A separate next_best_command provides a compact tree-summary inspection. (Repository tracker: pm-odw3j8; tracker records are excluded from npm.)

Graph Queries

pm graph ancestors <id> --max-depth 3
pm graph successors <id> --kind blocked_by --limit 20
pm graph paths <id> <target> --direction outgoing --max-paths 5
pm graph impact <id> --direction incoming --summary
pm graph analyze --limit 10
pm graph audit --sample 5 --exempt-isolate pm-root
pm graph communities --kind blocked_by --limit 5
pm graph redundancy --max-depth 6 --limit 20
pm graph dominators <id> --direction outgoing --limit 10
pm graph slack --limit 20
pm graph centrality --kind blocked_by --limit 10
pm graph articulation --limit 20
pm graph plan --sample 5 --limit 20
pm graph audit --save-baseline
pm graph audit --exempt-isolate-type Reminder,Event
pm graph index
pm graph index --rebuild
pm graph index --clear

graph is read-only and answers workspace-wide relationship questions through the public SDK graph toolkit. ancestors/descendants walk registered hierarchy kinds, predecessors/successors walk order-bearing kinds, paths enumerates bounded simple paths between two items with per-edge kinds, and impact reports the bounded blast radius with an explaining path per affected item. analyze returns workspace execution analytics (topological layers, prerequisite frontier, critical path, genuine ordering cycles) plus knowledge-graph structure (components, orphans, hubs), and audit runs the policy-aware governance audit with counts-first findings, severities, and bounded evidence samples. Audit severity gates on lifecycle: ordering cycles and duplicated edge spellings confined to closed/canceled items downgrade to informational legacy_ordering_cycle/legacy_duplicate_edge findings (history debt, never repaired ad hoc), while ordering_cycle errors and duplicate_edge findings are reserved for subjects touching at least one active item; duplicate_edge reports parallel same-family spellings — including reciprocal inverse pairs such as A blocked_by B plus B blocks A — that transitive-reduction redundancy deliberately cannot see. communities clusters the undirected structure with deterministic label propagation (reporting iterations and converged), redundancy finds stored edges implied by a longer path of the same semantic family (blocked_by/blocks and hierarchy spellings witness each other, each row carrying its witness path), and dominators reports the structural bottlenecks of one item's reachable subgraph — nodes every path from the root must pass through, ranked by how much work they gate (dominated_count). Three planning and structural analytics complete the surface: slack runs the Critical Path Method over the order-bearing DAG and reports each task's earliest start, latest start, total float (slack), and whether it is critical (zero slack), plus the overall makespan and critical path — unit task durations keep it deterministic and metadata-independent, and genuine cycles are reported apart from the schedule; centrality ranks nodes by exact shortest-path betweenness (Brandes) with Wasserman–Faust closeness, undirected degree, and dependency fan-in/fan-out (in_degree/out_degree, ordering predecessors/successors oriented by precedence) over the simple undirected graph; and articulation reports the graph's articulation points and bridges — the cut vertices and cut edges whose removal fragments the knowledge graph into more components (single points of failure). plan derives a dry-run remediation plan from the audit plus the redundancy scan: exact remove/retype/supersede/waive/investigate proposals with per-step policy code, evidence, rationale, and confidence — nothing is ever applied automatically and no proposal invents an edge. Every envelope is counts-first with explicit truncated, cost (visited nodes, inspected edges), and cache (workspace fingerprint plus assembly/result hit-or-miss) metadata; repeated identical queries in long-lived hosts such as the MCP server are answered from a fingerprint-keyed snapshot cache that invalidates on any relationship-relevant item change. --summary suppresses row collections entirely. --kind accepts registered relationship kinds (repeatable or comma-separated) and fails fast on unknown values (redundancy additionally requires directed ordering or hierarchy kinds); --after resumes hierarchy/ordering walks after a previously returned node id (see Relationship Graph).

The audit also reports storage-layer integrity and per-type coverage. duplicate_dependency_row (warning on active holders, informational legacy_duplicate_dependency_row on terminal ones) catches raw dependency rows whose exact identity — holder, kind, and case-insensitive target — is stored more than once on one item; graph edge-identity dedup hides these from every assembled-graph projection, so the pre-assembly scan is the only surface that can see them. The audit profile gains coverage_by_type, a per-item-type breakdown of active, isolated, and degree-at-most-one nodes (untyped items bucket under (untyped)), and --exempt-isolate-type <type> (repeatable or comma-separated, case-insensitive) suppresses isolate/sparse coverage findings for types whose disconnection is policy-valid (reminders, calendar events) while keeping them counted in the profile. --save-baseline persists the audit census (affected_subjects_by_code plus the profile) as the machine-local comparison point under .agents/pm/runtime/graph-audit-baseline.json; every later audit automatically attaches a baseline block with the signed drift since that census (same_snapshot, per-code deltas, profile deltas including per-kind edge counts) — the temporal primitive for tracking relationship-graph health across sessions without storing full finding collections.

index maintains the durable cross-process graph index at .agents/pm/runtime/graph-cache.json (gitignored, machine-local): a fingerprint-keyed, atomically written, crash-safe envelope of deterministic query results that lets one-shot CLI invocations reuse analysis computed by earlier processes. Every non-index graph envelope reports cache.durable (hit, miss, or off). Persistence is automatic at ≥500 items and otherwise opt-in: pm graph index reports status (fresh/stale/absent, entry count, size, thresholds), pm graph index --rebuild clears, re-creates, and warms the envelope with the counts-first analyze/audit census queries (and enables persistence on small workspaces), and pm graph index --clear deletes it. The index is never authoritative — any relationship-relevant item change invalidates the fingerprint and queries transparently recompute from item storage.

Linked Tests

pm test <id> --add command="node scripts/run-tests.mjs test -- tests/unit/output.spec.ts",timeout_seconds=240
pm test <id> --add command "node scripts/run-tests.mjs test -- tests/unit/output.spec.ts"
pm test <id> --add-json '{"command":"node scripts/run-tests.mjs test -- tests/unit/output.spec.ts","timeout_seconds":240}'
pm test <id> --run --progress
pm test <id> --run --match output
pm test <id> --run --only-index 2
pm test <id> --run --only-last
pm test-all --status in_progress --progress

Linked-test execution injects temporary PM_PATH and PM_GLOBAL_PATH for every command and checks stored-command provenance before process creation. Direct runner binaries such as vitest or node --test, shell-wrapped equivalents, package-manager scripts, and non-Node runners are accepted consistently; prefer node scripts/run-tests.mjs ... when available because it also reproduces repository build, coverage, and cleanup policy. The two-token form --add command "npm test -- parser" (and --add path "..." / --remove command "...") is accepted when the value is quoted into a single shell argument; it is normalized to --add command=... before parsing. Use --add-json when command strings contain commas, nested quotes, shell variables, or -- separators that are awkward to preserve through CSV-style --add parsing. Linked tests retain append order; consequently --only-last always selects the most recently added stored test. --match, --only-index, and --only-last select which linked tests execute without mutating the stored linked-test list. When a selector skips any test, the run receipt lists only the selected entries in tests[]. count still reports the full stored collection, selection records the selected 1-based indexes and the skipped count, and tests_omitted: { count, restore_with: "pm test <id> --list" } names the read that returns every stored test. The receipt no longer repeats the selection as a warning string. On a 58-test item, an --only-last receipt went from 8,662 to 2,212 bytes in TOON and from 18,826 to 2,695 bytes in JSON (pm-gh1630; repository-only tracker record excluded from npm). With --lean, a run that passed with no context mismatch and no failure category also replaces the sandbox execution_context with its modes, mismatch_detected: false, the trusted boolean (a passing run can be untrusted under an explicit override), the trust reason and restore_with: "not retained; a run without --lean reports the full context", and omits the all-zero failure_categories. The hint deliberately does not say to rerun: a rerun repeats any mutating flags of the original invocation, such as --remove-index. That run measured 865 bytes in TOON and 1,166 in JSON. A failure, refusal, skip or context mismatch keeps the full provenance and categories (pm-covpax; repository-only tracker record excluded from npm).

Strict linked-test guards:

pm test <id> --run \
  --check-context \
  --fail-on-context-mismatch \
  --fail-on-skipped \
  --require-assertions-for-pm

Search Reindex and Eval

reindex is provided by the search-advanced package (pm package install search-advanced --project).

pm reindex --mode keyword
pm reindex --mode semantic
pm reindex --mode semantic --full
pm reindex --mode hybrid --progress
pm reindex --mode keyword --eval --eval-fixtures tests/search-eval/golden-queries.json
  • --mode semantic and --mode hybrid are stale-first by default: only items whose updated_at no longer matches search/vectorization-status.json are re-embedded.
  • --full forces a complete semantic/hybrid re-embed and vector upsert, even when ledger entries are unchanged.
  • Progress now includes a stale-vs-total line so agents can estimate semantic reindex cost before embedding starts.
  • When pm reindex --mode keyword detects an embedding provider/model mismatch against the last semantic ledger, it emits a migration warning so agents can run pm reindex --mode semantic to rebuild vectors.
  • --eval runs the golden-query nDCG@5 harness and appends an eval summary to JSON output; --eval-fixtures overrides the fixture file path.

Calendar and Context

pm calendar --view week --date today --full-period
pm calendar --from today --to +7d --include deadlines,reminders,events
pm context --from today --to +7d --limit 10
pm context --section recently_created --section unparented --limit 10
pm context --depth full                  # every section, no per-section row cap
pm context --parent pm-epic1 --depth deep # scope the snapshot to one item's subtree
pm context --fields id,title,priority    # project focus rows to a field subset
pm context --no-tags                    # omit tag arrays from every focus row
pm context --format ndjson              # one focus-row object per line

pm context --depth full returns the comprehensive snapshot: every known section with no per-section row cap (overridable with an explicit --limit). pm context --parent <id> scopes the focus items, hierarchy, agenda, and all derived sections to that item plus its transitive descendants — the "what is the status of this epic?" view for large trackers.

pm context --fields <a,b,c> projects the focus rows (high-level, low-level, blocked-fallback, recently-created, unparented) to a chosen subset of fields for low-token reads — the same shaping pm list --fields and pm get --fields provide. Selectable fields: id, title, type, status, priority, order, deadline, assignee, tags, updated_at, parent, children_total, children_closed, completion_pct, created_at, tags_inherited. Repeated child tags are folded when the matching parent row is present: the child omits tags and reports tags_inherited: <parent-id>. --no-tags removes both tag values and inheritance markers. The projection applies across the markdown, TOON, and JSON renderings and is also available on the pm_context MCP tool via options.fields.

pm context --format ndjson concatenates the bounded high-level, low-level, and blocked-fallback focus rows in deterministic ranking order. Like list/search NDJSON, it emits no wrapper and no trailing count object. SDK and MCP callers retain the structured arrays; package authors can use the exported serializeNdjsonRows primitive when their transport needs line framing.

calendar defaults to markdown for human and agent readability. Other commands default to TOON unless configured otherwise. Calendar results publish matching total_count, returned count, zero-based page_start, remaining omitted_count, and has_more. summary_scope is returned_rows: summaries and day buckets cover this page. Repeat the original query with --after <next_cursor> to recover subsequent bounded pages; changing --limit is allowed. Relative bounds keep the first page's clock. Changed matching content refuses the cursor; restart without --after. With --limit 0, counts remain available; use --limit 1 or larger to retrieve rows.

For --include events without explicit --to, --recurrence-lookahead-days, or --occurrence-limit, recurring expansion is intentionally capped to a bounded default window and emits a warning with retry hints for broader windows.

Validation and Maintenance

pm validate --check-resolution --check-history-drift
pm validate --check-metadata --counts --json
pm validate --check-files --scan-mode tracked-all
pm validate --check-resolution --fix-hints --json
pm validate --auto-fix --dry-run --json
pm validate --auto-fix --fix-scope lifecycle
pm validate --auto-fix --fix-scope estimates --dry-run --json
pm validate --prune-missing --dry-run --json
pm normalize --dry-run --json
pm gc --dry-run
pm gc --scope locks --dry-run

Use dry-run modes before broad lifecycle or cleanup changes.

pm gc --scope accepts index, embeddings, runtime, locks, and checkpoints, comma-separated or repeated. Omitting the flag selects all scopes:

  • runtime clears runtime/test-runs/ and runtime/history-drift-cache.json. The next health check rescans history drift.
  • embeddings removes keyword/semantic artifacts (search/embeddings.jsonl, search/vectorization-status.json, search/lancedb/) plus search/pending-refresh.json and its gate, preventing a background worker from rebuilding a partial index against an empty ledger. Rebuild with keyword reindex and semantic reindex when enabled.
  • locks removes locks with an expired embedded created_at + ttl_seconds lease or positive same-host original-owner death evidence. It rechecks the observed receipt under the writer cleanup gate before deletion; replaced or initializing locks remain. Active, unreadable, or unparseable locks remain. The result reports scanned, removed, and retained counts.

The checkpoints scope prunes bulk-mutation rollback checkpoints under checkpoints/ (written by pm update many/pm close many) that are older than checkpoints.retention_days (default 14; set via pm config <scope> set checkpoints_retention_days <n>). Checkpoints whose created_at cannot be parsed are retained (safety-first, like the locks sweep), and the result includes a checkpoints summary (scanned/removed/retained/retention_days). Removing aged checkpoints permanently closes their --rollback window.

--fix-hints is a read-only flag: each failing check gains details.fix_hints, an array of pm command templates derived from the warning codes it raised (for example pm history-repair <id> for history drift, or pm update <id> --reviewer "<name>" for a missing reviewer). Generic hints may contain <id>/<field>/<path> placeholders the agent substitutes from the check's detail rows; the resolution check aliases concrete per-row commands and marks fix_hints_truncated when the list is summarized. It never mutates items. The mapping comes from the shared remediation registry that also backs pm health --json (see Self-Repair Remediation below), so agents gating on pm validate can auto-repair findings without hardcoding warning-code-to-command lookups.

pm validate --check-metadata also groups missing-required-field counts per item type in details.missing_by_type (for example { "Task": { "close_reason": 3 } }) — counts only, zero-suppressed, and limited to the active metadata profile's required fields, so remediation can be targeted by type without verbose row dumps.

--counts keeps the validation envelope, check statuses, warning codes, scalar counts/totals, nested count maps, and fix summary totals while recursively omitting diagnostic and fix row arrays. False *_truncated markers are omitted; true markers, other false values, and zero counts remain. It is the preferred agent projection when deciding whether drift exists; remove it only when the affected ids or remediation rows are needed. The public SDK projectValidateCounts helper applies the identical projection to an already-computed ValidateResult.

By default the human view caps each diagnostic *_item_ids list at 5 entries and sets the matching *_truncated flag. --json never truncates those lists (machine consumers always receive the complete arrays), and --all-affected-ids (equivalent to --verbose-diagnostics) emits the full lists in human mode too — so bulk remediation can pipe every affected id straight into pm update many:

pm validate --check-metadata --all-affected-ids
pm validate --check-metadata --json | jq -r '.checks[] | select(.name=="metadata") | .details.missing_acceptance_criteria_item_ids[]'

--auto-fix reports planned_fixes, applied_fixes, gated_fixes, and failed_fixes under fixes. Each row identifies the item, check, field, and equivalent standalone command:

  • Safe field backfills derive a missing terminal resolution from close_reason (falling back to "completed") or a missing close_reason from existing resolution. Every applied change uses the audited update path; auto-fix never closes, cancels, or deletes items.
  • --fix-scope lifecycle explicitly permits reparenting an active item from its terminal parent to an active grandparent, or clearing that parent link. These structural fixes are otherwise planned only.
  • --fix-scope estimates explicitly permits heuristic per-type estimates. Defaults in minutes are Epic/Milestone 2880, Feature/Story 480, Task/Plan 120, Issue/Bug 60, Chore 30, Decision 15, and fallback 120. Override validation.estimate_defaults_by_type with a type-to-minutes map.
  • --fix-scope is an exact, repeatable or comma-separated allowlist: metadata, resolution, estimates, lifecycle. Omitting it grants only the safe metadata/resolution scopes; specifying only estimates grants only estimates.
  • --dry-run previews without mutation. With no explicit check flags, auto-fix runs only metadata, resolution, and lifecycle checks. Output checks describe the pre-fix state; rerun validation to confirm convergence.

Note: acceptance-criteria gaps are intentionally not auto-fixed. Unlike resolution/close_reason (derivable from the item's own fields) or estimates (a config-driven type default), acceptance criteria have no deterministic source — synthesizing them from the description would fabricate content rather than derive it, violating the auto-fix safety invariant. They remain --fix-hints-only.

pm validate --check-lifecycle detects dependency cycles from explicit dependency edges, scalar blocked_by item ids, and exact pm-id references in definition_of_ready. That catches logical deadlocks such as A blocked by B while B's readiness text names A; --dependency-cycle-severity off|warn|error still controls whether those cycles warn, error, or remain informational in details.

pm validate --check-files joins stale paths, owning items, and relocation candidates in structured details.missing_linked_path_rows in both compact and full output:

  • moved means same-basename candidates exist; these are suggestions requiring verification. deleted means none exists. malformed marks ambiguous missing CSV or path:note literals and never infers a relocation or prune.
  • Counters distinguish unique paths from linked holder/field pairs, active holders from the closed subset, and all terminal holders. Only active missing paths cause the files warning; historical debt remains visible. Custom lifecycle roles determine the partition.
  • Active paths and owners appear first. Compact output bounds both dimensions and includes total counts and truncation flags; --verbose-file-lists restores every row. See Linked-file repair rows for the SDK contract.
  • Existing unlinked paths retain orphaned_path_classifications and orphaned_path_rows, with docs_unowned, tests_unowned, source_unowned, or unlinked_existing classifications and concrete ownership hints where available.
  • --prune-missing removes only links classified deleted, retaining moved and malformed references for review. It never removes real files, honors --dry-run, implies --check-files, and records equivalent files/docs removal commands in fixes.applied_fixes.

The default pm validate run also includes a format_version check that compares every item's stored item metadata format version (pm_format_version, absent meaning the implicit baseline version 1) against the version this pm runtime writes. details.outdated_items lists items that predate the current version (a future storage migration would rewrite them — advisory warn); details.ahead_items lists items written by a newer pm than the one running, which is an error because validation cannot vouch for fields it does not understand (upgrade pm to read them safely). At the current baseline version both lists are empty and the check is ok. The same pm_format_version integrity is surfaced by pm health under the integrity check (details.counts.item_outdated_format_version / item_ahead_format_version).

Telemetry Local Analytics

pm telemetry surfaces local queue/runtime telemetry state without running full health checks:

pm telemetry status
pm telemetry stats --limit 10
pm telemetry flush
pm telemetry clear
# Legacy-compatible alias for older agent scripts:
pm telemetry local-analytics status
  • status reports queue depth, endpoint, and latest flush metadata.
  • stats groups queued local telemetry events by command name. Each bucket also reports an always-available, zero-network performance and outcome signal derived from the bucket's command_finish payloads: latency percentiles (duration_p50_ms, duration_p95_ms, duration_max_ms, nearest-rank, present only when a finish event carries a finite duration_ms), success/failure tally (ok_count, error_count, error_rate; a finish event whose ok is missing or not strictly true is counted conservatively as an error), and command_resolution_counts (resolution → count, present only when non-empty). command_start/command_error events are excluded from these aggregates.
  • flush runs an immediate local queue flush attempt.
  • clear disables telemetry and deletes local queue/runtime telemetry artifacts.
  • local-analytics <status|stats|flush|clear> is accepted as a backward-compatible namespace alias for older scripts; new scripts should use pm telemetry <subcommand>.

Self-Repair Remediation

pm health --json annotates every non-extension check whose warnings have a known code with details.remediation_map, an object mapping each warning-code prefix to the executable pm command that fixes it:

// history_drift check
"remediation_map": { "history_drift_missing_stream": "pm history-repair <id>" }
// vectorization check
"remediation_map": { "vectorization_stale_items_remaining": "pm health --refresh-vectors" }
// locks check
"remediation_map": { "locks_stale_count": "pm gc --scope locks" }

When more than one history stream is drifted, the history_drift remediation commands are rewritten to pm history-repair --all so the whole tree is repaired in one audited pass instead of one command per stream.

remediation_map appears in default and --full output and is omitted in --brief/--summary to stay token-efficient. Extension checks keep their existing richer details.triage.remediation instead.

pm health also runs a read-only locks check alongside the storage check: it classifies every file in locks/ with the exact policy pm gc --scope locks acts on and reports active_lock_count, stale_lock_count, unreadable_lock_count, and unparseable_lock_count (counts appear in all projection modes; nothing is ever removed). Full details include at most twenty verified dead-owner samples (owner, PID, age and reason) and the cleanup command. Linux early-recovery identity and local audit receipts are explained in Configuration. It warns with locks_stale_count:<n> when stale locks exist (fix: pm gc --scope locks) and locks_unreadable:<n> when lock files cannot be read (inspect first: pm gc --scope locks --dry-run).

Single-commit merge settlement

Tracked by pm-gh1475. Use git merge --no-commit --no-ff <branch>, then pm merge reconcile --stage and git commit to include authenticated lossless audit history in the original merge commit. --stage --dry-run previews without writing. Automatic staging refuses competing values, forced acceptance, non-history validation failures, unstaged edits to reconciliation artifacts, and unresolved index entries. pm merge install --pre-commit-hook explicitly installs a merge-aware hook for that paused-merge workflow and preserves existing unowned hooks. Default installation remains hook-free. See Merge safety.

History and Recovery

Compact and provenance history default to newest-first, including reads with --limit. Full entries default to oldest-first. Use --order oldest_first or --order newest_first to choose the same direction for any projection and its value diffs. --limit N selects the latest N matching entries before ordering. The order receipt declares direction and total counts matches before limits. Physical indices and durable versions retain their original meaning. Use --output-cursor with the returned continuation to traverse the selected order. Verification always checks the full authoritative chain.

Compatibility: a compact/provenance latest-N read used to flip to chronological order. Chronological consumers should explicitly use, for example, pm history pm-a1b2 --diff --limit 20 --order oldest_first. Full reads keep their chronological default. Tracked by pm-gh1617 (tracker record excluded from npm).

After a branch merge that touched tracker history, preview and apply the one-command reconciliation gate:

pm merge reconcile --dry-run --json
pm merge reconcile --message "Reconcile branch histories" --json
# Only after reviewing every discarded-value receipt:
pm merge reconcile --force --message "Accept reviewed merge decisions" --json

The command scans every history stream, delegates repairs to the audited history-repair --all engine, then runs the history-drift and storage-integrity validation checks. Dry-run leaves drift intact and reports it in validation; apply exits nonzero if any stream fails or either invariant stays red. Lossless receipts remain visible in pm merge report but do not count as discarded-value decisions. When a receipt contains a discarded scalar value, apply refuses with merge_reconcile_discards_require_acceptance until the coordinator reviews or re-applies the value and passes --force. pm history-repair is the narrower drift-only tool and never substitutes for receipt reconciliation. No Git hook is installed automatically—teams may call this command from an explicit post-merge hook after opting into that policy.

pm history <id> --limit 20
pm history <id> --diff
pm history <id> --diff --field status
pm history <id> --full --diff --verify
pm history _workspace --verify
pm get <id> --at 12
pm get <id> --at 2026-07-01T12:00:00.000Z --fields id,title,status,body
pm history-compact <id> --dry-run
pm history-compact <id> --before 25 --message "compact early entries"
pm history-compact <id> --before 2026-06-01T00:00:00.000Z
pm history-compact --all-over 500 --dry-run
pm history-compact --closed --message "compact closed-item streams"
pm history-compact --ids pm-a1b2,pm-c3d4 --dry-run
pm history-redact <id> --literal "[redacted_path_prefix]/private" --replacement "[redacted_path]"
pm history-redact <id> --regex "/192\\.168\\.[0-9.]+/g" --dry-run
pm history-repair <id> --dry-run
pm history-repair <id> --message "re-anchor legacy drift"
pm history-repair --all --dry-run
pm history-repair --all --message "bulk re-anchor drifted streams"
pm history-repair --all --normalize-provenance --dry-run
pm history-repair --all --normalize-provenance --message "normalize invalid legacy provenance"
pm activity                         # item digest for the last 24 hours
pm activity --raw --id <id> --limit 50
pm activity --full --id <id> --limit 50
pm activity --full --unbounded
pm restore <id> <timestamp-or-version>

History is append-only. Restore appends a new restore event instead of rewriting old history. Bare pm activity is an item-centric digest for the last 24 hours, bounded to 15 most-recently-touched items. Each row joins current id, type, status, and title with the matching event count, first/last timestamps, and a bounded operation histogram. activity_summary states the effective window plus event, item, author, and operation counts, so an empty digest is distinguishable from a bounded one. Use --raw for the legacy compact per-event stream, --compact as its compatibility spelling, --full for events with patch payloads, or --provenance for patch-free provenance rows. Every mode reports total and omitted row counts plus the applied bound. Use --limit for a deliberate cap, or --unbounded for an explicitly unlimited read; the two flags are mutually exclusive. Continue a limited result by repeating the same query with --after <next_cursor>; page_start gives the page offset and omitted_count counts rows remaining after that page. window publishes the original anchor, inclusive lower bound, and exclusive upper bound. Bounded reads pin an omitted upper bound to the first page's clock. Later writes outside that window are excluded. Changed filters, projection, workspace, or matching content refuse continuation; restart without --after. Digest rows join current metadata, so metadata changes may also require a restart. Continuation does not retain a persisted or atomic cross-file snapshot. Use --from for a starting timestamp; --since refusals suggest that correction. Use an envelope (for example --json without --stream) when paging: stream mode emits rows without cursor metadata. Workspace-scoped mutations to settings, schema, profiles, init state, and extension/package activation are recorded in .agents/pm/history/_workspace.jsonl using the same patch/hash format. pm activity --id _workspace lists those events; pm history _workspace --verify verifies the chain. The stream is optional for trackers created before workspace auditing and is created on the first audited singleton mutation. pm get --at is the read-only counterpart: it reconstructs a recorded version through the shared restore replay kernel and labels the result so consumers cannot confuse historical state with current state. Checkpoint baselines produced by history-compact remain valid reconstruction roots.

--diff replays the history chain and emits, per entry, a changes array of { field, before, after } field-level value transitions (alongside the changed_fields name list) — so you can see exactly what each field changed from and to without comparing snapshots. It is independent of the compact/full projection. Provenance filters and limits select the same physical entries for the diff; excluded intermediate events still contribute to accurate before/after values. --field <name> narrows the diff to a single field's transitions (implying --diff), answering "when did <field> change?" — e.g. pm history <id> --diff --field status.

pm stats reports item and history totals plus a screen-sized lifecycle table. Every non-empty item type is one row with total, open, in_progress, blocked, draft, closed, canceled, and other counts. Empty registered types and statuses are suppressed and their combined count is retained in the omitted_zero_buckets scalar. The default TOON projection is gated at no more than 22 lines on the representative release fixture. Add --include-empty to restore every zero-filled type and status for schema-governance dashboards. Add --storage for aggregate history-stream metrics — total_streams, total_lines, total_bytes, the top streams by size (largest_by_bytes) and by depth (deepest_by_lines), and the global oldest_entry/newest_entry — to decide when to compact or redact streams and to plan storage:

pm stats
pm stats --include-empty
pm stats --storage --json
pm stats --metadata-coverage --json
pm stats --field-utilization --json
pm stats --by-assignee --by-priority
pm stats --by-tag --tag-prefix domain: --json

For governance dashboards, --metadata-coverage adds a metadata_coverage block reporting per-field present/applicable/percent for acceptance_criteria, estimated_minutes, resolution, tags, and parent — overall and by_type (resolution coverage is scoped to terminal items, its only applicable population). --field-utilization adds a field_utilization block reporting present/total/percent for each content field (notes, learnings, files, docs, tests, comments, deps, body, linked_command) across all items, so under-documented content dimensions are visible at a glance and pair naturally with the --has-*/--no-* list filters for drill-down. --by-assignee, --by-tag, and --by-priority add a breakdowns block with lifecycle-bucketed rows (open/in_progress/blocked/draft/closed/canceled/other + total) per group; blank keys render an explicit (unassigned)/(untagged) label. --by-tag accepts --tag-prefix to restrict counting to a tag namespace (for example domain:). All of these sections are gated behind their flags so the default pm stats remains readable at first glance. history-redact rewrites matching history payloads deterministically, recomputes hash chains, and appends an auditable history_redact marker entry when changes are applied. Its result reports only literal_count, regex_count, total_count, and whether the default replacement was selected; literal values, regex source text, and replacement text are never echoed in CLI, SDK, MCP, recovery, profile, or telemetry output. Treat regex text as sensitive input because it can contain the exact material being removed. --regex '/var/[a-z]+' accepts a leading-slash raw source. A slash-delimited value with a valid JavaScript flag suffix retains literal semantics, so use --regex '/\/var\//g' to match the path /var/ including its final slash. Invalid patterns identify the original one-based --regex index without echoing the value. Expressions receive the global (g) flag; sticky (y) expressions still match only at the current position. Omit y to search later positions. Tracked by pm-gh1689 (repository-only tracker record, excluded from npm). history-compact rewrites long streams into a synthetic checkpoint baseline plus a retained tail (--before accepts a 1-based version or ISO timestamp), re-anchors hashes, verifies integrity, and appends an auditable history_compact marker when applied. history-compact bulk mode (mutually exclusive with a positional <id>) compacts many streams in one audited pass. Select with --ids <a,b,c> (an explicit list — used on its own, not combined with the scan selectors below), or a scan: --all-over <N> (every stream with more than N entries) and/or a lifecycle filter --closed (terminal items only) or --all-streams (every stream). --closed and --all-streams are mutually exclusive. --min-entries <N> (default 3) skips already-compact streams; when history.compact_policy is enabled and --all-over is omitted, the policy's max_entries becomes the default threshold. --before is single-id only and is rejected in bulk mode. Each selected stream runs the same single-item compaction; one failing stream never aborts the rest — the result reports totals (streams_considered/selected/items_compacted/items_skipped/items_errored) plus one row per stream (compacted/skipped with a skip_reason/errored), and the command exits non-zero only if any stream errored. history-repair re-anchors a drifted history chain when pm health/pm validate --check-history-drift report stale hashes: it replays the stream, recomputes every before/after hash using the stream's detected hash epoch, repairs legacy patch ops that no longer strictly apply, reconciles the latest hash with the on-disk item, and appends an auditable history_repair marker. It never modifies item content and is a safe no-op on a clean stream. The result reports the before/after hash versions and whether the epoch was preserved or selected for an ambiguous broken stream, preventing old/new runtimes from alternately rewriting an otherwise consistent legacy chain. history-repair --all (mutually exclusive with <id>) runs the same drift scan pm health uses and applies the audited single-stream repair (ownership check, lock, post-repair no-drift verification, --message audit marker, per-stream --force) to every drifted stream in one pass. One failing stream never aborts the rest: the result lists one compact row per targeted stream (repaired / skipped_clean / failed) plus totals, and the command exits non-zero only if any stream failed. Add --normalize-provenance to include streams containing invalid boolean or single-digit provenance observations. The normalizer removes only those invalid observations and writes aggregate harness/dimension/kind counts in the audit receipt; it never echoes or retains the removed value.

Every pm health projection exposes findings and failed_because. A finding binds one warning token to its owning check, stable code, advisory|gate_failing severity, and either an executable remediation or no_safe_automatic_remediation. Extension health also publishes host_version_census, which enumerates the host and every pm-cli copy resolved by loaded extensions across npm/pnpm layouts. A version mismatch makes the extension check non-clean; matching workspace-linked or deduplicated copies remain healthy.

pm health --verbose-author-events --json includes the complete actionable unknown-author coordinate list and an explicit samples_truncated marker. After evidence review, pm history-author-acknowledge --all-actionable --attributed-author <principal> --reviewer <reviewer> --reason <evidence> records one append-only disposition for the current complete set. --all-actionable and repeatable --event <item-id:line> are mutually exclusive; reviewer, attributed author, and reason remain mandatory.

The maintenance engines are public SDK primitives rather than CLI-only implementations. PmClient.historyRedact, historyRepair, historyRepairAll, historyCompact, and historyCompactBulk return the same structured results shown here; the CLI commands are presentation shims over those SDK-owned engines.

Custom Item Types

Tracker references: pm-qq69, pm-1lkm, and schema evolution pm-dijg.

pm schema inspects and manages the runtime item-type registry. list and show include built-in, custom, and extension-provided types so agents can confirm project context before creating work. add-type registers a config-driven custom item type so agents can use pm create <Type> "..." for project-specific work categories without editing settings by hand. Custom definitions are merged from .agents/pm/schema/types.json (shape: { "definitions": [ItemTypeDefinition...] }). Custom statuses are managed with show-status/add-status/remove-status and persist in .agents/pm/schema/statuses.json (shape: { "statuses": [RuntimeStatusDefinition...] }).

pm schema list
pm schema show Task
pm schema show-status open
pm schema add-type Spike --description "Time-boxed investigation" --default-status open
pm schema add-type Spike --alias spike --alias research --folder spikes
pm schema remove-type Spike
pm schema add-status review --role active --alias in_review --description "Awaiting review" --order 25
pm schema remove-status review
pm schema add-field severity_level --type string --commands create,update --description "Bug severity" --required-on-create
pm schema list-fields
pm schema show-field severity_level
pm schema remove-field severity_level
pm schema apply-preset agile
pm schema add-type --infer --min-count 10
pm schema add-type --infer --apply
pm schema rename-type Spike --to Experiment --dry-run
pm schema rename-type Spike --to Experiment
pm schema rename-field severity --to impact --type Issue --dry-run
pm schema remap-status review --to verifying
pm create Spike "Investigate retry backoff"
  • pm schema list --json returns { builtin, custom, extension, counts, statuses: { builtin, custom, counts }, fields: { custom, counts } } for compact machine parsing. Each status entry includes id, source (builtin/custom), roles, and aliases; each field entry includes key, type, commands, cli_flag, cli_aliases, and the required/allow_unset flags.
  • pm schema show <Type> --json returns the resolved definition, including folder, aliases, default status, required create options, type options, command-option policies, and extension provenance when applicable.
  • pm schema show-status <id> --json returns one resolved status definition (builtin or custom) including id, source, roles, aliases, optional description, and optional order. Status aliases resolve automatically.
  • add-type is an idempotent UPSERT keyed on the type name (case-insensitive); re-running it merges aliases and overrides supplied fields while preserving everything else.
  • remove-type <Name> removes a custom type definition (case-insensitive). Built-in types are refused. It WARNS (non-blocking) with items_using_type:<N> when items of that type still exist, then removes the definition.
  • add-status <id> writes a custom lifecycle status (idempotent UPSERT keyed on the normalized id; re-adding sets replaced: true). Roles are validated against the runtime status roles: draft, active, blocked, terminal, terminal_done, terminal_canceled, default_open, default_close, default_cancel.
  • remove-status <id> removes a custom status. The six built-in statuses (open, in_progress, blocked, closed, canceled, draft) are refused. It WARNS (non-blocking) with items_using_status:<N> when items currently use that status.
  • Built-in types (Chore, Decision, Epic, Event, Feature, Issue, Meeting, Milestone, Plan, Reminder, Task) are reserved and cannot be redefined or removed.
  • add-field <key> registers a custom metadata field in .agents/pm/schema/fields.json (shape: { "fields": [RuntimeFieldDefinition...] }). Each custom field dynamically registers a CLI flag on create/update (and any other commands you list) so projects can capture typed project-specific metadata without hand-editing JSON. It is an idempotent UPSERT keyed on the normalized key, and refuses keys that shadow a built-in field. list-fields / show-field <key> inspect registered fields; remove-field <key> drops one and WARNS (non-blocking) with items_using_field:<N> when items still carry a value. See CONFIGURATION.md for the full schema/fields.json format.
  • apply-preset <agile|ops|research> batch-registers a domain type preset into an already-initialized project (the same vocabulary pm init --type-preset seeds); it is idempotent (re-running reports replaced entries) and shares its definitions with init.
  • add-type --infer scans existing item titles for stable PREFIX-/PREFIX: conventions and proposes them as custom types. It previews candidates by default (dry-run); pass --apply to register the non-shadowing candidates and --min-count <n> to tune the per-prefix threshold (default 10). Candidates whose name resolves to a built-in type are reported and skipped.
  • rename-type, rename-field, and remap-status are lossless migration verbs. The runtime derives a deterministic workspace-and-request-bound migration id for resumable retries; --migration-id remains an optional authoritative override for external orchestration. Start with --dry-run --json to inspect the derived id, fingerprint, affected count, and ordered per-item changes without persisting a plan. Execution stages the target definition, writes immutable history on every affected item and the workspace stream, then retires the source. A field rename refuses any item that already has the target key; --type <Type> optionally restricts a field rename.
  • Field flags (add-field): --type <string|number|boolean|string_array|array|object>, --commands <list> (repeatable/comma; defaults to create,update), --cli-flag <flag>, --alias <flag> (extra CLI flag aliases), --required, --required-on-create, --no-allow-unset, --required-types <list>. array and object accept validated JSON; string_array remains the repeatable comma/newline/list-oriented string collection.
  • Flags: --description <text>, --default-status <status>, --folder <dir>, --alias <name> (repeatable), --role <value> (repeatable; add-status), --order <n> (add-status), migration --to / --migration-id / --dry-run, plus --author/--force governance flags. Add --json for the machine envelope.
  • When pm create/pm update reject an unknown type, the error now points back here: To register a custom type, run: pm schema add-type "X" (writes .agents/pm/schema/types.json).

pm init --type-preset agile|ops|research registers common domain types during initialization:

  • agile: Story, Spike
  • ops: Incident, Runbook
  • research: Experiment, Hypothesis

The option composes with --defaults, --preset, --author, --agent-guidance, and --with-packages; re-running it is idempotent and reports registered_type_preset in JSON output. Already-initialized projects can adopt the same presets without re-running init via pm schema apply-preset agile|ops|research.

Project Profiles

Tracker references: pm-v37g, pm-bhmk, pm-j1fj.

pm profile composes the schema, config, template, and package primitives into complete project-management archetypes. Where a type preset only registers item types, a profile bundles item types, custom statuses, custom fields, per-type workflows, config knobs, create templates, and recommended packages so a fresh tracker can be tailored to an archetype in one idempotent command.

pm profile list                  # show built-in archetypes and their composition counts
pm profile show agile            # full composition of one profile
pm profile lint agile            # author-time consistency check (graded error/warning; never writes)
pm profile apply agile --dry-run # preview the diff without writing
pm profile apply agile           # stage every dimension (idempotent; re-runs are no-ops)

Built-in profiles:

  • agile: Story/Spike types, a review status, story_points/acceptance_owner fields, a Story workflow, a story template, and offline BM25 search config. Recommends templates, calendar, search-advanced.
  • ops: Incident/Runbook types, mitigating/monitoring statuses, severity/service fields, an Incident workflow, an incident template. Recommends lifecycle-hooks, governance-audit, calendar.
  • research: Experiment/Hypothesis types, an analyzing status, hypothesis/method fields, an Experiment workflow, an experiment template. Recommends search-advanced, templates, beads.

Apply reports a per-dimension diff (added / updated / unchanged). Re-applying an already-applied profile performs zero writes. Package recommendations are advisory — apply never installs packages; pm profile show <name> lists the suggested pm package install <spec> candidates. The same surface is available to agents through the pm_profile MCP tool.

pm profile lint <name> runs an author-time consistency check on a built-in or extension-contributed profile without touching the tracker, surfacing graded findings: error for problems that would make apply throw or silently lose data (invalid/duplicate types, statuses, or fields; unknown/invalid config knobs; malformed workflow transitions) and warning for structurally-valid but suspicious content (workflows governing or referencing types/statuses the profile never declares, templates creating an undeclared type, a non-canonical profile name, empty title/summary/package spec). It exits non-zero when the profile has any error-severity finding (warnings keep it green), so it gates cleanly in CI as pm profile lint <name>. The same check is exposed to package authors as the SDK lintProjectProfile / assertProjectProfile primitives — see SDK.md.

Profiles are extensible: a package can ship its own archetype with api.registerProfile(profile) (built on the public SDK defineProjectProfile primitive — see SDK.md). When the package is active, its profile appears in pm profile list (labelled with its source package) and resolves through pm profile show/apply exactly like a built-in. Built-in names are reserved, so a package profile can never silently shadow agile/ops/research. The bundled pm-kanban package registers a kanban continuous-flow archetype this way.

Plan Workflow

pm plan is the agent-optimized planning loop built on the first-class Plan item type. Plans persist ordered steps, evidence, decisions, discoveries, validation, and resume context. Each mutation appends a history entry; full hash-chain replay is preserved.

Plan creation shares the normal create contract. Governance fields such as --status, --deadline, --estimate, --acceptance-criteria, --assignee, review/risk fields, comments, notes, learnings, reminders, events, and linked resources are accepted directly and pass through the same strict/progressive schema validation as pm create.

Every positional plan action is also a discoverable virtual command path. Use pm plan create --help --json or pm help plan create --json for its exact arguments and effective flags, and use pm contracts --command "plan create" --flags-only --json for the matching machine contract. Root pm plan --help --json publishes the same action inventory under subcommands; omitting the required action fails as missing_required_argument before mutation dispatch.

pm plan create --title "Refactor lock retry" --scope "Improve retry semantics" --harness claude-code --parent pm-epic1 --related pm-rel1,pm-rel2 --claim
pm plan create --title "Fix flaky retry test" --step "Read lock.ts" --step "Write the fix" --step "Run the tests"
pm plan create --title "Investigate release failure" --template bug-investigation
pm plan add-step <plan-id> --step-title "Read lock.ts" --step-body "Inspect retry path" --depends-on pm-task1
pm plan update-step <plan-id> plan-step-001 --step-status in_progress --step-evidence "started reading lock.ts"
pm plan complete-step <plan-id> plan-step-001 --step-evidence "lock.ts read; retry path captured"
pm plan block-step <plan-id> plan-step-003 --step-blocked-reason "waiting on pm-task9 approval"
pm plan link <plan-id> plan-step-002 --link pm-rel3 --link-kind discovered_from --link-note "found related util"
pm plan decision <plan-id> --decision-text "Use exponential backoff" --decision-rationale "Avoid thundering herd"
pm plan discovery <plan-id> --discovery-text "Found existing util in src/util/retry.ts"
pm plan validation <plan-id> --validation-text "Coverage stays at 100%" --validation-command "node scripts/run-tests.mjs coverage"
pm plan resume <plan-id> --resume-context "step 2 pending; tests still failing on retry path"
pm plan approve <plan-id> --message "ready to execute"
pm plan materialize <plan-id> --steps plan-step-002,plan-step-003 --materialize-type Task --materialize-parent pm-epic1
pm plan show <plan-id> --depth brief
pm plan show <plan-id> --depth standard
pm plan show <plan-id> --depth deep
pm plan show <plan-id> --fields id,title,steps_summary

Subcommand cheatsheet:

Subcommand Purpose
create Create a Plan item with scope, harness, parent, related, blocked-by, and optional auto-claim
show Progressive-disclosure read (brief / standard / deep) with current step + next-action hints
add-step Append a step with title/body/owner/status/dependencies/files/tests/docs
update-step Patch one step (title, body, status, evidence, owner, blocked reason)
complete-step Shortcut for setting a step to completed with evidence
block-step Shortcut for setting a step to blocked with required --step-blocked-reason
reorder-step Move a step to a new 1-based order
remove-step Drop a step and renumber remaining steps
link / unlink Add or remove linked_items on a step (with optional --promote-to-item-dep)
decision Append a decision log entry (decision/rationale/evidence)
discovery Append a discovery log entry
validation Append a validation check (text/command/expected)
resume Replace the resume-context summary for stateless agents
approve Move plan_mode to approved (default) or any other mode via --mode
materialize Create real pm items (default Task) from selected steps with bidirectional links

Invariants:

  • Exactly one step is in_progress per Plan; pass --allow-multiple-active for explicit parallel branches.
  • create --template <name> seeds ordered pending steps from a built-in template. Available templates are bug-investigation, feature-implementation, and refactoring-sprint; templates cannot be combined with explicit --step or --step-title values.
  • Blocking a step requires --step-blocked-reason (or an already-recorded reason).
  • create accepts repeated --step <title> flags to seed ordered steps in argv order (values are never comma-split). When --step-title is also given it becomes the first step. Per-step detail flags (--step-body, --step-status, --file, ...) apply to a single initial step only; combining them with multiple --step values is a usage error — create the plan first, then refine steps with add-step/update-step. On step subcommands a single --step value still aliases --step-title.
  • materialize adds discovered_from + parent to each new item and an implements link back on the source step plus a child dependency on the Plan.
  • Promoting a step link preserves its semantic kind. depends_on normalizes to the canonical top-level blocked_by; implements and verifies remain first-class directed dependency kinds available to SDK relationship registries and graph consumers.
  • Search keyword corpus includes plan_scope, step titles/bodies, decisions, discoveries, validation, and step linked items.

pm close delete <id> --json reports an explicit outcome (deleted or would_delete), deleted boolean, and previous_status. Compact and --id-only output use the mutation outcome as status, so agents never mistake the deleted item's former lifecycle state for the delete result.

Machine Contracts

pm contracts --json
pm contracts --summary --json
pm contracts --command create --flags-only --json
pm contracts --action create --schema-only --json
pm help create --json

Agents should use runtime contracts instead of hard-coding flag lists. Contract output includes extension-provided command surfaces when active. Extension policy tokens are explicitly scoped by extension_contracts.policy_mode_scope and trust_mode_scope. Project-governance setters use the separate governance_contracts value domains; in particular, workflow enforcement is off|warn|strict, not the extension policy token enforce. Use pm contracts --summary --json first when bootstrapping in a tight context window. command_summaries contains one row per registered command path, including aliases and namespace children, with a meaningful intent plus default_max_estimated_tokens. The low-token summary intentionally does not repeat the 32 virtual positional Plan, Assurance, and workspace-snapshot action rows; grammar_contracts.positional_action_count and positional_action_parents retain their complete discoverability and route callers to root help. A command-scoped contract for an exact action restores its typed positional signature, exact flags, provenance, and format-specific TOON/JSON ceilings. Package commands contribute their intent through command registration (falling back to the registered description), so agents never need a generic Inspect flags. placeholder. Request heavier command-specific flags or schemas only after selecting the exact path.

Completion

pm completion is provided by the optional guide-shell package (pm package install guide-shell --project).

pm completion bash
pm completion zsh
pm completion fish

Generated completions resolve tags lazily by default. Use --eager-tags only when embedding static tags is required.

来自 pm 2026.11.13 中的 docs/COMMANDS.md。 在 GitHub 上查看