Skip to content

Plans and nodes ​

A plan is a DAG of work across repositories. Nodes are the pieces; edges are dependencies; and a node's status is derived from what Stoat observes, never from someone marking it done.

That last part is the whole design. Nobody maintains a status column, nobody can mark something finished that isn't, and a plan cannot quietly go stale — because there is nothing in it to go stale.

Node kinds ​

KindSatisfied by
issueA GitHub issue being closed. The number is optional — a node without one is unrealized, and bind_plan_node materializes it once the issue exists.
prA pull request. Merge policy is manual-review (default) or merge-on-green.
publishA NuGet version being genuinely downloadable. Declare a bump (fix/minor/major) or an exact version; the feed defaults to nuget.org.
consumeA repository taking an upstream package version.
test-run-gateA Bobcat suite passing, correlated by BOBCAT_RUN_TAG.
sliceAn Event Modeling slice, with a pattern of command/view/automation/translation. Its bound {Feature}/{Scenario} specs going green is the node's done; empty specs derive as unrealized. stoat run-plan builds these unattended.

Derived status ​

plan_status gives each node one of:

unrealized · unknown · missing · open · claimed · pr-open · abandoned · done · waiting · running · failed · mismatch

Two distinctions worth keeping straight:

  • unrealized vs missing. Unrealized means the node was never bound to a real thing — the issue has not been filed yet. Missing means it was bound and the thing is not there.
  • unknown is not missing. "No run yet" and "we cannot see whether there was a run" are different facts, and a plan that conflated them would report confidence it does not have.

Finding work ​

next_ready_nodes()

Ready means: every dependency is done — wherever it lives, including in another plan — and the node is not itself done, missing or running. Results are sorted by leverage: how many nodes finishing this one unblocks.

Authoring ​

Either tool by tool —

create_plan(slug: "critter-parity", title: "…")
add_plan_node(slug: "critter-parity", id: "fisher-1-10", kind: "publish", package: "Fisher", bump: "minor", …)
link_plan_nodes(slug: "critter-parity", node: "stoat-bump", dependsOn: "fisher-1-10")

— or in bulk, as a YAML document (schema 1):

import_plan(yaml: "…")

import_plan diffs rather than replacing: it creates the plan, or folds the document into an existing one so only what actually changed is recorded. Re-importing the same document records nothing.

Authoring is guarded rather than trusting. link_plan_nodes is refused if it would make a cycle. add_plan_node is refused, with the reason, for an unknown dependency, a cycle, or a field that does not belong to the kind. remove_plan_node is refused while anything still depends on it — dropping an edge is a decision, not a side effect of deleting something else.

Note that change_plan_node replaces a node wholesale: pass the complete node as you want it, because anything omitted is cleared. For edges alone use link/unlink; for the GitHub binding alone use bind_plan_node.

Claims ​

A claim is how one agent tells the others it is working on something.

claim_node(plan: "critter-parity", node: "fisher-1-10", agent: "…")
report_node(…, note: "reproduced; the fix is in the parser")
release_node(…)
  • A claim is a lease, default 30 minutes, 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.
  • Claiming a taken node is refused with the holder's name, and claiming a done node is refused outright.
  • Releasing changes no status. Finishing the work is proven by the issue closing, the version appearing, the suite passing — not by saying so.

agent_roster shows every Claude Code session on the box, what it holds, and whether anyone is actually working — running, idle, or stopped with the cause it stopped for.

Waiting without polling ​

The cross-repo case that motivated all of this: a downstream agent needs an upstream publish.

await_package_version(package: "Fisher", minVersion: "1.10.0")
await_dependencies(plan: "critter-parity", node: "stoat-bump")

Each blocks until the fact changes, and costs one open request. await_package_version queries the feed directly, so it works for any package — not only ones a plan happens to declare.

History ​

plan_history returns every change ever made, oldest first, with who made it and what it was: plan_created, node_added, node_changed, node_removed, dependency_added, dependency_removed, node_bound, and the plan header changes. The plan is an event stream, so this is the stream, not a log somebody remembered to write.

See also ​

Source-available under the Business Source License 1.1.