A kit for running a small fleet of long-lived role bots on your own computer — a manager, developers, reviewers, a personal helper.
Overview
A kit for running a small fleet of long-lived role bots on your own computer — a manager, developers, reviewers, a personal helper. Each bot keeps its identity, its rules, its skills and its sessions across restarts. Orca is the host: a bot is an Orca project and a session is a tab, on Claude Code or Codex. The kit is an npm package with an obk command and a set of skills. You are meant to manage the fleet by talking to Bot Father, the first bot, whose skills call the CLI for you — not by editing files. The design is in docs/prd/prd.md; the decisions with lasting consequences are in docs/adr/; what we know about Orca and the two harnesses is in docs/tech-notes.md. Paste this into any AI session that can run commands — a Claude Code or Codex session in a terminal, or anything else with a shell: Set up Orca Bot Kit on this machine: get https://github.com/guwenqing/orca-bot-kit, then follow SETUP.md in it to the end.
README
Orca Bot Kit
A kit for running a small fleet of long-lived role bots on your own computer — a manager, developers, reviewers, a personal helper. Each bot keeps its identity, its rules, its skills and its sessions across restarts.
Orca is the host: a bot is an Orca project
and a session is a tab, on Claude Code or Codex. The kit is an npm package with
an obk command and a set of skills. You are meant to manage the fleet by
talking to Bot Father, the first bot, whose skills call the CLI for you — not by
editing files.
The design is in docs/prd/prd.md; the decisions with lasting
consequences are in docs/adr/; what we know about Orca and the
two harnesses is in docs/tech-notes.md.
Start here
Paste this into any AI session that can run commands — a Claude Code or Codex session in a terminal, or anything else with a shell:
Set up Orca Bot Kit on this machine: get https://github.com/guwenqing/orca-bot-kit, then follow SETUP.md in it to the end.
It checks what your machine already has, tells you plainly about anything missing rather than installing it, asks you the only two things it cannot work out for itself — where your bots folder goes, and which harness Bot Father runs on — and leaves you talking to Bot Father, which is where everything after this happens.
SETUP.md is that page. It is written for the assistant rather than
for you, so you do not have to read it; nothing in it is hidden from you either.
The rest of this README is the reference: what each command does, and why the kit is built the way it is. Reach for it when you want to know how something works, not to get started.
Reporting a problem
When the kit itself goes wrong (an obk command fails in a way its own message
does not explain, or does something other than what it says), open an issue on
this repository’s Issues page (the address package.json gives as bugs) with
the form there. It asks for the command and its whole output, what you expected,
obk health --json, and the versions of obk, Node, Orca and the harness. Take
out anything private first: issues are public.
Bot Father is told the same thing. Tell it what went wrong and it gathers these details and, when you say so, files the issue or gives you the text to paste.
Status
Early. The CLI creates your bots folder, brings Bot Father up in Orca, creates
bots and their sessions on either harness, and builds each bot’s AGENTS.md
from its charter and the rules it carries.
Install
Node.js >= 24.21.0, git, and Orca with at least one of Claude Code or Codex
installed and configured. That line because the kit keeps writers out of each
other’s way in a bot’s book with a SQLite write transaction, through Node’s own
node:sqlite. That module is older than the 24 line; 24.21.0 is the first
release measured here that loads it without an experimental warning on stderr,
which would otherwise land in the output of every command.
npm install -g @assuredloop/orca-bot-kit
obk --version
The command is obk. If obk --version does not answer after that, npm has no
working release yet (an early placeholder has no obk in it): run
npm uninstall -g @assuredloop/orca-bot-kit, then install from a clone of this
repository with npm install and npm link. To work on the kit itself, run
your clone’s src/cli.js by its full path instead of linking it: the obk on
PATH stays the published one, and the kit calls itself back by the path of the
CLI that started it, so what your clone brings up calls back into your clone.
Releases go out from CI: publishing a GitHub Release tagged v runs
.github/workflows/publish.yml, which runs the suite and publishes that version
to npm with a provenance statement. Nothing else publishes.
Create your bots folder
obk init --bots /absolute/path/to/my-bots --harness claude
--harness claude|codex says which harness Bot Father itself runs on; there is
no default. This makes the folder a git repository and seeds it:
my-bots/
.gitignore # what obk links here, which no clone carries
defaults.yaml # rules and skills every bot gets
skills.yaml # online skill sources
rules/ # your own rule units
skills/ # your own common skills
bots/bot-father/bot.yaml # Bot Father: charter, rules, skills, sessions
The folder is yours. It holds your configuration and none of the kit’s code —
kit skills are linked from the installed package, never copied
(ADR 0014). Run init again
whenever you like: it adds what is missing and leaves everything else, including
your edits, exactly as it is. It does not commit for you.
The .gitignore keeps out the skill links, and the kit’s own record of which
ones it made (.obk-skills.yaml in each bot folder). The links point at
where the kit and your skill sources are installed on this machine, so they say
nothing true on another one, and obk up makes them again wherever you check
the repo out. So a clone carries your bots and not this machine’s paths, and two
computers do not take turns rewriting each other’s links. Anything of your own
in a bot’s skills directories is still yours: git add --force takes it in.
Add a bot and its sessions
obk bot create --bots /path/to/my-bots --name api-bot --harness codex \
--charter 'Api Bot owns the API clone.'
obk session add --bots /path/to/my-bots --bot api-bot --name daily \
--effort high --work-dir work/api \
--prompt 'Keep the API clone green. Ask before you touch main.'
obk up --bots /path/to/my-bots
bot create writes the bot’s folder — bot.yaml, the AGENTS.md built from
that file and the rule units, CLAUDE.md as a symlink to it, and a .gitignore
for work/.
session add writes one session into bot.yaml: its harness (the bot’s unless
it says otherwise), model, effort, context window, start prompt,
work dir and any extra arguments for the harness, and the approval level. A start prompt is either text
(--prompt) or, when it is long, a file in the bot home the session points at
(--prompt-file prompts/reviewer.md), and it reaches the harness as written,
apart from two things the kit does on purpose: blank space at either end is
trimmed, and when the session has a work dir the kit appends its one-sentence
note about it. Anything you leave out is the
harness’s own default; the kit names no model of its own. The approval level is
auto — the harness’s real auto mode — unless you ask for ask or, in those
words, dangerously-skip
(ADR 0015). Only
obk permission approval changes it, after your yes: session add and
session change take no approval, and refuse an extra argument that sets one
(ADR 0041)
or that widens what the session may write or run, such as --add-dir, Claude
Code’s --settings or Codex’s --profile. On Codex, -c passes only for a
short list of keys known to be safe, such as model_reasoning_effort
(ADR 0044).
A Codex session’s work dir stays in its bot folder: Codex may write in its work
dir, so session add and session change refuse one outside the bot folder for
a Codex session
(ADR 0045).
A bot or a session can also run on Pi (--harness pi, tested
with Pi 1.1.0). Pi asks before no tool call, so a Pi session is always
dangerously-skip, and the kit refuses auto and ask for it. Sign in to Pi
yourself first: obk up asks Pi which models it can use, and starts nothing
when Pi has none, or not the one the session names. The kit gives a Pi session
its id, so obk up resumes it after its tab is closed. Pi has no context
setting: name a model that carries it. Mail, temporary sessions, skills and
health for a Pi session come later, and so do obk init --harness pi and
obk session clear and compact on Pi.
Every bot gets the kit’s default permission rules, written by the kit with
nobody asked: its own commands for this bots folder, reading a long message,
git add and git commit, and Orca’s orca orchestration check --run
(ADR 0036).
obk permission, obk retire, obk pause, obk restart, obk bot create
and obk init are not in the set, and keep your yes
(ADR 0041).
A rule beyond the set is written by obk permission allow, after your yes to
the exact rule. A bot that still holds the obk restart or obk bot create
rule, which kit 0.26.0 gave every bot, keeps it: obk rules build and obk up
name it, with the obk permission disallow command that takes it back.
Neither command touches Orca. obk up is what makes it all real there: an Orca
project per bot, a tab per session, each started at the bot home with its own
launch command and its start prompt. It only ever adds — it never closes a tab —
so run it after a restart, or whenever something is missing. --bot and
--session bring up one thing rather than the fleet.
The rules a bot works by
Each bot has one AGENTS.md, shared by all its sessions: its charter, then the
rule units it uses
(ADR 0013). A rule
unit is one short block of always-on working rules. The kit’s own units live in
rules/ in this package; yours go in rules/ inside your bots
folder. What is always on stays short, because every session reads it on every
turn; the depth belongs in skills.
obk rules build --bots /path/to/my-bots [--bot api-bot]
That writes the file, and obk up does the same for every bot it brings up,
before any session starts — a session reads its rules as it comes up, so a
rebuild landing after the tab is open would reach nobody. The build owns a
marked block and nothing else in the file:
# Api Bot
## Charter
...
Write what you like above or below that block and it comes back exactly as you
left it. Edit inside it and the build stops, tells you, and leaves your words
alone — the checksum on the begin marker is how it knows its own text from
yours. A bot carries every kit unit marked applies: all without asking for
them; anything else is named in a rules: list, in defaults.yaml for every
bot or in one bot’s bot.yaml, as kit: for one of the kit’s or a bare
name for one of yours. Codex reads at most 32 KiB of an instructions file and
says nothing when it stops, so the build tells you when a bot’s file goes over.
Nine units suit every bot, whatever it does:
the-ask |
reading the whole request, which questions are yours to answer |
simple |
the smallest thing that answers it, and reusing what is there |
scope |
changing what the work needs and leaving the rest |
evidence |
what you saw, what you worked out, and not inventing either |
finishing |
the check you can run, run on the real thing, reported as it came |
limits |
the charter, what needs a yes, no quiet substitutes |
talk |
answer first, short, real names |
mail |
asking the kit for the road to another session; mail is queued |
profiles |
the few lines the fleet keeps on each bot, for choosing who to hand work to |
Two more are for bots that write code: tests-first, a failing test first and
someone else writing it, and review, someone who did not write the work
reading it.
They are the essence, not the depth: how to debug, how to review well, how to
write a test worth keeping and how to hand work over belong to the skills a bot
picks. They are also defaults, not fences — a bot uses the ones its owner
picks, your own units sit beside them, and a bot does what its user asks of it.
Why the set is this size is in
docs/proposals/41-common-rules.md.
A unit is a markdown file whose frontmatter carries name (the file’s own
name), title (the heading it gets in AGENTS.md) and applies (all or
code); the body is the rule text and nothing else.
test/kit-rules.test.js holds that shape, and the
size the always-on set may reach.
The skills a bot uses
The kit ships its own skills in skills/, one directory per skill,
named obk-
(ADR 0019). They are
organised by technique rather than by role: a role is a charter plus a choice
of skills. Each one gives techniques and defaults, never a process or
a way of working you have to adopt, and carries what it needs itself rather
than relying on another skill being loaded.
obk-tdd |
a failing test first, a separate test author, the mutation check |
obk-reviewing |
giving a review that is worth acting on, and answering one |
obk-debugging |
a check that goes red first, the real cause, proof where it broke |
obk-arch |
what is wanted, the shape, runnable and testable, work in slices |
obk-grilling |
interviewing a plan until it holds, and writing down what was settled |
obk-handoff |
stopping somewhere safe, and the note a cold reader can act on |
obk-recall |
where a piece of work actually stands, before carrying on with it |
obk-why |
why the code is like this, and how sure you are |
obk-teach |
explaining something so it lands, at the other person’s pace |
obk-personal-facilitation |
keeping someone’s own list true, and stopping before the step that cannot be taken back |
obk-researching |
finding it out from sources, and saying how well you know it |
obk-writing |
writing for a reader, in one kind of piece, with the claims checked |
obk-decision-memo |
the conclusion first, the alternatives, and the argument against it |
obk-bot-building |
what a bot owns, the charter interview, and the skills the job needs |
obk-fleet-review |
one verdict per bot, the cheaper answers first, and where a problem goes |
obk-grooming |
the daily pass: only what changed, one output checked, one short report |
obk-finops |
what a result cost, live prices, and which limits are really enforced |
A skill is where the depth lives. The always-on rules say that a change in
behaviour starts with a failing test someone else wrote and is read by someone
who did not write it; obk-tdd is how to write a test worth keeping and find
out whether the tests would catch a real mistake, and obk-reviewing is how to
read a change so the findings are worth acting on, and how to answer a reading
of your own.
A bot gets the ones its lists name:
obk skills build --bots /path/to/my-bots [--bot api-bot]
skills: in defaults.yaml gives them to every bot and skills: in a bot’s
bot.yaml to that one. An entry is kit: for one of the kit’s, a bare
name for a directory in skills/ inside your bots folder, or anything with a
/ in it for a skill directory anywhere on disk. obk up links them before it
opens a tab, and bot create gives a new bot what the lists already name.
A bot whose sessions are running gets a changed list without a restart.
When skills build links or takes away a skill, it tells each running
session of that bot. A Claude Code session gets /reload-skills typed into
its tab: Claude Code’s own reload, which a busy session runs when its turn
ends. Codex has no reload command and takes the change at the start of its
next turn, so nothing is typed there. The kit prints the new skills’
SKILL.md paths, and says that a restart would make a skill appear if it is
still missing after that turn. A session the kit could not tell is named,
with the reason.
What lands in the bot is a symlink, never a copy
(ADR 0014) — into
.claude/skills and .agents/skills in the bot home, which is where each
harness reads a project’s skills from. So a kit skill is read where npm
installed it, a running session reads an edited skill the next time it loads
it, and updating the kit updates every bot at once.
The kit takes away only what it can prove it put there. It writes down every
link it makes, in .obk-skills.yaml in the bot folder, and when no list names
one any more it takes that link away, whichever shelf it came from, as long as
the link is still the one it wrote. Anything else in those directories is left
alone and shown as yours: a skill you wrote there, a link of your own, or one of
the kit’s that you have since pointed somewhere else. If you want one of those
gone, delete it yourself; the skill a link points at is never touched.
Skills from somebody else’s repo
skills.yaml in your bots folder lists where else skills come from:
sources:
- name: someones-skills
repo: https://github.com/someone/skills
path: skills # subfolder inside the repo, optional
ref: v1.2.0 # branch, tag or sha
obk skills fetch --bots /path/to/my-bots [--source someones-skills]
obk skills update --bots /path/to/my-bots [--source someones-skills]
fetch clones each source into .skill-sources/, a sibling
of your repo and never inside it, at the version you pinned, and writes the sha
it got back beside your ref. A source that is already there it leaves exactly
as it is — no network, no moving — even when the ref is a branch that has gone
on ahead. update is the asking, and it is the only thing that moves one. So
the version your bots run changes when you say so and at no other time; obk up
never fetches.
A bot names one of those skills : in its skills: list, beside
the other three forms. Moving a source moves what every session reading it sees,
with no rebuild, because the link points at the clone.
The kit says one line when a source carries scripts or hooks. It does not read them and does not stand in your way — a third-party skill is your risk to take (ADR 0014).
A skill is a directory holding SKILL.md — frontmatter carrying name (the
directory’s own name) and description, then the body — plus any reference
files it links to and a NOTICE.md naming the sources it was built from and
their licences. The notice travels with the skill, which is what the licences
ask for, and stays out of SKILL.md, which a bot reads into its context every
time the skill is used. Those two keys are the only frontmatter both Claude Code and
Codex read, so a kit skill carries nothing else, and its links stay inside its
own directory, because a bot gets the directory alone.
test/kit-skills.test.js holds that shape.
Sessions that come back
Each bot keeps a book, sessions.yaml in its folder: which Orca tab each session
lives in, which harness session it is running as, and every id it ran as before,
with why that one ended and when
(ADR 0012).
The book stays true to the harness through a hook obk up puts in the bot’s own
folder — .claude/settings.json or .codex/hooks.json, never your user-level
settings (ADR 0022). Your own
settings in those files are kept. Codex asks you to trust a hooks file the first
time it sees one; answer its question in the tab.
A Claude bot gets one more hook there. Claude Code’s own messaging reaches every
Claude session on the machine, so after a session sends a native message to an
address that is none of your bots folder’s sessions, the hook tells it so, and
names the address obk message to would have given. The message goes either
way (ADR 0032).
So: kill a tab, or reboot, and obk up brings the session back with the
conversation it was having, rather than starting a new one. Clear a session —
/clear on Claude Code, /new on Codex — and the kit writes down the new id,
keeps the old one, and gives the session its start prompt again, because that
prompt is what tells one session’s duty from another’s.
Ask the kit to clear or compact a session, rather than typing into its tab:
obk session clear --bots /path/to/my-bots --bot api-bot --session daily
obk session compact --bots /path/to/my-bots --bot api-bot --session daily
Each waits up to 30 s for the session to be idle, and types nothing into one
that is busy or has a question on its screen. It types the harness’s own
command, checks the screen before it presses return, and then confirms the
result: for a clear, the new conversation in the book; for a compact, the
compaction in the harness’s own record, within 5 minutes, or it says it is not
confirmed yet. A harness that does not offer /compact is told so, and nothing
is entered. A cleared session reads its AGENTS.md again, so a clear also
brings it onto changed rules.
A session paused before its first turn can have an id in the book and no
conversation behind it: the harness has nothing on record for that id. There is
nothing to resume, so it starts again with its duty, and the book moves the id
into its history as no conversation.
One tab holds one session: the harness the kit started in it. Anything that
session runs inside the tab — a codex exec, a helper, a subagent’s own process
— is not the session, and never becomes the conversation the kit brings back.
And when the kit cannot say which conversation a session is, it does not pick one. The usual reason is a Codex hooks file trusted after the session had already begun talking: nothing was recorded for that first conversation, and trusting the file does not go back for it.
A bot’s sessions all live in one folder, and so does every harness they start
inside themselves, so a conversation sitting in that folder says nothing about
whose it is — and neither harness writes down anything that ties a conversation
that has ended to the session that had it. So the kit writes what it found into
the book as unclaimed, tells you the ids, and starts the session on a fresh
conversation with its duty rather than guessing. To bring one back yourself,
write it into sessions.yaml under that session as session: and run
obk up again.
Restarting a session
A restart is rare and never automatic: obk up only ever adds, because closing
a tab takes your screen away and Orca’s resume record with it. When you do want
one, ask for it.
obk restart --bots /path/to/my-bots --bot api-bot [--session daily]
It closes the session’s tab — one tab, by its own handle — and opens a new one
with the conversation the book holds, so the session comes back as itself. With
no --session it does that for every session of the bot.
What it will not do, in order of how much it would cost you. It closes only a
tab your book names, so Bot Father’s ops tab and anything you opened yourself
are left alone. It will not close a tab whose conversation the book cannot name:
that close would be the end of that conversation, so it refuses, tells you which
session and which file to settle it in, and touches nothing. The one exception
is a tab with only its shell in front: its harness has already quit, so there
is nothing in it to lose, and restart closes it and starts the session fresh
with its duty. It will not close
anything at all until everything that would stop the session starting again has
been settled — the same refusals obk up gives, made before the tab goes rather
than after. If Orca refuses to close a tab it stops there, rather than start a
second harness beside the first. And it waits for Orca’s own listing to agree
that the tab has gone before opening the new one, because Orca answers a close
before it stops reporting the tab.
Changing, pausing and retiring
Bot Father does these for you when you ask; these are the commands it runs.
obk bot change --bots /path/to/my-bots --bot api-bot --charter "…"
obk bot change --bots /path/to/my-bots --bot api-bot --role-cap reviewer=2
obk permission allow --bots /path/to/my-bots --bot api-bot --rule 'Bash(gh pr merge:*)'
obk permission disallow --bots /path/to/my-bots --bot api-bot --rule 'Bash(gh pr merge:*)'
obk permission approval --bots /path/to/my-bots --bot api-bot --session daily --approval ask
obk permission approval --bots /path/to/my-bots --bot api-bot --temps --approval dangerously-skip
obk session change --bots /path/to/my-bots --bot api-bot --session daily --model opus
obk session change --bots /path/to/my-bots --bot bot-father --session daily --harness codex
obk skills remove --bots /path/to/my-bots --bot api-bot --skill kit:obk-tdd
obk pause --bots /path/to/my-bots --bot api-bot [--session daily]
obk unpause --bots /path/to/my-bots --bot api-bot [--session daily]
obk retire --bots /path/to/my-bots --bot api-bot [--session daily]
A change to a charter or a setting is written at once, and a running session
takes it when it next starts. A session can move to the other harness with
--harness, Bot Father’s management session too: it then starts a new
conversation there, the book keeps the old one in its history, and its model,
effort and context go back to the new harness’s defaults unless you give them
too. A pause closes the tabs the way a restart does, with the
same refusals, and one more: it also refuses a tab with only its shell in front
whose conversation the book does not name. obk up leaves what is paused
closed until obk unpause brings it back with its conversations. Retiring a
session takes it off the bot and keeps its conversations in the book under
retired. Once its tab is closed, it stops what the session left running in
its work dir: each of your processes whose working folder is there, and what
they started, with SIGTERM and, after up to 3 seconds, SIGKILL. It names each
process it stopped, and each it left: one that only shares a process group with
them, or one it could not stop. Where it cannot read the processes, as inside
Codex’s sandbox, it says so and stops nothing. Retiring a bot closes
its tabs, removes its Orca project and moves its folder to retired/; it will
not touch a bot whose Orca project holds a tab your book does not name. The
folder moves only once Orca no longer lists the project; if Orca still lists it,
or its list cannot be read, retire says so and leaves the bot where it is, and
retiring it again finishes the job. Bot
Father itself, and its management session daily, are never paused or
retired; its other sessions are like any bot’s.
Daily grooming
Grooming is an optional daily pass over the fleet: what each bot did, what it
cost, and what to improve, sent as one short report to Bot Father’s management
session. It runs in a session of Bot Father’s called grooming, on Claude
Code’s own scheduler, at the model and effort you give that session.
obk session add --bots /path/to/my-bots --bot bot-father --name grooming --model sonnet --effort medium
obk up --bots /path/to/my-bots --bot bot-father
obk groom --bots /path/to/my-bots --now # run it once, by hand, and read it
obk groom --bots /path/to/my-bots --on --at 04:00 # then schedule it
obk groom --bots /path/to/my-bots # what is scheduled
It starts off, and Bot Father asks you before it turns it on, because it spends
tokens every day. It fires only while its tab is open in Orca and Claude Code is
idle there, up to half an hour after its time, and a day it misses is not made
up. Each run renews its own schedule, which Claude Code would otherwise end
after a week, and --on --at puts it back if it lapses. A /clear in its tab
leaves it running until the session is next restarted; after that, --on --at
puts it back.
obk groom --compact compacts its conversation between runs.
The grooming session is always a Claude Code one, even on a Codex Bot Father (add
it with --harness claude there): it is the clock, and where the reports arrive.
To have the grooming itself done on Codex, turn it on with --run-on codex, and
give the runs their model and effort:
obk groom --bots /path/to/my-bots --on --at 04:00 --run-on codex --model gpt-6-sol --effort medium
Each fire then makes a temporary Codex session of the grooming session’s, named
groom-, which does the grooming, mails its report back, and is
retired; the grooming session sends the report on to the management session. A
run an earlier fire left behind is retired at the next one. Each run also
carries the grooming session’s own prompt, as --on finds it: after you change
that prompt, run obk groom --at alone, which keeps the runs on Codex.
When something is wrong
obk health --bots /path/to/my-bots [--bot api-bot]
It reads your setup and says what it found, in plain sentences with --json
beside them for Bot Father to read. It writes nothing at all — not a file, not a
link, not a tab — so everything it finds is yours to decide about.
What it looks for:
- configuration that will not work: a
bot.yamlnothing can read, a key in it the kit does not know (a typo such asefort: high, which nothing reads), a session the kit would refuse to start, a bot with noAGENTS.md, a block somebody edited by hand, a file bigger than the 32 KiB Codex reads, aCLAUDE.mdthat is not this bot’s rules, and a hooks file without the kit’s own hook in it, which is how a book goes stale without anything saying so; - a skill that is not where its list says: a listed skill missing from one of the two harnesses, a skill of your own standing where a listed one would go, and a link with nothing at the end of it;
- a session the book knows that Orca does not: its tab was closed, or the
machine was restarted, and
obk upbrings it back; - a session whose tab is open and whose harness is gone: only the tab’s
shell is in front, because the harness quit or crashed.
obk upleaves an open tab alone, so the finding names theobk restart … --sessionthat brings it back; when the book names no conversation for it, restart would refuse, and the finding says to close the tab in Orca and runobk up … --session. A tab the kit cannot read is never called down; - a Claude session on an address the kit did not make: such as the bare
.every fleet’s sessions had before 0.10.0, which a session of another fleet can write to. Its next start through the kit gives it one of its own, so the finding names theobk restart … --sessionthat does it; - a process still running in a retired session’s work dir: one from before retire stopped them, or one it could not stop, with its pid and command;
- leftovers no book owns: an Orca project inside your bots folder with no bot in it, a tab in a bot’s project that the book does not name, a start prompt written for a session that has gone, a clone of a skills source you no longer list, and the conversations the kit found and would not assign;
- a session running on something other than its bot asks for: a model,
effort, context or approval level that the harness’s own record of the
conversation says differs from
bot.yaml, and a session that started before its bot’sAGENTS.mdlast changed, with what brings it up to date. Beside the findings, each running session gets a line saying, for each setting, whether it matches, differs, was not asked for, or cannot be told from the record; cannot be told is never taken for a match; - Orca’s own default launch arguments. Orca adds these to the agents it launches, relaunches and resumes, so when they carry a permission bypass every session runs in that mode whatever the kit asked for. The kit never sets that setting and says so every time it is on — including when Orca has no entry for a harness at all, because Orca’s own default for a missing entry is the bypass.
It exits 1 when it found something, so a script can tell. Nothing it prints is a verdict: which of these matter, and in what order, is for you or for Bot Father.
Sessions and bots that talk
Ask the kit how to reach a session, and it answers with the road and the address (ADR 0042):
obk message to --bots /path/to/my-bots --to api-bot/daily
obk message send --bots /path/to/my-bots --to api-bot/daily \
--subject 'the schema changed' --text 'orders.total is cents now'
obk message check --bots /path/to/my-bots --bot api-bot --session daily
Two roads, and a bot never picks. Claude Code to Claude Code in the same
approval class (auto and ask are one class, dangerously-skip the other)
is the harness’s own messaging: message to answers with the
session’s name, .., which obk up puts on its launch
line, and the sending session writes to that name itself, because no command
can send that message for it. The token is new for each new conversation and
kept when one is resumed, because Claude Code refuses a name that other fleets’
sessions also answer to. Ask message to each time rather than keeping the
name. Everything else goes through Orca’s mailbox, which the kit does carry.
That includes a Claude session still running under a name from before the
token, .: message to answers the mailbox road for it, never
that name, until the kit’s launch line next starts it (obk restart, or
obk up after its tab is gone), which gives it a token name of its own.
A session’s mailbox is made the first time the kit starts it, and written in the book beside its tab. It is an Orca Run rather than the session’s tab, because a tab’s address dies with the tab and a Run does not — so a message sent while a session is down is still there when it comes back.
Orca lets a tab bind a mailbox to itself and to no other, so the mailbox is made
and bound from inside the session’s own tab: the launch line the kit types there
runs obk session mailbox before the harness. For the same reason a session’s
mail is read in its own tab. message check from any other tab reads nothing,
binds nothing, and says so; the mail waits.
For one session, that step, a message check, and up writing a new tab for it
into the book take turns. So two starts of the same session at once leave its
mailbox bound to the tab the book names, and no mail is read in the other one.
Each waits a minute at most for its turn. A step that waits it out leaves the
mailbox where it was and says so, and the harness starts all the same. A check
holds its turn for 40 seconds at most: with more mail than it reads in that
time, it shows what it read and says the rest is still waiting.
Nothing in a mailbox wakes anybody, so each message gets one signal: Orca’s
own notice, which Orca types into the receiver’s tab once it is idle. A busy
receiver gets it a minute or a few after its turn ends. The kit types nothing
into the receiver’s tab, and the send says which road it took. For urgent mail,
obk message send --interrupt presses Escape once in a busy receiver’s tab to
end its turn, and then sends the message, so the notice comes at once. Codex
keeps a command it was running as a background terminal after an Escape. A
receiver reads its mail with obk message check, which marks it read: a bare
orca orchestration check leaves it unread, and Orca then gives no more
notices for that mailbox
(ADR 0042).
A message longer than 4 KiB is written to a file beside your bots folder and named in the message, so a long one arrives whole without landing a document in somebody’s context.
One thing to know about Codex bots: the kit launches them with
-c sandbox_workspace_write.network_access=true. Without it, the Orca CLI run
from inside a Codex session answers that Orca is not there — reachable: false,
and every mailbox call refused — so a Codex bot can neither read its mail nor
send any. Why is not established; what is, is that those calls fail without the
switch and work with it. It widens that session’s sandbox to the network generally — there
is no localhost-only setting — and that is the price of a Codex bot being in the
fleet at all. Turn it off for a session with your own
--extra-arg=-c --extra-arg=sandbox_workspace_write.network_access=false, and
the kit will report that session as out of reach of fleet mail rather than
failing quietly.
Working on the kit
npm test # the whole suite, in a few minutes
npm run test:system # what the system tests would drive, and nothing else
npm run test:system -- --yes # drive them, on this machine, for real
npm run test:system -- --yes test/system/groom.test.js # only the ones you name
npm run mutate # the mutation audit, on what your branch changed
The tests come in two layers, and every test file belongs to one of them.
npm test is test/*.test.js and test/dev/*.test.js: unit tests and
end-to-end runs of the CLI in a throwaway folder, with a fake orca on PATH. It
needs nothing but Node, so GitHub Actions runs it on
every pull request, split into shards that run side by side, on Node 25.8.0, the
current line. A release runs every shard on 25.8.0 and again on 24.21.0, the
floor engines.node promises users, with only the kit’s own dependencies
installed, before anything is published. The floor leaves out test/dev/, the
tests that need a development tool.
npm run test:system is test/system/*.test.js: the real obk against the
real Orca and the real harnesses on your own machine. No CI runner can do that,
so these are run by hand before a change that touches Orca or a harness is
merged. A system test touches only what it creates and removes it afterwards,
except the mailbox Runs Orca gives no way to delete, which the run names; it
never closes a tab it did not open. The trust its harnesses write into your
~/.codex/config.toml and ~/.claude.json for the run’s own throwaway folders
is taken out again after the run, and named; nothing else in them is changed.
An Orca project the run leaves in one of its own throwaway folders is named,
with its setup id, and fails the run; the command removes none itself.
They run on the machine the command is typed on, so the command will not start
them by itself. On its own it says what it is about to drive — whose machine,
which Orca, which files, and what they will do to it — and then stops without
driving any of it, answering non-zero so that a run which did not run them
cannot be read as one that passed. -- --yes is how you say you meant it. When
Orca is not answering, the command says which Orca it asked and skips, rather
than report a kit that is not broken. Name files after it to run only those;
each must be one of the *.test.js files under test/system/, links followed,
and a name that is not is refused before anything runs. This command is the
only way in: a system test loaded any other way, with node --test or an
editor’s runner, skips and says how to run it.
npm run mutate runs StrykerJS over the
JavaScript this branch changed against main — committed, still in the working
tree, or not tracked yet — and reports the mutants the tests do not kill. It is
an audit, not a step of every pull request
(PRD 7.3): the everyday
check is the implementer’s own hand check, and the tool runs over the whole
suite at a milestone or when the owner asks, or once, narrowed to the changed
logic, for a change at the core of the kit. A surviving mutant is a change to
the code that no test objects to. It is worth a test only where it shows a gap
in behaviour the requirement cares about, and that test is written by the
separate test author. Name targets yourself to check a whole area instead:
npm run mutate -- 'src/**/*.js'
Every mutant means running the tests again, so the check does not run them the
way npm test does. It runs scripts/mutation-suite.js
instead: the same test files, one at a time, stopping at the first file that
fails, trying the file that killed the last mutant first and then the quickest.
On a ten-core machine, src/up.js as it was when this was measured (128 lines,
76 mutants) took about twelve minutes that way. Running the whole suite for every mutant, as it used to, took
forty, and counted a mutant as killed whenever a run of it ran out of time. A
change across several files takes proportionally longer, so check one or two at
a time.
The check still takes minutes where the suite takes seconds, which is why it is a command of its own. StrykerJS is a development dependency of this repo, pinned to an exact version and needing Node 22 or newer; the kit itself neither ships it nor needs it.
Recommended Tools
Try a different keyword or remove a filter.
Install
npx skillfish add guwenqing/orca-bot-kit