Skip to content

An agent's first session ​

Stoat does not write code. Your agent does — in your editor, in a worktree — and Stoat is what it asks what to work on, and what it tells when it is done.

This page is the shape of one session, end to end. Step 1 is yours. Steps 2 to 8 are what your agent does once it has Stoat's MCP server: the calls below are its tool calls, not commands you type. In practice you ask in plain words ("find ready work in critter-parity, claim it and work it"), and the agent makes the calls.

1. Wire the repository: stoat setup ​

From the repository the agent works in, once:

bash
stoat setup

That writes the Stoat MCP server into the repository's .mcp.json and the session hooks into ~/.claude/settings.json, merged into what is there, and tells you whether a server answers. See Installation for what it changes and the flags, and the configuration it writes at the end of this page if you would rather write it by hand.

Stoat serves one streamable-HTTP MCP endpoint, stateless, carrying all 71 tools:

http://localhost:5526/api/mcp

Stateless means every tool call is a self-contained request, so a dropped connection costs nothing and several agents share one server without sessions to keep straight.

Teaching an agent the loop

The tool descriptions are enough for an agent to find its way, but the JasperFx AI skills teach it this whole loop: stoat-coordination-agent-workflow for finding, claiming and closing work, and stoat-coordination-plan-authoring for writing plans.

2. Recall before starting ​

The first thing a session should do is find out what earlier sessions learned — the trap they hit, the pin they discovered, the reason a thing is the way it is.

recall(query: "nuget local feed")
get_repository(slug: "JasperFx/stoat")

recall searches memories by full text, best match first. On a new server it finds nothing yet; it fills as sessions record what they learn (step 8). get_repository returns a repository's notes along with its declared packages, CI workflows and dependencies. Repositories are named owner/name, as on GitHub.

3. Start a plan ​

A plan is the work, as a graph of nodes Stoat can tell are done by observing them: GitHub issues and pull requests, NuGet publishes and the pins that take them, test-run gates, Event Model slices. A new server has none, and nothing below has anything to work on until there is one.

Ask your agent to start one. It uses the authoring tools:

create_plan(slug: "critter-parity", agent: "my-agent", title: "Critter parity")
add_plan_node(slug: "critter-parity", id: "fisher-1-10", kind: "test-run-gate", agent: "my-agent",
              title: "Fisher 1.10 suite green",
              steps: [{ id: "unit", run: "dotnet test --project src/Fisher.Tests" }])

Or write the plan as a YAML document and import it, with import_plan or PUT /api/plans/{slug}. Plans and nodes has the schema and every node kind. The plan appears on the ready board at http://localhost:5526 as soon as it exists.

Two ways to name a plan

The authoring tools name a plan slug and a node id; the work-loop tools below name them plan and node. A call with a missing or wrong name is answered with what is missing and what the tool takes, so the next call can be right.

4. Find work that is actually ready ​

next_ready_nodes(plan: "critter-parity")

Readiness is derived, never asserted. A node is ready when its dependencies are satisfied by observed facts — a GitHub issue that is actually closed, a NuGet version that is actually downloadable, a test run that actually passed. Nobody maintains a status column, and nobody can mark something done that isn't.

5. Claim it, work, report ​

claim_node(plan: "critter-parity", node: "fisher-1-10", agent: "my-agent", session: "$CLAUDE_CODE_SESSION_ID")
   … do the work in a worktree …
report_node(plan: "critter-parity", node: "fisher-1-10", agent: "my-agent", note: "…")
release_node(plan: "critter-parity", node: "fisher-1-10", agent: "my-agent")

A claim is an event, and its expiry is derived at read time rather than written down — so a session that dies without releasing does not hold a node forever, and nothing has to sweep.

get_node(slug: "critter-parity", id: "fisher-1-10") is the node zoomed in: its brief, the latest claim, people's notes, what it waits on and unblocks, and with includeGitHub: true the issue's text and each PR's failing checks. Read it before starting, and before picking up a node another session held (session_journal says what that session did).

When a decision is a person's, ask instead of guessing:

ask_person(plan: "critter-parity", node: "fisher-1-10", agent: "my-agent",
           question: "Keep the old wire name?", options: ["keep", "rename"], recommended: "keep")

The call parks until a person answers (up to four hours by default; your lease is renewed while you wait) and returns the answer. The question shows in Stoat's top bar on every page, and the person answers it in a chat. A timeout leaves it open: pass its questionId to park on it again, and only if you truly cannot wait, go with your recommended option and say so. See Answering an agent.

6. Run what closes it ​

A test-run-gate node is done on real runs, never on somebody's word. For a gate that declares steps, like the one above, run them through Stoat from the checkout:

bash
stoat gate run critter-parity/fisher-1-10

It runs each step's declared command, records when it started and how it finished, and the node goes done once every step has passed. --parallel <n> runs steps side by side, and --step <id> runs only that one. In a session stoat run-plan launched, the gate is already in BOBCAT_RUN_TAG, so stoat gate run needs no argument.

