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 (kept on the plan itself since 0.6.0, so it survives the runner; see When an agent asks 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 machine-resourcea machine resource it uses ( heavy, say) is held by another node, namedexclusive/draininganother node has the machine reserved; or this exclusive node is waiting for the machine to drain agent-capthe plan's agent cap is full session-limita repository's or the machine's session limit is full, or held at zero 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 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, and a lead agent cannot relay the runner's commands either.
Starting a runner from the page
A repository's band with no runner shows the command to start one in a terminal, and Start…, which launches it on the Stoat server's own machine. The dialog shows exactly what it will run, a stoat run-plan <plan> <checkout> --watch --url … --parallel … --launcher … --agent-args … line, and lets you change the checkout, the launcher, the slots and the agent args first. It reads the repository's settings on this machine, else the last runner registered for it here; what you start with becomes the repository's settings (PUT /api/repositories/{owner}/{name}/runner-launch sets them without starting).
The runner runs detached, in a process group of its own, so stopping Stoat or closing its terminal leaves it running. Its output goes to ~/.stoat/runners/{plan}-{owner}-{name}-{time}.log, and the band shows its start-up lines until it registers; from then on it is like any runner. Start is refused, with the reason, when the checkout is missing or is another repository's, a .stoat-stop sits in it (the runner would stop on its first poll), a runner already works the plan from that checkout, one started from the page has not registered yet, or the agent args name no permission posture.
Those agents act without asking, so the start refuses any request that is not Stoat's own page on this machine: a caller on another machine or behind a proxy, a host name that is not loopback (DNS rebinding), a request without the page's X-Stoat-Person header (which a browser sends cross-origin only after a CORS preflight Stoat never answers), and one whose Origin is not the server's own. Stoat:RunnerStarts=false turns the start off on a server. macOS and Linux only.
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.
Heavy work: one at a time on the machine
A plan's resources are the plan's. Some work is heavy for the whole machine: two local gates that each start a dozen containers will overload it whichever plans they belong to. uses: [heavy] needs no declaration and runs one at a time across every plan, runner and repository, while light work carries on beside it. A resource declared with scope: machine (docker-fleet: { capacity: 1, scope: machine }) is the machine's in the same way, and two plans declaring different capacities share the smaller. A card's ⛰ toggles heavy (and 🔒 exclusive) without clearing its go-ahead.
Unlike a plan resource, the server enforces it, for every claim, by hand or by a runner. A node holds it while it has a live claim, or while a stoat gate exec of it is still running on this machine, claim released or not. A claim on another node using it is refused with who holds it ("waiting on heavy, held by polecat/pc-gate for 41 minutes"), its card reads held, and the runner skips it. A stoat gate exec start waits for room and says who holds it every five minutes; --no-wait records at once.
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-attemptsor--max-rebasesand no consecutive-failure stop; the attempt it was on is given back, on the node's record too. - 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 16 tools a worker uses, not all 91. Measured when the profile had 12 tools and the full surface 71, that was ~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. A lean worker keeps every skill the plan's nodes use, the installed ones only, handed over as one generated plugin at ~/.stoat/worker-skills/<plan>/ (--plugin-dir, one link per skill; a skill a node names but ~/.claude/skills lacks is said at start). 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.
Fix budgets you can see. Each time the runner resumes an issue's agent on its own PR, it records which budget that spent on the node (POST …/nodes/{id}/fix-attempt): own against --max-fix-attempts, base-moved (another PR's merge made it conflict) against --max-rebases. The card's PR line says rebase 1/5 or fix 2/2 (red once spent), the Activity has a line per resume, and a restarted runner starts from what is recorded rather than from zero, so stopping and starting a runner never buys a stuck node more attempts. A restart of the node clears both.
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. Claude Code's per-model tokens ride along: the plan page and a node's zoom view show them per model with cache reads and writes in their own columns, the node and plan status carry usage, the plan_usage MCP tool gives a supervising session the same by model and by node, and the Activity says when an attempt used a model the launch did not ask for. Each attempt also records what it carried into its first call, which is how to tell whether --lean and the worker MCP profile are paying off. 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.
Holding work on the box
--parallel caps one runner and the agent cap caps one plan, but some conflicts belong to a repository or the machine. A long gate is running, or you're about to reset docker. Session limits are held by the server, across every plan and runner, and survive restarts. Zero is the hold: Hold on a repository's band, or the machine's hold in the top bar, stops new claims there at once, and every held node says who set it and why. The band says "held by {person} since {time}" and offers Release, which puts the earlier limit back. Work already running carries on, and a runner keeps its slots for it. The machine's own hold and limit are in the app's top bar on every page.
From a script, so a gate can bracket itself:
stoat limit --machine 0 --reason "PushGate"./build.sh DockerReset PushGatestoat limit --machine releasestoat limit JasperFx/marten 1 lets one session work in that repository, and stoat limit JasperFx/marten clear removes the limit. A new node needs room in the plan's cap, its repository's limit and the machine's. --parallel is then a request: a runner gets the lowest of them, and a limit lowered while runners are up takes effect at their next launch. A limit doesn't stop a hand-run stoat gate exec, which is how you run your own gate while the rest is held.
Local gates in place of CI
An issue node's quality check is GitHub CI, which doesn't fit a repository whose real gates are local. CritterWatch's CI is parked and its gates are ./build.sh CommitGate and ./build.sh DockerReset PushGate, so a PR there has nothing to be green on. A repository can declare those gates with a role:
declare_repository_gatewithrole: commitis a gate a worker runs on its final commit. The worker's prompt tells it to runstoat gate run <plan>/<node>, which records the commit it ran on.role: pushis a gate that runs before work reaches the default branch.checkout: primaryis for a gate that can't run from a worktree, such as one that resets docker.stoat gate runrefuses to run it from a worktree, and no worker is told to run it. Name it with--stepfrom the primary checkout.timeout: 90mkills the gate past that time. While it runs, a Bobcat suite under it shows its progress on the zoom view.
A pass counts only on the commit it ran on, with a clean tree, under the declared command, so a rebase makes it stale. A worker's word moves nothing. A resumed worker is told what its gate said, the same way it's told about red CI.
Merge-on-green reads CI until you say otherwise. On the Repositories page, stand in for CI on a gate lets merge-on-green read that gate in place of CI: it merges when every gate standing in passed on the PR's head, and holds with the reason otherwise. Only a person can turn this on, because an agent able to set it could merge on a gate that proves nothing. Changing the gate's command or checkout takes it back.
What a plan needs from you
The top of a plan page says what the plan needs from you, grouped by action: answer a question, start a runner (with the exact command), start a paused one, give a go-ahead, merge, accept partial work, do a node that's yours, bump a package, bind an issue. When work is waiting and nothing is running, the plan is stalled: the panel says so in a banner, and it also appears on the ready board. plan_status carries the same answer as needs, so a lead agent reads what you read.
A consume node whose package has shipped reads "2.97.0 has shipped: bump this pin", which names an action. One still waiting on its upstream says so, and nothing is needed from you.
A plan across several repositories
A runner works one checkout, so a plan across five repositories needs five. The plan page draws each repository the plan touches as a band, runner or not, upstream repositories first, and the band's header is that repository's runner: its state, what's ready there and why nothing is happening. Start… or Pause on a band moves that runner alone, and Start all and Pause all move them together. A band with ready work and no runner shows the command to copy, or starts one for you (Starting a runner from the page). A runner with callouts still stands open above the board, so nothing hides one. plan_status returns the same rows as runners, in the same upstream-first order. A second runner on a checkout that already has one is refused when it starts, because two runners in one checkout would launch into one working tree.
Letting a runner create issues
An issue node doesn't have to be bound to a GitHub issue first. Turn on runners create issues on the plan header, and a person can give an unbound node its go-ahead straight away. The runner in that repository then creates the issue from the node's title and brief, binds it, keeps the go-ahead and launches it. The Activity records who created which issue for which node. It's off by default and only a person can turn it on (or the plan's live lead relaying your word with set_realize_issues), because an issue may be public. If a runner dies between creating the issue and binding it, the next one finds the issue by a marker in its body rather than creating a second.
When a worker didn't finish
An issue closes on Fixes #n whatever the pull request delivered, and missing work breaks no test. So an issue's worker is told to write Fixes only if every item is done, and otherwise to write Part of #n, list what's left, and report it with report_node (leftOut, scopeDone, scopeTotal). The issue's scope stands whatever a brief lists.
A node with reported leftovers reads partial, even if its issue closed, and what depends on it waits. It shows in the top bar and says what was left out. To go on anyway, press accept on the card (c): the node counts as done, and what was left out stays on it. send back takes an acceptance back, and the node reads partial again. Restart the node, or add a node for the rest, to get the rest done. A worker that finishes the rest reports leftOut: [], which clears it. A report newer than your acceptance holds the node again.
Nodes a person does
Some work belongs on the plan because other nodes depend on it, but no agent should do it: a manual review, recording a demo. Mark an issue, slice, gate or review node with assignee: person, or use kind: person for a task with no issue behind it. No runner launches one, next_ready_nodes doesn't offer it, claim_node refuses it, and it needs no go-ahead. When it's ready it shows in the top bar as an action for you, with how many nodes it unblocks. An assignee: person issue is done when its issue closes, or when you press mark done on the card. A kind: person node is done when you press it. Nothing else can be marked done: every other node is done by what Stoat observed. An agent can't take assignee: person off a node.
Which skills a node's work uses
Installed skills are only a list a worker may or may not look at, and with a full pack the listing has no room for every description. So a node says what its work uses, and each session that takes it is told. skills: on a node wins, then skills: on the plan, then a default for the kind: a slice's is critterstack-sdd-code-from-specs, and the other kinds have none.
plan: storefront
skills: [critterstack-sdd-bobcat-authoring] # for a node that names none
nodes:
- id: import-board
kind: issue
repo: JasperFx/storefront
skills: [critterstack-sdd-event-model-import]Every launched prompt, for every kind, tells the worker to load them before it starts, naming only the ones it can load: from ~/.claude/skills, a lean worker's plugin, or the repository's own .claude/skills. One that's missing is said when the node launches. It's a request, not a force: the worker decides. A session that isn't launched by run-plan hears the same list, because claim_node, next_ready_nodes and get_node all carry it as skills. stoat setup says which skills your plans use that aren't installed.
A cap on agents across a plan
--parallel caps one runner, and a runner works one checkout, so a plan worked from several repositories can have more agents than any one runner sees. The agent cap on the plan header caps the whole plan: every runner and every agent working by hand. A claim past it is refused and the refusal names who holds the slots. A claim that is stalled, or parked on a question, still holds its slot; one whose session has gone does not. A slot frees when a claim is released or its lease lapses. The cap is a person's: over MCP only the plan's live lead relays it (set_agent_cap), and it's never part of an export.
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 keeps on the plan what the agent asked (or its closing words, when its node did not go done), with the session, the worktree and the resume command. It is stored on the plan's own stream, not in the runner, so a restarted runner or a redeployed Stoat loses nothing, and it shows on the card, the top bar, the strip and in notifications like any question. An asked question holds the node, never retried away, until a person answers it, dismisses it (the chat's Dismiss), or restarts the node. Closing words never hold it. 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.
A question parks the node, not the attempt. An attempt that ends with a question for a person still open, asked through ask_person or in its final words, spends no attempt: the runner carries the open question to the plan before it releases the claim, so the node waits for the answer instead of running on to a give-up.
Answering goes through the top bar's chat. The runner for the node's repository delivers the answer: it resumes the agent in its own session and worktree when that is on this machine and the launcher can resume, and otherwise the node's next attempt carries the question and the answer in its prompt. 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