Repository navigation
Isolate opencode-skein's own agent execution in per-change git worktrees #19
Description
Proposal
Why
Two or more opencode-skein agents (manually launched TUI instances, or /loop
sessions) working in the same checkout will happily switch branches out from
under each other — there is no isolation between concurrent git operations in
one working directory. This is a live, recurring pain, not a hypothetical: the
2026-07-26 incident that motivated session-summary-write-amplification also
surfaced two opencode-skein processes running concurrently against the same
/Users/andreas/dev/brick-now checkout, with no separation between them.
This is not a new problem for the ecosystem — it already has a working
solution one level up. The external skein orchestrator creates a dedicated
git worktree per change before dispatching a coder agent, confirmed directly
from this repo's own change handoffs:
openspec/changes/error-boundaries-plan/.skein/coder-context.md:
Repo root: /Users/andreas/dev/opencode-worktrees/error-boundaries-planopenspec/changes/refactor-context-management-to-intr/.skein/coder-context.md:
Repo root: /Users/andreas/dev/opencode-worktrees/refactor-context-management-to-intr
opencode-skein's own /loop and Task-tool build agents have no equivalent.
This gap has already been named twice in the backlog and punted both times:
loop-spec-queue's Non-Goals: "No parallel execution of changes. Serial
only; parallel edits to one working tree need worktree isolation, which
is a separate change."provider-slot-leases's Non-Goals: "Not leasing anything other than
provider slots — file locks and repo/worktree contention between
instances are a separate problem."
This change is that separate change.
What Changes
- A worktree lifecycle module,
packages/opencode/src/git/worktree.ts,
following this repo's existing precedent for shelling out to git
(packages/opencode/src/snapshot/index.ts'sgit()helper over
ChildProcessSpawner;script/sync-upstream.ts's
git worktree add -b <branch> <path> <base>invocation):ensure(slug)— reuse../opencode-worktrees/<slug>if it already
exists (from a previous run, or fromskeinitself — same convention,
same path, so the two systems never collide); otherwise
git worktree add -b <branch> <path> <base>.merge(slug)— from the main checkout,git merge --no-ffthe
worktree's branch locally. Never pushes — matchesloop-spec-queue's
already-decided authority boundary (edit/test/verify/commit locally,
stop before push).cleanup(slug)—git worktree remove, only after a successful merge.
- Wired into
/loop's start path, behind an experimental flag
(experimental.agent_worktree_isolation, default off — this changes
where an agent writes files and which branch it operates on, so it stays
opt-in until proven safe, unlikelocal_subagent_placement's default-on
opt-out convention). - Sibling-directory convention documented:
../opencode-worktrees/<slug>,
matching skein's existing layout exactly, so a human or either tool can
find and reuse the same worktree.
Non-Goals
- No "pick the next spec automatically" behavior. That is
loop-spec-queue's job, once it exists — this change only makes worktree
isolation available to whatever drives a loop into a change. It does not
itself decide which change to work on next or call specsync. - No blocking on
loop-spec-queue. That change has zero implemented
tasks and is itself gated behind three other unimplemented changes. This
change is usable standalone: point a loop at one change, it gets a
worktree. - No Task-tool subagent worktrees. Subagents dispatched via the Task tool
are mostly read-heavy or make small, short-lived edits; the collision risk
observed so far is between full/loop/build sessions. Revisit only if
subagents are observed causing the same problem. - No cross-host coordination. This is single-machine, single-checkout
isolation. Fleet-wide coordination isfleet-instance-presence/
agent-coordination-bus's domain. - No file-level locking across processes, and no attempt to prevent two
different opencode-skein instances from independently choosing the same
change —ensure()reusing an existing worktree by path handles the common
case (same slug → same directory) but is not a distributed lock.
Impact
- New:
packages/opencode/src/git/worktree.ts. - Modified:
packages/opencode/src/loop/*(start/stop path, behind the flag). - Config: new
experimental.agent_worktree_isolationboolean. - No effect on any existing behavior when the flag is off (the default).
Disposition (2026-09-18)
Archived — superseded/obsolete. Dropped 2026-09-18: src/git/worktree.ts was never imported; upstream's Worktree service + TUI move-session-to-worktree covers per-session isolation. Queue-mode auto-isolation + merge-back can be a new change on upstream's service.
Tasks
Phase 1: Worktree lifecycle module (this session's implementation target)
- 1.1
packages/opencode/src/git/worktree.ts: implementensure(repoRoot, slug, base?)—
compute../opencode-worktrees/<slug>relative torepoRoot; if it
exists and is a valid git worktree, return its path; otherwise run
git worktree add -b <branch> <path> <base>(branch name:
loop/<slug>, matchingloop-spec-queuetask 4.4's convention) and
return the new path. Verify: unit test against a scratch git repo —
calling twice with the same slug returns the same path and does not
error the second time.- Signature takes
repoRootexplicitly rather than reading it from
instance state, so Phase 1 is testable standalone without pulling in the
Effect service/InstanceState machinery. Phase 2 wires it to the real
repo root when integrating with/loop.
- Signature takes
- 1.2 Implement
merge(slug)— from the main checkout,git merge --no-ff
the worktree's branch. Never runsgit push. Verify: unit test — after
a commit in the worktree,mergebrings that commit into the main
checkout's current branch; the main checkout's branch is unchanged if
the worktree has no commits ahead. - 1.3 Implement
cleanup(slug)—git worktree removethe path. Only
call after a successfulmerge. Verify: unit test — the worktree
directory and its git metadata are gone after cleanup; a second
cleanupon an already-removed slug does not throw. - 1.4 Error handling:
ensureon a path that exists but is not a valid
git worktree (e.g. a stray directory) fails with a clear error rather
than silently reusing it or corrupting it. Verify: unit test. - 1.5 Full typecheck (
bun run --cwd packages/opencode typecheck) and the
new unit test suite green.packages/opencode/test/git/worktree.test.ts— 7 tests, all passing,
against real scratch git repos (init, commit, worktree add/merge/remove).
Phase 2: Wire into /loop (follow-up, not this session)
- 2.1 Add
experimental.agent_worktree_isolation: Schema.optional(Schema.Boolean)
topackages/core/src/v1/config/config.ts, default off (only enabled
when explicitlytrue, opposite polarity fromlocal_subagent_placement). - 2.2 On loop start with the flag on: resolve the target change's slug,
callensure(slug), and run the loop's work rooted at that worktree
path instead of the main checkout. - 2.3 On loop completion (success): call
merge(slug)then
cleanup(slug). On halt/failure: leave the worktree in place (matches
loop-spec-queue's "leave the working tree as-is" halt semantics) and
report its path so the user can inspect or resume it manually. - 2.4 Manual end-to-end: start a loop on change A with the flag on,
confirm it runs in../opencode-worktrees/Aand the main checkout is
untouched; start a second loop on change B concurrently, confirm it
gets its own worktree and neither loop's git operations affect the
other's branch. - 2.5 Manual: confirm a
skein-created worktree for the same slug is
reused rather than duplicated (same path convention).
Phase 3: Documentation
- 3.1 Document the
../opencode-worktrees/<slug>convention and the flag
in whatever surfaces/loop's other experimental flags today (CLI
help,--helpoutput, or the relevant docs file — match however
local_subagent_placementis documented, if at all).
Unchecked items above: see Disposition in proposal.md (2026-09-18).
Plan changes
5 done
Activity
androidand commentedon Sep 17, 2026
Archived 2026-09-18 — see the change's Disposition section.