Repository navigation
Expand file tree
/
Copy pathworktrees.html
More file actions
115 lines (111 loc) · 17.3 KB
/
Copy pathworktrees.html
File metadata and controls
115 lines (111 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Per-agent worktrees — atrium docs</title>
<meta name="description" content="atrium documentation: Per-agent worktrees. tmux for coding agents, zero third-party dependencies.">
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<header><div class="wrap bar">
<span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span>
<span class="title"><a href="index.html"><b>atrium</b></a> — docs</span>
<nav>
<a href="index.html">home</a>
<a href="https://crates.io/crates/atrium">crates.io</a>
<a href="https://github.com/nativelite/atrium">github</a>
</nav>
</div></header>
<main class="wrap doc">
<div class="docnav"><a href="index.html">home</a><a href="architecture.html">architecture</a><a href="agent-status.html">agent status</a><a href="identity.html">identity</a><a href="fleets.html">fleets</a><a href="control-plane.html">control plane</a><a href="trust-and-security.html">trust & security</a><a href="worktrees.html" class="active">worktrees</a><a href="dev-fleet.html">dev fleet</a><a href="reaping.html">reaping</a><a href="session-recovery.html">session recovery</a></div>
<h1>Per-agent worktrees</h1>
<p>> <b>Status: shipped.</b> Opt-in per-agent git worktrees, the structural fix for > coordination finding #5 (shared-checkout gating). Configured in > <code>atrium.fleet.json</code>; dormant unless a fleet asks for it.</p>
<h2>The problem</h2>
<p>A fleet's agents all edit <b>one working tree</b>, and <code>dev.py check</code> gates the <b>whole tree</b>. Two failure modes follow, both seen in the ctl-fixes run:</p>
<ul><li><b>Gate contamination.</b> One agent's half-finished, unformatted WIP fails <code>cargo fmt --check</code> (which runs <i>first</i>), so a <i>different</i> agent's clean, review-passed work <b>can't get a green gate to commit</b>. (bus #105: <i>"your ctl.rs pure seams FAIL cargo fmt --check … is NOT dev.py-green"</i>.)</li><li><b>Same-file interleave.</b> Two agents editing the same files in one tree can't cleanly separate their commits; it takes manual "commit-sequencing" and "commit-split" gymnastics. (bus #106/#111: <i>"W1 and W2 both edit the SAME files (ctl.rs + main.rs)"</i>.)</li></ul>
<p>Today's only mitigation is discipline — <i>keep-tree-green</i> + <i>path-scoped commits</i>. Fragile and human.</p>
<h2>Goals / non-goals</h2>
<p><b>Goals</b></p>
<ul><li>Let each agent (or group of agents) work in an <b>isolated working tree</b> on its own branch, so one agent's WIP can never fail another's gate.</li><li>Keep the mechanism <b>opt-in</b>, <b>language-agnostic</b>, and <b>general-purpose</b> — a non-coding fleet (research, ops, anything) is completely unaffected.</li><li><b>Never destroy unmerged work</b> on teardown.</li><li>Make the resulting branches <b>easy to merge</b> by reusing the coordination primitives atrium already has (the board and the bus), not by building a new merge engine.</li></ul>
<p><b>Non-goals (for v1)</b></p>
<ul><li>An automatic merge engine / conflict resolver. v1 isolates; integration is surfaced and assisted, not automated.</li><li>Any build-system or language knowledge baked into atrium.</li></ul>
<h2>Design overview</h2>
<p>Isolation is switched on <b>per agent</b> via a single config key, <code>worktree</code>. The value is a <b>group name</b>:</p>
<ul><li><b>distinct value →</b> that agent gets its <b>own</b> worktree + branch (solo isolation).</li><li><b>shared value →</b> every agent with that value <b>co-develops one</b> worktree + branch (a squad working the same concern together).</li><li><b>absent →</b> the agent uses the <b>main tree</b> — exactly today's behavior, fully backward-compatible.</li></ul>
<p>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.</p>
<pre><code>{
"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
]
}
}
}</code></pre>
<p>A fleet-level shorthand <code>"worktrees": true</code> means "every agent gets its own worktree, named after itself" — the common "full fan-out" case without repeating the key.</p>
<h3>Language-agnostic: atrium invents no build logic</h3>
<p>atrium hosts panes running arbitrary commands; it does not know or care whether an agent compiles Rust, runs Python, or does research. So <b>build caching is not atrium's concern</b> — it is a per-language choice the fleet expresses in config:</p>
<ul><li><b>Interpreted languages</b> (Python, JS, Ruby): no compile, no cache — isolation just works.</li><li><b>Compiled languages</b> (Rust, Go, C++): each worktree builds independently by default. A fleet that wants a shared compiler cache sets the env var itself on its agents; atrium passes env through, it does not manage it.</li></ul>
<p>This keeps every <b>build topology</b> a config choice, never a hardcode:</p>
<table><thead><tr><th>Topology</th><th>How</th><th>When</th></tr></thead><tbody><tr><td>Isolated builds</td><td>default (separate worktrees)</td><td>correctness first; disk/CPU cheap</td></tr><tr><td>Shared cache</td><td>set <code>CARGO_TARGET_DIR</code> (etc.) on agents</td><td>faster, accept some <code>cargo</code> contention</td></tr><tr><td>Syntax/review per agent, <b>compile last</b></td><td>agents lint/review in their trees; one integration step compiles the merged result</td><td>the "divvy it back, compile once" flow</td></tr><tr><td>Dedicated build agent</td><td>one agent owns compilation; others only edit/review</td><td>heavy builds, one source of truth</td></tr></tbody></table>
<h2>Lifecycle</h2>
<p><b>On <code>fleet up</code></b> (only when at least one agent names a <code>worktree</code>, and the cwd is a git repo):</p>
<ol><li>For each <b>distinct</b> worktree name, run <code>git worktree add <dir> -b <branch></code> off the current <code>HEAD</code>.</li><li>Each pane's working directory is set to its worktree <code><dir></code>. Agents with no key stay in the main tree.</li></ol>
<p><b>Naming (deterministic, greppable):</b></p>
<ul><li>Branch: <code>atrium/<fleet>/<worktree></code></li><li>Directory: <code>../.atrium-worktrees/<fleet>/<worktree></code> — a <b>sibling</b> of the repo by default, so the worktrees never show up as untracked files inside it. Override the base with the fleet-level <code>"worktree_base"</code> key (resolved against the fleet dir; absolute used as-is) — set it to give two concurrent sessions on the same repo <b>distinct</b> bases so they don't collide on the same directories and branches.</li></ul>
<p>The worktree/branch name is slugged to a safe single path/ref component (anything outside <code>[A-Za-z0-9._-]</code> → <code>-</code>), 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.</p>
<p><b>On teardown — never destroy unmerged work:</b></p>
<ul><li><code>git worktree remove <dir></code> <b>only if</b> the tree is clean <b>and</b> its branch has no commits that aren't already on the base.</li><li>Otherwise <b>leave it</b> and report the path + branch in the exit banner, so you can inspect or merge it deliberately.</li><li><code>git worktree prune</code> clears orphaned entries on the next launch (an agent that crashed, a directory deleted by hand).</li></ul>
<p>Once you have merged (or discarded) the work in a kept worktree, reclaim it with <code>atrium fleet clean <name></code>, 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 <code>fleet</code> subcommand, not <code>ctl cleanup</code>: the cleanup happens <i>after</i> the fleet has exited, so there is no running instance for ctl's IPC to reach.</p>
<h2>Seeding untracked shared state</h2>
<p><code>git worktree add</code> checks out only the files git <b>tracks</b>. Anything untracked — <code>.env</code>, local config, secrets — <b>does not appear</b> in a fresh worktree, so an agent can land in a tree that's missing the <code>.env</code> it needs to run.</p>
<p>The fix is an optional, configurable <b>seed list</b> that atrium <b>links</b> (not copies — copies go stale and clutter) into each worktree, with a Windows-friendly, no-privilege strategy:</p>
<pre><code>"worktree_seed": [".env", "config.local.json"]</code></pre>
<ul><li><b>Files</b> → <b>hardlink</b> (same inode; edits propagate; no admin needed on Windows).</li><li><b>Directories</b> → <b>junction</b> (no admin needed on Windows).</li><li><b>Copy</b> only as a fallback if linking fails.</li><li><b>Build caches are never seeded</b> — those belong to the env-var path above.</li><li>Default is <b>empty / opt-in</b>: atrium seeds nothing unless told, so behavior is never surprising.</li></ul>
<h2>Keeping merges easy (reuse, don't rebuild)</h2>
<p>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 <b>convention over existing tools</b>, not a new subsystem:</p>
<ul><li><b>Disjoint ownership via <code>board claim</code>.</b> An agent claims the files/modules it will edit before touching them; two worktrees cannot both claim <code>ctl.rs</code>. This turns the <i>single-owner-per-file</i> working agreement into something <i>enforced</i>, and directly prevents the #111 "both edit the SAME files" conflict.</li><li><b>Conflict escalation via <code>bus pub --to <owner></code>.</b> When a real conflict does arise at integration, it is routed to the owning agent over the bus (the delivered-wake <code>--to</code> path), instead of silently blocking a commit.</li><li><b>Merge-train (future, out of v1 scope).</b> Because atrium knows every agent's branch, a later helper can merge each green/clean branch into the base in turn, rebasing the rest, and route any conflict to its owner. v1 stops at isolation + ownership + escalation.</li></ul>
<h2>Configurable and general-purpose</h2>
<p>Everything git/merge-related is <b>opt-in</b>. With no <code>worktree</code> key, atrium behaves exactly as it does today. A fleet used for <b>research, ops, or anything non-coding</b> 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.</p>
<h2>Ad-hoc worktrees — <code>ctl spawn --worktree</code></h2>
<p>A <code>fleet.json</code> entry is the right home for a standing team. But a quick ad-hoc teammate does not need a fleet definition. Pass <code>--worktree <name></code> directly to a live <code>ctl spawn</code> and atrium handles the rest:</p>
<pre><code>atrium ctl spawn --role scout --worktree probe -- claude
atrium ctl send scout "explore the auth module and report findings"</code></pre>
<p>What happens:</p>
<ul><li>atrium looks up (or creates) a git worktree named <code><name></code> under a fixed <b><code>adhoc</code></b> slot — branch <code>atrium/adhoc/<name></code>, directory <code><worktree_base>/adhoc/<name></code>. The name is slugged the same way fleet names are (anything outside <code>[A-Za-z0-9._-]</code> → <code>-</code>), so <code>feat/login</code> becomes <code>atrium/adhoc/feat-login</code>.</li><li>The spawned pane's working directory is set to that worktree, so its <code>dev.py check</code> and <code>git commit</code> run isolated from every other tree.</li><li>The <b>worktree behavioral norms</b> — "you are in worktree <code><name></code> on branch <code><branch></code>, do not cd, do not create new branches, commit on your current branch, announce tersely on the bus" — are injected as a system-prompt append, exactly as fleet members get them. No fleet file required.</li></ul>
<p>If the worktree <code><name></code> already exists (an earlier teammate in the session created it), the spawn <b>reuses it</b>. Two teammates with <code>--worktree squad</code> co-develop one tree and one branch — the same group-share semantics as the fleet <code>"worktree"</code> config key.</p>
<p>The worktree can be reclaimed after the session with <code>atrium fleet clean</code>, which applies the same clean-and-merged safety test it applies to fleet-created worktrees.</p>
<h2>Respawning a pane into a worktree — <code>ctl respawn</code></h2>
<p>Once a pane's child process is running you cannot <code>chdir</code> 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 <b>respawn-in-place</b>: kill the child, then relaunch it with the new cwd.</p>
<pre><code>atrium ctl respawn scout --worktree probe</code></pre>
<p>What <code>respawn</code> does:</p>
<ol><li>Sends the pane's current child a graceful stop and waits for it to exit.</li><li>Creates the named worktree if it does not yet exist (same naming rules as <code>spawn --worktree</code>).</li><li>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 <code>--worktree</code>, the pane restarts in the directory it was in.)</li><li>Re-injects the worktree behavioral norms into the fresh session.</li></ol>
<p>The pane id and role are preserved. Teammates that target <code>scout</code> 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.</p>
<p><b>When to use each:</b></p>
<table><thead><tr><th>Goal</th><th>Command</th></tr></thead><tbody><tr><td>New teammate, isolated tree from the start</td><td><code>ctl spawn --worktree <name> -- <cmd></code></td></tr><tr><td>Move an existing pane into a worktree mid-session</td><td><code>ctl respawn <pane> --worktree <name></code></td></tr></tbody></table>
<h2>Implementation plan</h2>
<p>Single-owner-per-file, pure seam + regression test per change, as usual.</p>
<ol><li><b>Config (<code>fleet.rs</code>).</b> Add <code>worktree: Option<String></code> to <code>Agent</code> and <code>worktrees: Option<bool></code> + <code>worktree_seed: Option<Vec<String>></code> to <code>Fleet</code>; parse with clear errors; tests for parse/absent/shorthand. (Mirrors the <code>topics</code> work just landed.)</li><li><b>Pure naming seam (new <code>worktree.rs</code>).</b> <code>plan_worktrees(fleet, cwd) -> Vec<WorktreePlan { name, dir, branch, agents }></code> — no side effects, fully unit-tested (distinct vs shared names, legacy passthrough, branch/dir strings, the <code>worktrees: true</code> shorthand).</li><li><b>Effectful git ops (behind the seam).</b> <code>git worktree add/remove/prune</code>, dirtiness/unmerged checks, seed linking (hardlink/junction/copy fallback). Thin wrapper over the plan; the decision logic stays in the pure seam.</li><li><b>Spawn wiring (<code>fleet_cli.rs</code>).</b> Create worktrees before spawning panes; set each pane's cwd to its worktree dir; thread the plan into teardown.</li><li><b>Teardown safety.</b> Clean+merged → remove; else keep + report. Prune orphans on launch.</li><li><b>Docs.</b> Flip this banner; add <code>worktrees</code> to <code>docs/build.py</code> <code>PAGES</code>.</li></ol>
<h2>Bootstrap note</h2>
<p>The <b>first</b> implementation cannot run <i>inside</i> 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.</p>
<h2>Resolved decisions</h2>
<p>The three open questions from the design phase, as shipped:</p>
<ul><li><b>Worktree dir location</b> — sibling <code>../.atrium-worktrees/<fleet>/</code> by default, <b>configurable</b> via <code>"worktree_base"</code> so two concurrent sessions on one repo don't collide.</li><li><b>Explicit cleanup</b> — yes: <code>atrium fleet clean <name></code> reclaims kept worktrees after a manual merge; <code>git worktree prune</code> still runs automatically on every launch for orphans.</li><li><b><code>worktrees: true</code> shorthand</b> — kept; it expresses the common full-fan-out case without repeating the key, and an explicit per-agent <code>worktree</code> still overrides it so a squad can group.</li></ul>
</main>
<footer><div class="wrap">
<div class="row">
<a href="index.html">home</a>
<a href="https://crates.io/crates/atrium">crates.io/atrium</a>
<a href="https://github.com/nativelite/atrium">github.com/nativelite/atrium</a>
<a href="https://github.com/nativelite/marketplace">marketplace</a>
<a href="https://github.com/nativelite">nativelite</a>
</div>
<div>Built on the nativelite stack. Zero third-party dependencies. MIT licensed.</div>
</div></footer>
</body>
</html>