Fix output_format override corrupting all command output, and adopt pm-cli 2026.7.27

Merged
UN
3mo ago
+195-23

Summary

Fixes a bug that corrupted the output of every pm command in any workspace with pm-starter
installed, and adopts pm-cli 2026.7.27.

Under pm-cli 2026.7.27 an output_format service override's bare return value IS what the host
renders
. This extension declined payloads by returning the inbound ctx.payload, which
2026.7.27 no longer reads as "not handled". The result: unrelated commands printed the whole
command context instead of their own output.

Reproduced against real data

$ pm init probe --json >/dev/null && pm create --title "Real item" --type Task --json >/dev/null

$ pm list --limit 2                      # before install
items:
  - id: "probe-yuf8"
    status: "open"
    type: "Task"
    title: "Real item"
count: 1

$ pm install ./pm-starter --project --json | jq .ok
true

$ pm list --limit 2                      # AFTER install — item list gone
global:
  quiet: false
  noChangedFields: false
  idOnly: false
  ...
format: "toon"
options:
  ...

After this fix, in a fresh workspace with two seeded items:

$ pm install /…/pm-starter --project --json     # ok=true warnings=[]
$ pm list --limit 3
items:
  - id: "probe-mer2"
    status: "open"
    type: "Issue"
    title: "Seeded issue"
  - id: "probe-be2g"
    status: "open"
    type: "Task"
    title: "Seeded task"
$ pm list --limit 1 --json      # valid JSON, count=1
$ pm starter --help             # registers

The fix

The decline path now returns the { handled: false } decision. pm's
isServiceOverrideDecision predicate accepts that only when handled is false and the
object carries exactly that one key, so the literal is written deliberately rather than spread
into a larger object.

The SDK does export declineServiceOverride() (from sdk/authoring), which returns the same
object — but it is a runtime value, and a standalone-installed extension loads only its own
dist/ and cannot resolve the SDK at runtime (see the note at the top of index.ts). The
matching ServiceOverrideDecision type is not part of the public SDK surface either, so
there is nothing to type-import. Both points are raised upstream.

Why the dependency bump ships in the same PR

pm-cli 2026.7.26 has no isServiceOverrideDecision at all, so the decision object is not
understood there. The regression test can only exercise the real contract once the dependency is
bumped. This was visible during development: against the pinned 2026.7.26 tree the harness
reported handled: true with result: { handled: false }; on 2026.7.27 it correctly reports
handled: false.

  • @unbrained/pm-cli peerDependencies >=2026.7.26 → >=2026.7.27
  • @unbrained/pm-cli devDependencies ^2026.7.26 → ^2026.7.27
  • package version → 2026.7.27

The test that should have caught this

test/smoke.test.ts used a hand-rolled api double whose registerService was a no-op
(registerService: () => {}). It discarded the registered override, so the override's return
value was never evaluated — this entire bug class was invisible to the suite.

The new test drives pm's real service runner via the SDK harness
(createExtensionTestHarness + runRegisteredServiceOverrideForTest) and asserts
handled === false, that a declined payload is returned to the host untouched, and that no
extension_service_override_* warnings are emitted.

Falsification checked — this test is not vacuous:

index.ts stateresult
old ctx?.payload form9 pass / 1 fail (new test fails)
{ handled: false } fix10 pass / 0 fail

Upstream

Filed as unbraind/pm-cli#776: the semantics
change shipped under a Fixed changelog entry ("First-party service collision: builtin-calendar
and builtin-guide-shell both override the global output_format service") with no breaking marker
and no migration note, it fails silently and globally with exit code 0, and
ServiceOverrideDecision is unexported while its runtime constructors are exported. pm-graph and
pm-slack-standup carried the same payload-echo pattern and are being fixed too.

Gates

build, typecheck, check, 10/10 tests, changelog:full + changelog:check (after
git fetch origin --tags --force, since stale local tags otherwise red the CI changelog check).

Production proof captured above with the built extension installed into a throwaway workspace —
not the dev runner. pm health's only remaining warning is
extension_migration_pending:project:pm-starter:pm-starter-0001-noop, which is the template's
own deliberate demo migration, not an activation failure.

Version-metadata fix (b383b26) — caught in review

A pm extension declares its version in three places: package.json, manifest.json, and a
version: constant inside the module. The initial commit bumped only package.json, so an
installed pm-starter would have reported a stale 2026.7.26 to the host and to
pm extension --manage. Greptile flagged the mismatch; b383b26 aligns manifest.json and the
index.ts constant and rebuilds dist/.

The sibling agent-driven bumps (pm-changelog, pm-gantt-chart, pm-graph) had handled manifest.json
correctly — this was a gap in a hand-run bump that rewrote only package.json. The same drift was
fixed proactively in pm-slack-standup.

Gates re-run after the fix: build, typecheck, 10/10 tests, changelog:check up to date.

pm items

  • pm-starter-ejr6 — Issue: output_format decline fix + 2026.7.27 adoption