This week: I explained my bot fleet. Then my agent audited it.
Half the room asked how to get more than one agent's worth of work without being the bottleneck. I answered in the Discord, then asked my own agent what I was missing: about 19 of every 20 sessions run on one bot. Plus bots are this month's product, and the build: give each bot a desk.
The Discord asked one question all week, in five different ways. One member runs everything through a single agent and says it never really runs on its own. Another uses Hermes like a coding tool, a fresh chat for every task, and is only now trying bots with specific jobs. A third has ten pieces of a data project in flight and keeps forgetting which one they were on. Two more wanted to know how I use my bots, and what each one is for.
Underneath all of it is the same thing: how do I get more than one agent's worth of work without becoming the person who routes every request?
The vendors heard it too. Grok Bot, Hermes Bot Mode, OpenClaw and a new iPhone app for Hermes bots spent October pitching the same picture: a team of named bots, each with its own job, talking to each other while you sleep. OpenAI's dots, launched a month earlier, started with one. I believe in that picture. I also just found out I don't run it.
On the bench
Wednesday night I posted a long brain dump in the Discord about how I set up my bots. I had answers. Each bot keeps its own skills, tools and memory, so a bot that only does one kind of work builds up memory about only that work. Specialist tool stacks keep each bot's prompt lean. You can pin different model families to different bots, so a group chat gets more than one model's opinion for free. I have two cloud specialists that know how each vendor wants things configured, a security bot, one that's my wife's, and an adversarial reviewer whose standing instruction is to pick apart every idea and find the flaws, no matter how small. I also admitted that routing between them hasn't been reliable and that most of the time I just talk to my main bot and it does the work. I thought that was a quirk. It turned out to be the whole story.
That same night I asked my main agent what I was missing. Its first answer blamed how I reach it, as if my bots sat behind a door I never used. That part was wrong. I do nearly all my work in the Hermes desktop app, and my main bot has had its own permanent Bot Chat since August. The tools were right there. The real answer was worse.
I reran the numbers this morning. Over the last 30 days, my main bot ran about 19 of every 20 sessions on the machine. It had 86% of my conversations and every single scheduled run. Of the 24 routines switched on, 21 belong to it. Three of my nine other bots own one routine each, and the other six own nothing. Three of the specialists have the same one line in memory, 142 bytes telling them where to find the brain. The adversarial reviewer's memory is empty.
And it wasn't for lack of handing work off. In those 30 days my main bot delegated 83 subagent tasks across 29 sessions. Every one went to a throwaway subagent running the main bot's own model, which starts from a blank conversation and is gone when the task ends. Not one went to the specialists I built for exactly that work. Its tool for messaging another bot directly: zero calls. The plugin that lets the main bot send a subagent off as a specific bot has been installed on my machine since October 26. Its list of bots is empty.
Here's the confession. On Wednesday I described a team. What I actually run is one very busy employee who hires a temp for every task and never calls the staff, plus nine name tags. The specialists aren't bad at their jobs. They don't have jobs. A bot only learns from work it does, and I never gave mine any. I've been telling people for months to find one good harness and put all their effort into running it well. Then I spent the effort on the org chart and none on the work.
That's the sort for this issue: a bot with a job versus a bot with a name.
The gauges
One agent or a fleet: the room split three ways
What I saw: the members who asked this week fall into three camps. Some run one main agent for everything, with a project folder and a review tier for anything it builds, and it works but it never moves without them. Some use a fresh chat per task, the way you'd use a coding tool, and haven't built bots at all. And a few have started handing work out. One member tells the main agent to send work to the specialist bots and report back, and says that's worked better than anything else they've tried. Another runs two agents on one home server, theirs and their partner's: separate Linux users, separate profiles, keys and memory, with a one-way door so they can step into their partner's setup to fix things, never the reverse. That's the cleanest two-bot design I saw all week, and it has nothing to do with a framework.
The counter-case is real, and it comes from the people selling the models. Anthropic's Applied AI team published a piece on folding subagents back into the main agent loop. In their comparisons across enterprise deployments, "a single agent with skills frequently outperformed both the one-prompt-for-everything design and the subagent design on quality, and often at a lower cost and latency per task." Their diagnosis: "too often we see enterprises give each team or problem type its own subagent, with the agent architecture becoming a copy of their org chart." Their own research-system writeup puts a number on the cost: multi-agent systems use "about 15× more tokens than chats." And on Reddit, the top answer to "do you actually use multi-agent orchestration?" opens with "I've built multi-agent workflows a few times and regretted it every time," because "most of the tokens went on running the workflow."
Should you pay attention? Yes. This is the decision most of you are about to make, and both sides have receipts.
My position: start with one agent that works, and add a bot only when a job needs its own context that the main agent shouldn't carry around. Anthropic's rule for when a split is worth it is the right one for a small business too: divide "by context," give the bot "a piece of work that needs little from the rest of the system," and have it hand back "a compact result." A nightly bank sync passes that test. "The marketing bot" doesn't. If you can't name the recurring job a bot owns, you don't have a specialist. You have a second login.
Bots are this month's product
What I saw: every major harness now ships named, always-on bots. They split on whether you start with a team or with one.
- ChatGPT dots are OpenAI's entry, launched at DevDay on September 29 for Pro and Business Premium. A dot runs on GPT-6 Astra with its own cloud computer, and you can reach it in ChatGPT, Slack or Teams. OpenAI took the other road from everyone above: "Today, you can start with your primary dot, give it a name, and make it your own. Over time, we envision teams of dots working together on your behalf." For now you get one dot, and per the docs it "can use background agents to work on several things in parallel." Specialists with "their own identity, credentials, and access" exist only as enterprise pilots.
- Grok Bot launched in August and kept shipping all month with a picture straight from an org chart: "A chief of staff sits on top, with a specialist for each lane." Bots message each other and coordinate in group chats.
- Hermes Bot Mode turns profiles into a roster. The pitch on the docs page is "Build a specialist Bot once and it is there forever, one click away." Each bot gets one permanent chat, its own routines, and group rooms of two to six bots with "Hard caps (10 messages per send, 3 rounds) keep rooms from spinning."
- OpenClaw added a team preset that builds a coordinator plus a researcher, writer and reviewer. Its docs are refreshingly honest about the routing setting: delegation mode "prefer" "is prompt guidance, not a scheduler."
- Herald, an iPhone app for Hermes bots, opened a public TestFlight beta on October 22, with group chats and approvals, and its developer says a desktop version is coming. A member tested it on a train into New York and liked working on the go.
- Nous also shipped an official advanced-delegation plugin for Hermes. One mode lets the main agent send a subagent off "as one of your Hermes profiles, with that profile's own model, credentials, skills and
SOUL.md." That's the piece I was missing: it turns the main bot's throwaway subagents into hand-offs to the bots you actually built.
Should you pay attention? Yes, if you already have one agent you trust with a recurring job. If you're still working mostly in chat, no. A roster of bots won't fix that.
My position: look hard at the dots design, because it's what my setup turned into by accident. One front door, plus throwaway background agents for the parallel work. OpenAI ships that on purpose as step one and holds specialists back until they can have their own identity and access. I think that's the honest order. Start with one bot you talk to, and add a specialist only when it can own a job. Grok Bot, Hermes and OpenClaw agree on the end state: you talk to one bot, and it briefs the specialists and checks their work. What the launch posts skip is that a specialist with no standing job never gets better, because nothing ever lands in its memory. And don't switch harnesses to get bots. Every harness you already run has a version of this now (the playbook maps all five). Use the one you have.
Follow-up: give the bot a desk, keep your own filing cabinet
What I saw: one member said they save everything their bots make to a folder on their own Mac that backs up automatically, and that they "certainly would not rely on files saved in" the bot's own computer. It's a good instinct. Grok Bot's docs, updated October 28, explain the model: "All of your Bots use the same cloud computer, sharing its files, browser sessions, and app logins," so "treat anything placed on it as available to every Bot you run." The same week, one user posted that Grok Bot lost five weeks of bot work saved on its computer and told the user they were back to September 15. That's one self-reported post, not a pattern, and I haven't seen xAI respond. But it's exactly the failure I'd plan for.
Should you pay attention? Yes, if any of your bots keep work product on a computer you don't own.
My position: in August I wrote about running agents on two machines, where the rule was one owner for each kind of state. Bots don't change that rule. A bot can have a desk, meaning its own memory, skills and chat. Your work product still goes in your own filing cabinet: your drive, your database, your backup. And remember that one shared computer means one shared set of logins. Last week's key ledger applies to every bot on it.
Quick reads
- The 512GB Mac Studio has a price. Apple added the 512GB option to the configurator this week. VideoCardz read it at $22,299 minimum, with the memory upgrade alone adding $15,500, and $29,799 with 16TB of storage. Ship dates run into late November and early December. In August I ran the rent-or-own math and said local hardware replaces the $20 tier, not the $200 tier. At $22,299 the cheapest 512GB box costs over nine years of a $200 plan. One member ordered one this week and is already thinking about cancelling.
- Discord will put some public messages on Google. Starting mid-December, messages from public channels in Discoverable servers (the large public ones listed in Discord's directory) can show up in web search, without usernames or avatars (Discord's help page). Our Discord isn't a Discoverable server (those need 1,000+ members and a directory listing), so nothing in it is affected. If you post in big public seller servers, the opt-out is in User Settings > Data & Privacy: turn off "Show my message content from Discoverable channels on web pages." It's off by default in the EU, UK, Canada, Brazil, Australia and South Korea.
- Walmart is giving agents their own profiles. Walmart announced Agent Profiles, launching within the month: each agent gets its own profile inside your account, so its purchases show up separately, with approval on every purchase by default or a spending limit you set. Agents that don't follow Walmart's terms "will be blocked." It's this issue's thesis from the retailer's side: give the agent its own seat and its own limit, and you can see what it did.
Your move
Ten minutes. Use whichever harness you live in.
- (2 min) Count the seats. Every bot, profile, custom agent or subagent file you've set up. Write the number down.
- (3 min) Write one job next to each. The recurring work it owns, with a schedule. If there isn't one, write "none." Be honest. Mine had six "none"s.
- (3 min) Open your main agent's scheduled jobs. Next to each one, write the name of the bot it should belong to, or "main" if it really needs everything the main agent knows.
- (2 min) Circle one job to move. Pick one that runs often, needs its own context, and is cheap to get wrong or easy to check. Don't move it yet. The playbook shows how to move it so you can tell whether it worked.
Hit reply with two numbers: seats, and seats with a job. I'll share the range next week.
One honest note on last week: I asked for your key counts and promised to share the range. Zero replies came in. I'm choosing to believe you were all busy rotating keys.
The meme

