command hq: the api, mounted and documented
full REST surface for categories, tags and commanders. core permission (personal: ['/'], no grant to hold — every query is scoped to the caller's own user_id at the sql layer regardless). registered in the permission registry and PROTECTED_MOUNTS together, so totality holds by construction. GET /commanders is the grid endpoint: everything /chat/sessions already returns per session, plus categoryId/tagIds/running. `running` is derived live off collectRunningTasks (now exported from websocket.ts) folded over chat_session_events — not a stored column, on purpose, matching the design doc's stance that a status field would go stale exactly when a sidecar restart silently kills a watcher. GET /commanders/:sessionId deliberately does NOT duplicate the transcript — that stays GET /chat/sessions/:id, unchanged. a client calls both. /chat/sessions now excludes anything filed as a commander (listCommandHqSessionIds, .catch()'d so a hiccup on this feature can't take the whole chat list down) — the other half of the one predicate this whole design turns on. there is deliberately no POST /commanders/new: creation is an ordinary chat turn in the commanders directory, which is the next commit. docs/command-hq.md gains an API reference table for the mobile dev, and a status line at the top saying plainly what is real and what is not yet: needs `bun db:push` before any route returns real data, frontend not started. bunx tsgo clean. suite at baseline (same 7 pre-existing failures, all registry/totality drift and the pty socket test — none touch this router). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+49
-1
@@ -1,7 +1,55 @@
|
||||
# Command HQ — design record
|
||||
|
||||
Spec conversation 2026-08-18, owner + agent, back and forth. This is the write-up asked for partway through
|
||||
it, kept current as answers landed. **Not implemented yet** — this is the plan, not the code.
|
||||
it, kept current as answers landed.
|
||||
|
||||
**Status:** data model and REST API built and mounted 2026-08-18 (`bunx tsgo` clean, suite at its 7
|
||||
pre-existing baseline failures — none of them this feature's). **Needs `bun db:push`** before any route
|
||||
answers with real data; without it every route throws a Postgres "relation does not exist" 500. **Frontend
|
||||
not built yet.** Every claim below about what exists is checked against the code, not aspirational — see
|
||||
"API reference" for what a client can call today.
|
||||
|
||||
## API reference — build against this
|
||||
|
||||
Base path `/command-hq`, `core` permission (every signed-in account, no grant to hold), every query scoped
|
||||
to the caller's own `user_id` regardless. Router: `src/servers/api/command-hq/router.ts` — read it directly
|
||||
for exact request/response shapes; the table below is the map, not the full contract.
|
||||
|
||||
| Method | Path | Body | Notes |
|
||||
| ------ | ------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| GET | `/categories` | — | `{ categories: [{id,name,createdAt}] }` |
|
||||
| POST | `/categories` | `{name}` | 409 on a duplicate name for this user |
|
||||
| PATCH | `/categories/:id` | `{name}` | rename |
|
||||
| DELETE | `/categories/:id` | — | uncategorises its commanders, does not delete them |
|
||||
| GET | `/tags` | — | schema exists; no UI planned for v1 |
|
||||
| POST | `/tags` | `{name}` | |
|
||||
| DELETE | `/tags/:id` | — | |
|
||||
| GET | `/commanders[?category=<id>\|none]` | — | omitted = all, `none` = uncategorised bucket. See below |
|
||||
| GET | `/commanders/:sessionId` | — | Command HQ's own metadata only — NOT the transcript, see below |
|
||||
| POST | `/commanders` | `{sessionId, categoryId?}` | files an existing session; idempotent; 404 if the session isn't in the commanders directory |
|
||||
| PATCH | `/commanders/:sessionId` | `{categoryId?, name?}` | `name` proxies to `renameClaudeSession` — the same rename `/chat` uses |
|
||||
| POST | `/purge` | — | sweeps rows whose transcript no longer resolves; returns `{removed}` |
|
||||
| POST | `/commanders/:sessionId/tags` | `{tagId}` | |
|
||||
| DELETE | `/commanders/:sessionId/tags/:tagId` | — | |
|
||||
|
||||
**A commander's conversation is `GET /chat/sessions/:id`, unchanged.** This router deliberately does not
|
||||
duplicate it — `GET /commanders/:sessionId` returns only `{sessionId, categoryId, tagIds, running,
|
||||
createdAt}`. A client wanting the full picture calls both.
|
||||
|
||||
**`GET /commanders` is the grid endpoint** and returns the richest shape: every field `/chat/sessions`
|
||||
already returns per session (`id`, `title`, `cwd`, `createdAt`, `updatedAt`, `messageCount`, …) plus
|
||||
`categoryId`, `tagIds`, and `running` — a boolean, derived live from the same `task:started`/
|
||||
`task:notification` fold `/chat`'s own background-job tray uses, not a stored value. This is the field to
|
||||
poll if a client wants to know "did my watcher die" without opening it.
|
||||
|
||||
**A row whose transcript is gone is silently absent from every response.** Never a tombstone, never a 404
|
||||
with a special shape — it simply is not in the list. See "The /chat ↔ /command-hq relationship" below for
|
||||
why that is the design rather than an oversight.
|
||||
|
||||
**There is no "create a new commander" endpoint**, and that is deliberate, not a gap — see "How a commander
|
||||
is created". A commander begins as an ordinary chat turn over the existing `/chat` websocket, in the
|
||||
commanders directory, and the row is written server-side the moment a real transcript id exists. `POST
|
||||
/commanders` above is the low-level primitive that write uses, not the thing a composer calls directly.
|
||||
|
||||
## What it is
|
||||
|
||||
|
||||
Reference in New Issue
Block a user