dispatch is anything that leaves, and the deeplink now goes somewhere

the definition was "doing something with a result beyond keeping it",
which does not obviously cover a notification ABOUT classification failing.
it should: classification decided where the item goes and it went to
unclassified/. telling somebody is a different step, and its rule belongs
in DISPATCHING.md. the test is never what the message is about — it is
whether anything left.

and the commander notification pointed at officer://command-hq/{id} where
{id} is the DIRECTORY name, while /command-hq/:sessionId addresses a
commander by its transcript id. so the tap landed nowhere.

the two want different ids and cannot be reconciled today: threading wants
the directory name, stable from creation; the route wants the transcript
id, which does not exist when the commander is created. rather than make a
commander fetch its own transcript id before it can speak — an errand it
would rightly skip, as the language rule already proved — the tap opens
Command HQ and the owner picks. one tap worse, and correct.

the honest fix is a route that accepts a directory name. noted where
whoever does the mobile deep-link work will find it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 08:02:34 +00:00
co-authored by Claude Opus 5
parent 141e470aee
commit a35df2afb2
2 changed files with 21 additions and 6 deletions
+8 -3
View File
@@ -228,9 +228,14 @@ export function commanderClaudeMd(params: CommanderClaudeMdParams): string {
' subdirectory of `data/` named for that category. It changes where the item IS, never what it is.', ' subdirectory of `data/` named for that category. It changes where the item IS, never what it is.',
' Optional, and most commanders do not need it: it earns its place only when there are enough items', ' Optional, and most commanders do not need it: it earns its place only when there are enough items',
' that finding one matters. It is a STEP, not a place — the place is always `data/`.', ' that finding one matters. It is a STEP, not a place — the place is always `data/`.',
'- **Dispatch** — doing something with a result beyond keeping it. Telling the owner, handing it to', '- **Dispatch** — anything that LEAVES this commander. Telling the owner, waking another conversation,',
' another agent, sending it somewhere. Optional, off until the owner arms it, and the only one of the', ' handing work to an agent, sending something somewhere. Optional, off until the owner arms it, and the',
' four that acts outside this directory.', ' only one of the four that acts outside this directory.',
'',
' Note what that includes: a notification saying classification could not place an item is DISPATCH,',
' not classification. Classification decided where the item goes and it went to `unclassified/`.',
' Telling somebody about it is a different step and its rule lives in `DISPATCHING.md`. The test is',
' never what the message is about — it is whether anything left.',
'', '',
'Storing the result is NOT dispatch. Every action keeps what it produced, here, by default — that is', 'Storing the result is NOT dispatch. Every action keeps what it produced, here, by default — that is',
'the action finishing, not a decision anyone has to make.', 'the action finishing, not a decision anyone has to make.',
+13 -3
View File
@@ -127,8 +127,18 @@ const CORE_ENTRIES: Record<string, NotificationEntry> = {
A commander with something to say. A commander with something to say.
ONE entry rather than one per commander, and the commander travels as `{id}` — so arming a new ONE entry rather than one per commander, and the commander travels as `{id}` — so arming a new
commander needs no catalogue change and no app resync. `{id}` is the commander's row id, which is what commander needs no catalogue change and no app resync. `{id}` is the commander's DIRECTORY name, which
`officer://command-hq/{id}` opens. is what threads its notifications together.
The route is the LIST, deliberately, and not `officer://command-hq/{id}`. That is what it was, and it
was wrong: `/command-hq/:sessionId` addresses a commander by its TRANSCRIPT id, so a deeplink built
from the directory name lands nowhere. Threading wants the directory name (stable from creation, before
any transcript exists); the route wants the transcript id (which does not exist yet when the commander
is created). Rather than make a commander fetch its own transcript id to send a notification, the tap
opens Command HQ and the owner picks — one tap worse, and correct.
Worth revisiting when the app handles deep links properly: the honest fix is a route that accepts a
directory name, not a commander doing a lookup before it can speak.
This entry is what a commander sees when it sends nothing but a code and an id: "Voice Memos" would be This entry is what a commander sees when it sends nothing but a code and an id: "Voice Memos" would be
a lie without knowing which one, so the generic is honest and the deeplink does the rest. In practice a a lie without knowing which one, so the generic is honest and the deeplink does the rest. In practice a
@@ -163,7 +173,7 @@ const CORE_ENTRIES: Record<string, NotificationEntry> = {
title: 'Commander: {title}', title: 'Commander: {title}',
body: '{body}', body: '{body}',
target: 'mobile', target: 'mobile',
route: 'officer://command-hq/{id}', route: 'officer://command-hq',
thread: 'commander:{id}', thread: 'commander:{id}',
}, },
}; };