Summary
2026.7.27 changed what an output_format service override's return value means. Returning
the inbound context.payload used to be the idiomatic "I decline this payload, render it
normally" signal. It now means "render exactly this", so an override that passes the payload
through makes every command in the workspace print the whole command context instead of its
own result.
This shipped under Fixed as "First-party service collision: builtin-calendar and
builtin-guide-shell both override the global output_format service (pm-ixoa)" — with no
BREAKING marker and no migration note for third-party override authors. The new contract
({ handled: false }) is also not discoverable from the public SDK surface (see below).
I found it because three of my own extensions silently corrupted all CLI output after the
upgrade.
Reproduction
An extension whose output_format override is a pure pass-through:
api.registerService("output_format", (ctx: ServiceOverrideContext) => ctx?.payload);
$ pm init probe --json >/dev/null && pm create --title "Real item" --type Task --json >/dev/null
$ pm list --limit 2 # BEFORE installing the extension
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 — the item list is gone
global:
quiet: false
noChangedFields: false
idOnly: false
noExtensions: false
noPager: false
profile: false
defaultOutputFormat: "toon"
format: "toon"
options:
quiet: false
noChangedFields: false
...
Every command is affected, not just the extension's own. Changing the decline path to the
documented decision object restores correct output:
api.registerService("output_format", (_ctx: ServiceOverrideContext) => ({ handled: false }));
$ pm list --limit 2
items:
- id: "probe-yqx3"
...
count: 1
Why this was easy to get wrong
- The old pattern was the safe-looking one. Returning the payload unchanged reads as an
inert pass-through, and it was one. Two of my extensions carried an explicit comment saying
"return the incoming payload UNCHANGED (returning a fabricated value here would corrupt every
command's output)" — advice that inverted meaning in 2026.7.27.
- It fails silently and globally. No error, no warning, exit code 0.
pm health and
pm validate both stay happy. The only symptom is that unrelated commands render the wrong
thing, which looks like a host bug rather than an extension bug.
ServiceOverrideDecision is not publicly exported. declineServiceOverride(),
handleServiceOverride() and defineServiceOverride() are all in ./sdk and
./sdk/authoring per sdk/public-surface.json, but the type they return is not. So an
author who cannot take a runtime dependency on the SDK — which is the documented situation for
a standalone-installed extension that loads only its own dist/ — has no way to type the
return and must hand-write { handled: false } against an unexported contract.
Suggested fix
- Export
ServiceOverrideDecision from ./sdk and ./sdk/authoring. Runtime helpers are
exported but the type is not, which is backwards for the one case (standalone extension) that
can only use the type.
- Add a migration note for the semantics change, and ideally flag it as breaking for
extension authors even though the underlying motivation was a first-party collision fix.
- Consider warning on the ambiguous case: if an override returns an object that is
reference-identical to context.payload, that is almost certainly a stale pass-through rather
than a deliberate "render the context". A one-time diagnostic naming the extension would have
turned three silent breakages into an immediate fix.
The third point is what would have saved the most time — the failure gave no indication which
extension was responsible.
Environment
- pm-cli 2026.7.27 (
@unbrained/pm-cli), Node 26.5.0, Linux
- Affected extensions (now fixed): pm-graph, pm-starter, pm-slack-standup — all had a
payload-echo decline path
pm-starter is a starter template, so the broken pattern was being handed to every new
extension author as the reference implementation
Summary
2026.7.27 changed what an
output_formatservice override's return value means. Returningthe inbound
context.payloadused to be the idiomatic "I decline this payload, render itnormally" signal. It now means "render exactly this", so an override that passes the payload
through makes every command in the workspace print the whole command context instead of its
own result.
This shipped under Fixed as "First-party service collision: builtin-calendar and
builtin-guide-shell both override the global output_format service (
pm-ixoa)" — with noBREAKING marker and no migration note for third-party override authors. The new contract
(
{ handled: false }) is also not discoverable from the public SDK surface (see below).I found it because three of my own extensions silently corrupted all CLI output after the
upgrade.
Reproduction
An extension whose
output_formatoverride is a pure pass-through:Every command is affected, not just the extension's own. Changing the decline path to the
documented decision object restores correct output:
Why this was easy to get wrong
inert pass-through, and it was one. Two of my extensions carried an explicit comment saying
"return the incoming payload UNCHANGED (returning a fabricated value here would corrupt every
command's output)" — advice that inverted meaning in 2026.7.27.
pm healthandpm validateboth stay happy. The only symptom is that unrelated commands render the wrongthing, which looks like a host bug rather than an extension bug.
ServiceOverrideDecisionis not publicly exported.declineServiceOverride(),handleServiceOverride()anddefineServiceOverride()are all in./sdkand./sdk/authoringpersdk/public-surface.json, but the type they return is not. So anauthor who cannot take a runtime dependency on the SDK — which is the documented situation for
a standalone-installed extension that loads only its own
dist/— has no way to type thereturn and must hand-write
{ handled: false }against an unexported contract.Suggested fix
ServiceOverrideDecisionfrom./sdkand./sdk/authoring. Runtime helpers areexported but the type is not, which is backwards for the one case (standalone extension) that
can only use the type.
extension authors even though the underlying motivation was a first-party collision fix.
reference-identical to
context.payload, that is almost certainly a stale pass-through ratherthan a deliberate "render the context". A one-time diagnostic naming the extension would have
turned three silent breakages into an immediate fix.
The third point is what would have saved the most time — the failure gave no indication which
extension was responsible.
Environment
@unbrained/pm-cli), Node 26.5.0, Linuxpayload-echo decline path
pm-starteris a starter template, so the broken pattern was being handed to every newextension author as the reference implementation