Skip to content

Running specs from the UI — stoat runner ​

Not the plan runner

This page is stoat runner, which keeps a test suite ready so you can run its specs from Stoat's web UI. The plan runner (stoat run-plan, and the runner panel on a plan's page) launches agents to work a plan; that is The chapter runner.

Keep a Bobcat suite resident, so the Run this slice button on Stoat's Event Model page can run its specs: started in resident mode, rebuilt and relaunched on every source change, and relaunched at once when a person asks for a new runner.

bash
stoat runner <projectDir>

<projectDir> is the spec project — a .csproj, or the folder holding exactly one. The runner and its status reports go to Stoat's run receiver, http://localhost:5527 (STOAT_RUN_RECEIVER_URL or --url to change it). The runs it makes go there and to wherever this terminal's runs already went — bobcat ui on 5525 by default — with Stoat first, because a resident runner takes its commands from the first URL in BOBCAT_MONITOR_URL.

NOTE

The resident mode itself is Bobcat's and works without this command. This is the convenience around it. It needs Bobcat 1.0 in the suite and, for a projected suite, the bobcat tool.

Which suites can be resident ​

SuiteHow stoat runner runs itModes
A projected suite: specs written as xUnit, TUnit, NUnit or MSTest testsbobcat resident <its test host> beside itcold, warm
A suite whose own Main runs Bobcat (BobcatRunner.Run)the suite itself, with --residentcold, warm

A projected suite's entry point belongs to its test framework, which knows nothing of --resident, so the runner lives beside it: the bobcat tool lists the suite once and, for a cold command, launches its test host, filtered. stoat runner tells the two kinds apart by the project's package references, and needs the tool for a projected suite:

bash
dotnet tool update -g Bobcat.Console

Pass --bobcat <path> to use another copy.

A warm command for a projected suite runs in a test host the tool keeps open between commands. Each command is still its own run, with its own id. What stays warm is the .NET runtime (JIT, loaded assemblies, generated code), not the suite's fixtures, which its test framework creates and disposes for every command. A suite with its own Main keeps its booted host warm instead. Stoat offers warm only for a runner that registers it.

A host that never registers (a suite without Bobcat's resident mode) makes stoat runner say so and wait rather than relaunch. The --resident argument makes such a host fail on an unknown option instead of quietly running its whole suite.

What it does ​

  1. Builds the project with dotnet build, and says so to Stoat ("rebuilding").
  2. Launches it from the checkout's root, because that is where Bobcat reads the repository it registers: bobcat resident beside a projected suite, or dotnet run --no-build -- --resident for a suite with its own Main.
  3. Watches the checkout. On a change — after half a second of quiet — it stops the runner, builds, and launches again. Build output, .git, editor swap files and the like never count.
  4. Reports what the runner cannot say for itself: "rebuilding", "build failed: …" with the first errors, and "exited". A runner that vanished mid-build reads as building, not gone.

Ctrl+C, kill or closing the terminal stops the runner with it.

The rules it keeps ​

  • Every launch is a fresh process from a finished build. No hot reload: patching in place is where Wolverine's and Marten's generated code can go stale, and green-on-stale-code is the one outcome this must never produce. A change that lands during a build makes that build stale, and a stale build is built again rather than launched.
  • A restart is immediate; a crash waits for an edit. "Force a new runner" sends the runner a restart command, it exits 75, and stoat runner relaunches it. An exit code of 0 relaunches only when that launch had registered: a host that never went resident also exits 0, after running its whole suite, and relaunching that would run it in a loop. Any other exit waits for a file to change, because relaunching a runner that crashed would crash it again.
  • One command at a time, and Stoat holds the rest. A resident runner refuses a second command as busy rather than queueing it, so Stoat sends one and waits for its answer. A command refused as busy (as one sent the instant a run finishes can be) is sent again two seconds later; any other refusal is final.
  • A person starts it. The Stoat server never launches a runner: its runner endpoints are unauthenticated localhost endpoints, and a server that launched processes from anything arriving over HTTP would let any local page run code from a folder of its choosing.
  • Your own checkouts by default. It refuses an agent's worktree (Claude Code's .claude/worktrees, or a stoat run-plan branch) unless you pass --agent-worktree: a watching runner there contends with the agent's builds for the same obj/, bin/ and CPU.

Why not dotnet watch ​

The loop is the same one, but stoat runner does three things dotnet watch cannot: it knows a build failed rather than guessing from console output, it relaunches at once when the runner asks for a restart instead of waiting for a file change, and it never patches code in place.

The launch contract ​

What the launched runner finds in its environment:

VariableValue
--resident and BOBCAT_RESIDENT=1an in-process suite reads either (bobcat resident for a projected one)
BOBCAT_RUNNER_ID (or --id to bobcat resident)the runner id: the checkout's folder, the project's name and a hash of the project's path, the same on every start, so Stoat keeps the runner's queue and history across rebuilds (--runner-id to choose one). Per project, because one repository often holds two suites.
CLAUDE_CODE_SESSION_IDremoved. A person presses the run buttons, not the session that started the runner, and Bobcat drops the session from any run that carries a command.
BOBCAT_MONITOR_URLwhere to register and listen

Options ​

Flag
--url, -uStoat's run receiver (default STOAT_RUN_RECEIVER_URL, else http://localhost:5527)
--runner-idthe runner's id, instead of the one derived from the path
--agent-worktree, -arun in an agent's worktree anyway
--debouncemilliseconds of quiet after a change before rebuilding (default 500)
--bobcatthe bobcat tool to run a projected suite with (default bobcat on the path)

Licensed under the Business Source License 1.1.