The two kinds of work in a commander have opposite economics. A script runs per event and costs nothing, so volume has no ceiling. An agent runs per batch on material a script assembled, and costs a turn each time. So the rule for building any commander is to push as much as possible into the script and let the agent see only what needs judgement. The tiers are not fixed, and that is the part that matters. The commander owns its scripts — they are files in its own directory — so every correction moves work downward: "this one was missed, it had eight digits" turns a judgement into a rule. The agent tier thins as the free tier improves. A commander that runs for six months should cost less per event than it did in week one, not more. That is the opposite of how automation usually ages, and it follows from two decisions rather than from hope: the agent owns its scripts, and corrections arrive through dialogue rather than a ticket to somebody else. Nothing improves a system it cannot edit. Records the two requirements that make it safe. A verdict is reproduced rather than logged, because the input is still on disk and the script is deterministic. And a change is re-run over the existing corpus before it ships, or the fix for eight-digit codes quietly starts flagging every order confirmation, unattended, at three in the morning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
219 lines
13 KiB
Markdown
219 lines
13 KiB
Markdown
# A commander is a directory, not a session
|
|
|
|
**Status: phase 1 built 2026-08-24** — schema, directory creation, `CLAUDE.md` at creation, the inverted
|
|
`POST /commanders` → start-turn → point-at-transcript order, `resolveChatCwd` resolving server-side, and the
|
|
frontend composer split (`NewCommanderSetup` → `ActiveNewCommander`) in `src/servers/api/command-hq/` and
|
|
`src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx`. `bunx tsgo`/`./node_modules/.bin/tsgo`
|
|
clean, suite green. **`bun db:push` has not been run** — the owner reviews and runs the database work
|
|
separately; see the schema section below for what changed and why nothing here is a migration. Phase 2
|
|
(a commander becomes a git repository) is still 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 two tiers, and why a commander should get cheaper
|
|
|
|
This is the point of the whole design, and it generalises past any particular commander.
|
|
|
|
**The script gathers. The agent judges.**
|
|
|
|
A commander has two kinds of work and they have opposite economics. A script runs per event — every email,
|
|
every voice memo — and costs nothing, so there is no ceiling on volume. An agent runs per batch, on
|
|
material a script already assembled, and costs a turn every time it wakes. So the rule for building any
|
|
commander is: push as much as possible down into the script, and let the agent see only what actually
|
|
needs judgement.
|
|
|
|
Detecting a one-time code in an email is a script. Reading fifty messages and deciding which of them the
|
|
owner would want to know about is an agent. A watcher that transcribes a recording is a script; deciding
|
|
what a week of transcripts means is an agent.
|
|
|
|
**And the tiers are not fixed, which is the part that matters.**
|
|
|
|
Because the commander owns its own scripts — they are files in its directory, and it can edit them — every
|
|
correction moves work downward. The owner says "this one was missed, it had eight digits rather than six",
|
|
and a decision that required judgement becomes a rule that does not. The agent tier gets thinner; the free
|
|
tier gets better.
|
|
|
|
**A commander that runs for six months should cost less per event than it did in week one, not more.**
|
|
|
|
That is the opposite of how automation usually ages, and it is a direct consequence of two decisions made
|
|
here rather than a hope: the agent owns its scripts, and the corrections arrive through dialogue rather
|
|
than through a ticket to somebody else. Nothing improves a system it cannot edit.
|
|
|
|
Two things this requires, and both are cheap if done from the start:
|
|
|
|
- **The script's verdict must be reproducible.** Not logged — RE-RUN. The input is still in the mailbox or
|
|
the folder, and the script is deterministic, so diagnosing a miss is running it again and watching. A
|
|
log would be a second copy of something already on disk.
|
|
- **A change is checked against what came before.** The corpus already exists — the last few hundred
|
|
messages, the transcripts so far. Re-run the old script and the new one over it and compare; anything
|
|
whose verdict changed is either the bug just fixed or one just introduced. Without that, the fix for
|
|
eight-digit codes quietly starts flagging every order confirmation, unattended, at three in the morning.
|
|
|
|
`capabilities/skills/commander/SKILL.md` should teach this. Somebody building the fourth commander needs
|
|
the rule more than they need the worked example.
|
|
|
|
## 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.
|