COMMS: notes live in the repo now, starting with command-hq

Andre: keep these in the platform repo with proper tracking rather than scp-ing
them to a directory on alpha.

A note on a server is invisible to whoever picks the work up next, has no history,
and cannot be reviewed beside the change it argues about. In git it is diffable, it
arrives with a pull, and the commit that answers a note sits next to the note.

First one is the mobile side of Command HQ: what the API got right, and the single
thing a client cannot do — create a commander, because no endpoint returns the
commanders directory and only the web composer flags the spawn. The decision reached
with Andre is recorded there, with the two rejected alternatives and why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 07:29:48 +01:00
co-authored by Claude Opus 5
parent eb94af87f4
commit f2ae58d29a
2 changed files with 88 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
# Command HQ — the mobile side, and the one thing it cannot do
Written 2026-08-18 by the mobile side, after building `apps/commandhq` against
`src/servers/api/command-hq/router.ts` at `eb94af87`.
## The API needed nothing changed to be usable
`/api/command-hq` was read rather than described: the types in the app are copied from the router, so
the two cannot drift. Everything the grid needs is already there — categories, commanders with
`categoryId`/`tagIds`/`running`, rename via PATCH, purge.
Two decisions in the design record turned out to matter more than they look, and both were right:
**No name column.** Renaming proxies to `renameClaudeSession`, so there is exactly one title and no
second field to disagree with it.
**`running` is derived, not stored.** The client re-reads it on a timer and trusts it, which it could
not do with a status column. The grid polls every 30s — a watcher polls its own source every ten
minutes or more, so half a minute behind is still the truth.
And one that saved the whole build: **the detail route deliberately does not return the conversation.**
Because the transcript is `GET /chat/sessions/:id` unchanged, a commander's detail on mobile is the
*existing* chat screen. The composer, the streaming, the tool blocks and the background-job sheet all
work inside a commander for free. Had that route returned its own transcript shape, mobile would have
needed a second reader.
## What mobile cannot do: create a commander
The client has no way to start one, and this is the only gap.
Creation requires a session in the commanders directory. The design record is explicit that this is
`DATA_PATH/<email>/commanders/`, one shared directory per user — but **no endpoint returns that path**,
and `POST /command-hq/commanders` only files a session that already exists there. A listed commander
carries `cwd` (spread from the session summary), so a client with one commander could infer it; a
client with none cannot create its first.
The web composer does not have this problem: per the design record it flags the spawn and the server
picks the directory.
### The decision, made with the owner
**The API spawns it. The client is not responsible for knowing where commanders live.**
Something of the shape:
```
POST /command-hq/commanders/new
body: { categoryId?: number | null, prompt?: string }
-> { sessionId, cwd }
```
- The server resolves `getCommandersDir(email)` itself, spawns the session there, writes the
`command_hq_sessions` row, and returns the id.
- **The response must carry `sessionId`**, or the client has to poll the list and guess which row is
new — which is a race with nothing to break the tie, since a commander has no name at creation.
- `cwd` alongside it is convenient but not required; the app only needs it to open the conversation,
and it can equally come from the commander list afterwards.
Rejected on the way: a `GET /command-hq/workspace` returning the path. It hands the client an absolute
directory so the client can construct what the server could have constructed — which is how two answers
to "where does a commander live" come to exist.
Also rejected: a `commandHq: true` flag on the initial chat message, mirroring the web composer.
Reasonable, and the reason not to is specific — mobile spawns chats over a websocket with its own
parameter set, so the flag would have to be carried correctly by two independent clients, and one that
gets it wrong silently creates an ordinary chat in the wrong directory. A POST is one endpoint, one
code path, and testable with curl.
## Status of the mobile app
`apps/commandhq` exists, typechecks, and is committed — categories, the commander grid with liveness,
rename, move, purge, and the conversation as its detail. It has never made a request: the routes are
not reachable until the server restarts. Nothing has been built or published.
The first real run is the one that finds where this and the router actually disagree.