You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Isolate opencode-skein's own agent execution in per-change git worktrees #19
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:
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's git() 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 from skein itself — 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-ff the
worktree's branch locally. Never pushes — matches loop-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, unlike local_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 is fleet-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_isolation boolean.
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.
1.1 packages/opencode/src/git/worktree.ts: implement ensure(repoRoot, slug, base?) —
compute ../opencode-worktrees/<slug> relative to repoRoot; 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>, matching loop-spec-queue task 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 repoRoot explicitly 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.
1.2 Implement merge(slug) — from the main checkout, git merge --no-ff
the worktree's branch. Never runs git push. Verify: unit test — after
a commit in the worktree, merge brings 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 remove the path. Only
call after a successful merge. Verify: unit test — the worktree
directory and its git metadata are gone after cleanup; a second cleanup on an already-removed slug does not throw.
1.4 Error handling: ensure on 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)
to packages/core/src/v1/config/config.ts, default off (only enabled
when explicitly true, opposite polarity from local_subagent_placement).
2.2 On loop start with the flag on: resolve the target change's slug,
call ensure(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/A and 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, --help output, or the relevant docs file — match however local_subagent_placement is documented, if at all).
Unchecked items above: see Disposition in proposal.md (2026-09-18).
Proposal
Why
Two or more opencode-skein agents (manually launched TUI instances, or
/loopsessions) 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-amplificationalsosurfaced two opencode-skein processes running concurrently against the same
/Users/andreas/dev/brick-nowcheckout, 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
skeinorchestrator creates a dedicatedgit 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-intropencode-skein's own
/loopand 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. Serialonly; parallel edits to one working tree need worktree isolation, which
is a separate change."
provider-slot-leases's Non-Goals: "Not leasing anything other thanprovider slots — file locks and repo/worktree contention between
instances are a separate problem."
This change is that separate change.
What Changes
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 overChildProcessSpawner;script/sync-upstream.ts'sgit worktree add -b <branch> <path> <base>invocation):ensure(slug)— reuse../opencode-worktrees/<slug>if it alreadyexists (from a previous run, or from
skeinitself — 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-fftheworktree's branch locally. Never pushes — matches
loop-spec-queue'salready-decided authority boundary (edit/test/verify/commit locally,
stop before push).
cleanup(slug)—git worktree remove, only after a successful merge./loop's start path, behind an experimental flag(
experimental.agent_worktree_isolation, default off — this changeswhere an agent writes files and which branch it operates on, so it stays
opt-in until proven safe, unlike
local_subagent_placement's default-onopt-out convention).
../opencode-worktrees/<slug>,matching skein's existing layout exactly, so a human or either tool can
find and reuse the same worktree.
Non-Goals
loop-spec-queue's job, once it exists — this change only makes worktreeisolation available to whatever drives a loop into a change. It does not
itself decide which change to work on next or call specsync.
loop-spec-queue. That change has zero implementedtasks and is itself gated behind three other unimplemented changes. This
change is usable standalone: point a loop at one change, it gets a
worktree.
are mostly read-heavy or make small, short-lived edits; the collision risk
observed so far is between full
/loop/build sessions. Revisit only ifsubagents are observed causing the same problem.
isolation. Fleet-wide coordination is
fleet-instance-presence/agent-coordination-bus's domain.different opencode-skein instances from independently choosing the same
change —
ensure()reusing an existing worktree by path handles the commoncase (same slug → same directory) but is not a distributed lock.
Impact
packages/opencode/src/git/worktree.ts.packages/opencode/src/loop/*(start/stop path, behind the flag).experimental.agent_worktree_isolationboolean.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)
packages/opencode/src/git/worktree.ts: implementensure(repoRoot, slug, base?)—compute
../opencode-worktrees/<slug>relative torepoRoot; if itexists 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) andreturn 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.
repoRootexplicitly rather than reading it frominstance 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.merge(slug)— from the main checkout,git merge --no-ffthe worktree's branch. Never runs
git push. Verify: unit test — aftera commit in the worktree,
mergebrings that commit into the maincheckout's current branch; the main checkout's branch is unchanged if
the worktree has no commits ahead.
cleanup(slug)—git worktree removethe path. Onlycall after a successful
merge. Verify: unit test — the worktreedirectory and its git metadata are gone after cleanup; a second
cleanupon an already-removed slug does not throw.ensureon a path that exists but is not a validgit worktree (e.g. a stray directory) fails with a clear error rather
than silently reusing it or corrupting it. Verify: unit test.
bun run --cwd packages/opencode typecheck) and thenew 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)experimental.agent_worktree_isolation: Schema.optional(Schema.Boolean)to
packages/core/src/v1/config/config.ts, default off (only enabledwhen explicitly
true, opposite polarity fromlocal_subagent_placement).call
ensure(slug), and run the loop's work rooted at that worktreepath instead of the main checkout.
merge(slug)thencleanup(slug). On halt/failure: leave the worktree in place (matchesloop-spec-queue's "leave the working tree as-is" halt semantics) andreport its path so the user can inspect or resume it manually.
confirm it runs in
../opencode-worktrees/Aand the main checkout isuntouched; 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.
skein-created worktree for the same slug isreused rather than duplicated (same path convention).
Phase 3: Documentation
../opencode-worktrees/<slug>convention and the flagin whatever surfaces
/loop's other experimental flags today (CLIhelp,
--helpoutput, or the relevant docs file — match howeverlocal_subagent_placementis documented, if at all).Plan changes
5 done