Running it day to day
Keeping the chapter runner resident and steering it from the plan page, sharing a machine and its infrastructure between agents, and what each worker starts with and costs.
Resident, and steered from the plan page
Resident: --watch
Without it, the run ends when nothing is left that it could do. With it, the loop stays resident and picks up new ready work as it appears — a node unblocked upstream, an issue someone just approved, a node added to the plan — until a stop condition or .stoat-stop ends it.
Steering it from the plan page
A resident run registers with Stoat as the plan's runner, and the plan page gets a panel for it:
Start / Pause.
--pausedmakes the run register and wait for a person's Start, so a plan can be set up and then released from the page. Pause stops new claims and nothing else, like.stoat-stop: agents already out there finish, and the loop stays resident to be resumed.↻ restart on a node the runner is working, or gave up on. A running agent is stopped first, the claim released, and the node launched again from a fresh attempt. Its worktree stays.
What it is waiting on a person for joins the ready board's "needs a human" strip, as callouts:
go-ahead: an issue ready but without its go-ahead;question: an agent that exited with a question for a person;give-up: a node given up on;unmergeable: a node done but not landed (a conflict on the integration branch);fix: an agent's PR still red or conflicting after its fix attempts;merge: a merge-on-green PR the runner is holding, with why;main-red: the default branch is red, holding every PR that fails only main's checks;drift: the base would conflict with the integration branch;cloud: a node kept for a cloud agent, ready and not launched;review: a PR waiting on a person's review;limit: a stop condition it reached;usage-limit: agents paused on a usage limit, shown as information.
A callout about a node an agent of the runner ran carries the way back into it:
cd <worktree> && claude --resume <session>, on the panel, the card and the strip. Ago-aheadorlimitnever does, and neither does an agent that is still alive.A runner that went away says so. Each
run-plan --watchregisters anew, so a crash or a restart leaves the old one behind. A runner with no open stream readsgoneat once when its process is dead on this machine, anddisconnectedafter 30 seconds (inside them the panel says "reconnecting…"). Either reports nothing running and no callouts, only what it was doing; a command to it is refused rather than left pending.A ready node says why it is not launching. The plan's status carries its runner (
GET /api/plans/{slug}/status,plan_statusandnext_ready_nodesover MCP all read the same field):json"runner": { "state": "paused", "paused": { "by": "stoat-ui", "at": "2026-10-26T14:12:00Z" }, "parallel": 2, "slotsInUse": 1, "runnerId": "bobcat-ui-phase-0-466aa99a", "host": "box" }stateisnonewhen no runner is registered, else what the runner reported (running,paused,waiting,limited,stopping) orgone/disconnected. Each ready node of a kind the runner launches, with nobody on it, carries alaunchsaying why it is not being launched, or that it is next:launch.reasonMeans pausedheld by the pause: "runner paused by stoat-ui at 2026-10-26 14:12Z" waiting-for-slotevery slot is in use: "waiting for a slot (2/2 in use)" no-runnerno runner attached, it is gone, or it works another checkout than the node's repository; the detail names the command to start one stopping/limitedthe runner is stopping, or waiting for a usage limit to reset heldthe runner gave up on it, or its agent asked a person and waits for an answer resourcea shared resource it uses is full go-aheadan issue still waiting for a person's go-ahead nextnothing holds it: the runner launches it on its next poll A lead or supervising agent reads it before waiting on a launch that cannot happen. The card shows a ⏸ paused badge on every ready node the pause holds (and waiting for a slot for a full runner), and the runner panel says who paused it and how many of its
--parallelslots are in use. The node's zoom gives the same sentence as its readiness. Presentation only: the derived status and readiness never change.
stoat run-plan serverless-with-outbox ~/code/wolverine --watch --paused --agent-args "--dangerously-skip-permissions"The runner is always the client: it registers, holds a command stream open, and reports its state. Stoat never launches a process, so a plan with no runner (or one that has gone) says so and shows the command to start one, rather than a button that could not work. Stoat being unreachable never stops the run; the page just cannot steer it until Stoat answers again. People only: there is no MCP tool for any of this, as there is none for the go-ahead.
Shared resources
Parallel agents in one repository share its infrastructure: one Postgres, one Docker daemon, one port. A plan can say what it has and how many nodes may hold each at once:
plan: wolverine-providers
resources:
postgres: { capacity: 1 }
nodes:
- id: postgres-tests
kind: test-run-gate
uses: [postgres]declare_plan_resources and PUT /api/plans/{slug}/resources set the same map, replacing it. A node's uses must name a declared resource, and a resource something still uses cannot be removed.
The runner leases a node's resources all or none before spawning it, counting what running and verifying nodes hold. A ready node held back reads "waiting on postgres" on its card instead of "ready", while its derived status stays as it is. Only run-plan honours capacity: an agent working the plan over MCP takes no lease. --parallel is still the cap for the machine.
This machine is shared
The worktree bounds an agent's file edits. It bounds nothing else.
A slice agent doing routine cleanup — pkill -f testhost, docker compose down, dotnet build-server shutdown — reaches into every other session on the box. The prompt tells every agent not to touch processes, containers or services it did not start.
WARNING
Shared infrastructure is not per-worktree. If the spec suite resets a database, every parallel agent resets the same one — and the failure presents as another slice's scenarios failing for no visible reason. Stoat cannot make a repository's infrastructure per-worktree, but it can hand each agent its own name for it.
A database or schema per agent: --agent-env
--agent-env KEY=VALUE... sets variables in each agent's environment, filled per node. {ident} is the plan and node folded into a lower-case SQL identifier (at most 63 characters, never truncated onto a neighbour's), for a schema or database name:
stoat run-plan crittercrush-v3 ~/code/crittercrush \
--agent-env "ConnectionStrings__marten=Host=localhost;Database=postgres;Search Path={ident}" \
--agent-setup "psql -d postgres -c 'create schema if not exists {ident}'"--agent-setup runs once in each new worktree, with that environment, before its agent starts — a failing setup releases the claim and starts nothing. The variables are printed on the spawn line and named in the agent's prompt as its own, with an instruction not to "fix" them back to the shared database. What the application does with a variable is the repository's business: an ASP.NET Core app reads ConnectionStrings__marten as ConnectionStrings:marten with no code at all.
The base drifting
Every five minutes a resident runner with an integration branch fetches the base (--base, else the remote's default branch) and asks git how far behind the branch is and whether merging it would conflict (rev-list --count and git merge-tree --write-tree --name-only), touching no worktree and merging nothing. A conflict is a drift callout naming the files and the base's commits that touch them. Behind but clean is a line on the runner panel only, because catching up is a person's decision.
Usage limits
One account's quota covers every agent the runner launches, so they all stop together when it runs out, and all can carry on together when it resets. The runner recognises a limit in what an agent printed or ended with, in the forms the tools say it: Claude Code's You've hit your session limit · resets 4pm (America/Chicago) (and its older usage limit reached|<epoch>), an API rate_limit_error, codex's try again in 1h 5m and gemini's Quota exceeded … retry in 34s / RESOURCE_EXHAUSTED. Ordinary text about limits is not one.
- Never a question, never a failure. A limited agent raises no
questioncallout and counts toward no attempt, no--max-fix-attemptsand no consecutive-failure stop; the attempt it was on is given back. - Paused until the reset. While any limit has not reset, the runner launches nothing new and resumes no fix (each would die on its first request); merges and cloud agents, on another quota, go on. Its bar says
paused for a usage limit — resumes at 16:02 CDT, and oneusage-limitcallout lists the agents, shown in the top bar as information rather than counted, and off the notification defaults. - Resumed at the reset. Two minutes after the reset it read (or 30 minutes after, when the message named none), each limited agent is resumed in its own session and worktree, under its claim, told to carry on where it stopped: staggered, within
--parallel. A launcher that cannot resume (codex, gemini) has its node launched again instead. - A hang on a limit is caught. An agent still alive whose last output, two minutes old, is a limit message is not "fixing it": the runner stops it and resumes it at the reset like the rest.
What a worker starts with
Most of what a worker costs is what it carries into every call: its model, its tools and its context. Left alone, a worker would run on your own interactive model with every MCP server, skill and plugin you have installed, and each attempt's first call can carry over 100K tokens before the agent does anything. Four things decide what a worker starts with.
1. An explicit model. run-plan never leaves the model to ~/.claude/settings.json. The most specific word wins:
| Who says | Where |
|---|---|
| the node | model: on the node (a hard node asks for a bigger one) |
| the run | --model |
| the plan | model: in the plan's header |
| Stoat | the launcher profile's default: claude-sonnet-5-5 for claude (defaultModel in ~/.stoat/launchers.json changes it) |
codex and gemini have no default, so with nothing asked their own setting decides, and the launch says so. The run's banner and every launch line name the model:
→ attempt 1 on 'cli-flags' · claude-sonnet-5-5 · in ~/code/bobcat-cli-flags · log: ~/code/stoat-agent-cli-flags.log
→ attempt 1 on 'run-data' · claude-opus-5-5 (node) · lean · in … · log: …2. A worker MCP profile. Workers get <--url>/api/mcp?profile=worker, the 12 tools a worker uses, not all 71: ~15.7K characters of tool schema per call instead of ~69K. Supervising sessions keep the full surface at /api/mcp. The runner also puts each worker on the agent roster when it launches it (declare, the longest lease), so a worker's report_activity renews a declaration rather than being refused with "session has no agent declaration".
3. --lean: no user context. With --lean, each claude worker also gets --strict-mcp-config --setting-sources project,local, measured on Claude Code 2.1.310:
plain claude -p | --lean | |
|---|---|---|
| tools | 197 (166 MCP: claude.ai connectors, plugins' servers) | 28 + Stoat's worker tools |
| skills | 161 | 22 + the ones it keeps |
| plugins | 5 | the bundled ones + Stoat's |
It drops: every MCP server but the one --mcp-config names (Stoat's), ~/.claude/settings.json (the user's model, hooks, permissions and enabled plugins), and with it the user's skills in ~/.claude/skills. It keeps: the repository's own CLAUDE.md, .claude/settings.json and .claude/skills, Claude Code's built-in tools and bundled skills (no flag drops those short of --disable-slash-commands, which drops every skill), and auto memory. The skills a lean worker keeps are the plan's skills: plus the one the runner's prompt names (critterstack-sdd-code-from-specs), the installed ones only, handed over as one generated plugin at ~/.stoat/worker-skills/<plan>/ (--plugin-dir, one link per skill; a skill the plan names but ~/.claude/skills lacks is said at start). Without the sdd skill a slice worker is pointed at the repository's own conventions instead. Dropping user settings drops Stoat's own session hooks too, so a lean worker's permission prompts are not seen as awaiting input: give it a posture that has none (--dangerously-skip-permissions). Only claude has a lean mode; --lean with another launcher is refused.
4. Cost you can see. Each attempt's cost (Claude Code's total_cost_usd) is posted to the node as it exits (POST …/nodes/{id}/cost), so the plan page's header shows the plan's running total ($37.87 · 5 attempts), the node's status carries spentUsd, and the Activity has a line per attempt. The runner prints this run's total on every exit line. --max-total-usd works without --budget-usd: it then counts the cost the agents reported, which lags (an agent reports as it exits), so it stops claiming at the cap and the agents still running can carry the total past it. With --budget-usd it still counts the budgets handed out, the hard bound. A killed attempt reports nothing, so the total is a floor.
Agents get the Stoat MCP server
Every agent the claude launcher starts is handed this host's MCP server (--mcp-config '{"mcpServers":{"stoat":{"type":"http","url":"<--url>/api/mcp?profile=worker"}}}', inline, so nothing is written into the worktree; it wins over a repository .mcp.json server of the same name), and its prompt names the claim's identity (plan, node, --agent, session) to pass to ask_person. A launcher whose profile takes no mcpArguments (codex, gemini) is told instead how to ask over REST (POST /api/plans/{slug}/nodes/{id}/questions, which does not wait) and to end with a Question for a person: paragraph. With an --allowedTools posture rather than --dangerously-skip-permissions, allow mcp__stoat__* too, or the agent is asked before every call.
When an agent asks a person
An agent asks with ask_person and parks; one that cannot (it exited, or its launcher has no MCP) ends with a paragraph starting Question for a person:. When an attempt settles, the runner keeps standard output back from Claude Code (--output-format json, so the final message, cost and turn count are data), writes the final message to the log as text, and raises a question callout from what the agent asked, or from its closing words when its node did not go done. An asked question holds the node, never retried away, until a person answers it, dismisses it, or restarts the node. A node that went done with nothing asked raises nothing, and a launcher with no JSON result (codex, gemini) has nothing guessed from its text.
Answering goes through the top bar's chat, which resumes the agent in its own session with the answer: see Answering an agent.
Launchers: not only Claude Code
How an agent is started is a launcher profile: a command and argument templates.
| Profile | ||
|---|---|---|
claude (default) | verified | claude -p {prompt}, with --max-budget-usd, --model (default claude-sonnet-5-5), a preset --session-id so the claim and the agent's test runs link exactly, --mcp-config naming this Stoat host's worker MCP profile, and a lean mode |
codex | unverified | codex exec {prompt}, with --model. No spend cap, no preset session |
gemini | unverified | gemini --prompt {prompt}, with --model. No spend cap, no preset session |
Unverified profiles are written from each tool's documented flags and say so when chosen. Add or replace profiles in ~/.stoat/launchers.json (or --launchers <file>):
[
{
"name": "aider",
"command": "aider",
"arguments": ["--message", "{prompt}", "--yes"],
"modelArguments": ["--model", "{model}"],
"postureFlags": ["--yes"]
}
]Placeholders: {prompt} (required), {budget}, {model}, {session}, {mcp}, {skills}, each in its own list so a tool without one leaves it out. defaultModel is what its workers ask for when nothing else names a model, leanArguments what --lean adds, and skillArguments how a lean worker is handed its skills' plugin directory; a profile with no leanArguments refuses --lean. Asking for something a tool cannot take is refused, not dropped: --budget-usd with the codex launcher stops the run before it starts, because an unenforced spend cap is not a cap. The permission-posture check uses each profile's own postureFlags. A tool that does not load the critterstack-sdd skills is pointed at the repository's CLAUDE.md or AGENTS.md instead.
The honest limit
Stoat can start a headless agent of any tool that has a non-interactive mode, and offer work to agents that are already running. It cannot push a task into an interactive session of another tool that is already open. Those agents pull: from the ready board, over REST (GET /api/plans/ready, then claim), or through the await_* tools.
See also
- The chapter runner — the flags, and the rules it follows
- A person's controls — what waits on you, and answering an agent