diff --git a/CLAUDE.md b/CLAUDE.md index 3b56e1e0..40f20f09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -537,6 +537,10 @@ explaining why it was safe. - `docs/editor-buffer-corruption.md` — an open bug with a trap set for it: background-job output appearing inside a file open in the editor. Read it BEFORE investigating a report of that, and before re-testing anything it already rules out +- `docs/commander-directory.md` — **designed, not built**: a commander is a directory, not a session. Why + its identity is currently borrowed from Claude's transcript id and what that costs — an ordering problem, + a first-turn interlock that is a side effect rather than a signal, and a commander that dies with its + conversation. The CLAUDE.md in its own working directory is the living document, for free - `docs/plugin-capabilities.md` — **designed, not built**: a plugin's tasks, skills and affordances are READ where they live, never copied into `capabilities/`. Why copying breaks a forkable repository, why it goes stale on update and survives a disable, and why separate directories beat namespaced paths — diff --git a/docs/commander-directory.md b/docs/commander-directory.md new file mode 100644 index 00000000..8c308cd5 --- /dev/null +++ b/docs/commander-directory.md @@ -0,0 +1,105 @@ +# 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//`. 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//commanders// + CLAUDE.md written at creation, extended by the commander as it learns the job + spawned session ids of the agents its loop started + 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//` 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.