One OpenCode per project: worktree sessions run in-app, reuse the project instance #265
Labels
No labels
backend
bug
chore
duplication
effort:complex
effort:medium
effort:trivial
enhancement
frontend
fullstack
priority:high
ready-for-agent
refactor
security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dries/ocman#265
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
One OpenCode Per Project — Worktree Sessions Run In-App
Problem Statement
Today, every worktree session (
/wt, MCPnew_sessionwith a worktree,loops
spawn_worktree) shells out togit worktree addand then launchesa brand-new
opencode --port 0inside its own tmux window. A userworking across several worktrees of one project ends up with N opencode
processes and N tmux windows for a single repo. This is cruft: it eats
RAM, clutters
tmux list-sessions, and multiplies the number of liveagent processes the user has to reason about.
The user wants one opencode per project. Additional worktrees (and
other directories) of that project should reuse the project's single
opencode instance instead of each spawning their own process and tmux
window.
Solution
A worktree session no longer spawns its own opencode. Instead:
at the project's main checkout. If none is running when a session is
requested, ocman launches one (in tmux, rooted at the main checkout).
using OpenCode's per-session
directoryparameter to point the sessionat the worktree path. ocman drives the session over OpenCode's HTTP API
and surfaces it in its own UI. There is no per-worktree tmux window
and no per-worktree opencode process.
tool calls trigger OpenCode's
external_directorypermission. ocmanpre-authorizes that permission for paths under the project's own
.worktrees/root by seedingOPENCODE_PERMISSIONwhen it launches theproject instance.
The old "TUI per worktree" flow is removed — there is no opt-in to
keep it. Net effect per project: 1 opencode + 1 tmux session instead
of N.
Background Facts (verified)
directoryonPOST /sessionvia the?directory=query param orthe
x-opencode-directoryheader. Verified live: a single opencodelaunched in the ocman checkout created sessions rooted at an unrelated
external directory (
/private/tmp), and the returned session objectreported the correct
directory,path, andprojectID.directory(GET /sessionandGET /session/{id}), so a session's worktree can be read back ratherthan inferred from the process cwd.
external_directoryis a first-class OpenCode permission key("Any tool that reads or writes files outside the project worktree").
OPENCODE_PERMISSION(inline JSON, loaded at launch, precedenceover project/global config) is how ocman already plans to seed child
permissions (tracker #101). It can carry an
external_directoryallowrule for the
.worktrees/root.opencode.CreateSessionalready drives sessions over HTTP(
prompt_async,shell,command,abort, permissions) but currentlyuses
req.Directoryonly for port discovery — it POSTs/sessionwith a literal
{}body, so today a session's directory always equalsthe opencode process's launch cwd.
User Stories
existing opencode, so that I don't accumulate one opencode process per
worktree.
existing tmux session, so that
tmux list-sessionsstays clean.inside ocman's UI, so that I don't need to switch to a terminal TUI to
work in a worktree.
running for that project, I want ocman to launch exactly one opencode
(rooted at the main checkout) automatically, so that the flow always
succeeds without a manual "start opencode first" step.
project, I want it to reuse the same opencode that the first one
launched, so that only one process ever exists per project.
run without being blocked by
external_directorypermission promptsfor paths ocman itself created under
.worktrees/, so that the sessionis usable immediately.
the
.worktrees/root to be silently auto-allowed, so that thepre-authorization stays scoped to what ocman vouches for.
new_sessionwith a worktree, I want it toreuse the project's single opencode instead of spawning its own, so
that agent-split worktrees follow the same one-per-project rule.
spawn_worktreeaction, I wanteach spawned worktree session to reuse the project's single opencode,
so that a long-running loop doesn't spawn a fleet of opencode
processes.
new_sessionin the same directory, I wantocman to launch the project opencode if none is running, so that
same-directory splits self-heal instead of failing with
"no running OpenCode instance".
created session ID (the thing I navigate to in-app), rather than a
tmux target, so that the UI can open the session directly.
worktree with its sessions by the session's own
directory, so thatthe association is right even though all those sessions share one
opencode process whose cwd is the main checkout.
per-worktree flow to keep working until I close them, so that the
change doesn't forcibly tear down my running work.
isn't a git repository or opencode can't be launched, so that
failures are legible rather than silent.
per project" to run on the host that owns the project directory
(local or a remote), so that the process is launched where the files
are.
external_directorypre-authorization tosurvive across worktree sessions of the same project, so that the
second and later worktrees don't re-prompt — it is seeded once, at the
project instance's launch.
Implementation Decisions
D-1: New central host capability
EnsureProjectOpencodehostsvc.Host:EnsureProjectOpencode(ctx, req) (result, error)where the requestcarries the project directory (any path inside the repo) and the result
carries the discovered/launched opencode port (and the resolved repo
root + tmux session name for observability).
directory; discover a running opencode for that repo root; if none,
launch exactly one
opencode --port 0in tmux rooted at the maincheckout, seeded with
OPENCODE_PERMISSION(see D-4), and wait for itsport to become discoverable; return the port. Idempotent: a second call
for the same project returns the already-running instance without
launching a second one.
host that owns the directory (local
Host, or a remote's in-processHostvia gRPC), following the existingHost/Router(
ForDir/ForRemote) seam. The remote gRPC surface gains one RPC thatmarshals/unmarshals through the existing
JsonReq/JsonRespenvelope(no protobuf message regeneration needed).
D-2:
EnsureProjectOpencodeis the single seam all three callers share/wt—local.Host.CreateWorktreeSessionstops callingLaunchWorktreeTmux. It creates the git worktree, callsEnsureProjectOpencode(repoRoot), then creates an in-app sessionpointed at the worktree path against the returned port.
new_session(worktree) + loopsspawn_worktree—mcp.SessionLauncher.LaunchWithWorktreereplaces its currentlaunchTmux(repoRoot, worktreePath)+waitForPort(worktreePath)withEnsureProjectOpencode(repoRoot), then callsLaunchwithDirectory = worktreePath.EnsureProjectOpencodeis injected intoSessionLauncheras a dependency (replacing thelaunchTmux/discoverPort/waitForPorttrio for the worktree path).new_session(same directory) —SessionLauncher.Launch(orits callers) calls
EnsureProjectOpencode(dir)beforeCreateSession,so same-directory splits self-heal instead of returning
ErrPlatformUnreachable.D-3:
opencode.CreateSessionmust send the session directoryopencode.Adapter.CreateSessioncurrently usesreq.Directoryonly forport discovery and POSTs
{}. It must additionally pass the directoryto OpenCode so the project instance can create a session rooted at a
worktree: send
x-opencode-directory(URL-encoded) or?directory=on
POST /session.project instance's port (discovered by repo root / provided by
EnsureProjectOpencode), not a port discovered by the worktree path.The directory sent to OpenCode is the worktree; the port is the project
instance's.
platforms.CreateSessionRequestmay gain an explicit port/target field(or the adapter is told the port by the caller) so the caller that
already ran
EnsureProjectOpencodedoesn't re-run discovery. Keep theinterface change minimal; prefer threading the known port over a second
lsof scan.
D-4: Pre-authorize
external_directoryviaOPENCODE_PERMISSIONat launchEnsureProjectOpencodelaunches the project instance, it setsOPENCODE_PERMISSION(inline JSON) with anexternal_directoryallowrule scoped to the project's
.worktrees/<repo>/**root..worktrees/root for thatproject are allowed. Directories outside that root are not blanket
allowed (US-7).
already running (launched by the user, or a prior ocman launch),
ocman does not restart it to inject the rule; in that case worktree
tool calls fall through to OpenCode's normal permission flow (which
ocman's autoapprove pipeline can still handle at runtime). This is an
accepted limitation, noted in Further Notes.
permissions): both write
OPENCODE_PERMISSIONat launch; theexternal_directoryrule is merged with any inherited permission setrather than overwriting it.
D-5:
WorktreeSessionResultreturns a session ID, not a tmux targethostsvc.WorktreeSessionResultchanges: the primary output becomes thecreated session ID (plus worktree path, branch, reused flags). The
tmux-target / opencode-launched fields that drove
tmux switch-clientare removed for this path.
/api/worktree/create-and-launchresponse shape changesaccordingly; the frontend navigates to the session in-app instead of
switching tmux.
D-6: Retire the per-worktree tmux machinery
tmux.LaunchWorktreeWindow,LaunchWorktreeWindowWith, theWorktreeSession("ocman-worktree") constant, and the per-worktreewindow naming / AD-3 / AD-4 idempotent-relaunch logic from the
worktree-sessions spec. The one remaining tmux launch is the single
project instance (via
EnsureProjectOpencode, reusingLaunchOpencode-style session-per-main-checkout).teardown logic is added.
D-7: Frontend joins worktree ↔ session by the session's own directory
directory equality, but the source of truth is each session's own
directory(now distinct from the opencode process cwd), which ocmansurfaces per session. No change to the join shape, only to ensuring
the per-session directory is available and used.
D-8: Capability flag semantics unchanged, preconditions adjusted
worktreeSessionscapability stays server-wide. Its preconditionset is unchanged in spirit (git + tmux + opencode on PATH + an OpenCode
adapter registered) since the single project instance still runs in
tmux.
Testing Decisions
Good tests here assert external behavior at the seams, not internal
call order:
EnsureProjectOpencode(host level) — the primary new seam. Testagainst a fake runner/discoverer:
that port and launches nothing (idempotency / one-per-project).
OPENCODE_PERMISSIONwith the scopedexternal_directoryrule, waitsfor the port, returns it.
launch.
git.ErrNotARepo).Prior art:
internal/tmux/launch_test.go(fakeRunner),internal/mcp/launchertests (injectedlaunchTmux/discoverPort).opencode.CreateSessionsends directory (adapter level) — against afake OpenCode HTTP server, assert that
POST /sessioncarries theworktree directory (query param or header) and that the returned
session's directory is honored. Prior art:
internal/platforms/opencode/operations_test.go(
TestCreateSessionPinsReturnedSessionPort,TestCreateSession_TitleIsSetAfterCreation).local.Host.CreateWorktreeSession(host orchestration) — with atemp git repo and fakes for
EnsureProjectOpencode+ session create,assert: worktree created, project instance ensured, session created
with
Directory == worktreePath, result carries the session ID.SessionLauncher.LaunchWithWorktree/Launch— assert both nowroute through the injected
EnsureProjectOpencodeand no longer callthe retired per-worktree tmux launcher. Same-directory
Launchself-heals (ensures instance) instead of erroring.
HTTP handler —
handlers_worktree_test.go: response now carries asession ID; status-code mapping for the error paths preserved.
Frontend —
WorktreesViewassociates sessions by per-sessiondirectory;
api.worktree.createAndLaunchround-trip returns a sessionID and navigation targets the session (not tmux switch). Prior art:
existing worktree view +
api.test.ts.Coverage ratchet applies: new functions (
EnsureProjectOpencode, theOPENCODE_PERMISSIONbuilder, the directory-sending branch ofCreateSession) ship with tests in the same PR.Out of Scope
external_directoryrule is seeded once at instance launch (D-4);re-seeding a pre-existing instance is out of scope.
previous flow (US-13) — they are left running.
external_directoryrule — out of scope (runtime autoapprove coversthe gap).
existing worktree feature.
.worktrees/— unchanged from thecurrent deterministic layout.
this spec only requires that D-4's
OPENCODE_PERMISSIONseeding mergewith, not conflict with, #101's mechanism.
Further Notes
OPENCODE_PERMISSIONatlaunch. Implementation must merge the
external_directoryrule with anyinherited permission set rather than overwrite. Sequencing: whichever
lands second must account for the first.
already running (user-launched, or launched before this feature), the
external_directoryrule was not seeded into it. Worktree tool calls inthat case fall through to OpenCode's normal permission flow; ocman's
runtime autoapprove pipeline can still allow them. This is acceptable
for v1 and should be documented in
docs/alongside the feature.EnsureProjectOpencoderuns on the owning host (R-C),same as
CreateWorktreeSessiontoday. The hub never launches opencodefor a remote's project.
AGENTS.md(worktree paragraph — no longer "oneopencode per worktree"),
docs/architecture.md(session/worktree dataflow), and any worktree-sessions user docs.