Agent roots
An agent root answers “what does this agent need to know before it starts?” It’s a named bundle of domain context: repo, branch, subdirectory, runtime, model, goal, and default artifacts.
The shape of a root
Section titled “The shape of a root”From roots.json:
"zimmer": { "name": "zimmer", "display_name": "Zimmer", "description": "The Zimmer orchestrator itself — self-hostable AI coding agent orchestration.", "url": "https://github.com/tadasant/zimmer.git", "default_branch": "main", "user_invocable": true, "default_goal": "open-reviewed-green-pr"}| Field | Meaning |
|---|---|
url / default_branch | Which repo to clone, and from where |
subdirectory | Scopes the agent to a subtree of the repo (monorepo case) |
user_invocable | Whether it appears in the “new session” picker |
default_goal | Seeds session.goal |
default_runtime / default_model | Seeds the runtime and model |
The default_skills, default_mcp_servers, default_hooks, default_plugins, and
default_subagent_roots fields you’ll see at runtime are not written in roots.json — AIR
computes them by inverting default_in_roots from
each artifact’s own entry.
Omitting a list is not the same as asking for an empty one
Section titled “Omitting a list is not the same as asking for an empty one”On the three surfaces that create a session against a root — the MCP start_session tool, POST /api/v1/sessions, and the new-session form — an omitted
mcp_servers/skills/plugins/hooks takes the root’s defaults, while an explicit [] takes
none of that artifact. They are two different requests and Zimmer keeps them apart.
This matters most for MCP servers, because a root’s defaults can carry real privilege (SSH access to
a production host, a secrets store). A caller that narrows to [] is asking for least privilege, and
silently handing it the full default set instead is the failure mode this distinction exists to
prevent.
An empty mcp_servers column is otherwise ambiguous: it is also where a session lands when the
catalog resolve was incomplete at create time, which McpServerBackfill heals by restoring the
root’s current defaults. Zimmer therefore records a deliberate “none” on the session
(metadata.mcp_servers_explicitly_empty), and the heal skips those sessions — so an explicit []
survives to job start rather than being restored when the runtime config is regenerated. Every path
that lets someone name the list sets it, including the mid-life ones (change_mcp_servers, PATCH /api/v1/sessions/:id/mcp_servers, and the session page’s editor).
Two things are deliberately outside that rule:
Session.create_from_agent_root!(the dashboard quick prompt, the chat bubble, and triggers) treatsniland[]alike as “take the defaults”. ATrigger’smcp_serverscolumn isdefault: [], null: false, so[]there is an untouched trigger rather than a request for none — reading it as “no servers” would strip every existing trigger’s servers.- Injected servers (the self-session server, and the subagent-spawning server for roots that
declare
default_subagent_roots) are added bySelfSessionInjector, not by this resolution. A session spawned withmcp_servers: []still receives them, by design.
The eleven roots that ship
Section titled “The eleven roots that ship”| Root | Invocable | Repo | Notes |
|---|---|---|---|
zimmer | ✅ | tadasant/zimmer | Work on Zimmer itself. Every skill but awaken-waiting-sessions defaults here. |
zimmer-router | ❌ | tadasant/zimmer | The baseline router. Session::ROUTER_AGENT_ROOT; every quick-router / chat-bubble submission is created against it. Ships with no default artifacts — it cannot yet dispatch downstream sessions (why). |
general-agent | ✅ | tadasant/zimmer | The catch-all. AgentRootsConfig::DEFAULT_ROOT. |
fleet-maintenance | ❌ | tadasant/zimmer | The deployment’s own scheduler. The quota_available trigger dispatches it; it runs awaken-waiting-sessions and starts parked spot work in precedence order. Defaults to the zimmer-fleet server, which is the only thing that gives it the tools that skill calls. |
agent-orchestrator | ✅ | tadasant/zimmer-catalog | Scoped to agents/agent-orchestrator |
agents | ✅ | tadasant/zimmer-catalog | Scoped to agents — the catalog artifacts |
catalog-management | ❌ | tadasant/zimmer-catalog | Lead root; fans out to the four below |
catalog-mgmt-research | ❌ | ↳ subagent phase | default_in_roots: [catalog-management], model sonnet |
catalog-mgmt-configs | ❌ | ↳ subagent phase | same |
catalog-mgmt-proctor | ❌ | ↳ subagent phase | same |
catalog-mgmt-save | ❌ | ↳ subagent phase | same |
Subagent roots
Section titled “Subagent roots”A root whose default_in_roots names another root becomes a subagent root of it. AIR computes
default_subagent_roots on the parent, and the lead root’s agent can then spawn sessions against
those phases.
This is how catalog-management decomposes into research → configs → proctor → save. A root never
becomes its own subagent, even via the "*" wildcard.
How a root seeds a session
Section titled “How a root seeds a session”At session creation (Session#create_from_agent_root!), the root supplies defaults that the caller
can override:
Once seeded, the session owns its own lists. The UI’s PATCH endpoints mutate them directly, and
air prepare is called with --without-defaults so AIR won’t re-add anything the user removed.
Changing roots
Section titled “Changing roots”Roots live in roots.json at the repo root and are resolved through AIR like every other artifact.
Adding one is a PR. Zimmer’s own zimmer-change-ai-artifact skill is the guide, and the invariant it
enforces is the one that matters:
no dangling references.