Framed this as a credential-safety rule; it is a boundary. A commander does not hold a Whisper URL, does not name one and has no parameter for it — it knows the platform can transcribe and asks the platform. So there is usually nothing sensitive in a commander to leak, and sharing is simple for a reason rather than by luck. The credential rule survives for the case where a commander genuinely needs a secret of its own. Also records a scope decision. Whether the platform is configured at all is a system administration problem, not something to solve with a capability registry — and the commander skill already covers it: do the first run by hand, then arm. An unconfigured endpoint fails in front of the person doing it, and the same rule catches somebody forking the commander onto a machine that lacks it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
171 lines
9.9 KiB
Markdown
171 lines
9.9 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.
|
|
|
|
## What the directory holds: definition versus state
|
|
|
|
Two kinds of thing live in there and they must not mix, because one of them travels and the other must
|
|
never.
|
|
|
|
```
|
|
$DATA_PATH/<email>/commanders/<commander id>/
|
|
CLAUDE.md DEFINITION — the living document, extended as the job is agreed
|
|
parameters.* DEFINITION — the things a fork has to change. Paths, cadences, thresholds
|
|
procedures/ DEFINITION — what a spawned agent reads at fire time
|
|
scripts/ DEFINITION — the loops
|
|
state/ INSTANCE — what it has handled, what it spawned, its loop's pid
|
|
```
|
|
|
|
**Separate from the first commit, not sorted out later.** A shareable bundle is "everything except
|
|
`state/`" only if that boundary is structural; mixed together, extracting one means picking files apart by
|
|
hand and getting it wrong once. Same reasoning as plugin capabilities being read in place rather than
|
|
copied — a directory boundary beats a convention, because a convention is what a tired person violates.
|
|
|
|
The failure it prevents is specific: a recipient's commander starting up believing it has already handled
|
|
your recordings, and silently doing nothing.
|
|
|
|
**Parameters are separate from procedure, or forking means editing code.** With `~/Documents/VoiceMemos`
|
|
baked into a script, a fork is a debugging session. Declared in one place, a fork is a conversation — the
|
|
new owner is asked where their memos land and the commander rewrites its own file. That difference is the
|
|
whole point of the thing being an agent rather than a template.
|
|
|
|
## Phase 2 — a commander becomes a git repository
|
|
|
|
**Not now.** Recorded because the phase 1 layout has to leave room for it, and it costs nothing today to
|
|
leave that room.
|
|
|
|
**There is no button.** The owner asks the commander, in conversation, to make itself shareable, and it
|
|
does — `git init`, a `.gitignore` that excludes `state/`, a first commit. That is the same shape as
|
|
everything else here: the commander maintains its own directory, and the platform does not reach in.
|
|
|
|
**What it needs to know goes in the commander SKILL, not an affordance.** The layout, what is tracked,
|
|
what is ignored and how credentials are handled is reference material — read on the one day somebody asks
|
|
for it. An affordance's body is charged to every turn of every commander forever, and this would earn that
|
|
cost approximately never. `capabilities/skills/commander/SKILL.md` gains a section.
|
|
|
|
**A commander never holds a service's connection details, and that is a boundary rather than a
|
|
precaution.** The Whisper URL is not the commander's business: it knows the platform can transcribe and
|
|
asks the platform, which holds the connection. So there is usually nothing sensitive in a commander to
|
|
leak, and the sharing story is simple for a reason rather than by luck.
|
|
|
|
Where a commander genuinely does need a secret of its own, the same rule applies for the obvious reason: a
|
|
credential is definition-shaped — it looks like configuration — so excluding `state/` does not protect it,
|
|
and a shared repository is exactly the artefact where one gets published and stays published. Name what
|
|
must be configured; let the platform hold the value.
|
|
|
|
**Whether the platform is actually configured is not this system's problem.** A commander depending on
|
|
transcription can be armed on a server where nobody set up a Whisper endpoint, and it would produce
|
|
nothing. That is a system administration mistake, and the answer is already in the commander skill rather
|
|
than in new machinery: **do the first run by hand, then arm.** A first run against something unconfigured
|
|
fails in front of the person doing it. The same rule catches the shared case — somebody forking this onto
|
|
their own machine finds out on their first run instead of at 3am.
|
|
|
|
The forking story then reads: clone, open a conversation, answer where your memos land and what you want
|
|
done with them, and the commander rewrites its own parameters and arms.
|
|
|
|
## 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.
|