atrium — docs
homearchitectureagent statusidentityfleetscontrol planetrust & securityworktreesdev fleetreapingsession recovery

Per-agent worktrees

> Status: shipped. Opt-in per-agent git worktrees, the structural fix for > coordination finding #5 (shared-checkout gating). Configured in > atrium.fleet.json; dormant unless a fleet asks for it.

The problem

A fleet's agents all edit one working tree, and dev.py check gates the whole tree. Two failure modes follow, both seen in the ctl-fixes run:

Today's only mitigation is discipline — keep-tree-green + path-scoped commits. Fragile and human.

Goals / non-goals

Goals

Non-goals (for v1)

Design overview

Isolation is switched on per agent via a single config key, worktree. The value is a group name:

One key therefore expresses solo work, parallel squads, and the legacy single-tree mode. Nothing engages unless the key is present, so this is purely additive.

{
  "fleets": {
    "ctl-fixes": {
      "allow_ctl": true,
      "agents": [
        { "name": "lead",         "cmd": ["claude"] },                    // main tree
        { "name": "flake",        "cmd": ["claude"], "worktree": "flake" },// solo tree+branch
        { "name": "controlplane", "cmd": ["claude"], "worktree": "cp" },   // solo
        { "name": "cp-helper",    "cmd": ["claude"], "worktree": "cp" }    // shares cp's tree+branch
      ]
    }
  }
}

A fleet-level shorthand "worktrees": true means "every agent gets its own worktree, named after itself" — the common "full fan-out" case without repeating the key.

Language-agnostic: atrium invents no build logic

atrium hosts panes running arbitrary commands; it does not know or care whether an agent compiles Rust, runs Python, or does research. So build caching is not atrium's concern — it is a per-language choice the fleet expresses in config:

This keeps every build topology a config choice, never a hardcode:

TopologyHowWhen
Isolated buildsdefault (separate worktrees)correctness first; disk/CPU cheap
Shared cacheset CARGO_TARGET_DIR (etc.) on agentsfaster, accept some cargo contention
Syntax/review per agent, compile lastagents lint/review in their trees; one integration step compiles the merged resultthe "divvy it back, compile once" flow
Dedicated build agentone agent owns compilation; others only edit/reviewheavy builds, one source of truth

Lifecycle

On fleet up (only when at least one agent names a worktree, and the cwd is a git repo):

  1. For each distinct worktree name, run git worktree add <dir> -b <branch> off the current HEAD.
  2. Each pane's working directory is set to its worktree <dir>. Agents with no key stay in the main tree.

Naming (deterministic, greppable):

The worktree/branch name is slugged to a safe single path/ref component (anything outside [A-Za-z0-9._-] → -), so an agent name carrying a slash or a control char can't produce an invalid branch or escape the base dir. Two distinct names that slug alike would collide — a documented v1 sharp edge; keep worktree names simple.

On teardown — never destroy unmerged work:

Once you have merged (or discarded) the work in a kept worktree, reclaim it with atrium fleet clean <name>, run from the directory the fleet launched in. It re-applies the same clean+merged test and removes the ones that now pass, still refusing to destroy anything dirty or unmerged. It is implemented as a fleet subcommand, not ctl cleanup: the cleanup happens after the fleet has exited, so there is no running instance for ctl's IPC to reach.

Seeding untracked shared state

git worktree add checks out only the files git tracks. Anything untracked — .env, local config, secrets — does not appear in a fresh worktree, so an agent can land in a tree that's missing the .env it needs to run.

The fix is an optional, configurable seed list that atrium links (not copies — copies go stale and clutter) into each worktree, with a Windows-friendly, no-privilege strategy:

"worktree_seed": [".env", "config.local.json"]

Keeping merges easy (reuse, don't rebuild)

A git merge is conflict-free when no two branches touch the same file. atrium already has the two primitives to arrange that — so "easy merges" is a convention over existing tools, not a new subsystem:

Configurable and general-purpose

Everything git/merge-related is opt-in. With no worktree key, atrium behaves exactly as it does today. A fleet used for research, ops, or anything non-coding never touches worktrees or git — it keeps using the board and bus as general coordination primitives. "Coding mode" is a layer you switch on, not a change to what atrium fundamentally is.

Ad-hoc worktrees — ctl spawn --worktree

A fleet.json entry is the right home for a standing team. But a quick ad-hoc teammate does not need a fleet definition. Pass --worktree <name> directly to a live ctl spawn and atrium handles the rest:

atrium ctl spawn --role scout --worktree probe -- claude
atrium ctl send scout "explore the auth module and report findings"

What happens:

If the worktree <name> already exists (an earlier teammate in the session created it), the spawn reuses it. Two teammates with --worktree squad co-develop one tree and one branch — the same group-share semantics as the fleet "worktree" config key.

The worktree can be reclaimed after the session with atrium fleet clean, which applies the same clean-and-merged safety test it applies to fleet-created worktrees.

Respawning a pane into a worktree — ctl respawn

Once a pane's child process is running you cannot chdir it from outside: the process owns its working directory and there is no cross-process chdir primitive on any supported platform. The only way to move a live pane into a different directory is respawn-in-place: kill the child, then relaunch it with the new cwd.

atrium ctl respawn scout --worktree probe

What respawn does:

  1. Sends the pane's current child a graceful stop and waits for it to exit.
  2. Creates the named worktree if it does not yet exist (same naming rules as spawn --worktree).
  3. Relaunches the same command — same argv, role, identity, mode, deny rules and context store — as a new session, with its working directory set to the worktree. (Without --worktree, the pane restarts in the directory it was in.)
  4. Re-injects the worktree behavioral norms into the fresh session.

The pane id and role are preserved. Teammates that target scout by role keep reaching it after the respawn. Board state is yours to carry forward or reset — a new session does not automatically inherit the previous session's conversation context.

When to use each:

GoalCommand
New teammate, isolated tree from the startctl spawn --worktree <name> -- <cmd>
Move an existing pane into a worktree mid-sessionctl respawn <pane> --worktree <name>

Implementation plan

Single-owner-per-file, pure seam + regression test per change, as usual.

  1. Config (fleet.rs). Add worktree: Option<String> to Agent and worktrees: Option<bool> + worktree_seed: Option<Vec<String>> to Fleet; parse with clear errors; tests for parse/absent/shorthand. (Mirrors the topics work just landed.)
  2. Pure naming seam (new worktree.rs). plan_worktrees(fleet, cwd) -> Vec<WorktreePlan { name, dir, branch, agents }> — no side effects, fully unit-tested (distinct vs shared names, legacy passthrough, branch/dir strings, the worktrees: true shorthand).
  3. Effectful git ops (behind the seam). git worktree add/remove/prune, dirtiness/unmerged checks, seed linking (hardlink/junction/copy fallback). Thin wrapper over the plan; the decision logic stays in the pure seam.
  4. Spawn wiring (fleet_cli.rs). Create worktrees before spawning panes; set each pane's cwd to its worktree dir; thread the plan into teardown.
  5. Teardown safety. Clean+merged → remove; else keep + report. Prune orphans on launch.
  6. Docs. Flip this banner; add worktrees to docs/build.py PAGES.

Bootstrap note

The first implementation cannot run inside worktrees (they don't exist yet — chicken and egg), but it can still use the fleet's board/bus coordination. Once landed, atrium can dogfood its own worktrees for subsequent work.

Resolved decisions

The three open questions from the design phase, as shipped: