Files
platform/docs/commander-directory.md
T
pastilhasandClaude Opus 5 27d85f934c design: a commander is a directory, not a session
Its 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 on disk. Three problems fall out of that.

The working directory is fixed when the session spawns, before the id exists, so
there is no point in the current flow where a commander can start in a directory
named after itself. The first-turn interlock is a side effect being used as a
signal — commanderBlock cannot resolve its paths on turn one, and the skill turns
that emptiness into "you must be on your first turn". And losing the conversation
loses the commander, because its state lives in the transcript and in a second
tree keyed on the same borrowed id.

So the platform owns the id, creates the directory first, writes CLAUDE.md into
it, and spawns the session there. The transcript id becomes a pointer on the row
rather than the thing the commander IS. Nothing waits on the CLI, so the ordering
problem stops existing.

Two things it buys beyond that. Claude Code reads a CLAUDE.md from the working
directory, so the living document needs no mechanism at all and commanderBlock's
per-turn injection goes away. And a commander survives its conversation: a dead
session can be replaced in the same directory, with its scripts, procedures and
record of what it has handled intact.

The API does not change. The server just gets to do things in an order that works.

Flags the consequence nobody would notice until it bit: the skill's "turn one is
orientation, read where you are" assumes cwd is the target. Under this design the
commander's own directory is nearly empty and the interesting thing is elsewhere,
so that guidance moves with the code or every new commander orients on nothing.

One commander exists on this machine and it is not armed, so the migration is
cheap now and gets dearer with each one started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-24 00:33:44 +00:00

5.7 KiB

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

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. Existing rows have a transcript id and no directory, so a migration has to create one and write a CLAUDE.md into it for each.

There is one commander on this machine and it is not armed, so the migration is cheap today and gets more expensive with every commander anybody starts.

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.