A suite that reports to Bobcat can be the evidence directly. The correlation is BOBCAT_RUN_TAG, an opaque string Bobcat stamps on a run and never interprets: <plan>/<node>/<step> for a step, or <plan>/<node> for a gate that declares no steps.

bash
BOBCAT_RUN_TAG=critter-parity/fisher-1-10/unit dotnet test --project src/Fisher.Tests

The run publishes to Stoat's receiver on 5527 (list it in BOBCAT_MONITOR_URL beside 5525, or run it through stoat gate exec, which does). bobcat ui is where you watch it; see Runs and evidence.

Only committed work proves anything: a run on a checkout with uncommitted changes passes its step and says so, and proves no commit. Commit before the run that should count.

7. Park instead of polling ​

The tools that matter most to an agent are the ones that let it stop:

await_dependenciesblock until a node's dependencies are satisfied
await_package_versionblock until a NuGet version is genuinely downloadable
await_memoriesblock until a memory is recorded or changed in a scope, such as the repository you are working in
await_run_completionblock until a suite settles — finished, orphaned or timeout

Each is one open request, not a loop. An agent waiting on an upstream publish costs a socket; an agent polling every fifteen seconds costs a sweep for every one of them, forever.

TIP

With several publishers on the box, a no-runId await_run_completion latches whichever run is in flight — which may be another session's worker. Pass the runId from list_runs.

8. Record what the next session should know ​

record_memory(title: "…", body: "…", agent: "my-agent", scope: "repository", scopeKey: "JasperFx/stoat")

A repository's own notes, packages and CI live on the repository, which is registered once:

register_repository(slug: "JasperFx/stoat", agent: "my-agent")
add_repository_note(slug: "JasperFx/stoat", markdown: "…", agent: "my-agent")

A memory is scoped (to this session, a plan, a repository, or everything) and can be revised, refiled to a different scope once it turns out to be about something broader, or archived. The point is that the next session's recall finds it.

9. Let Claude Code keep the journal ​

A session's journal (session_journal, and the agent page) is what it did: when it started and ended and why, what it claimed, what it decided, what it ran, what it committed and which pull requests it opened. Most of that Stoat already has. The commits, the pull requests, and why a session started or ended come from Claude Code's own hooks, posted to Stoat as they are, and stoat setup installed them in step 1.

Each is fire-and-forget: with Stoat down, curl fails at once and || true keeps the hook quiet. A commit is read off what git commit printed, so one that commits nothing records nothing; a pull request is the URL gh pr create printed. Nothing else a hook sends is kept. Nothing of the transcript is captured: this is metadata, on purpose.

No MCP client? ​

Every step above has an HTTP twin — GET /api/plans/ready, then POST /api/plans/{slug}/nodes/{id}/claim|report|release. See the REST API.

Pass session (your $CLAUDE_CODE_SESSION_ID) when you claim: it is what lets the agent roster tell a stalled claim from a held one instead of leaving it unlinked.

The configuration setup writes ​

stoat setup writes exactly this, and it is here for reference: to read what setup did, or to write it by hand on a machine where you would rather not run it. Setup recognizes a hand-written copy that matches, leaves it in place, and never removes it.

The MCP server, in .mcp.json at the repository root:

json
{
  "mcpServers": {
    "stoat": {
      "type": "http",
      "url": "http://localhost:5526/api/mcp"
    }
  }
}

The hooks, in ~/.claude/settings.json. The two sidecars write Claude Code's own hook payload to ~/.claude/stoat/hooks/, which is how the agent roster tells an agent waiting on a permission prompt from one that is thinking, and a person closing a session from a crash. The three journal hooks post the payload to Stoat:

json
{
  "hooks": {
    "Notification": [
      { "hooks": [ { "type": "command",
          "command": "mkdir -p ~/.claude/stoat/hooks && cat > ~/.claude/stoat/hooks/$CLAUDE_CODE_SESSION_ID.json" } ] }
    ],
    "SessionEnd": [
      { "hooks": [ { "type": "command",
          "command": "mkdir -p ~/.claude/stoat/hooks && cat > ~/.claude/stoat/hooks/$CLAUDE_CODE_SESSION_ID.json" } ] },
      { "hooks": [ { "type": "command",
          "command": "curl -s -m 2 -o /dev/null -H 'Content-Type: application/json' --data-binary @- http://localhost:5526/api/agents/hooks || true" } ] }
    ],
    "SessionStart": [
      { "hooks": [ { "type": "command",
          "command": "curl -s -m 2 -o /dev/null -H 'Content-Type: application/json' --data-binary @- http://localhost:5526/api/agents/hooks || true" } ] }
    ],
    "PostToolUse": [
      { "matcher": "Bash",
        "hooks": [ { "type": "command",
          "command": "curl -s -m 2 -o /dev/null -H 'Content-Type: application/json' --data-binary @- http://localhost:5526/api/agents/hooks || true" } ] }
    ]
  }
}

With CLAUDE_CONFIG_DIR set, setup writes the sidecar path under that folder instead, since that is where the server reads them.

Next steps ​

Licensed under the Business Source License 1.1.