pm-starter
Complete starter/scaffold extension for pm-cli showing all capability types
Instalar
Con pm instalado
pm install npm:pm-starter --projectCon npm, sin instalar pm
npx -y @unbrained/pm-cli install npm:pm-starter --projectCon Bun, sin instalar pm
bunx @unbrained/pm-cli install npm:pm-starter --projectREADME
The CANONICAL scaffold for pm-cli extensions. A single, heavily-commented index.ts demonstrates all 9 SDK capability types with small, SAFE, inert examples.
Use this as a starting template for building your own extensions: copy the capability you need and delete the rest.
Installation
pm install github.com/unbraind/pm-starter --project
Capabilities
Every capability declared in manifest.json is demonstrated in index.ts. Each
maps to one or more register*/hooks.* calls on the ExtensionApi.
| # | Capability | ExtensionApi call(s) |
What the demo registers |
|---|---|---|---|
| 1 | commands | registerCommand |
pm starter greet, pm starter summary, pm starter demo, pm starter plan, pm starter context, pm starter search, pm starter setup |
| 2 | renderers | registerRenderer |
json + toon renderer overrides (reshape only the starter demo payload, pass through everything else) |
| 3 | hooks | hooks.beforeCommand, hooks.afterCommand, hooks.onWrite, hooks.onRead, hooks.onIndex |
All five lifecycle hooks (observe-only; opt-in logging via PM_STARTER_HOOKS) |
| 4 | schema | registerItemFields, registerItemTypes, registerMigration |
Optional fields starter_origin/starter_score, a StarterNote item type, and a no-op migration pm-starter-0001-noop |
| 5 | importers | registerImporter, registerExporter |
pm starter-demo import (inert dry-run) and pm starter-demo export (read-only JSON dump) |
| 6 | search | registerSearchProvider, registerVectorStoreAdapter |
Search provider starter-substring and in-memory vector store adapter starter-memory |
| 7 | parser | registerParser |
Identity pass-through parser override for the native list command |
| 8 | preflight | registerPreflight |
Pass-through preflight decision override (no behavior change) |
| 9 | services | registerService |
Pass-through override of the output_format core service |
Flags: the demo commands declare typed
flags, andregisterFlags("list", …)adds an inert--starter-tagflag to the nativelistcommand. Flag registration is part of the commands capability — it does not need its own manifest entry.
Commands
pm starter greet
pm starter greet
pm starter greet --name Developer --emoji 🚀 --uppercase
pm starter summary
pm starter summary
pm starter summary --verbose
pm starter demo
pm starter demo # structured payload reshaped by the starter renderer
pm starter demo --json
pm starter plan
Integrates with pm's agent-optimized plan workflow (pm plan show).
pm starter plan pm-cli-website-6t9b # show a plan summary
pm starter plan pm-cli-website-6t9b --steps # include per-step breakdown
pm starter context
Integrates with pm's token-efficient project context snapshot (pm context).
pm starter context # compact context snapshot
pm starter context --depth deep # deeper context
pm starter search
Integrates with pm's keyword/semantic/hybrid search (pm search).
pm starter search authentication # keyword search (default)
pm starter search "bug fix" --mode hybrid # hybrid search
pm starter search deployment --mode semantic # semantic search
pm starter setup
Guided setup helper for scaffolding a new extension.
pm starter setup --interactive # interactive wizard
pm starter setup --name my-ext --capability commands # scaffold plan
pm starter setup --name my-ext --capability commands,search,hooks
Importer / exporter command paths
registerImporter("starter-demo") and registerExporter("starter-demo") auto-create:
pm starter-demo import # inert dry-run preview
pm starter-demo export # read-only JSON dump of items
Human-facing commands are namespaced under
pm starter …while the importer/exporter live underpm starter-demo …so the two command paths never collide.pm extension doctorreports 0 collisions as a result.
Backend-neutral capability laboratories
Run a single capability against directory and synthetic remote adapters with identical, compact typed receipts and an unsupported-mutation probe before either source read:
pm starter lab --capability commands --json
pm starter lab --capability importers --json
pm starter-lab export --json
pm starter lab --operation mutate --json # typed refusal, exit 1
The command example counts statuses; the exporter returns deterministic byte/digest
proof. Both use the injected SDK's certified complete read. The SDK exports
runCapabilityLaboratory, executeCapabilityLaboratory, both adapter factories,
and command/exporter handlers. See the executable examples and contract.
The synthetic transport uses structured cloning of certified values; hosted-provider
acceptance is a separate concern. A changing source fails parity explicitly.
Without git
All starter commands, the paginated consumer, the read-only exporters, both laboratories and the failure-first recovery recipes work in a plain initialized tracker with no Git executable. Creating or running a laboratory needs no repository. Cloning the starter and repository release/changelog automation need Git. Laboratory commands require a host that injects the SDK (tested with 2026.10.20) and return an actionable refusal when it is absent.
Architecture
Each capability lives in its own setup* function for clarity. Delete the ones
you don't need and prune manifest.json to match:
setupCommands(api)—registerCommand(with typed flags,failure_hints,arguments, and--interactiveguided setup)setupRenderers(api)—registerRendererforjson/toonsetupHooks(api)—hooks.beforeCommand/afterCommand/onWrite/onRead/onIndexsetupSchema(api)—registerItemFields/registerItemTypes/registerMigrationsetupImportExport(api)—registerImporter/registerExportersetupSearch(api)—registerSearchProvider/registerVectorStoreAdaptersetupParser(api)—registerParsersetupPreflight(api)—registerPreflightsetupServices(api)—registerServicesetupFlags(api)—registerFlags
The zero-runtime-coupling pattern
Standalone-installed extensions load only their own dist/ at runtime, so
@unbrained/pm-cli is not resolvable as a runtime value. index.ts therefore
imports defineExtension as a type only and provides a trivial identity
implementation; the real CLI supplies the live api object at activation time.
For the same reason the EXIT_CODE / CommandError error contract is
re-implemented locally rather than imported from the SDK.
Complete SDK reads
starter demo and starter-demo export use ctx.sdk.client.listAllComplete
with full bodies, including closed and canceled items. The SDK calls
certifyCompleteListResult itself: there is no external JSON envelope parser
or local completeness policy left in this package. Both commands then call the
SDK history action with verify: true for each item. That action reads fresh
history and uses verifyHistoryEntries, including comparison with the current
item, before returning a verification receipt. A cached event index is never
accepted as integrity evidence. These reads work without pm or git on PATH.
Install and activation require pm-cli >=2026.8.18, the first published stable
release with PmClient.listAllComplete. The development SDK remains pinned to
2026.10.20. See the published API inventory for every used
capability and its first-release artifact evidence. Hosts without SDK injection
receive an explicit failed-read outcome. Failed or incomplete reads
continue to fail with exit code 1, including exports; they never become an empty
workspace. SDK certification is intentionally stricter about counts, unique IDs,
full projections, unbounded delivery and omission receipts. Invalid history now
fails explicitly, even if a previous cached read succeeded. PM_JSON_MAX_BUFFER
continues to apply to the other subprocess examples; SDK reads do not use it.
The exported readPmItems helper now accepts the host SDK context and returns a
Promise. The former describeListAllIncompleteness helper was removed with its
local policy. SDK rows can retain optional properties with value undefined
that CLI JSON omitted; their serialized values are identical. Renderer ownership and callback guards retain their existing scope.
SDK 2026.7.6 patterns
The starter demonstrates several patterns introduced or stabilized in the 2026.7.6 SDK:
failure_hints: command definitions can include afailure_hintsstring array that surfaces actionable guidance when a command exits non-zero. Used onstarter summary,starter plan,starter context,starter search, andstarter setup.arguments: command definitions can declare typed positional arguments (name,required,variadic,description) surfaced in help. Used onstarter plan(requiredid) andstarter search(variadickeywords).--interactiveflag pattern:starter setup --interactivewalks through a guided setup wizard, demonstrating the pattern for agent- or human-driven interactive configuration.- Better error messages: all commands that shell out to
pminclude the first line of stderr in theirCommandErrormessage and suggest a concrete fix (e.g.pm init,pm get <id>,pm plan create).
Creating a New Extension
# 1. Clone this repo
git clone https://github.com/unbraind/pm-starter.git my-extension
cd my-extension
# 2. Edit files
# - manifest.json: update name, description, capabilities
# - index.ts: keep only the capabilities you need, implement your logic
# - package.json: update name
# 3. Build and test
npm install
npm run build
# 4. Install locally
pm install ./my-extension --project
Recommended extension tests
Use createExtensionTestHarness from @unbrained/pm-cli/sdk/testing for positive
registration and handler tests. It activates the real host registration API and
runs registered handlers through host dispatch. Pair it with an initialized
PmClient tracker in a temporary directory, including a tracker outside Git:
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { PmClient } from "@unbrained/pm-cli/sdk";
import { createExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing";
import extension from "../index.ts";
test("registered command reads a real tracker", async (t) => {
const directory = mkdtempSync(join(tmpdir(), "extension-test-"));
t.after(() => rmSync(directory, { recursive: true, force: true }));
const pmRoot = join(directory, ".agents", "pm"); // Tracker storage, not workspace root.
const client = new PmClient({ pmRoot, noExtensions: true, author: "test" });
await client.init("example", { defaults: true, mergeFence: false, agentGuidance: "skip" });
await client.create({ id: "example", type: "Task", title: "Example" });
const manifest: unknown = JSON.parse(readFileSync(new URL("../manifest.json", import.meta.url), "utf8"));
const harness = await createExtensionTestHarness({ default: extension, manifest });
t.after(() => harness.deactivate());
harness.assertCommandContract({ name: "starter greet", flags: ["--name", "--uppercase"] });
harness.assertFlags({ name: "list", flags: ["--starter-tag"] });
harness.assertItemField({ name: "starter_origin", type: "string" });
harness.assertItemType({ name: "StarterNote", folder: "starter-notes" });
// A nonempty pmRoot makes the harness inject a real host-bound ctx.sdk.
const outcome = await harness.runCommand({ command: "starter demo", pmRoot });
assert.equal(outcome.handled, true);
assert.deepEqual(outcome.warnings, []);
assert.equal((outcome.result as { item_count: number }).item_count, 1);
});
Keep the manifest's capabilities accurate; this example uses the package's actual manifest so missing declarations fail activation. Copy the assertion names and expected receipts to match your own extension. The pinned development SDK supplies these testing helpers; they do not change the installed extension's host floor.
Use assertHook, assertImporter, assertExporter and the other assert*
methods to verify registered surfaces, then runCommand, runRendererOverride,
runImporter, runExporter or runHook to exercise behavior. Importers and
exporters also register command paths, so they appear in the host command registry.
The harness dispatches one requested surface; it does not run the entire CLI
parser/preflight/render pipeline or automatically install hooks on a separate
PmClient. Pass parsed command flags in options, global host options in global,
and real committed mutation data when invoking an observe-only write hook.
Retain package rules as assertions: USAGE errors, inert importer and laboratory
mutation refusals, renderer ownership, and receipt contents are starter behavior
the SDK cannot prescribe. Pure option tests and the focused callback/subprocess
failure doubles in test/commands.test.ts stay unit tests; use
activateExtensionForTest directly for deliberate failed-activation or old-host
compatibility tests. Positive smoke tests must use the supported harness instead
of a handwritten ExtensionApi registration sink. See test/smoke.test.ts for
registration, invocation, output receipt and mutation-context examples.
Run node --test --test-concurrency=2 test/smoke.test.ts and npm run build:test
when adapting this pattern, then the package's full gate.
License
MIT
Release Automation
This package is release-ready for GitHub, npm, and Bun-compatible installs. CI runs type checking, build, production dependency audit, package packing, Bun install verification, and pm-changelog validation. The daily release workflow publishes only when commits exist after the latest release tag and uses pm-changelog to generate CHANGELOG.md and GitHub release notes.
Multi-agent merge safety
This repo tracks its project management in .agents/pm/ and ships a committed .gitattributes
that maps those tracker artifacts to pm-cli's field-aware Git merge drivers, so concurrent-branch
tracker edits merge cleanly instead of hard-conflicting. The driver definitions live in
per-clone Git config; npm install / npm ci wires them automatically via the prepare script, scripts/prepare-merge-driver.ts: the launcher template pm-ops ships, copied unchanged, which a test compares byte for byte with the pinned template. It runs pm-ops's installer, which calls pm merge install when the pm CLI is on PATH and skips with a notice when it is not. A production install of a clone (npm ci --omit=dev) has no pm-ops, so the launcher skips with one notice, while a stale or broken pm-ops fails the install. Registry installs of this package never run prepare. Being Node-based, it behaves identically on POSIX shells and Windows cmd.exe. To (re)run manually: npm run merge:install.
After merging a branch that touched .agents/pm/, reconcile any residual history-hash drift with
pm merge reconcile (pm-cli ≥ 2026.7.22): preview with pm merge reconcile --dry-run, apply with
pm merge reconcile --message "post-merge reconcile", then confirm with pm validate, which scans the
whole tracker and flags remaining history drift across every affected item (pm merge reconcile
itself lists each affected stream in its output; pm history --verify <id> spot-checks one item). The field-aware driver already unions every author's
content, so reconcile only re-greens the hash chain (no data loss) — see the authoritative
pm-cli merge-safety guide. The
older blunt pm history-repair --all remains available as a lower-level primitive.
Complete tracker reads and certification
The shared demo/export/search reader requests pm list --status all --full --include-body --no-truncate --strict-read --json. Full projection and no truncation preserve complete item bodies and rows beyond the CLI's default output budget; strict reads and completeness receipts keep malformed or partial trackers as failures.
Fresh clones install the managed GitHub extension before a read-only preview:
npx pm package install npm:[email protected] --project
npx pm github sync --repo unbraind/pm-starter --dry-run
This does not enable scheduled sync. See docs/certification-2026.10.4.md for validation and copied-real-tracker evidence. Whole-executable and statement coverage remains tracked in pm-starter-puy5.
Failure-first task recipes
pm starter recover --json deliberately encounters three native refusals and
finishes each read through a structured recovery contract. Use
--recipe missing-item|unsupported-capability|insufficient-budget for one loop,
or --output-format toon for compact TOON receipts. The typed
runFailureFirstRecipes(ctx.sdk, { recipe: 'all' }) API uses the same code path.
The workflows discover a valid item ID, select an advertised graph read, and recover an infeasible output budget using the SDK's certified complete-list helper. They read existing trackers without Git and never execute suggested shell text or write item data. Each receipt asserts its native discriminator, remedy and successful retry; the missing-item SDK classifier is labeled explicitly. See the runnable CLI and SDK examples for contracts, refusal codes and concurrency limits.
Cancellation-safe paginated consumer
pm starter consume --page-size 2 --max-pages 2 --json consumes native host pages
under a cumulative task budget and returns a caller-owned checkpoint. Resume with
--checkpoint '<checkpoint JSON>'. The typed consumePaginated SDK API accepts
an AbortSignal, admits whole pages and retains the last admitted cursor on
cancellation. Output cursors preserve native snapshot refusals and typed remedies.
No source tracker writes or git repository are required.
See the CLI and SDK recipe, cancellation boundaries and recovery contract.
Agent task cost baselines
pm starter baseline --mode declare --scenario all --corpus-id example --json
pins expected answers for orientation, targeted reads, and cursor resume. Replay
the emitted declarations with --mode run; --mode probe proves that one extra
full-list call increases both SDK call and delivered-byte budgets. Repeated runs
report distributions; byte/4 token figures are explicitly estimates. This is a
read-only consumer example, with no model-performance claim. It works without Git.
See the protocols, typed SDK example, refusals and public-data validation.
Del README del paquete en GitHub. Ver en GitHub