a commander can say something in its own words

asked whether the voice-memos commander could send a notification, the
answer was no, three times over. the platform skill gives it the prefix
/notify and not the route. the route wants a catalogue code, not a title
and a message. and of the six codes that exist, none means "a commander has
something to tell you" — the nearest renders "Test notification".

so: officer/core:commander, one entry rather than one per commander, with
the commander travelling as {id}. arming a new one needs no catalogue
change and no app resync.

and `title`/`body` on the notification itself, which override the
catalogue. this is a deliberate exception to the rule the whole design
rests on — content does not travel — and it is the owner's decision, made
2026-08-24. a commander's output is one sentence only it could write; there
is no entry to render it from, and "1 item" is not worth waking a phone
for. sendoff.landed already carries the same relaxation for the same kind
of reason.

the control sits at the PRODUCER, not the transport. the commander's
CLAUDE.md now says what may go in those two fields and what may never — and
that a notification is an interruption, so send few. this file cannot judge
what is sensitive and does not try.

`title` is the opt-in, not `body`. the existing smuggle test passes a stray
`body` and asserts it does not reach the wire; a body that travelled on its
own would have silently defeated it. pinned on both transports.

FCM gets a `notification` block only for producer text — data-only is what
lets the app compose from its catalogue, and text with no entry to compose
from would arrive and show nothing.

worth recording: there is NO Notification Service Extension, in this repo
or the mobile one. mutable-content: 1 is set for something never built, so
aps.alert is what the phone actually shows and this works with no app
release. i said the opposite an hour ago, from the comments rather than
from looking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-24 15:13:39 +00:00
co-authored by Claude Opus 5
parent 00baa83a2c
commit 478ddb13e9
8 changed files with 183 additions and 14 deletions
+6
View File
@@ -32,6 +32,12 @@ export type Rendered = { title: string; body?: string; thread: string };
*/
export function renderNotification(n: Notification, catalogue: Catalogue): Rendered {
const entry = catalogue.entries[n.code];
// The producer's own words win. Only it knows what happened — a commander's whole output is a sentence
// the catalogue could not have held. Falls back to the code's template for everything else, which is
// almost everything.
if (n.title) return { title: n.title, ...(n.body ? { body: n.body } : {}), thread: threadFor(n, catalogue) };
if (!entry) return { title: FALLBACK_TITLE, thread: n.code };
const body = entry.body ? interpolate(entry.body, n) : undefined;