Skip to content

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.

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"
}
FieldMeaning
url / default_branchWhich repo to clone, and from where
subdirectoryScopes the agent to a subtree of the repo (monorepo case)
user_invocableWhether it appears in the “new session” picker
default_goalSeeds session.goal
default_runtime / default_modelSeeds 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) treats nil and [] alike as “take the defaults”. A Trigger’s mcp_servers column is default: [], 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 by SelfSessionInjector, not by this resolution. A session spawned with mcp_servers: [] still receives them, by design.
RootInvocableRepoNotes
zimmertadasant/zimmerWork on Zimmer itself. Every skill but awaken-waiting-sessions defaults here.
zimmer-routertadasant/zimmerThe 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-agenttadasant/zimmerThe catch-all. AgentRootsConfig::DEFAULT_ROOT.
fleet-maintenancetadasant/zimmerThe 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-orchestratortadasant/zimmer-catalogScoped to agents/agent-orchestrator
agentstadasant/zimmer-catalogScoped to agents — the catalog artifacts
catalog-managementtadasant/zimmer-catalogLead root; fans out to the four below
catalog-mgmt-research↳ subagent phasedefault_in_roots: [catalog-management], model sonnet
catalog-mgmt-configs↳ subagent phasesame
catalog-mgmt-proctor↳ subagent phasesame
catalog-mgmt-save↳ subagent phasesame

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.

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.

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.