Commit Graph
4 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 2aef23c8ba a commander starts its own conversation, instead of being handed one
Starting a commander meant writing a good opening prompt, which is the hard part
of the whole thing — so in practice somebody else wrote it and pasted it in. That
is not a flow, it is a person acting as the intake form.

Now the two decisions that cannot be guessed stay with the owner (a name and a
working directory) and the rest is a button. It sends a brief the MODEL reads —
"I have not told you what I want yet, orient yourself and ask me one question" —
while the transcript shows "Set me up.", which is what a person would have typed.
That split is `effectivePrompt` vs `displayText` and the chat path already had it.

What to ask, in what order, and to read the directory BEFORE asking anything, all
live in the Commanders skill under "When you started yourself". Deliberately not
here: capabilities are re-read from disk every run, so rewording a question costs
nothing, while the same words in commander-block.ts cost a restart each time. The
block carries a pointer and no doctrine.

Orienting first is what makes this work on a real request. Told "automate the
platform deploys", a commander that has read the directory finds `deploy/`, the
previous releases and the deploy skill, and asks whether that is still how you
want it — instead of inventing a scheme next to the one already there.

`sendPrompt` gained an options object rather than a tenth positional parameter.
Nine is already past where a call site reads as a row of undefineds.

The composer stays enabled beside the button: typing your own opener still works,
it just is not the only way in any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:04:25 +00:00
pastilhasandClaude Opus 5 75affec091 a commander is identified by its row, not by where it lives
Groundwork for choosing a commander's working directory. Identity was the CWD — every commander shared one
directory, so the directory was the answer and it needed no query. The moment a commander can choose its own,
that test is wrong in BOTH directions: a commander in the platform checkout reads as an ordinary chat, and an
ordinary chat in the old shared directory reads as a commander.

So identity is now the `command_hq_sessions` row, which is the only thing that survives the cwd being free.
The isolation the shared directory provided is no longer carrying anything — the surfaces that needed it now
filter explicitly, which is what made this possible.

Four surfaces moved, and the shape of the change is the same in each: the transcript scanner stops inferring
it and the ROUTE decorates from the rows.

- `listClaudeSessions` says nothing about commanders any more. It is synchronous over a directory scan and
  identity now needs a database read, so `withCommanderFlag` applies it at the caller.
- the sessions list, `/chat/live` and `GET /chat/sessions/:id` each read `listCommandHqSessionIds` once per
  request. A set membership test per row rather than a lookup per row: commanders are few, and the
  alternative turns one list into fifty queries.
- `commanderBlock` resolves the row instead of parsing the account out of the path. It needs the email for
  the record paths, so that is a second read — once per session spawn, beside the affordances and the skills
  index, which are read there for the same reason.

`commanderBlock` had to be hoisted out of `createSession`, which is synchronous, into `spawnClaudeStreaming`,
which is async and already holds the account. Passing it in as a parameter rather than making the whole
session constructor async.

`isCommandersDir` survives but is documented as no longer being the identity test — it remains only for the
places that legitimately care about that specific path: creating it for a new OS account, and as the
historical home of commanders created before this.

A commander mid-first-turn has no row and so reports false for one turn. That is the same turn it must not
arm anything on, and it corrects itself the moment it is filed.

Verified against real data: a filed commander renders the block, an unknown id and a null id both return
null, and the live gate still answers 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 18:07:00 +00:00
pastilhasandClaude Opus 5 cabc4825ef a commander never arms on its first turn, and the missing paths are how it knows
The first-turn gap is closed by a rule rather than a mechanism, and the two halves interlock instead of
conflicting.

A commander cannot be told its record paths on the very first turn: the transcript id is minted as that turn
begins, so there is no id to key a path on. That was written up as a limitation. It is not one, because
arming on the first turn is not permissible anyway — the first exchange is for agreeing what to watch and
what to do about it, and a watcher armed before that agreement is armed on a guess. A wrong condition fails
silently in both directions: never fires, or fires constantly, discovered days later.

So the absence became the signal, and the block says so: "If you were given no paths above, you ARE on your
first turn: that is what their absence means." An agent with no paths is on the turn it must not arm on, and
an agent that armed anyway would be unstoppable and undeletable — the same rule reached from the other side.

The rule is in both places on purpose. The block, always loaded, carries the instruction and the interlock in
four lines. The Commanders skill carries why, at length, for the agent that is about to arm one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:54:47 +00:00
pastilhasandClaude Opus 5 87ff6ed326 a commander is told where to record itself, however its loop gets started
The record instruction lived in one place: the preamble the background-job dialog composes. Fine while the
dialog was the only way to arm a commander, and wrong for the flow being built — the commander arming itself
from the conversation, with the owner never touching the launcher. An agent starting its own background job
never sees that preamble, so it would record nothing, and delete would quietly go back to leaving orphaned
loops.

So the paths are compiled into a commander's session preamble, beside the affordances and the skills index.

THE SPLIT, which is the part worth keeping: the METHOD lives in the Commanders skill — read on demand, as
long as it needs to be — and the PATHS are compiled in always, because they are a fact about this session
that the agent cannot derive. That is exactly the line between a skill and an always-loaded block, and it
falls in the same place here as everywhere else.

The account is derived from the CWD rather than passed in, because the caller does not have it: the sidecar
knows a member's osUser and home, and an owner's turn carries no email at all. A commander's cwd is
DATA_PATH/<email>/commanders, so the email is in the path — and it is verified by reconstructing the
directory from it, so a coincidentally-shaped path cannot pass.

Keyed on the cwd rather than a database lookup, for the same reason every other surface uses
isCommandersDir: a session must not be a commander in one place and not another. It also works before the
row exists, which a lookup would not — and the first turn is when the row does not exist yet.

Stated in the file rather than left to be found: a brand-new commander's FIRST turn gets no block, because
the transcript id does not exist until that turn begins. In practice the first turn is the conversation about
what to watch and the loop starts on a later one, but a commander that armed itself on its opening message
would not have been told where to record.

Verified on the three cases: a real commanders cwd renders the block with both paths, an ordinary chat gets
null, and a session with no id yet gets null.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:47:55 +00:00