diff --git a/skills/commander/SKILL.md b/skills/commander/SKILL.md index aac1d12..055516c 100644 --- a/skills/commander/SKILL.md +++ b/skills/commander/SKILL.md @@ -28,7 +28,7 @@ from inside it. 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 host, or write a report. So +**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. @@ -45,7 +45,7 @@ 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 -deploy repeatable next month. A prompt typed into a dialog is not. +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: @@ -53,8 +53,8 @@ if it is money, or a duplicate message, or a half-applied change, the action mus 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 a new tag" is not a -condition. A watcher armed today sees everything that already exists, and unless something says otherwise it +**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. @@ -84,8 +84,9 @@ 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 a lock file lands somewhere nothing reads and every release looks permanently undeployed. Set it - explicitly in the loop. + 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. @@ -106,10 +107,10 @@ Four details the mechanics do not carry, each of which was learned by getting it 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. -- **Poll a local address, never a public hostname.** Where the thing being watched is on the same machine or - the same tailnet, reach it directly. A public hostname through a reverse proxy shares one source IP with - everything else behind it, and rate limiting there refuses connections for long stretches — which shows up - as an access-rights error and is not one. +- **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. @@ -127,6 +128,6 @@ In this order, because each rules out the one below: # Where to look next -For endpoints, permissions or why something is refused, open the **Officer platform** skill. For a deploy -specifically, `prompts/deploy-commander.md` in the platform repository is the worked example: a real -procedure, with the reasoning for each rule written beside it. +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.