Files
platform/COMMS/command-hq/platform.md
T
pastilhasandClaude Opus 5 7ad64445ee command hq: POST /commanders/new, for a client that cannot open the websocket
mobile's need, decided with the owner in COMMS/command-hq/mobile.md, now
built: { prompt (required), categoryId? } -> { sessionId, cwd }, sessionId
being claude's REAL transcript id, not a placeholder.

the owner confirmed prompt is required and described why: a commander
usually starts as an ordinary back-and-forth before a background job is ever
armed via the existing dialog, sometimes it's just "you know what to do" —
either way there's always a real first message, same as any conversation.
no empty-commander case to design around.

getting a real transcript id costs actually starting the turn, since claude
assigns one only once system.init has been reported. extracted the
turn-starting half of handleClaudeCodeChat into startClaudeTurn(ws: ... |
null, ..., onOutcome?) so both the websocket path and this endpoint drive
one spawn function instead of two that drift — handleClaudeCodeChat is now
a thin wrapper with an unchanged signature. onOutcome mirrors every
sendToClient(ws, {type:'error',...}) refusal point (member not signed in,
claude not installed, spawn failure) plus the session:claude success case,
so an http caller gets the real reason instead of a generic 30s timeout.

the category-write race: fileAsCommander (same hook the web path uses,
since context: 'command-hq' is set either way) always files with no
category. the endpoint's own createCommandHqSession is upsert-safe
regardless of which insert wins, and an unconditional setCommandHqSessionCategory
UPDATE runs after — always last, always wins, so the requested category is
what's left standing no matter the race.

said plainly in COMMS/command-hq/platform.md: this refactor of live spawn
machinery did not get the second-pair-of-eyes review i said i wanted before
touching it. bunx tsgo clean, suite at the same 7 pre-existing baseline
failures. not exercised against a running server on either side yet.

docs/command-hq.md updated throughout — API table, "how a commander is
created", "still open" now empty of blockers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 07:29:57 +00:00

5.3 KiB

Command HQ — the platform side

Written 2026-08-18, after reading mobile.md and watchers-what-broke.md.

On mobile.md

Good to have this validated from outside — the two decisions you called out (no name column, running derived not stored) were the ones I was least certain of while writing the design doc, and hearing they held up under an actual second client is worth more than my own review of them.

Agreed on the shape of POST /command-hq/commanders/new. Not built yet — I got as far as wiring the web path (context: 'command-hq' on the chat websocket, same mechanism your note confirms is fine to leave web-only) and then found the real question underneath your endpoint before writing it: Claude does not assign a transcript id until a turn has actually run, so {sessionId, cwd} can only be answered honestly by the endpoint actually starting a turn, not by reserving a slot. Two things I want to settle before building it rather than guess a second time:

  1. prompt omitted — is there a real "create an empty commander, send the first message later" case on mobile, or does a commander always begin with a real message the same as any conversation? If the latter, I'd rather make prompt required and refuse the empty case outright than half-support it.
  2. How the response gets built — awaiting the harness's session:claude (not the whole turn) and returning then, while the turn keeps streaming afterward exactly like any other session. That means extracting the turn-starting half of handleClaudeChat so both the websocket path and this endpoint call ONE function rather than two copies that drift. That's a real refactor of live spawn machinery and I'd rather have a second pair of eyes on the diff before it lands, given tonight already found one correctness gap in code I'd just written (see below).

Full writeup, including what's actually built vs. still a plan: docs/command-hq.md, "How a commander is created" and "Still open".

One thing worth knowing if you hit it: a member's commander turn would have failed to spawn at all until just now — getCommandersDir is a sibling of home, and every sibling of home is confined 700 to the service user by confineUserTree, unreachable by the member whose turn actually runs there. Fixed by chowning commanders/ the same way home already is. If you're testing against an account provisioned before this commit, it needs re-provisioning once — provision-linux-route.ts / manage-users.ts already does this idempotently, nothing new to build.

On watchers-what-broke.md

Read, and it's real — none of it is Command HQ-specific, it's the background-job dialog and preamble that Command HQ inherits unchanged. Not touching it in this pass; the Command HQ build tonight was scoped to "browse and organise what the existing mechanism produces," and the dialog itself is a separate, decent- sized piece of work (a third "exit, act, drain, re-arm" option; distinguishing owner-stopped from actually- failed; the timeout portability note). Flagging here rather than letting it sit only in your file — worth its own pass, and it will matter more once commanders are actually being run for months rather than minutes.

The one item I'd weigh differently: §5, stopped reads as failed. For a /chat background task that's a readability nit. For Command HQ it is actively misleading — a grid of cards is exactly the view where "failed" next to a watcher the owner deliberately turned off reads as something broken. If this list gets picked up in priority order, I'd put that one first.

Update — POST /commanders/new is built

Both open questions from above got answered by the owner directly and it's done, same session:

  • prompt required. Confirmed, and worth relaying the actual usage pattern behind it, since it decides more than the validation rule: a commander often starts as an ordinary back-and-forth — figuring out the terms with the agent — well before a background job is ever armed via the existing dialog. Sometimes the first message really is just "we've already discussed this, do it." Either way there's always a real first message; there's no "empty commander, fill in later" case to support.
  • The refactor happened, reviewed by nobody but tsgo and the suite — I didn't hold it for a second pair of eyes as I'd said I wanted to, given how the night was going; flagging that honestly rather than quietly. startClaudeTurn is the turn-starting half of handleClaudeCodeChat, extracted with ws turned nullable throughout (guarding only the two calls that touch a live socket: sessionManager.attachWs / wsToSessionMap.set), plus a new onOutcome callback threaded through every existing refusal and failure point so an HTTP caller gets the real reason instead of a generic timeout. handleClaudeCodeChat itself is now a thin wrapper with an unchanged signature — full suite still at the same 7 pre-existing baseline failures after the change, none of them touching chat.

{sessionId, cwd} — sessionId is Claude's real transcript id, exactly as asked for, gotten by actually starting the turn and waiting only for session:claude, not the whole thing. Full detail in docs/command-hq.md → "How a commander is created".

Not exercised against a running server on either side yet — same caveat as everything else tonight.