commanders: a commander is a directory, not a session

Implements phase 1 of docs/commander-directory.md. The row owns its own identity
and its own directory; session_id becomes a nullable pointer to whichever
conversation is current.

The ordering was the bug. POST /command-hq/commanders took a sessionId the caller
already held, so a chat existed first and was filed as a commander second — and
the working directory is fixed when that session spawns, before any id exists. It
now takes {name, referencePath}, creates the row, its directory and its CLAUDE.md,
and returns the cwd to start the chat in. Nothing waits on the CLI.

commander-block.ts is deleted. Claude Code reads a CLAUDE.md from the working
directory, so what it injected per turn is written once at creation and the
commander extends it as the job is agreed. resolveChatCwd no longer trusts
msg.cwd at all: it looks the row up server-side. The containment reasoning that
made a client-chosen path safe still holds — it just no longer applies, because
the client has nothing left to choose.

Four things found in review, none of which the build had.

Ownership. confineUserTree chowns commanders/ once, at provisioning, so anything
created afterwards belongs to the service user — a MEMBER's commander directory
would be one their own claude cannot write into, failing as an agent that dies on
its first command in a directory it can see. Invisible because it works perfectly
for the owner, who has no osUser. Handed over on creation, best effort, and the
error is returned as well as logged.

getCommanderDir is guarded. Two callers rm -rf what it returns, and createCommander
inserts dir_name as '' before fixing it up from the serial id — an empty name
resolves to the commanders ROOT and would take every commander the account has.
The transaction closes that window; the guard means it does not have to.

The fallback in resolveChatCwd was silent. A commander whose row is not found runs
in the account's home with no CLAUDE.md, which reads as an agent that forgot its
job rather than a lookup that missed. It says so now.

And sv-SE is renamed alongside the other five locales. It did not exist when this
work was written.

Verified: SQL applied against a scratch database first — idempotent, several
commanders can hold a null pointer at once, one-per-transcript still enforced.
Then here: db:push emits no DDL and does not prompt, officer restarts clean, and
POST /commanders returns a real directory holding state/ and a CLAUDE.md.
1428 pass, tsgo clean.

The single existing commander was deleted rather than migrated, per the design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-24 02:10:37 +00:00
co-authored by Claude Opus 5
parent 75d5a795a2
commit b6d6de7e53
25 changed files with 870 additions and 674 deletions
+11
View File
@@ -3,6 +3,17 @@
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.
**Superseded in part, 2026-08-24, by `docs/commander-directory.md`.** A commander's identity moved from
Claude's transcript id to its own `command_hq_sessions` row, and it gained its own directory
(`$DATA_PATH/<email>/commanders/<dirName>/`, `CLAUDE.md` + `state/`) created BEFORE any turn spawns. That
makes several sections below describe the PREVIOUS mechanism rather than the current one — notably
"How a commander is created" (creation is no longer session-first), "Working directory" (a commander no
longer shares one directory per user, nor chooses an arbitrary one — it has its own), and the `POST
/commanders` / `POST /commanders/new` rows in "API reference" (both now require `name` and `referencePath`
and no longer accept an arbitrary `cwd`). This file was not rewritten to match — read
`docs/commander-directory.md` first for anything those sections describe, and treat the code
(`src/servers/api/command-hq/`) as the source of truth where the two disagree.
**Status:** data model, the full REST API including both creation paths (web and `POST
/commanders/new`), built and mounted 2026-08-18 (`bunx tsgo` clean, suite at its 7 pre-existing baseline
failures — none of them this feature's). `bun db:push` and a restart of `officer` have both since run