Managing agents like a team
Source: docs/guide.md · v0.1.2
The setup treats agents like staff on a line, each with a clear role and limited rights:
- The owner (you) decides what gets built and when it ships. Only you approve a hard line, create release tags, and change repo settings. An area lead of another model may approve its area (records 0026 and 0047).
- The lead is the session you talk to. It plans, splits the work, reviews, and opens pull requests through a bot, never with your account.
- Workers are sessions the lead starts for one task each, in their own worktree
under
.worktrees/, withYOKOTEN_ROLE=worker. They change code and report back. They never push. A fix task also getsYOKOTEN_FIX_TASK=1. Then the guard and the commit check stop a change to a test file. The stop hook runs the brief’s done-when command. After three refusals the worker may stop. Its message to the lead starts with “NOT DONE: checks failing”. In a linked worktree,.yokoten-agent.jsonrecordschecks. The lead reads that file. Its/api/stateshows it as the commit check. - The bot is a GitHub App installed on the repo. It opens pull requests. If it has
the Workflows permission, CODEOWNERS still sends every CI change to you, and a release
waits for you in the protected
releaseenvironment. A ruleset requires your review, so nothing it opens merges without you.
A worker runs its commands under the guard, so write the brief to fit it:
- Start with
cd <worktree> && cmd && cmd. Thecdstands on its own, joined by&∨: not in a subshell, a pipe or the background. Use no pipe after it. - No
git -C:cdinto the worktree first. - No
VAR=valuebefore a command, exceptTZ,CI,NODE_ENV,NO_COLOR,FORCE_COLOR,DEBUG,LANG,LC_ALLand the namesyokoten.jsonlists inworker.settings. - If the task needs another setting, such as a test’s capture file, the lead makes it and hands the worker the file.
Seven skills carry the method. dispatch briefs a worker. inspect reviews one exact
commit in three passes. rca finds the cause, adds a check, then spreads it. record
writes decisions down. release picks the next version and prepares the changelog. The
owner tags. merge merges through the bot and stops when the new commit is not UTC.
yokoten adopts the practices. Three agents do the work: worker, reviewer (reads
only), and verifier (runs, never fixes).
Who reviews what
Review follows risk. The tier on a pull request’s first line says how hard a mistake is to undo (record 0004):
| Tier | Kind of change | Who reviews |
|---|---|---|
| C | Standard: docs, wording, styles | The lead |
| B | Normal: other code, with tests | An agent of another model reviews; the lead approves |
| A | Hard to undo: owned files, or a change that makes a rule looser | The owner, or an area lead of another model for a path that is neither a hard line nor a looser change |
In yokoten’s own repo, .github/CODEOWNERS lists the owned files: the core rules,
CODEOWNERS, CI that publishes or holds a secret, the bot token, guard config and
package.json (record 0037). In a new project, init writes one for the
lead-only paths. With “require review from code owners” on, the
host asks the owner to review a pull request that changes one of them.
check --pr ties the two together. A pull request that changes an owned file fails
unless its first line is tier: A. The paths set the lowest tier. The author can raise
a tier, never lower it. The check reads CODEOWNERS from the base branch, so a pull
request can’t edit the file to lower its own tier. Without a base commit, on a local run,
it reads the working tree. The message says which one it read. No CODEOWNERS file, no
check.
An approval is for one commit, and it must come from a code owner of the pull
request’s changed files: the owner’s login approves as a collaborator, so the account
never counts, only CODEOWNERS membership does. When the changed files name no owner,
any owner named anywhere in CODEOWNERS counts. yokoten bot refuses gh pr merge of
a tier A pull request whose newest code-owner review is not an Approve on the head. A
newer changes-requested or dismissed review refuses too, while other reviews never
count. When the head is newer, the merge goes on only for a clean merge of the base
branch. The pull request’s own diff must be the same as at the Approve. Otherwise it
refuses: the Approve is on gh pr status through the bot shows the same check for
tier A pull requests. Keep GitHub’s dismiss-stale-approvals setting on too, but
yokoten never depends on a host setting a project may not have.
Area leads approve in their area
Record 0026 moves the lock for paths that are not a hard line from your code-owner review to yokoten’s merge check. An area lead approves with a GitHub review. The last line of the review body names the lead, its vendor and its model:
Approved by area lead Sonnet (Anthropic, Claude Sonnet 5.5)
GitHub records the commit the review was given on. A later commit needs a new review.
The lead must be another model than every writer on the Made with line (record
0047). Claude Sonnet may approve work by
Claude Opus, and a lead from another vendor may too. The check compares names, not
spellings: claude-opus-5-5 and Claude Opus 5.5 are one model. When the Made with
line names no writer, the check refuses. A file that .github/CODEOWNERS owns, and a
looser change, still needs your approval. The release pull request changes
package.json, so it always needs you. Until the diff tier check (#697) lands, a change to a test that already
exists counts as looser; a new test does not.
Nothing here changes your repository settings. When you are ready, follow these steps.
- Check that
.github/CODEOWNERSlists only the hard lines. In this repo it already does (record 0037): the core rules,.github/CODEOWNERSitself, CI that publishes or holds a secret, the bot token, guard config andpackage.json. If a path there is not a hard line, remove it: a path you remove becomes lead-approvable. - Keep one required approval. A lead’s review is a real GitHub review, so keep “Required approvals” at 1. Do not let GitHub Actions approve a pull request.
- Keep the CI check required. The branch ruleset should still require the CI check
that runs
yokoten check --pr. The bot’s merge check is the second lock: it reads the lead approval beforegh pr merge. - Turn off “Require review from Code Owners” when the hard lines are safe. After step 1 the setting names only hard lines, so leaving it on still asks you only for those. Turning it off lets the bot’s check decide the rest.
yokoten doctor still expects code-owner review today. Expect that one step to note the
change until it learns the new lock. When a merge is refused, the message names the
reason and the next step: a lead of another model, a new review on the head, or the owner
for a hard line or a looser change.
A reviewer finds the tier through labels, so check --pr also reads them. A pull request
needs one type label and one tier label. A type label is a title type (feat, fix,
docs, refactor, ci and the others) or a common name for one: bug, hotfix or
incident for fix, feature or enhancement for feat, documentation for docs.
The tier label is tier A, tier B or tier C (tier: B also counts). It must match
the tier: line in the body. Each missing or wrong label gets its own message.
Server setup, step by step: docs/server.md.
Review bots you will see on pull requests
A review bot reads a pull request after you open it. It posts comments on the pull request page. You may see a walkthrough of the change, a risk level, a list of pre-merge checks, and a suggested fix. Some bots can push that fix.
Two kinds exist:
- Commercial. The bot is closed source. The company runs it, and your code goes to the company’s servers. Many are free for public repos and paid for private ones.
- Open source. You run the bot yourself, with your own model API key. You pay the model vendor for the tokens it uses, and the code stays on your machine.
Prices, licences and data handling change. This guide names no bot as a recommendation. Read the bot’s own page before you turn it on. A bot’s comment usually links to it.
yokoten works before the pull request, on your machine:
- the guard stops a risky command, a secret or a private detail;
- the
inspectskill reviews the change in your agent session, at no extra cost; - the checks and the merge rules stop a merge or release you did not approve.
A review bot works after the pull request. The two fit together. The bot sees the pull request; yokoten sees the session that wrote it.
You do not need a review bot to start. yokoten’s own review is enough. Add a bot when
your team wants a second reader on the pull request page. doctor never asks you to
add one: it names yokoten’s own next step.
yokoten’s findings on the pull request page come from its own checks only. It does not repeat another bot’s findings or review the code a second time (#91).
Keeping pull requests up to date
Every merge leaves the other open pull requests behind. A conflict waits for nobody, so
the lead runs yokoten pr sync after every merge. It merges each branch’s base branch in,
runs typecheck and the tests of the touched files, and pushes as the bot. This runs after
every merge, clean or resolved, and in UTC so the merge commit is too. It resolves only
two mechanical conflicts: two added blocks at one spot, and two changed import lists.
Anything else stops with the files and the hunks, and the lead hands them to a worker with
the dispatch brief. Each report line names the base it merged. The list stops at 100 open
pull requests, and the report says when it did.
The owner never resolves a conflict on GitHub. The web editor would commit as the owner, and the bot must be the one that pushes. Related: issue #320, the merge train.
The desk
A desk is a fenced place for one AI to do one task, the same for every vendor (#637). Start one with an issue number or a task in quotes:
yokoten desk 42
yokoten desk "fix the login bug" --agent codex
yokoten desk makes the desk ../<repo>-desks/<n>/ (its own clone, branch desk/<n>),
writes a brief, and starts the AI there inside the fence. It never pushes; --pr pushes
the branch and opens the pull request. When the AI exits, it runs the check and the tests
in the fence and shows the evidence. With no words, it lists your desks.
The older form, for scripts, starts any command in a fenced clone you made yourself:
git clone ../yokoten ../desks/42
yokoten desk claude-code ../desks/42 -- claude -p "$(cat brief.md)"
The fence is one contract. Each system builds it its own way:
| Rule | In the desk |
|---|---|
| Writes | Only in the desk folder (.git too, so the worker commits) and the desk’s own state folder for the agent. .git/config and .git/hooks stay read-only |
| Reads | Everything else the agent can see is read-only |
| Other projects | Not visible. Your home and the desk’s parent folder are hidden. On Linux, /run is hidden too: its sockets lead out, and on WSL2 one of them starts Windows programs. Node, the agent program and yokoten are shown again, read-only |
| Your agent state | Not visible. Your ~/.claude, ~/.codex, ~/.grok and Muse folders stay hidden: no settings, hooks, memory, MCP config, history or sessions |
| Child processes | Work, with pipes, so tests can start programs |
| Network | Commands get no network of their own. The proxy outside lets through only the agent’s API hosts. Any command inside can use the proxy too, so a command can reach those hosts |
| Environment | YOKOTEN_ROLE=worker and TZ=UTC. No token or key from your shell (see What an agent’s environment holds) |
| Team | The team folder is read-only. Only the agent’s own outbox in it is writable (see Messages between agents) |
The desk must be its own git clone. A worktree’s git folder lives in another checkout,
which the desk hides, so a worktree is refused.
Who the agent is. The desk reads the model and provider from the agent’s own config:
for Codex, model and model_provider in config.toml. The kanban shows that model. When
its vendor is not the harness’s own, the session starts with one line that says who the
agent is, and that the harness is its tool. A DeepSeek model runs through Codex, whose
prompt names OpenAI; without the line, the worker signs its work as OpenAI.
The desk’s state folder. Each desk gets its own state folder for the agent, next to
it: <desk>.state/<agent>. The desk points the agent there with the agent’s own variable.
Only the login file is copied in from yours. A newer login of yours is copied again on the
next start. Remove the state folder with the desk.
| Agent | <agent> |
Points the agent at its state with | Login file copied | API hosts | Command inside the desk |
|---|---|---|---|---|---|
| Claude Code | claude-code |
CLAUDE_CONFIG_DIR |
.credentials.json |
api.anthropic.com, and platform.claude.com to renew a login |
claude -p "<brief>" |
| Codex, and DeepSeek through Codex | codex |
CODEX_HOME |
auth.json |
api.openai.com, api.deepseek.com |
codex exec --sandbox danger-full-access "<brief>" |
| Grok | grok |
GROK_HOME |
auth.json |
api.x.ai |
grok -p "<brief>" |
| Muse Code | muse |
XDG_CONFIG_HOME, XDG_DATA_HOME |
muse/auth.json |
api.meta.ai |
muse exec "<brief>" |
- Claude Code renews a login at
platform.claude.com. A token fromclaude setup-tokeninCLAUDE_CODE_OAUTH_TOKEN, or an API key, needs no renewal and passes in as a login. - Codex runs with
--sandbox danger-full-accessinside the desk. Its ownworkspace-writesandbox makes.gitread-only and blocks the pipes that tests use. The desk is the fence instead. Yourconfig.tomlstays out: give a provider with-con the command. A ChatGPT login also needs--host chatgpt.com,auth.openai.com. - Grok: turn off
--sandboxinside the desk if it stops a commit or a test. - More hosts:
--host <host>lets the agent reach another API host on port 443. Separate several with commas. - More files:
--read <path>shows another path, read-only. A path that would open your home, a hidden folder or your own agent state is refused.
The proxy. Each of the four agents names HTTPS_PROXY in its program, and the desk sets
HTTPS_PROXY, HTTP_PROXY and ALL_PROXY in both cases. It also sets
NODE_USE_ENV_PROXY=1, so Node’s own fetch uses the proxy too. The proxy takes CONNECT
only, to a listed host, and never reads the traffic. It keeps at most 64 tunnels open, and
closes a tunnel that is still for 10 minutes. When it refuses a host, the desk names the
host at the end.
Review from outside. The worker writes in the desk, so treat the desk as untrusted. Do not run git or any other command inside the desk before you check the work. Fetch the branch into your own checkout, then review it there:
git fetch ../desks/42 HEAD:refs/desks/42
git diff main refs/desks/42
.git/config and .git/hooks are read-only in the desk, so a worker can’t plant a hook or
an alias there. Set an upstream or other config from outside, before you start the desk.
The commit check and your private marks. The commit check reads your private-marks
file. An agent that can read the file can also print it. So the desk hides it by default,
and the check in the desk does not see your marks. Until marks are kept as hashes (#665),
run the check yourself, outside the desk. Check out the fetched branch in your own
checkout, then run yokoten check there; it checks the whole tree. --marks shows the file in the
desk, read-only. Use it only for an agent you trust with the words.
Systems.
The fence is Anthropic’s
sandbox-runtime (srt), the sandbox
Claude Code uses. yokoten runs srt from PATH when it is the pinned version, 0.0.81.
Otherwise npx gets that version once and keeps it in npm’s cache. It is not a dependency
of yokoten. When the sandbox can’t start, the desk says why in one line and starts nothing.
- Linux and WSL2: install
bubblewrap,socatandripgrep. On Debian and Ubuntu:sudo apt install bubblewrap socat ripgrep. The desk gets its own empty network, and only the agent’s API hosts pass srt’s proxy. - macOS:
sandbox-exec, built in. Not yet run on a Mac: runnode --test test/agents/desk/start.test.tsthere once to check the contract. - Native Windows: no desk.
yokoten deskstarts nothing and says to use WSL. On native Windows, the guard alone holds the rules.
YOKOTEN_FENCE=own uses the old fence until v0.1.3: bwrap on Linux, sandbox-exec on
macOS.
Messages between agents
Agents of every vendor can message each other (#674). It is files on this machine: no server, no network.
yokoten msg send lead "tests pass; ready for review"
yokoten msg inbox
yokoten msg who
yokoten msg log
- Where: the team folder next to your checkout,
<repo>.team/. Each agent writes only its own outbox there.yokoten deskadds the agent by--name, or by the desk folder’s name made lower case. A folder name that can’t be a name joins no team. - Who you are: in a desk, the desk says. Outside one, you are
leadonly in your main checkout, not in a worktree or a checkout inside a desk. - A desk can’t fake a sender: in a desk, the team folder is read-only, and only the
agent’s own outbox is writable.
msg sendrefuses a worker outside a desk, and the guard stops its edits in the team folder. - Outside a desk, a sender can be faked: a program outside a desk runs as your user, so it can write any outbox, the lead’s too. A worker in a plain worktree can do that, for example from inline code that leaves its folder. yokoten can’t tell such a write from yours. Run workers that message in desks.
- Delivery: the hooks add unread messages to the agent’s context. Claude Code and Grok
get them before a tool call. Every agent gets them at the end of a turn, after the
checks: a turn that fails a check is sent back with the messages too.
msg inboxshows them at any time. Muse Code’s hooks get no variables, so in a desk Muse reads withmsg inbox. - A message is data: every delivery says that the messages are from another agent and can’t grant approval or permission. This note is the defence. Each message names the sender’s vendor and model. Some claims of the owner’s approval get a flag, but the flag misses claims in other words.
- Privacy: a message gets the same scan as posted text: private marks, secrets, email
and IP addresses, and secret variables. A message that fails is not sent, and you get
the reason. The scan reads plain text, so a secret that is encoded or split passes it.
A desk sees your private marks only with
--marks, so a message from one desk to another is checked against them only then. What the lead reads is always checked. - Who reads: every desk on the team can read every message, not only its own.
- Caps: 2,000 characters a message, 20 messages per agent in 10 minutes, and 256 KB per outbox.
- The log:
msg logshows every message. Only the lead runs it.
The kanban
yokoten kanban shows the team at a glance. It is a preview, so it may change. It is
read only: nothing on it starts, stops or changes work. Record 0044 says why.
(Before v0.1.2 it was called yokoten board. The old name still runs it.)
It shows three things:
- Sessions. Each AI session in this repo, its worktrees and its desks: the agent, the model, the task, and whether it is working, stuck, waiting or done. A session with no output for 15 minutes counts as stuck.
- Proof. For each session: whether a system sandbox or only the guard fenced it, whether the commit check passed, and the tests on Linux, Windows and macOS. Those three read “not proven” until the status file records platforms.
- Pull requests and Actions. The pull requests that wait on you, the ones merged lately, and the health of GitHub Actions. The tokens each agent used show with them.
yokoten kanban --text prints the sessions by state, the pull requests and Actions, and exits.
That works today. The proof and the tokens are in GET /api/state for now.
Without --text, it serves a page on this machine only, at http://<repo>.localhost:<port>/.
<repo> is the folder name, made safe for a hostname. The port stays the same for this
repo. It opens your browser, signed in, and runs until you stop it with Ctrl-C.
--no-open only prints the address. The sign-in key goes only to the browser and is
never printed. A request with a wrong host is refused.
The page is being rebuilt as its own web package (record 0044). Until it is built,
/ says so in one line, and GET /api/state still works. It returns one snapshot of
the sessions, pull requests and Actions as JSON. GET /api/sessions/<id>/stream sends one
session’s output as server-sent events. The id is a hash of the session’s folder, never
the path. Any request but GET or HEAD gets a 405.
The kanban reads GitHub through gh, in the background, one read at a time. When a read
fails, it keeps the last lists. The guard checks each agent, and pushes and merges
still go only through the bot.
Agents show by themselves. Start an agent in this repo, in one of its worktrees,
or in a desk (yokoten desk), and yokoten’s hooks keep its status in
.yokoten-agent.json at that checkout’s root. This works for every vendor:
- The session start writes
vendor,model,role(leadin the main checkout,engineerfor a worker, by the guard’s own rule),worktree,state: workingandstarted.startedis written once. Grok has no session-start hook: its first call writes them. - Each allowed tool call sets
doingandupdated, at most once every 5 seconds. - A refused call sets
blocked. The next allowed call clears it. - The stop sets
state:donewithchecksfor a worker,waitingfor the lead.
The hooks write the file only in a repo with yokoten.json. A worker never writes the
main checkout’s file: that one is the lead’s. The first write adds the file to the
repo’s own .git/info/exclude, so git status never shows it. A desk is a plain
clone, so git worktree list misses it: yokoten desk notes it in the repo’s git
folder, and the kanban shows it too.
An agent may also write the file itself, for task; the hooks keep the fields they do
not set. It leaves by removing the file. The format is open: other tools may read it
too. One file per worktree.
| Field | Means |
|---|---|
name |
The agent’s name. Without it, the worktree’s folder name is used |
vendor |
Which vendor it runs on, such as claude-code, codex or grok |
model, effort |
The model and effort, such as opus and medium |
role |
lead, engineer, reviewer, verifier or helper |
task |
What it works on, such as PR #615 Run rules locally |
worktree |
The worktree it works in |
doing |
What it does now, in a few words |
state |
working, waiting (it needs you) or done |
checks |
passing or failing, from its last test run. It is in GET /api/state as the commit check |
updated |
When it last wrote, such as 2026-10-22T10:00:00Z. Quiet for 15 minutes shows as stuck |
started |
When this work started, such as 2026-10-22T08:00:00Z. Without it, the file’s birth time is used, then updated |
blocked |
A guard stop that needs a decision, in one line |
Every field is text. Every field may be missing. A file that is not a JSON object is
skipped. A state or checks outside the words above is ignored.
What agents write shows only as text, never as page code, and is masked like a session.
The kanban loads nothing from the internet. Its interface is /api/state; the page is
only one view of it.