the bot's OWN brief is thorough and lives in the bots directory, rewritten every turn. what was missing is the doc for everyone else: what a bot is, where it lives, why it has not fired, and how it differs from a commander. the line is not size. one thing watched and one action is a bot; anything that has to remember what it already handled is a commander. the bot brief tells bots not to read the commander skill for exactly that reason. carries the thing that is easy to lose: <sessionId>.bot.json is the ONLY link between a conversation and a detached shell pid. a bot deleted on 2026-08-26 was still polling a website eleven minutes later because that file did not exist yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.9 KiB
name, title, description, version
| name | title | description | version |
|---|---|---|---|
| bots | Bots | Use when the owner asks about a bot — what one is, how to start one, why one has not fired, how to stop one, or why it is a bot rather than a commander. Also read it before setting one up FOR them. Carries where a bot lives, the record that is the only thing able to stop it, and the two ways one dies quietly. | 1 |
A bot is one thing watched and one thing done
"Notify me when x.com is down." "Tell the todo guy when NYT has a new headline." That sentence is the whole specification, and the first message arms it — a bot that interviews the owner first has already failed at the thing that makes it a bot.
Bot or commander — the line, and it is not size
- A bot watches ONE thing and reacts. A shell loop that exits when a condition is true. No ledger, no
procedures, no dispatch policy. Arming is
run_in_background: trueon a script and nothing else. - A commander runs a JOB. It classifies, it decides, it spawns agents, it keeps
JOB.mdand a record of what it has already handled, and it is set up in a conversation before it is armed.
If the answer to "what does it do when it fires" is one action, it is a bot. If the answer needs a paragraph, or the thing has to remember what it already did, it is a commander. Reach for commander there — and note that the bot brief tells bots explicitly NOT to read it, because a bot that starts building a dispatch loop has stopped being a bot.
Where everything is
$DATA_PATH/<owner-email>/bots/
├── CLAUDE.md the brief every bot runs under — PLATFORM-OWNED, rewritten every turn
├── <name>.sh the watch itself, one per bot
├── <name>.state its baseline, so a restart resumes instead of re-firing
└── <sessionId>.bot.json the record: script, state, pids, what it watches
Every bot shares that one directory — getBotsDir(email) — which is why the record is named for the
session id rather than filed under it.
The chat list for them is GET /api/chat/sessions?context=bots. The context IS the address: no cwd
needed, because there is only one bots directory per owner.
<sessionId>.bot.json is the only thing that can stop a bot
A watch is a detached shell process. Nothing else on the machine knows it belongs to a bot — not the
transcript, not the session list, and not the process table, which sees an anonymous bash. The record is
the only link between the conversation and the pid.
That cost was paid before the file existed: a bot deleted on 2026-08-26 was still polling a website eleven minutes later, with nothing in the product able to find it. So when a bot re-arms, it rewrites the record with the new pid, in the same turn. A record that is stale is worse than absent — it names a pid that may now belong to something else entirely.
Why a bot has not fired — check in this order
- Is the process alive? Read the record, then
ps -p <pid>. A watch dies with a reboot and nothing re-arms it automatically; the bot re-arms itself on its next turn, which needs a message. - Has it fired already and not re-armed? The exit IS the signal. A bot that fired, reported, and never re-armed looks identical to one that is watching quietly.
- Is the baseline wrong? A "has it changed" watch that wrote its state before exiting will not fire on the same change twice. That is correct, and it is also the usual reason a test does not reproduce.
- Is it watching the wrong thing?
watchesin the record is the bot's own description of its job. Compare it to what the owner actually asked for before debugging any further down.
The two ways a bot dies quietly
- A reboot. The loop is a plain background process; nothing supervises it. The record survives, which is what lets the next turn re-arm from it — so a bot that has been silent since a restart is not broken, it is unstarted.
- A fired-and-forgotten watch. It exited, said its line, and the turn ended without re-arming. Act, re-arm, record — in one turn — is the rule in the brief precisely because forgetting the third step is invisible until the thing never fires again.
Setting one up for the owner
Say the sentence and let the bot arm itself. The brief in that directory is thorough and current — it is
rewritten on every turn from src/servers/api/bots/brief.ts, so it cannot drift from the platform the way
a copy here would.
Do not paste the mechanism into the conversation. If you find yourself explaining the script shape, the record, or the naming rules to a bot, it has not read its own brief and the fix is to point at it.
Naming
A bot's automatic title is the owner's first sentence, truncated, typos included. It is a prompt, not a
name. Two to four words, the subject and what is watched about it — newweb uptime, NYT front page,
gitea disk space. Not "Website Monitor Bot": every bot is a monitor, so the word earns nothing in a list
of them.