Agent git writes are worktree-isolated off origin/main; the primary checkout is the human's
Status: open/accepted
Summary
Section titled “Summary”- No agent process mutates the primary checkout’s working tree, index, or
HEAD. Every agent git write happens in an ephemeral worktree created offorigin/mainand lands via push (or PR). The primary checkout belongs to the human. - This is the isolation half of serverless concurrency. Leasing
(
github-ref-leases/README) already owns the coordination half — who may work a task; this ADR owns where that work physically happens. The two compose and neither replaces the other. - It generalizes a discipline already proven in
task-workandtask-close-out(B-P502-commit-task-state-and-planning-docs-via-ephemeral-worktrees) into a universal, enforced invariant, retiring the shared-checkout race class. - State-write strategy (push-per-op vs an append-only event log) is out of scope here — this ADR fixes where writes happen, not how state is modeled. The event log (B-M9JX-append-only-event-log-with-frontmatter-cache) is the declared endgame, decided separately.
Status
Section titled “Status”Accepted and in force — every agent state-write flow runs through the isolation path below. Extends D-VSLI-distributed-work-runner-architecture (which names lease coordination the invariant that lets parallel agents work without a server) by adding the complementary execution-isolation invariant. Does not modify the lease protocol or the entity substrate.
Context
Section titled “Context”SDLC ran many concurrent agents against one git working checkout, using branches as the unit
of work. But a working tree, its index, and HEAD are singular, mutable, shared state — so
operations that switch, reset, stage, or commit on the shared checkout race each other. The same
root cause surfaced five times in one session:
| Symptom | Root |
|---|---|
lint-state-origin false-positives (#470) | state commits pile on local main, ahead of origin |
docs-drift false-positives (#471) | regen reads the shared dirty tree → a sibling’s edits contaminate the union docs |
| close-out autostash sweep (#472) | git pull --rebase --autostash stashes the whole shared tree |
| backlog-create desync (#475) | branch-switch + regen on the shared checkout |
caller stranded on backlog-capture (B-0H4P) | git checkout -B on the shared checkout yanks its branch |
Coordination was already solved: the lease CAS-claim at refs/sdlc/tasks/<id> prevents
double-dispatch, and none of these races were double-dispatch. What was not solved was isolation —
where the owned work runs. Some flows isolated (task-work worktrees, the task close-commit op);
others mutated the shared checkout (backlog-create, start_task/ensure_ready local-main commits,
the docs regen). The race lived in exactly that gap.
Decision
Section titled “Decision”Adopt the isolation invariant below and run every agent git-write flow through it. All formerly
shared-checkout flows — start_task/ensure_ready state commits, backlog-create, close-out — are
converted.
The invariant
Section titled “The invariant”No agent process mutates the primary checkout’s working tree, index, or HEAD. All agent git
writes happen in an ephemeral worktree created off origin/main (or off a freshly-fetched ref) and
land via git push HEAD:main or an opened PR. The worktree is torn down after. The primary
checkout is read-only from the agents’ perspective — it is the human’s.
Why a worktree off origin/main
Section titled “Why a worktree off origin/main”A worktree gives each unit of work its own working tree, index, and detached HEAD, sharing only
the object database. Creating it off origin/main means the tree starts clean and current, so:
regen runs against an uncontaminated corpus (no sibling-edit forward contamination); there is no
shared tree to autostash; and there is no local-main-ahead-of-origin divergence to make gates
false-positive. A push race (another worker advanced main) is recovered by re-applying onto the
fresh tip — reset the worktree to the new origin/main, re-apply the mutation, re-regenerate
(deterministic off the new base, so union artifacts never conflict), re-push. This is the
task close-commit pattern (#472), generalized.
Leasing is unchanged
Section titled “Leasing is unchanged”This ADR does not touch task selection or ownership. Leasing
(github-ref-leases/README) remains the coordination layer: a CAS-claimed ref answers who owns
a task. Isolation answers where the owner runs. Even once the event log lands, leasing stays —
an append-only log gives history, but mutual exclusion needs a compare-and-swap primitive, which
is exactly the lease ref. Log = append-for-state; lease = CAS-for-ownership.
Enforcement
Section titled “Enforcement”Enforcement is structural plus a tripwire. Structural: every task-state call site — start_task,
ensure_ready_mutate --commit-on main, task close-commit — commits through the shared
commitToMainViaWorktree helper (plugin/lib/services/git/commit-to-main.ts); backlog-create
commits captures through its own ephemeral commit worktree. Tripwire:
plugin/lib/services/git/tests/primary-checkout-isolation.test.ts asserts each task-state path
leaves the primary checkout’s HEAD, tree, and index byte-identical while the change lands on
origin/main — a regression fails the test that names the broken path. worktree-scope-guard
(sdlc gate worktree-scope) remains the pre-commit net on the primary checkout: it self-skips
inside a linked worktree and rejects commits on main whose staged paths collide with an active
worktree.
The state-write layer
Section titled “The state-write layer”start_task/ensure_ready push each state commit to origin/main directly through the shared
helper, retry-on-race — the task close-commit template. Push-per-op relocates contention to the
main ref; the append-only event log
(B-M9JX-append-only-event-log-with-frontmatter-cache) is the declared endgame that retires the
retry-by-reapply — it is decided separately, not bundled here.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- The shared-checkout race class is gone: no agent branch-switch on the human’s tree, no autostash sweep, no dirty-tree regen contamination, no local-main divergence.
- The human’s checkout becomes stable — agents never touch it. The B-0H4P strand cannot recur.
- Per-flow gate workarounds collapse: a clean worktree regenerates clean, so #471’s staged-index concern is subsumed for converted flows.
Obsidian / file-first ergonomics
Section titled “Obsidian / file-first ergonomics”- Reading task state in Obsidian is preserved. Status stays in the frontmatter on
main; the invariant only changes where agents commit it. The former parallel churn on the shared checkout is replaced by a stable local vault, at the cost of agit pullto pull in the latest statuses (commits land on origin, not local main — “stable, pull-to-refresh” instead of “live-but-chaotic”). - This trade is isolation’s only Obsidian cost, and it is favorable. The deeper file-first trade-off — losing the ability to hand-edit status in Obsidian and have it stick — belongs to the event log, not this ADR: there, status becomes a projection of the log, so a raw frontmatter edit would be overwritten unless the projection treats it as an appended event. That constraint (“preserve Obsidian read AND write ergonomics; keep the cache materialized and committed”) is recorded here as input to the future event-log decision.
- Each isolated op pays worktree setup (~hundreds of ms + object-shared disk). Tolerable at
handful-of-agents scale; a worktree pool or
--no-checkout/sparse is the mitigation if it bites. - Push-per-state-commit makes
origin/maina hotter write ref. Bounded by retry-by-reapply; unbounded parallelism is the argument for bringing the event log forward.
What this does not decide
Section titled “What this does not decide”- The state model. Whether state is mutable frontmatter or an append-only event log (B-M9JX-append-only-event-log-with-frontmatter-cache) is a separate decision; this ADR only fixes where writes happen.
- Push cadence. The invariant fixes where state commits happen, not how often; any batching affordance over the current push-per-op cadence is an implementation call.
Open questions
Section titled “Open questions”- Scale. At dozens of concurrent agents, does
main-ref push contention argue for pushing state to per-agent refs a reconciler folds in (nevermaindirectly), i.e. pulling the event log forward sooner? - Worktree cost. Is a reusable worktree pool worth it, or is per-op create/teardown fine at the target concurrency?
- Lease ↔ log coexistence. When the event log lands, what is the exact boundary — lease ref for ownership CAS, log for everything else — and how does a hand-edit become an appended event so the file stays Obsidian-editable?
References
Section titled “References”- D-VSLI-distributed-work-runner-architecture ·
github-ref-leases/README - B-P502-commit-task-state-and-planning-docs-via-ephemeral-worktrees (the proven pattern) · B-0H4P-sdlc-backlog-create-strands-the-caller-s-checkout-on-backlog (the motivating violation, fixed by the backlog-create conversion) · B-M9JX-append-only-event-log-with-frontmatter-cache (the state-model endgame)
- Shipped point-fixes that this invariant generalizes: #470 (lint base-ref), #471 (docs-drift
staged-index), #472 (
task close-commitvia worktree), #475 (backlog format-before-regen).