Skip to content

The chapter runner — stoat run-plan ​

Build a plan's Event Modeling slice nodes unattended: for each ready slice, claim it, spawn an agent in its own git worktree, and let the slice's spec-identity gate decide whether it is done.

bash
stoat run-plan <plan-slug> <projectDir> --agent-args "--dangerously-skip-permissions"

IMPORTANT

Read the shared-machine note before choosing that permission flag.

What it does each poll ​

  1. Reap agents that exited, and release their claims — whatever the exit code. The gate is the verdict, not the process.
  2. Read the plan's derived status from the host.
  3. Announce anything newly done, skipped, or given up on.
  4. Claim, then spawn for each ready slice, up to --parallel, staggered.

The decisions are a pure function (ChapterRun.Decide) over the plan's status; the command is process and HTTP orchestration only.

The rules it was built around ​

Each of these is a failure someone paid for operationally.

It claims before spawning, and the prompt names the claim. An agent left to re-derive "the next ready node" finds a different one and orphans the claim it is holding.

One worktree per agent, created as a sibling of the project directory (<project>-<nodeId>) on branch stoat/<plan>/<nodeId>. Shared trees lose updates.

Spawns are staggered (--stagger-seconds, default 5). Simultaneous starts race.

Retry is bounded and the give-up is loud (--max-attempts, default 2). The worktree is left in place so the work can be inspected.

Nothing is ever asserted. An agent exiting cleanly moves no status. Done means the slice's bound {Feature}/{Scenario} identities are green, and nothing else.

No automatic merge. A green gate proves the specs pass in the worktree, not that the merge composes. Finishing prints the command and leaves the decision to a human:

✓ 'propose-home-check' is done — its specs are green. Merge when ready:
    git -C /path/to/app merge stoat/my-plan/propose-home-check

.stoat-stop in the project directory halts new claims without touching work in flight.

Two refusals before it starts ​

The runner would rather not start than burn a run that cannot succeed.

No permission posture ​

If --agent-args names no permission flag, every agent stops and asks before its first file write, and the run makes no progress — presenting as a hung agent, not a missing flag. The runner refuses and says so.

It takes no view on which posture, only that one was chosen. Any of these counts: --dangerously-skip-permissions, --permission-mode, --permission-prompt-tool, --allowedTools / --allowed-tools, --disallowedTools / --disallowed-tools.

Override with --allow-prompting (-p).

Evidence is blind ​

Checked on the first status, not before the loop — the runner has to see the plan's own view of its evidence, which only exists once the host has answered.

If every slice reports unknown, the evidence source itself is absent: no run console, or a spec suite publishing to none. Every agent would do its work correctly and every slice would still run to give-up. Every, not any — one unknown slice is ordinary, its specs simply have not run.

Override with --allow-unprovable (-e).

Why these have explicit short forms

Two flags starting allow- both want -a, and an ambiguous alias is not resolved — it is dropped. -a would print usage and not start the run, reading like a malformed command rather than a naming clash. Hence -p and -e.

BOBCAT_RUN_TAG — how a gate finds the right run ​

Every agent runs the whole suite from a worktree holding only its own slice's work. Without a tag, each reports on every other slice's identities from a tree that does not contain their fixes — and the last agent to finish decides the plan.

So the runner stamps BOBCAT_RUN_TAG=<plan>/<nodeId> on the process it spawns. Bobcat's publisher reads it with no configuration, and the gate reads it back.

WARNING

The prompt tells the agent not to touch it, because observed agents do. A first real chapter run produced five tag shapes across ten slices: the canonical one, bare node ids, a truncated node id, and one with a timestamp appended. Every non-canonical one made a genuinely passing run invisible to its own gate — so the agent finished the work and the slice ran to give-up.

Matching accepts the canonical form and the bare node id, and stops there. Fuzzy matching is the dangerous direction: a tag attributed to the wrong node marks a slice done off another slice's run, which is the exact failure the mechanism exists to prevent — and these ids share long prefixes. An unrecognised tag stays ignored; losing evidence is the safer error.

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. An unscoped pkill has already cost another session about 23 minutes of clean test lanes. The prompt carries an explicit fence against all of it, and that fence is pinned by a test.

WARNING

The target repository owns its own isolation. Stoat can fence processes through the prompt; it cannot make a repository's shared infrastructure 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. Isolate shared infrastructure per worktree before running agents in parallel.

The prompt is deliberately thin ​

It carries the claim, the worktree, what done means, the tag rule and the shared-machine fence — and nothing about how to build a slice. That lives in the installed critterstack-sdd skills, so the runner works for any repository carrying them and teaches nothing of its own.

Flags ​

FlagDefault
-u, --urlhttp://localhost:5526The Stoat host
--parallel2Agents in parallel
--max-attempts2Attempts per slice before giving up loudly
--budget-usd—Per-attempt spend cap, passed as --max-budget-usd
--agent-args—Appended to every agent invocation. The runner sets no permission posture itself
--agentstoat-runnerAgent identity for claims
--agent-commandclaudeThe agent executable
--stagger-seconds5Between spawns
--poll-seconds15Between status polls
-p, --allow-promptingoffStart without a permission posture
-e, --allow-unprovableoffStart with no observable evidence

Each agent's output is piped to stoat-agent-<nodeId>.log beside the project directory. The command exits non-zero if anything was given up on.

See also ​

Source-available under the Business Source License 1.1.