The one commander here is unarmed and has never fired, so a migration would be a code path that runs once and never again. Deleting it and re-arming on the new system also exercises the flow the design exists to fix, which a migration would route around. Notes the shelf life: free while nothing depends on a commander, not free after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
110 lines
6.0 KiB
Markdown
110 lines
6.0 KiB
Markdown
# A commander is a directory, not a session
|
|
|
|
**Status: designed, not built.**
|
|
|
|
## What is wrong now
|
|
|
|
A commander's identity is borrowed. `command_hq_sessions.session_id` is Claude's **transcript id**, minted
|
|
by the CLI as the first turn begins — verified: the id on the live row has a transcript file under
|
|
`~/.claude/projects/`. Everything about a commander hangs off that one borrowed value, and three problems
|
|
fall out of it.
|
|
|
|
**An ordering problem.** `POST /command-hq/commanders` takes a `sessionId` the caller already holds and
|
|
validates the transcript exists, so a chat session is created first and filed as a commander second. But
|
|
the working directory is fixed when the session spawns — before the id exists. There is no point in the
|
|
current flow where a commander can be started *in a directory named after itself*.
|
|
|
|
**A first-turn interlock that is a side effect.** `commanderBlock` compiles the paths a commander must
|
|
record itself at into every turn, and on the first turn they cannot be resolved, so the block is empty.
|
|
The commander skill turns that into a rule: *"if you have not been given paths, you are on your first
|
|
turn."* It works, and it is a signal derived from a failure rather than something anybody said. That
|
|
block's own comment explains the awkwardness — it rides `effectivePrompt` rather than the session
|
|
preamble, because the preamble is built once and the first turn has no id to build it from.
|
|
|
|
**Losing the conversation loses the commander.** State lives in the transcript and in a second tree,
|
|
`commander-state/<transcript id>/`. A session that dies takes its commander with it.
|
|
|
|
## The design
|
|
|
|
**The directory IS the commander. The session is how you talk to it.**
|
|
|
|
```
|
|
$DATA_PATH/<email>/commanders/<commander id>/
|
|
CLAUDE.md written at creation, extended by the commander as it learns the job
|
|
spawned session ids of the agents its loop started
|
|
<whatever it writes> scripts, procedures, its record of what it has handled
|
|
```
|
|
|
|
The id is one the **platform** owns, generated when the commander is created. Nothing waits on the CLI.
|
|
|
|
The flow becomes linear:
|
|
|
|
1. The user gives a name and a reference path, and clicks Start.
|
|
2. The platform creates the row and the directory.
|
|
3. It writes `CLAUDE.md` into the directory: the name, the reference path, and the standing guidance
|
|
`commanderBlock` currently injects.
|
|
4. It spawns the session with `cwd` set to that directory.
|
|
5. The transcript id arrives later and is recorded **on the row**, as a pointer to the current
|
|
conversation.
|
|
|
|
**The API does not change.** Same endpoints, same shape from the client's side. What changes is the server
|
|
being able to do things in an order that works.
|
|
|
|
## What this buys
|
|
|
|
**`CLAUDE.md` needs no mechanism.** Claude Code reads one from the working directory at session start. So
|
|
the living document is free: the commander edits its own file, and the next turn has it. No injection, no
|
|
per-turn cost, no block riding `effectivePrompt`.
|
|
|
|
**A commander survives its conversation.** Everything it knows is on disk. A session that dies can be
|
|
replaced by a fresh one in the same directory, and the commander carries on with its scripts, its
|
|
procedures and its record of what it has already handled. Today that is a total loss.
|
|
|
|
**One directory instead of two.** `commander-state/<id>/` collapses in. Deleting a commander becomes
|
|
deleting a directory, which is also the only way to be sure nothing is left behind.
|
|
|
|
**The first turn can be stated rather than inferred.** With the file written before the session exists,
|
|
the platform can simply say which turn it is on. The interlock stops being a trick.
|
|
|
|
## The reference path
|
|
|
|
**What was the working directory becomes a reference** — what the commander operates ON, not where it
|
|
lives. One is collected in the start dialog, because a person can name one thing and asking for a list up
|
|
front gets a worse answer than asking later.
|
|
|
|
**More can be added in conversation.** A voice-memos commander watches one folder; a life manager might
|
|
watch memos, scans and a mailbox. So this is a list on the row, seeded with one, and the commander writes
|
|
additions into its own `CLAUDE.md` as they are agreed.
|
|
|
|
## The consequence for the skill
|
|
|
|
`capabilities/skills/commander/SKILL.md` says turn one is orientation — *"read where you actually are
|
|
before you ask anything"* — and that assumes `cwd` is the thing worth exploring. Under this design the
|
|
commander's own directory is nearly empty on turn one and the interesting thing is somewhere else.
|
|
|
|
That guidance has to change with the code, or every new commander will orient on an empty folder and ask
|
|
a worse question than it does today.
|
|
|
|
## The schema change, and why there is no migration
|
|
|
|
`session_id` stops being the commander's identity and becomes a nullable pointer to its current
|
|
conversation. The row gains its own id, its directory, and its reference paths.
|
|
|
|
**Nothing is migrated.** The one commander on this machine is deleted and re-armed from scratch on the new
|
|
system — the owner's call, made on 2026-08-24, and the right one: it is unarmed and has never fired, so
|
|
migrating it would mean writing and testing a path that runs exactly once and then never again. Re-arming
|
|
also exercises the flow this design exists to fix, which a migration would route around.
|
|
|
|
That decision has a shelf life. It is free while one commander exists and unarmed; it stops being free the
|
|
moment somebody is relying on one. Build this before that happens, or write the migration.
|
|
|
|
## What this does not decide
|
|
|
|
**Whether a commander's loops are scripted or agentic.** Both, in practice — a watcher that transcribes
|
|
does not need judgement and should not spend a turn per file, while a job that reads transcripts and
|
|
decides what to do with each of them is agentic by nature. The directory holds the procedure either way.
|
|
|
|
**How a commander learns what the platform can do.** It reads the generated platform skill and, for a
|
|
plugin, that plugin's own skill. That is a separate gap: a plugin must ship a skill documenting its routes
|
|
or a commander can see it is installed and still not know how to call it.
|