feat: one OpenCode per project — worktree sessions run in-app (#265) #273

Merged
dries merged 5 commits from integration/one-opencode-per-project into main 2026-07-09 14:29:36 +02:00
Owner

Summary

One OpenCode instance per project. Worktree and same-directory
sessions now run in-app on that single instance instead of each
spawning their own opencode --port 0 in a dedicated tmux window.

Net effect per project: 1 opencode + 1 tmux session instead of N.

Implements the spec at spec/one-opencode-per-project/requirements.md
(parent #265) across four tickets.

What changed

  • #266opencode.CreateSession sends the session's directory
    (URL-encoded x-opencode-directory header) so one instance can create
    a session rooted at an external directory (a worktree). Optional Port
    on CreateSessionRequest skips lsof discovery when the instance is
    already known.
  • #267 — new hostsvc.Host.EnsureProjectOpencode: discovers or
    launches exactly one opencode per project (rooted at the main
    checkout), seeded with a scoped external_directory
    OPENCODE_PERMISSION rule for the project's .worktrees/<repo>/**
    root. Idempotent; remote gRPC passthrough included.
  • #268 — wires /wt, MCP new_session (worktree + same-dir), and
    loops spawn_worktree to create sessions in-app on the project
    instance. WorktreeSessionResult carries the created session ID;
    SessionLauncher drops the tmux launcher for an injected ensurer;
    frontend navigates to the session in-app.
  • #269 — retires the per-worktree tmux machinery
    (LaunchWorktreeWindow, the ocman-worktree session, dead result
    fields, the frontend tmux-switch button) and updates AGENTS.md +
    docs/architecture.md.

Also includes a standalone test-infra fix: internal/mcp worktree test
now cleans GIT_* env so git init in a tempdir isn't hijacked by
hook-injected GIT_DIR.

Notes / limitations

  • If a project's opencode was already running before this change, its
    launch was not seeded with the external_directory rule; runtime
    autoapprove covers that gap. (Documented in AGENTS.md.)
  • The OPENCODE_PERMISSION merge helper composes with #101 (inherit
    parent always-allow) — that ticket is out of scope here.

Testing

make test && make lint && make build green. New tests cover the
directory header + URL-encoding, EnsureProjectOpencode
(discover/launch/idempotent/non-repo/timeout), the permission builder
(scoped, merge-preserving), and the in-app /wt wiring.

Reviewed via the two-axis code-review (standards + spec); the one
finding — a stale frontend tmux-switch button — is fixed in #269.

Closes #265
Closes #266
Closes #267
Closes #268
Closes #269

## Summary One OpenCode instance **per project**. Worktree and same-directory sessions now run **in-app** on that single instance instead of each spawning their own `opencode --port 0` in a dedicated tmux window. Net effect per project: **1 opencode + 1 tmux session** instead of N. Implements the spec at `spec/one-opencode-per-project/requirements.md` (parent #265) across four tickets. ## What changed - **#266** — `opencode.CreateSession` sends the session's `directory` (URL-encoded `x-opencode-directory` header) so one instance can create a session rooted at an external directory (a worktree). Optional `Port` on `CreateSessionRequest` skips lsof discovery when the instance is already known. - **#267** — new `hostsvc.Host.EnsureProjectOpencode`: discovers or launches exactly one opencode per project (rooted at the main checkout), seeded with a scoped `external_directory` `OPENCODE_PERMISSION` rule for the project's `.worktrees/<repo>/**` root. Idempotent; remote gRPC passthrough included. - **#268** — wires `/wt`, MCP `new_session` (worktree + same-dir), and loops `spawn_worktree` to create sessions in-app on the project instance. `WorktreeSessionResult` carries the created session ID; `SessionLauncher` drops the tmux launcher for an injected ensurer; frontend navigates to the session in-app. - **#269** — retires the per-worktree tmux machinery (`LaunchWorktreeWindow`, the `ocman-worktree` session, dead result fields, the frontend tmux-switch button) and updates AGENTS.md + `docs/architecture.md`. Also includes a standalone test-infra fix: `internal/mcp` worktree test now cleans `GIT_*` env so `git init` in a tempdir isn't hijacked by hook-injected `GIT_DIR`. ## Notes / limitations - If a project's opencode was **already running** before this change, its launch was not seeded with the `external_directory` rule; runtime autoapprove covers that gap. (Documented in AGENTS.md.) - The `OPENCODE_PERMISSION` merge helper composes with #101 (inherit parent always-allow) — that ticket is out of scope here. ## Testing `make test && make lint && make build` green. New tests cover the directory header + URL-encoding, `EnsureProjectOpencode` (discover/launch/idempotent/non-repo/timeout), the permission builder (scoped, merge-preserving), and the in-app `/wt` wiring. Reviewed via the two-axis code-review (standards + spec); the one finding — a stale frontend tmux-switch button — is fixed in #269. Closes #265 Closes #266 Closes #267 Closes #268 Closes #269
TestNewSession_WorktreeCreatesWithModel shelled out to git init with the
ambient environment, so GIT_DIR/GIT_INDEX_FILE injected by a git hook
made it operate on the wrong repo and ResolveRepoRoot later failed.
Strip the git context vars via gitexec.CleanEnv, matching the pattern in
internal/git and internal/server tests.
CreateSession now sends the requested directory via the
x-opencode-directory header so a single opencode instance can create a
session rooted at an external directory (a worktree). Adds an optional
Port field to CreateSessionRequest so callers with a known instance port
skip lsof discovery.
New Host.EnsureProjectOpencode guarantees exactly one opencode instance
per project, rooted at the main checkout: discovers a running instance
and, if none, launches one in tmux seeded with a scoped
external_directory OPENCODE_PERMISSION rule and waits for its port.
Idempotent. Wired through the local host and the remote gRPC passthrough;
no caller uses it yet (#268).
Wire /wt, MCP new_session, and loops spawn_worktree through
EnsureProjectOpencode (#267) + directory-aware CreateSession (#266):

- CreateWorktreeSession ensures the project's single opencode instance,
  then creates the session in-app rooted at the worktree — no
  per-worktree tmux window. Returns the created session ID.
- WorktreeSessionResult gains SessionID; stops populating the
  tmux-switch fields.
- SessionLauncher drops launchTmux/discoverPort/waitForPort for a single
  injected project-opencode ensurer; worktree and same-dir splits
  self-heal instead of failing when no instance is running.
- URL-encode the x-opencode-directory header (paths with spaces).
- Frontend /wt navigates to the session in-app; Worktrees view joins by
  per-session directory.

LaunchWorktreeTmux is left wired but uncalled; #269 removes it.
refactor(tmux): retire per-worktree tmux launcher; update docs (#269)
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m6s
CI / Backend (pull_request) Failing after 2m20s
CI / Frontend (pull_request) Successful in 4m38s
CI / Coverage Ratchet (pull_request) Failing after 4m43s
CI / Playwright E2E (pull_request) Successful in 6m6s
CI / Build (pull_request) Successful in 3m56s
CI / Semantic Tag (pull_request) Has been skipped
7de0376c2d
Worktree sessions run in-app on the project's single opencode instance
(#268), so the per-worktree tmux machinery is dead. Remove:

- tmux.LaunchWorktreeWindow / LaunchWorktreeWindowWith and the
  WorktreeSession ("ocman-worktree") constant + their tests.
- the LaunchWorktreeTmux host dep and its server wiring.
- WorktreeSessionResult.{TmuxSession,TmuxTarget,OpencodeLaunched}.
- the frontend per-worktree tmux-switch button in WorktreesView and the
  now-dead tmuxTargetForWorktree / tmuxWindowNameForWorktreePath helpers
  (worktree rows now only offer in-app "Open session").

Update AGENTS.md, docs/architecture.md, and spec/worktree-sessions to
describe the one-opencode-per-project in-app model, the scoped
external_directory OPENCODE_PERMISSION seeding, and the
pre-existing-instance limitation.
dries force-pushed integration/one-opencode-per-project from 7de0376c2d
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m6s
CI / Backend (pull_request) Failing after 2m20s
CI / Frontend (pull_request) Successful in 4m38s
CI / Coverage Ratchet (pull_request) Failing after 4m43s
CI / Playwright E2E (pull_request) Successful in 6m6s
CI / Build (pull_request) Successful in 3m56s
CI / Semantic Tag (pull_request) Has been skipped
to f1d1ffb7f1
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m8s
CI / Coverage Ratchet (pull_request) Failing after 3m36s
CI / Frontend (pull_request) Successful in 4m53s
CI / Backend (pull_request) Has been cancelled
CI / Playwright E2E (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
2026-07-09 13:37:21 +02:00
Compare
dries force-pushed integration/one-opencode-per-project from f1d1ffb7f1
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m8s
CI / Coverage Ratchet (pull_request) Failing after 3m36s
CI / Frontend (pull_request) Successful in 4m53s
CI / Backend (pull_request) Has been cancelled
CI / Playwright E2E (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Semantic Tag (pull_request) Has been cancelled
to ecea0136f5
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m5s
CI / Frontend (pull_request) Successful in 5m35s
CI / Backend (pull_request) Failing after 5m36s
CI / Coverage Ratchet (pull_request) Failing after 5m52s
CI / Build (pull_request) Successful in 3m48s
CI / Playwright E2E (pull_request) Successful in 4m52s
CI / Semantic Tag (pull_request) Has been skipped
2026-07-09 13:43:03 +02:00
Compare
dries force-pushed integration/one-opencode-per-project from ecea0136f5
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 1m5s
CI / Frontend (pull_request) Successful in 5m35s
CI / Backend (pull_request) Failing after 5m36s
CI / Coverage Ratchet (pull_request) Failing after 5m52s
CI / Build (pull_request) Successful in 3m48s
CI / Playwright E2E (pull_request) Successful in 4m52s
CI / Semantic Tag (pull_request) Has been skipped
to 5181602888
Some checks failed
CI / Build Desktop (macOS arm64) (pull_request) Successful in 4m4s
CI / Backend (pull_request) Successful in 4m26s
CI / Frontend (pull_request) Successful in 4m47s
CI / Coverage Ratchet (pull_request) Successful in 7m31s
CI / Build (pull_request) Successful in 4m2s
CI / Playwright E2E (pull_request) Successful in 5m28s
CI / Semantic Tag (pull_request) Has been skipped
CI / Frontend (push) Has been cancelled
CI / Coverage Ratchet (push) Has been cancelled
CI / Backend (push) Has been cancelled
CI / Playwright E2E (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Build Desktop (macOS arm64) (push) Has been cancelled
CI / Semantic Tag (push) Has been cancelled
2026-07-09 14:18:28 +02:00
Compare

Coverage ratchet: pass

Suite Baseline This PR Δ
go 71.50% 71.80% +0.30
frontend 61.42% 61.52% +0.10

Tolerance: -0.1%. Baseline stored on gh-pages.

<!-- coverage-ratchet --> ### Coverage ratchet: ✅ pass | Suite | Baseline | This PR | Δ | | |---|---|---|---|---| | go | 71.50% | 71.80% | +0.30 | ✅ | | frontend | 61.42% | 61.52% | +0.10 | ✅ | _Tolerance: -0.1%. Baseline stored on `gh-pages`._
dries merged commit 5181602888 into main 2026-07-09 14:29:36 +02:00
dries deleted branch integration/one-opencode-per-project 2026-07-09 14:29:36 +02:00
Sign in to join this conversation.
No description provided.