REST API
Coordination's HTTP surface, on 5526. It is the twin of the MCP tools — same commands, same handlers, same semantics; the endpoints only relay.
For the run console's routes on 5525 — ingestion, runs, exports, the Event Model wire — see Ports and wire contracts.
What is gated, and what is not
| Licence-gated | The work loop: /api/plans/ready and the claim/report/release endpoints. A missing licence answers 402 with the same envelope the MCP tools return. |
| Not gated | Reads, and plan/repository authoring. |
That split is deliberate rather than an oversight. The rule of record is that every MCP tool and every mutating coordination endpoint is gated; the work loop is the product's core, so it is gated exactly like the tools. See the note about the gate being temporarily disabled in this build.
The agent roster
The read side of the fourth observed source — the filesystem. Stoat reads ~/.claude to see the Claude Code sessions on the box, and joins them to claims.
| Route | |
|---|---|
GET /api/agents | The whole board: every session, every working tree, and the claims nothing could be linked to. |
GET /api/agents/{sessionId} | One session, or 404. |
Read-only, and gated by nothing — there is nothing here to mutate. Stoat observes sessions, it does not own them, so there is no "kill this agent" to expose. Session id is a first-class key, which is what makes /api/agents/{sessionId} a deep-link target.
What it answers that a lease cannot
The lease answers "will this eventually let go". The roster answers "is anyone working it right now" — which is the question two supervision failures needed and nothing could answer.
Everything interesting is cross-source: a claim on its own cannot be stalled, a session on its own cannot be orphaning anything. The value of the fourth source is entirely in the join.
Claim health
held | The session holding it is running. |
stalled | The wait-trap. The session is alive but idle — it said it was working and then stopped between turns. The lease still stands, so the node looks taken and nothing is happening to it. |
orphaned | The session is gone. The claim outlives it until the lease expires — this is what makes that expiry explainable instead of mysterious, and it carries the stop cause as the reason. |
unknown | A session was correlated, but we cannot tell what it is doing. |
unlinked | No session correlated. The claim behaves exactly as it did before this source existed, and says so rather than implying health it has not got. |
Stoat marks, it never releases. A stalled claim is still a claim: the lease is the mechanism, and quietly freeing a node because a session looked idle would be Stoat deciding something it merely observed. Marking is the whole intervention.
Claim health also rides on each node's status view, so the DAG shows it without a second request.
Tree ownership
Sessions grouped by the git root above their cwd. A worktree is its own root, so two agents in two worktrees of one repository are correctly not sharing.
contested— more than one live session in a tree.needsCleanup— a.claude/worktreestree whose sessions are all gone and which is still on disk: a worktree an agent died inside.
Correlation says how strong it is
claim and report take an optional session (agents pass $CLAUDE_CODE_SESSION_ID) — that is the strong link. Without it, a claim is matched to the single live session whose repository matches the node's, and reported as inferred.
Ambiguity refuses to guess: two live sessions on one repository leaves the claim unlinked — and that ambiguity is itself a contested tree the roster surfaces.
The work loop
The REST twin of next_ready_nodes / claim_node / report_node / release_node, for an agent that can reach HTTP but has no MCP client. Without these, such an agent can author and observe plans yet works unclaimed, and the board's claims column lies by omission.
GET /api/plans/ready
Ready work across every plan, or one with ?plan=, sorted by leverage. Each entry carries claimedBy when someone holds it. A ?plan= naming no valid plan answers 404.
POST /api/plans/{slug}/nodes/{id}/claim
{ "agent": "my-agent", "leaseMinutes": 30, "session": "$CLAUDE_CODE_SESSION_ID" }| 200 | { claimed: true, expiresAt, node, readyWarning } — readyWarning is set when the node is not ready, rather than refusing: its dependencies may not be done, and that is worth saying but not worth blocking. |
| 409 | { claimed: false, heldBy, heldUntil } — another agent holds a live lease. |
| 400 | The node is already done — there is nothing to claim. Or agent is missing. |
| 404 | No such plan or node. |
leaseMinutes defaults to 30.
POST /api/plans/{slug}/nodes/{id}/report
{ "agent": "my-agent", "note": "reproduced; the fix is in the parser" }Attaches or replaces the claim's note and renews the lease. Only the holder can — anyone else gets 409 telling them to claim first.
POST /api/plans/{slug}/nodes/{id}/release
{ "agent": "my-agent" }Status stays whatever observation says. Finishing is proven by the issue closing or the suite passing, never by this call.
Plans
Reads:
| Route | |
|---|---|
GET /api/plans | Active plans. |
GET /api/plans/{slug} | One plan's full DAG. |
GET /api/plans/{slug}/status | Per-node derived status plus the ready set. |
GET /api/plans/{slug}/history | Every event on the plan's stream, with who and when. 404 for a stream with no events — the plan never existed here. |
Authoring — the same commands the MCP tools send, with ?by= naming who:
POST /api/plans · PUT /api/plans/{slug} (import/diff) · DELETE /api/plans/{slug} (archive) · POST|PUT|DELETE /api/plans/{slug}/nodes[/{id}] · POST|DELETE /api/plans/{slug}/nodes/{id}/depends-on · POST /api/plans/{slug}/nodes/{id}/bind · POST /api/plans/rescan
TIP
The depends-on endpoints take the dependency in the body or query, not the path — a cross-plan reference is plan/node, and a slash in a path segment does not survive routing.
POST /api/plans/rescan re-imports the Stoat:PlansPath directory: the "I just edited the file" button. A directory import runs in seed shape — it adds and changes but never removes a node the file lacks, because a stale file must not delete API- or MCP-authored work. The kept nodes are named in the itemized report.
Repositories
GET /api/repositories and GET /api/repositories/{owner}/{name} — read-only. Registering, declaring and note-taking are MCP-only.
See also
- MCP tool reference — the same surface for agents that speak MCP
- The chapter runner —
stoat run-plandrives the work-loop endpoints - Ports and wire contracts — the run console on 5525