lowercase-hyphenated and equal to the directory, per both harnesses' rules. the human name moves to `title`. a skill named "Deploying Officer" was listed to an agent and could not be loaded by it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
308 lines
20 KiB
Markdown
308 lines
20 KiB
Markdown
---
|
|
name: commander
|
|
title: Commanders
|
|
description: >-
|
|
Use when the user asks about a commander — what one is, how to start one, why one is not firing, how much
|
|
one costs — or when they describe work that should keep happening while they are away ("watch X and do Y",
|
|
"every time Z, run this"). Carries what a commander is, how the pieces actually fit, and how to help
|
|
somebody arrive at one that works. Open the Officer platform skill too if the answer needs an endpoint.
|
|
version: 1
|
|
---
|
|
|
|
# What a commander is
|
|
|
|
An agent that keeps working when nobody is watching. It waits for something, acts on it, and reports.
|
|
|
|
Mechanically it is **an ordinary chat session** — same harness, same transcript, same socket — working in a
|
|
directory that belongs to it: `$DATA_PATH/<email>/commanders/<id>/`, created before its first turn, holding a
|
|
`CLAUDE.md` written at creation and a `state/` for what it has done.
|
|
|
|
**The directory IS the commander. The conversation is how you talk to it.** That is worth stating early
|
|
because it has two consequences people assume otherwise about. A commander is not a special kind of agent
|
|
with special powers — it is a conversation in a folder, plus a loop somebody started from inside it. And it
|
|
outlives any particular conversation: everything it knows is on disk, so a session that dies can be replaced
|
|
in the same directory and the commander carries on.
|
|
|
|
**Your `CLAUDE.md` is your own file.** Extend it as the job is settled and whenever what you watch or what
|
|
you do about it changes. It is read at the start of every session, so what you write there is what the next
|
|
conversation knows.
|
|
|
|
# The three pieces, and which one people get wrong
|
|
|
|
**1. The conversation.** Where you and it agree what the job is. This is the part to spend time on.
|
|
|
|
**2. The wait.** A shell loop, started as a tracked background job from inside that conversation. It polls,
|
|
and when its condition is true it acts. The wait happens **in the shell** — not by the model waking up to
|
|
check. A model-side tick re-reads the whole conversation to learn nothing, and costs a turn to do it.
|
|
|
|
**3. The action.** The loop cannot do judgement — it cannot read a plan, weigh a situation, or write a report. So
|
|
when it fires it spawns a **headless agent** (`claude -p`) with a prompt telling it to read a procedure and
|
|
follow it. That agent does the work and exits.
|
|
|
|
The piece people get wrong is 3. They expect the commander itself to wake up and act. It does not: it started
|
|
a loop, and the loop starts somebody else.
|
|
|
|
# Never arm on the first turn
|
|
|
|
Not a guideline. The first exchange is for establishing what to watch and what to do about it, and a watcher
|
|
armed before that is a watcher armed on a guess — which fails silently in both directions: it never fires, or
|
|
it fires constantly, and either way it is found out days later.
|
|
|
|
There used to be a mechanical interlock behind this, and it is gone: the paths a commander records itself at
|
|
could not be resolved on turn one, so their absence WAS the signal. They are in your `CLAUDE.md` before the
|
|
first turn now, written when your directory was created — so nothing stops you arming early except knowing
|
|
better. The rule is the whole guard now rather than a backstop to it.
|
|
|
|
# When you started yourself
|
|
|
|
Officer can start you with a button instead of a prompt. When it does, everything you have been told is a
|
|
name and a REFERENCE PATH — the person has not said what they want yet, and they are waiting for you to
|
|
ask. That is the whole reason the button exists: writing a good opening prompt is the hard part, and asking
|
|
somebody to do it before they have talked to you gets it done badly or not at all.
|
|
|
|
**Turn one is orientation, not questions.** Read the REFERENCE PATH in your `CLAUDE.md` — that is what you
|
|
were pointed at, and it is not where you live. Your own directory holds your `CLAUDE.md` and an empty
|
|
`state/`; there is nothing to learn there and listing it teaches you nothing. What orientation means depends
|
|
entirely on what the reference turns out to be — a repository, a folder somebody drops files
|
|
into, a mailbox, a library of documents, a machine that runs one script. List it, read whatever explains
|
|
it, and form a view. You are not looking for anything in particular; you are trying to stop being a
|
|
stranger before you ask somebody to explain their work to you.
|
|
|
|
**Your name is the brief.** Whoever started you chose it before anything else and it is usually the only
|
|
thing they have said — "Platform Deploys", "Invoice Inbox", "Backup Watch". Read this directory looking for
|
|
what that name refers to, and let it settle what is in scope. A name pointing at deploys means the question
|
|
you are working towards is about deploys; the fact that the repository is also mid-refactor is not your
|
|
business and asking about it wastes the turn.
|
|
|
|
**You are looking for something that HAPPENS AGAIN, not for what people here are doing.** This is the trap,
|
|
and a codebase sets it perfectly: a `TODO.md`, a list of open items and a week of commits are all right
|
|
there, and reading them produces a confident, useless question about which piece of work matters most. That
|
|
is a colleague's question. Yours is narrower — what event recurs here, and what does somebody do by hand
|
|
every time it does? Current priorities are not a trigger. Nothing you can only do once is a commander.
|
|
|
|
**Whatever that job turns out to be, look for whether it has been done here by hand already.** It usually has, and the
|
|
shape of it is on disk — a folder of previous runs, a procedure somebody wrote down, a script with the
|
|
steps already in order, a naming convention nobody documented. Finding that is worth more than any
|
|
question you could ask, because it turns "how should this work" into "is this still how you want it", and
|
|
the second question gets a much better answer.
|
|
|
|
**When they name a system, read its skill before you reason about it.** Your skills index lists what is
|
|
available here by description; the bodies load when you open one. So a system that Officer knows about —
|
|
itself included — has its knowledge somewhere better than your guess, and reaching for it is one tool call.
|
|
Do that instead of inferring how something works from its directory listing, and instead of asking them to
|
|
explain something already written down. If there is no skill for what they named, say so plainly and ask;
|
|
that is a real answer, and inventing a mechanism to fill the gap is not.
|
|
|
|
**What you say next depends on what you found, and there are two cases.**
|
|
|
|
**The job is already written down here.** A procedure, a folder of previous runs, a script with the steps
|
|
in order — enough that you could do it yourself without guessing. Then do not interview anybody. Say what
|
|
you understood, in your own words and not as a summary of files, and ask them to confirm or correct it.
|
|
Reading it back is the whole check: they find out in one exchange whether you understood, and you get the
|
|
correction while it is still cheap.
|
|
|
|
**It is not written down, or only partly.** An empty directory, a folder somebody drops things into, a
|
|
job that lives in their head. Then ask what you should be watching for, and ask only that. Wait for the
|
|
answer, follow it where it goes, and settle it before you ask what to do when it fires — the questions
|
|
further down are the ones to work through, one at a time.
|
|
|
|
The honest test between the two: *could I write the procedure from what is here, or would I be guessing?*
|
|
Guessing counts as not written down.
|
|
|
|
**Both cases end the same way, and this is the part that is easy to leave out.** Once it is agreed, do
|
|
the first run yourself, and then arm. A question left hanging for the owner to turn into action is not a
|
|
finished intake — they came here to stop doing this by hand.
|
|
|
|
**You never cause the thing you are watching for.** Not while setting up, not on a first run, not as a
|
|
favour. If a backlog is sitting there and clearing it would trigger you, that is the owner's to start —
|
|
say it is waiting, and leave it. The temptation is real and it is always polite: the work is genuinely
|
|
there, and offering is genuinely helpful-seeming. It is still the wrong side of the line, and a first run
|
|
you triggered yourself proves nothing about whether the watching works.
|
|
|
|
"Do the first run" means running the action against something that has ALREADY happened, so the action is
|
|
known to work before it runs unattended. It never means manufacturing an occasion to run.
|
|
|
|
**No formatting in that opening.** No bullets, no headings, no backticks, no commit hashes, tag names, file
|
|
paths or table names. If a specific belongs anywhere, it belongs in the OPTIONS — when you offer choices,
|
|
they carry the detail and the question carries none. Those options were the good part of the message above;
|
|
the preamble is what needed deleting.
|
|
|
|
**Then work through the questions below, one at a time, in that order.** Follow the answers where they go —
|
|
this is a conversation and the interesting parts are the corrections. Do not move on from a trigger that
|
|
cannot be reduced to a shell test; that is the one question where "roughly" is not good enough.
|
|
|
|
**Before you arm anything, write the specification down and show it.** Trigger, action, baseline, what must
|
|
never happen twice, what a report contains. In their words, not a config dump. Two things come out of this:
|
|
they catch the misunderstanding while it is still cheap, and you end up with the procedure file you were
|
|
going to need anyway. Ask them to confirm it. Then, and only then, arm — and the rule below still holds,
|
|
so it is not on this turn.
|
|
|
|
# Helping somebody shape one
|
|
|
|
Most of the value is here, and it is a conversation rather than a form. Four questions, in this order:
|
|
|
|
**What is the trigger, exactly?** Not "when there is a new release" — *what file, tag, row or output changes,
|
|
and what command reveals it*. If the trigger cannot be reduced to a shell test that exits 0 or 1, there is no
|
|
commander yet, only an intention. Push until it can.
|
|
|
|
**What happens when it fires?** If the answer needs judgement, the action is a spawned agent and it needs a
|
|
written procedure to follow. Say that out loud — a procedure in a file, versioned, is what makes the same
|
|
the same job repeatable next month, and reviewable by somebody else. A prompt typed into a dialog is not.
|
|
|
|
**What must never happen twice?** This is the question nobody volunteers and it is the expensive one. A
|
|
trigger that stays true after a failed action fires again, and again. Ask what the cost of a second run is:
|
|
if it is money, or a duplicate message, or a half-applied change, the action must **claim** its work before
|
|
starting and refuse a second attempt. One attempt per unit of work, enforced by something the loop cannot
|
|
undo — a file created with `mkdir`, not a flag it sets itself.
|
|
|
|
**What does "new" mean — and this one you must supply, not ask for.** "Watch for new X" is not a condition, whatever X is —
|
|
a tag, a file in a folder, a row, an unread message, an alert. A watcher armed today sees everything that already exists, and unless something says otherwise it
|
|
will treat all of it as news and act on every item in turn. Nobody says "and ignore the ones already there",
|
|
because to a person that is obviously implied. It is not implied to a shell loop.
|
|
|
|
So a watcher needs a **baseline**, and there are only two honest kinds:
|
|
|
|
- **A record of what has been handled.** Best where acting is expensive or must not repeat: the loop skips
|
|
anything with a record and creates one before acting. This also survives a restart, which a remembered
|
|
baseline does not.
|
|
- **A snapshot taken at arming.** Fine where the history is genuinely irrelevant — "anything that lands from
|
|
now on". Cheap, and it forgets everything the moment the loop is restarted, so say that out loud.
|
|
|
|
Choose one and say which, in the condition itself. A watcher with neither looks identical to a working one
|
|
until the moment it fires on a backlog.
|
|
|
|
**What does "done" look like to you?** A report is worth more than a success flag, and the parts worth
|
|
reading are the ones that are hard to reconstruct: what it skipped, what it could not verify, what it left
|
|
outstanding. Ask for those explicitly or you will get a summary of what went right.
|
|
|
|
# Writing the loop, if you are the one writing it
|
|
|
|
The background-job dialog already sends the load-bearing mechanics — use the harness's own background
|
|
mechanism, keep the wait in the shell, put the test into the shell condition, and **ask first if the
|
|
condition is not completely clear** rather than arming a guess. That last one matters more than it reads: a
|
|
wrong condition fails silently, either never firing or firing constantly, and it is found out days later.
|
|
Arming late costs a minute.
|
|
|
|
Four details the mechanics do not carry, each of which was learned by getting it wrong:
|
|
|
|
- **`OFFICER_ROOT` is empty in the sidecar's environment.** Any path built from it resolves to `/data/...`,
|
|
so anything the loop writes to record its own progress lands where nothing
|
|
reads it — and a watcher whose record is invisible to itself has no baseline, so it acts on the same
|
|
work for ever. Set it explicitly in the loop.
|
|
- **Keep stderr out of a `--output-format=stream-json` pipe.** The CLI prints warnings there — a
|
|
connectors notice, among others — and one of them landing in the stream corrupts the first `jq` parse.
|
|
Redirect it to its own reader.
|
|
- **And filter that connectors notice before you narrate stderr.** On this machine `ANTHROPIC_BASE_URL`
|
|
points at a local proxy which holds the real credential, so `ANTHROPIC_API_KEY` is set to a token for
|
|
that proxy — and the CLI sees a key set, assumes the worst, and warns on every single spawn:
|
|
|
|
```
|
|
⚠ claude.ai connectors are disabled because ANTHROPIC_API_KEY or another auth source is set…
|
|
```
|
|
|
|
Nothing is wrong and nothing here uses connectors. But a loop that reports stderr whenever it is
|
|
non-empty will report this every time, which trains whoever reads the job panel to skim the one channel
|
|
that exists to carry real failures. Drop it and report what is left:
|
|
|
|
```bash
|
|
if grep -qv 'claude.ai connectors' "$errlog" 2>/dev/null; then
|
|
say "stderr: $(grep -v 'claude.ai connectors' "$errlog" | tr '\n' ' ' | cut -c1-200)"
|
|
fi
|
|
```
|
|
|
|
Filter by the known line, never by suppressing stderr wholesale. The reason to keep the channel is the
|
|
failure you have not seen yet.
|
|
- **`jq --unbuffered`**, or the job panel shows nothing for as long as the work takes and reads as hung.
|
|
A background job that produces no output is indistinguishable from a dead one.
|
|
- **The claim goes before the spawn, not inside the action.** If the action claims its own work, an action
|
|
that dies before claiming has claimed nothing and will run again.
|
|
- **The `description` passed to a background job becomes its permanent title in the job panel** — not a
|
|
status line, a name. Two things follow. First, it must say what the loop DOES, not the history of this
|
|
particular launch: "Transcribe new recordings", never "Re-arm cycle A with per-file timing logged" or
|
|
"Arm the classification sweep loop (2-minute testing cadence)" — the owner does not care that this is
|
|
the third arming or what changed since the last one, only what they are looking at now. Second, drop the
|
|
commander's own name from it: the job panel is already scoped to one commander, so a title that repeats
|
|
it — "Voice Memos: transcribe new recordings" on a commander named Voice Memos — is redundant on every
|
|
single row. Name the loop, not the commander it belongs to.
|
|
|
|
# Telling the owner, without a second channel
|
|
|
|
A loop writes to its job panel, and nobody reads a job panel. So a watcher that fires while the owner is
|
|
asleep has done its work and told no one, which from their side is indistinguishable from not firing.
|
|
|
|
Say two things in the CONVERSATION, and only two:
|
|
|
|
- **When it fires.** One line: what appeared, and that you are handling it. Nothing else — the detail is
|
|
in the job panel by definition, and repeating it in chat means both places are half-read.
|
|
- **When the work ends.** A short report: what was done, what was skipped, what went wrong and what the
|
|
owner has to do about it. "Five servers, nothing to report" is a complete message. So is "four went,
|
|
one needs a console and here is why".
|
|
|
|
Nothing in between. A progress commentary in chat is a notification per step, which is how somebody turns
|
|
notifications off.
|
|
|
|
## How, with nothing new
|
|
|
|
You cannot post into your own conversation from a shell loop — you only speak when a turn runs. But you can
|
|
cause a turn, and both halves of that already exist:
|
|
|
|
1. **The child writes its report to a file**, named for its own session id, somewhere under your state
|
|
directory. Same machine, same user, no authentication to arrange.
|
|
2. **The loop inserts a row into `nudges`** for your own transcript id, over the local Postgres socket, with
|
|
a message telling you what happened and where the file is. The nudger ticks every 60 seconds, wakes this
|
|
conversation, and you read the file and write the report yourself.
|
|
|
|
So the shape is: the loop knows the FACTS and puts them in a file; you provide the JUDGEMENT and put it in
|
|
the chat. It costs one turn per event, which is exactly what buys a summary rather than a pasted log tail.
|
|
|
|
Three things about that mechanism, each of which will bite otherwise:
|
|
|
|
- **`nudges` holds one row per (user, session), deliberately.** A second schedule for the same conversation
|
|
produces doubled prompts, so overwrite your pending nudge rather than queueing behind it. Two events in
|
|
the same minute become one turn covering both, which is the better message anyway.
|
|
- **The nudge message is a PROMPT you will be handed**, not a notification the owner sees. Write it to
|
|
yourself: what fired, where the file is, and that you are to report briefly. What the owner reads is
|
|
whatever you say on that turn.
|
|
- **Up to 60 seconds late**, because the nudger polls on a minute. Fine for anything a watcher does; if
|
|
something genuinely needs to be instant, it is not a watcher.
|
|
|
|
Once you have spoken, the ordinary chat notification path takes it to the owner's devices — so the phone
|
|
tap lands in this conversation, with the job panel one step away. Nothing to build for that either.
|
|
|
|
# Things that are true and surprising
|
|
|
|
- **Its cost line understates.** The recorded cost counts the commander's OWN turns — one row per `result`
|
|
that passes through the platform's chat socket. A spawned agent's cost never touches that socket, so a
|
|
commander that spent several dollars through its children can honestly display cents. Do not quote the
|
|
number as total spend.
|
|
- **A spawned agent cannot be found from its parent.** Nothing links them: the child's transcript looks
|
|
exactly like any other session, and entry types that appear to mark one (`queue-operation`, `ai-title`,
|
|
`last-prompt`) are the harness's own and appear in both. If a link is wanted, the loop must **print or
|
|
record the child's session id itself** — there is no way to recover it afterwards.
|
|
- **A child transcript is not permanent.** It is readable while it lives and for a while after. Two from a
|
|
previous day were gone. Anything worth keeping must be captured while it runs.
|
|
- **Reach what you are watching by the shortest path available.** A public hostname for something sitting on
|
|
the same machine goes out and back through whatever fronts it, and shared-IP rate limiting there refuses
|
|
connections for long stretches — presenting as an access-rights error, which it is not. Minutes of
|
|
detection latency become hours. The Officer-specific instance of this is in the **Deploying Officer** skill.
|
|
- **The platform may not know a watcher exists.** Its bookkeeping engages only for the fire behaviour that
|
|
hands back and re-arms. Started another way, the loop runs perfectly and the row still reads unarmed, so
|
|
the UI shows no watcher and has nothing to disarm.
|
|
|
|
# When something is not firing
|
|
|
|
In this order, because each rules out the one below:
|
|
|
|
1. **Is the loop alive?** It is a tracked background job — look at the job, not at the agent.
|
|
2. **Is its condition ever true?** Run the test by hand, in the same shell form the loop uses.
|
|
3. **Can it reach what it polls?** An access-rights error on a repository or an endpoint that plainly exists
|
|
is usually a rate-limited public path, not a credential.
|
|
4. **Has it already claimed this work?** A refusal to act twice looks identical to a failure to notice.
|
|
5. **Only then** the model, the prompt or the procedure.
|
|
|
|
# Where to look next
|
|
|
|
For endpoints, permissions or why something is refused, open the **Officer platform** skill. For a deploy —
|
|
the worked example of everything above, with a real procedure and a real fleet behind it — open the
|
|
**Deploying Officer** skill.
|