The plan was a team. The org chart was one guy.
The playbook: give the bot a desk
Operators: this is the build behind the bench. It's a census script that shows who in your fleet actually works, a desk card that defines a bot's job before it gets one, a harness-by-harness map of where the seat, the standing job, the hand-off and the memory live, a one-job move with a before-and-after check, and an acceptance test. Everything harness-specific below is from each vendor's docs as of this week. Both scripts ran clean against a sandbox before I sent this, and the census ran against my own machine to produce the bench numbers.
The idea
Six rules, each with its reason.
- A bot earns a seat by owning a recurring job. Not a topic, a job: something that runs on a schedule and produces something. Why: a bot's memory only grows from work it does. My specialists had a role description and nothing to remember.
- Divide by context, not by org chart. A bot gets a job when that job needs context the main agent shouldn't carry: a vendor's docs, one data source, one client's history. Why: split by department and you get the telephone game Anthropic describes, with each hand-off losing detail. Split by context and the bot hands back a small, checkable result.
- One front door. You talk to one bot. It briefs the others and checks their work. Why: if every bot is its own front door, you're the router again. Grok Bot, OpenClaw and Hermes all ship this chief-of-staff shape for the same reason, and ChatGPT dots ships only the front door so far. The catch is that the front door has to actually send work through, which is rule 4.
- Standing rules beat judgment calls for hand-offs, and the rule has to name the bot. "Anything expensive to undo goes to the reviewer before we build it." "Every bank-sync question goes to the finance bot." Why: model routing is the unreliable part. I said so in the Discord, and OpenClaw's docs say so in writing. Left to itself, my main bot handed off 83 tasks in a month and sent every one to a nameless temp. A written rule plus a route to a named bot triggers the right hand-off without anyone having to remember.
- A specialist's "done" is its own report. The main bot checks the output against the source before it tells you. Why: in August, the run was clean and the work didn't happen. Adding bots adds places for that to happen.
- Move one job at a time, and measure. Before-and-after numbers, a two-week trial, and a written reason to fold it back. Why: Anthropic's advice runs the other way, folding subagents back in "only if the folded version scores worse." Same discipline, opposite direction. Bots cost tokens and attention, so each one has to earn its seat.
Which jobs move first
The best first job is one that runs on a schedule, reads one source, and produces something you can check in a minute.
- Reseller: the nightly inventory or repricing check moves to an inventory bot that only knows your catalog and your marketplace data.
- Service business: quote follow-ups move to a lead bot that only knows your pipeline and your templates. It drafts, and you send.
- Consultant: the weekly hours-and-billing ledger moves to a finance bot that only knows your time records and rate sheet.
- Everyone: the daily bank or card sync moves to a finance bot with read-only access. That's the one I'm moving first.
Leave anything with judgment across your whole business (strategy, pricing calls, anything that talks to a customer without a draft step) on the main bot until the simple moves have proven out.
The census
This is the script from my bench. It reads each Hermes profile's session database, scheduled jobs and memory file, and prints who did the work over the last N days. It never prints session or memory contents, only counts and sizes. Read-only, standard library only. Save a census before you move a job and compare after, and you can see whether the bot picked up the job and whether its memory started to grow.
#!/usr/bin/env python3
"""fleetcensus.py: who in your Hermes fleet actually does the work.
Usage: python3 fleetcensus.py [--home ~/.hermes] [--days 30]
[--save census.json] [--compare last.json]
Read-only. For the main agent and every profile (Bot) it counts, over the
last N days: conversations you had with it, scheduled runs, routines it
owns, and the size of its memory file. It prints each bot's share and
flags bots that hold a seat but no job. With --compare it shows what
changed since a saved census, so you can see whether a bot's memory grew
after it got a routine. It never prints session or memory contents.
"""
import argparse, json, os, sqlite3, sys, time
# Sources that are you talking to the bot. Everything else is machinery.
SCHEDULED = {"cron"}
MACHINERY = {"cron", "subagent", "oneshot", "tool", "kanban"}
IDENTITY = ("config.yaml", ".env", "SOUL.md", "profile.yaml", "auth.json", "state.db")
def bots(home):
yield "main", home
pdir = os.path.join(home, "profiles")
if os.path.isdir(pdir):
for name in sorted(os.listdir(pdir)):
path = os.path.join(pdir, name)
# Hermes only treats a folder as a profile if it has an identity file.
if not name.startswith(".") and any(
os.path.exists(os.path.join(path, f)) for f in IDENTITY):
yield name, path
def sessions(path, since):
db = os.path.join(path, "state.db")
if not os.path.exists(db):
return 0, 0
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
try:
rows = con.execute(
"select source, count(*) from sessions where started_at > ? group by source",
(since,)).fetchall()
finally:
con.close()
chats = sum(n for src, n in rows if src not in MACHINERY)
scheduled = sum(n for src, n in rows if src in SCHEDULED)
return chats, scheduled
def load_jobs(path):
f = os.path.join(path, "cron", "jobs.json")
if not os.path.exists(f):
return []
data = json.load(open(f))
return data.get("jobs", []) if isinstance(data, dict) else data
def memory_bytes(path):
f = os.path.join(path, "memories", "MEMORY.md")
return os.path.getsize(f) if os.path.exists(f) else 0
def census(home, days):
since = time.time() - days * 86400
rows = {}
for name, path in bots(home):
chats, scheduled = sessions(path, since)
rows[name] = {"chats": chats, "scheduled": scheduled, "routines": 0,
"memory_bytes": memory_bytes(path)}
# Routines: enabled jobs in each bot's own cron file. A job in the main
# file named "[bot:<name>] ..." belongs to that bot, not to main.
for name, path in bots(home):
for job in load_jobs(path):
if not job.get("enabled", True):
continue
owner = name
label = job.get("name") or ""
if label.startswith("[bot:") and "]" in label:
tagged = label[5:label.index("]")]
if tagged in rows:
owner = tagged
rows[owner]["routines"] += 1
return rows
def share(part, whole):
return f"{100 * part / whole:.0f}%" if whole else "n/a"
def report(rows, days):
tot = {k: sum(r[k] for r in rows.values()) for k in ("chats", "scheduled", "routines")}
print(f"Fleet census, last {days} days\n")
print(f"{'bot':<16}{'chats':>7}{'sched runs':>12}{'routines':>10}{'memory':>9} flags")
for name, r in rows.items():
flags = []
if name != "main":
if r["routines"] == 0:
flags.append("no routine")
if r["chats"] == 0 and r["scheduled"] == 0:
flags.append("idle")
if r["memory_bytes"] < 300:
flags.append("memory near empty")
print(f"{name:<16}{r['chats']:>7}{r['scheduled']:>12}{r['routines']:>10}"
f"{r['memory_bytes']:>8}B {', '.join(flags)}")
m = rows["main"]
print(f"\nmain bot share: {share(m['chats'], tot['chats'])} of chats, "
f"{share(m['scheduled'], tot['scheduled'])} of scheduled runs, "
f"{share(m['routines'], tot['routines'])} of routines")
seats = [n for n in rows if n != "main"]
working = [n for n in seats if rows[n]["routines"] > 0]
print(f"specialists with a routine: {len(working)} of {len(seats)}")
return tot
def compare(rows, old):
print("\nChanged since the saved census:")
changed = False
for name, r in rows.items():
o = old.get(name)
if o is None:
print(f" {name}: new bot")
changed = True
continue
bits = []
for key in ("routines", "memory_bytes"):
if r[key] != o[key]:
bits.append(f"{key} {o[key]} -> {r[key]}")
if bits:
print(f" {name}: " + "; ".join(bits))
changed = True
for name in old:
if name not in rows:
print(f" {name}: removed")
changed = True
if not changed:
print(" nothing")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--home", default=os.environ.get("HERMES_HOME", "~/.hermes"))
ap.add_argument("--days", type=int, default=30)
ap.add_argument("--save")
ap.add_argument("--compare")
a = ap.parse_args()
home = os.path.expanduser(a.home)
if not os.path.isdir(home):
sys.exit(f"no Hermes home at {home}")
rows = census(home, a.days)
report(rows, a.days)
if a.compare:
compare(rows, json.load(open(a.compare)))
if a.save:
json.dump(rows, open(a.save, "w"), indent=1)
print(f"\nsaved {a.save}")
if __name__ == "__main__":
main()
How to read it: "chats" are conversations you had with that bot. Scheduled runs, subagent calls and other machinery are counted separately or left out. A specialist flagged "no routine" has a seat and no job. One flagged "memory near empty" hasn't learned anything yet. The memory column is a byte count, not a quality score. It tells you whether anything is landing, and that's all you need at this stage.
Not on Hermes? The script reads Hermes files. On the other harnesses, paste this into your main agent instead:
Build a fleet census for me. Count only what files and logs show. Write "no record" where you can't see something. Never estimate.
1. List every agent I have set up: Claude Code subagent files in ~/.claude/agents/ and .claude/agents/,
Codex custom agents in ~/.codex/agents/ and .codex/agents/, or OpenClaw agents from `openclaw agents list --bindings`.
2. For each one: its description line, any scheduled job that runs it (Claude Code Desktop scheduled tasks or routines,
OpenClaw `openclaw automations list --all`), and whether it has its own memory folder
(Claude Code: ~/.claude/agent-memory/<name>/). Report memory as a file size only. Never show its contents.
3. Tell me which agents have no scheduled job and no memory. Those are seats without jobs.
On a ChatGPT dot, there's one seat, so the question is different: what does it own? The docs say to "Ask it to list recurring work and confirm what's scheduled," and to use Activity in its profile to review delegated tasks. A dot with nothing under Scheduled is a chat window with a cloud computer. Give it one recurring job from the desk card below before you wish for a team.
The desk card
Write this before a bot gets a job. If you can't fill in "Owns," the bot doesn't need to exist yet.
# Desk card: <bot name>
Role (one line; this is also its description field):
Owns (recurring jobs, with schedule):
Does not own (send these back to the main bot):
Reads from (the one or two sources it needs):
Hands back (format, and the folder or table it writes to, which you own):
Should remember (what its memory should hold after 30 days):
Standing hand-off rule (when the main bot must send work here):
Kill test (when the job moves back to the main bot):
A filled-in example, with invented details:
# Desk card: finance
Role: Reads bank and card feeds and reconciles them against the books. Read-only.
Owns: daily bank sync, 6:30am. Weekly uncategorized-transactions list, Monday 7am.
Does not own: paying anything, emailing anyone, changing categories in the books.
Reads from: the bank aggregator (read-only), the books export.
Hands back: a short summary in its chat. Full output to ~/Business/finance/daily/.
Should remember: which feeds lag and on which days, recurring vendors and how I categorize them.
Standing hand-off rule: any question about a bank or card transaction goes here first.
Kill test: two wrong reconciliations in two weeks, or I had to fix its output more than once.
Where the desk lives in your harness
Checked against each vendor's docs this week.
| The seat | Its standing job | How work reaches it | Memory of its own | |
|---|---|---|---|---|
| Hermes | A profile. Bot Mode shows it in the roster. The docs warn: "Never point two agent processes at the same profile" | The bot's Routines pane. Routines are cron jobs named [bot:<name>], and "Runs land in the Bot's own chat history" |
@mention it in a group room, or the main bot calls message_agent, which only exists inside a chat titled exactly "Bot Chat." For hand-offs, the advanced-delegation plugin's profile routes run a subagent as that profile (Hermes 0.22.5 or newer). Without it, every delegate_task hand-off runs as a throwaway subagent on the main bot's model |
Its own memories/ folder in the profile. Check whether routed runs write to it with the census --compare |
| Claude Code | A subagent file in ~/.claude/agents/ with a name, description, tools and model |
A Desktop scheduled task or a cloud routine. /loop only runs while the session is open |
Claude delegates by matching the description field. The docs say to add phrases like "use proactively" to encourage it |
Add memory: user and it gets ~/.claude/agent-memory/<name>/. The first 200 lines or 25KB of its MEMORY.md load every time it runs |
| Codex | A TOML file in ~/.codex/agents/ with name, description and developer_instructions (docs) |
Not covered in the subagent docs. Run the job from whatever scheduler you already use | Codex delegates "after a direct request or applicable project or skill instruction." Put the hand-off rule in AGENTS.md |
The subagent docs don't give a custom agent its own memory. Point it at a notes file you own, and tell it to update that file |
| OpenClaw | openclaw agents add <id>: its own workspace, state folder and session store (docs). openclaw agents team create builds a coordinator plus three specialists |
openclaw automations create "<cron>" "<prompt>" --agent <id> (docs) |
subagents.allowAgents on the coordinator. Delegation mode "prefer" is prompt guidance, so write the rule into its AGENTS.md too |
Per-agent workspace. Memory Wiki is one shared vault unless you set vault.scope: "agent" |
| ChatGPT dots | Your one dot. More dots and specialist dots are announced, but specialists are enterprise pilots only (OpenAI) | Recurring tasks, listed under Scheduled in its profile. "Ask it to list recurring work and confirm what's scheduled" (docs) | Message it in ChatGPT, Slack or Teams. It runs parallel work as background agents and can create Codex or Work tasks | "Its own saved notes about your preferences, decisions, and ongoing work," which carry across channels |
Two warnings. On Hermes, the delegation plugin's docs say plainly that listing a profile "lets this profile's subagents use that profile's credentials, credential pool, SOUL.md and terminal policy," so only route to bots whose keys you're fine with the main bot using. And on any harness, a bot that copies the main profile's keys at creation now holds another copy of every key. Run last week's sweep after you create one.
The move: one job, two weeks
- Pick the job you circled in Your move. Write its desk card.
- Save a census.
python3 fleetcensus.py --save before.json - Create the routine on the bot. In Hermes, open the bot in the Bots tab and add it in the Routines pane. From a shell,
hermes -p <bot> cron createworks too; runhermes cron create --helpfor the schedule flags on your version. Give it the same prompt the old job had, plus one line: "When you learn something about this job that will matter next time, save it to memory." - Route delegated work to the bot, too. On Hermes, if your main bot hands off work with subagents, install the advanced-delegation plugin, run
hermes delegation config, pick Profile routes, and add the bot with a one-line description of its job. Until you do, every hand-off goes to a throwaway subagent on the main bot's model. That was my whole problem. Routed runs don't show up as chats or scheduled runs in the census, so watch the bot's memory column for those. - Pause the old job on the main bot. Don't delete it:
hermes cron pause <job-id>. If the move fails, you resume it. - Let it run twice. Read both outputs in the bot's own chat. Check one against the source yourself.
- Compare.
python3 fleetcensus.py --compare before.json. You want the main bot's routines down by one, the bot's up by one, at least two scheduled runs on the bot, and its memory bytes up. If memory didn't move, the bot is doing the job without learning from it. Fix the instruction before you move a second job. - Run the kill test at two weeks. If the bot's output was worse, or you had to fix it more than once, move the job back: resume the old one, pause the new one. That's a result, not a failure. You just learned that this job needed the main bot's context.
The setup prompt
Paste this into your main bot once:
You are my chief of staff. I talk to you. You brief the other bots and check their work.
Rules, in order of importance:
1. Never print the contents of another bot's memory, sessions or keys. Counts and sizes only.
2. Run the fleet census (fleetcensus.py, or the census prompt if it can't run here). Count only what the
files show. Write "no record" where you can't see something. Never estimate.
3. For each scheduled job you own, propose an owner: the bot, the context it needs that you shouldn't carry,
and what happens if it gets the job wrong. Propose at most two moves.
4. Write a desk card for each proposed owner. Write "unknown" in any field you can't fill from what I've told you.
5. Do not create, move, pause or delete any job. I approve each move.
6. Standing hand-off rules: anything expensive to undo goes to the reviewer bot before we build it.
[Add one line per desk card.]
7. When a bot reports done, check its output against the source before you tell me it's done.
If you can't check it, say so.
8. Every Monday, rerun the census with --compare against last Monday's file. Tell me only what changed.
Rule 5 is an instruction, not a lock. The model will usually follow it, but nothing in the harness enforces it. Re-run the census after any session where schedules came up, and you'll see if something moved without you.
Acceptance test
Run this before you trust the census on your real setup. It builds a fake Hermes home in a throwaway folder with a main bot and three specialists, runs the census, moves a routine to a specialist, lets it "run" and write memory, compares, then moves the job back and compares again. It prints check, expected, actual and PASS or FAIL, then deletes the sandbox. Your real ~/.hermes is never touched. Save it next to fleetcensus.py and run python3 test_fleetcensus.py.
#!/usr/bin/env python3
"""Sandbox acceptance test for fleetcensus.py. Builds a fake Hermes home with a
main bot and three specialists, runs the census, moves one routine to a
specialist, lets it "run" and write memory, runs the census again with
--compare, prints check/expected/actual/PASS-FAIL, then deletes the sandbox.
Your real ~/.hermes is never read. Hand-computed expectations are in comments."""
import json, os, shutil, sqlite3, subprocess, sys, tempfile, time
HERE = os.path.dirname(os.path.abspath(__file__))
SCRIPT = os.path.join(HERE, "fleetcensus.py")
NOW = time.time()
DAY = 86400
sandbox = tempfile.mkdtemp(prefix="fleetcensus-sandbox-")
results = []
def check(name, expected, actual):
results.append((name, expected, actual, "PASS" if expected == actual else "FAIL"))
def make_bot(path, sessions, jobs, memory):
os.makedirs(os.path.join(path, "cron"), exist_ok=True)
os.makedirs(os.path.join(path, "memories"), exist_ok=True)
con = sqlite3.connect(os.path.join(path, "state.db"))
con.execute("create table sessions (id text primary key, source text, started_at real)")
for i, (source, age_days) in enumerate(sessions):
con.execute("insert into sessions values (?,?,?)", (f"s{i}", source, NOW - age_days * DAY))
con.commit(); con.close()
json.dump({"jobs": jobs}, open(os.path.join(path, "cron", "jobs.json"), "w"))
open(os.path.join(path, "memories", "MEMORY.md"), "w").write(memory)
def run(*args):
return subprocess.run([sys.executable, SCRIPT, "--home", sandbox, *args],
capture_output=True, text=True, timeout=60, cwd=sandbox)
try:
main = sandbox
books = os.path.join(sandbox, "profiles", "books")
research = os.path.join(sandbox, "profiles", "research")
reviewer = os.path.join(sandbox, "profiles", "reviewer")
# main: 18 chats + 20 cron runs in window, 1 old chat outside it (40 days), 1 subagent (machinery)
make_bot(main, [("desktop", 1)] * 12 + [("telegram", 2)] * 6 + [("cron", 1)] * 20
+ [("desktop", 40)] + [("subagent", 1)],
[{"name": "daily bank sync", "enabled": True},
{"name": "inbox summary", "enabled": True},
{"name": "old report", "enabled": False}, # disabled: not counted
{"name": "[bot:research] weekly scan", "enabled": True}], # belongs to research
"x" * 2000)
make_bot(books, [("desktop", 3)], [], "one line\n") # 1 chat, no routine
make_bot(research, [("desktop", 5), ("desktop", 6)], [], "y" * 500)
make_bot(reviewer, [], [], "") # idle seat
os.makedirs(os.path.join(sandbox, "profiles", ".deleted", "logs")) # leftover dir: not a bot
os.makedirs(os.path.join(sandbox, "profiles", "logs-only", "logs")) # no identity file: not a bot
first = run("--save", "before.json")
before = json.load(open(os.path.join(sandbox, "before.json")))
# chats: main 18 (12+6; old chat and subagent excluded), books 1, research 2, reviewer 0 -> 21 total
check("leftover folders not counted as bots", ["books", "main", "research", "reviewer"], sorted(before))
check("main chats in window", 18, before["main"]["chats"])
check("main scheduled runs", 20, before["main"]["scheduled"])
check("main routines (disabled + tagged excluded)", 2, before["main"]["routines"])
check("tagged routine credited to research", 1, before["research"]["routines"])
check("main chat share printed", True, "main bot share: 86% of chats" in first.stdout) # 18/21
check("idle seat flagged", True, any("reviewer" in l and "idle" in l for l in first.stdout.splitlines()))
check("books flagged no routine", True, any(l.startswith("books") and "no routine" in l for l in first.stdout.splitlines()))
check("specialists with a routine", True, "specialists with a routine: 1 of 3" in first.stdout)
# Move the bank sync to books, let it run twice and write memory.
jobs = json.load(open(os.path.join(main, "cron", "jobs.json")))["jobs"]
moved = [j for j in jobs if j["name"] == "daily bank sync"]
jobs = [j for j in jobs if j["name"] != "daily bank sync"]
json.dump({"jobs": jobs}, open(os.path.join(main, "cron", "jobs.json"), "w"))
json.dump({"jobs": moved}, open(os.path.join(books, "cron", "jobs.json"), "w"))
con = sqlite3.connect(os.path.join(books, "state.db"))
con.execute("insert into sessions values ('r1','cron',?)", (NOW - 3600,))
con.execute("insert into sessions values ('r2','cron',?)", (NOW - 60,))
con.commit(); con.close()
open(os.path.join(books, "memories", "MEMORY.md"), "a").write("bank feed lags a day on Mondays\n")
second = run("--compare", "before.json", "--save", "after.json")
after = json.load(open(os.path.join(sandbox, "after.json")))
check("main routines after move", 1, after["main"]["routines"])
check("books routines after move", 1, after["books"]["routines"])
check("books scheduled runs", 2, after["books"]["scheduled"])
check("compare shows books memory grew", True, "books: routines 0 -> 1; memory_bytes 9 -> 41" in second.stdout)
check("compare shows main routine moved", True, "main: routines 2 -> 1" in second.stdout)
check("reviewer unchanged, not listed", False, "reviewer:" in second.stdout.split("Changed since")[1])
# Interleaving: move it back (a revert), then compare against after.json.
json.dump({"jobs": jobs + moved}, open(os.path.join(main, "cron", "jobs.json"), "w"))
json.dump({"jobs": []}, open(os.path.join(books, "cron", "jobs.json"), "w"))
third = run("--compare", "after.json")
check("revert shows routine back on main", True, "main: routines 1 -> 2" in third.stdout)
check("revert keeps books memory (memory is not undone)", True,
"memory_bytes" not in [l for l in third.stdout.splitlines() if l.strip().startswith("books:")][0])
secret_free = "bank feed lags" not in first.stdout + second.stdout + third.stdout
check("memory contents never printed", True, secret_free)
check("exit codes", [0, 0, 0], [first.returncode, second.returncode, third.returncode])
finally:
shutil.rmtree(sandbox, ignore_errors=True)
w = max(len(r[0]) for r in results)
for name, exp, act, verdict in results:
print(f"{name:<{w}} expected={exp!s:<8} actual={act!s:<8} {verdict}")
fails = sum(r[3] == "FAIL" for r in results)
print(f"\n{len(results) - fails}/{len(results)} PASS. Sandbox deleted: {not os.path.exists(sandbox)}")
sys.exit(1 if fails else 0)
All 19 checks should pass. The last block is the one that matters in practice: when you move a job back, the census shows the routine back on the main bot, and the specialist keeps what it learned. Memory doesn't roll back with the schedule, so a failed trial still leaves you a bot that knows a little more.
What not to build
- A bot per business, client or department. That's the org-chart trap. Bots are roles defined by context, and one role can serve three businesses.
- A router bot whose only job is routing. Your main bot already does that. A second layer just adds a hand-off that loses detail.
- Rooms that talk until they agree. Hermes caps group rooms at three rounds for a reason. If three rounds didn't settle it, the call is yours.
- Two bots on one profile, or one shared memory both write to. Hermes warns that two writers on one home "compound each other's state until it stops being anything you configured." Share through files and the brain, never through a shared home.
- Specialists that hold keys they don't need. Create bots with a fresh credential store where you can, and give each one only the keys its desk card names.
- Moving every job at once. You won't know which move helped, and if it goes wrong, you'll fold the whole fleet back in out of frustration.
Steal this: tonight, run the census (or the census prompt) and circle the one scheduled job on your main bot that most clearly belongs to someone else. Write its desk card. Move it Monday, let it run twice, and run --compare. If the bot's memory grew, you just hired someone. If it didn't, you found out cheaply that your fleet is a costume, and the instruction you add next is the first real job description you've written.
Hit reply and tell me what's on your bench this week. I read every one.