# 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//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.