From 28e134873ef72d06c4bbff7ac0e214baab0dcfb9 Mon Sep 17 00:00:00 2001 From: Kilian Date: Sat, 10 Oct 2026 17:46:21 +0200 Subject: [PATCH] fix(opencode-local): run in the realized workspace (#11438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Paperclip adapters start each agent in its selected workspace > - Paperclip can realize a different workspace path for local and remote runs > - The OpenCode CLI prefers its inherited `PWD` when no `--dir` option is present > - The process `cwd` option does not update the inherited `PWD` value > - This pull request passes the realized workspace to OpenCode with `--dir` > - The benefit is that OpenCode sessions and tools use the workspace that Paperclip selected ## Linked Issues or Issue Description No matching issue exists. Refs #8287. That earlier pull request describes the realized remote workspace path. It does not make the OpenCode CLI use that path. I searched open and closed issues and pull requests for `opencode --dir`, `opencode cwd PWD`, and `opencode workspace directory`. I found no duplicate. **What happened?** Paperclip gives the OpenCode process the correct operating system `cwd`. However, the adapter does not pass OpenCode's `--dir` option. OpenCode uses `process.env.PWD` before `process.cwd()` when `--dir` is absent. A server `PWD` that differs from the realized workspace can therefore place the OpenCode session and its tools in the wrong directory. **Expected behavior** OpenCode must use the workspace that Paperclip realized for the run. An explicit `--dir` in `extraArgs` must remain valid and must not be duplicated. **Steps to reproduce** 1. Start the Paperclip server from directory A. 2. Configure an OpenCode agent with directory B, or realize directory B on a remote execution target. 3. Start a run without a manual `--dir` value in `extraArgs`. 4. Inspect the OpenCode session directory or run a directory-sensitive tool. 5. Before this change, OpenCode can use directory A from the inherited `PWD`. 6. After this change, OpenCode receives `--dir B`. **Paperclip version or commit** `b38d6ddb811b3ff3145ff5df60174eb9e7313fe6` **Deployment mode** Self-hosted server. The same adapter path is used in local development and remote execution. **Installation method** Built from source. **Agent adapter(s) involved** OpenCode. **Database mode** Not database-related. **Additional context** OpenCode v1.17.13 defines `--dir` and uses the inherited `PWD` as its fallback root. See the [OpenCode run command source](https://github.com/anomalyco/opencode/blob/10c894bdeef3618f5666fb506ef7f9491bb964d8/packages/opencode/src/cli/cmd/run.ts#L333-L339). ## What Changed - Add the realized execution workspace as `--dir` for OpenCode runs. - Preserve an explicit `--dir` or `--dir=...` value from `extraArgs`. - Stop option detection at the first `--` terminator. - Extend the remote execution tests for realized, explicit, and post-terminator directory arguments. ## Verification - `pnpm exec vitest run packages/adapters/opencode-local/src` passed: 7 files and 40 tests. - `pnpm --filter @paperclipai/adapter-opencode-local typecheck` passed. - `pnpm --filter @paperclipai/adapter-opencode-local build` passed. - `pnpm -r typecheck` passed for all 31 selected workspace projects. - `pnpm build` passed for all selected workspace projects. - `git diff --check` passed. I also ran `pnpm test:run`. It completed with 334 test files passing and 2 test files failing. It reported 3,790 passing tests and 4 skipped tests. The two failures are in `issue-monitor-scheduler.test.ts` and `workspace-runtime.test.ts`. Each failure reproduces on the unchanged base commit after this pull request is removed. Neither test imports or covers the OpenCode adapter. ## Risks - The behavior change is limited to OpenCode run arguments. - A custom old OpenCode binary that does not support `--dir` will reject the option. The current official OpenCode v1.17.13 CLI supports it. - An explicit operator `--dir` remains authoritative. The adapter does not add a duplicate option because OpenCode's parser converts repeated string options to an array. - This change does not modify schemas, migrations, APIs, telemetry, or user data. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex based on GPT-5. The runtime did not expose a more specific model ID or context-window size. The agent used high-depth reasoning, repository access, shell execution, and GitHub tools. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: commitperclip-bot <305974387+commitperclip-bot@users.noreply.github.com> --- .../opencode-local/src/server/execute.remote.test.ts | 7 ++++++- packages/adapters/opencode-local/src/server/execute.ts | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/adapters/opencode-local/src/server/execute.remote.test.ts b/packages/adapters/opencode-local/src/server/execute.remote.test.ts index 7472e11ffe..2271749e33 100644 --- a/packages/adapters/opencode-local/src/server/execute.remote.test.ts +++ b/packages/adapters/opencode-local/src/server/execute.remote.test.ts @@ -153,6 +153,7 @@ describe("opencode remote execution", () => { config: { command: "opencode", model: "opencode/gpt-5-nano", + extraArgs: ["--", "--dir"], ...(managed ? { managedAiConnection: { provider: "openrouter", method: "api_key" }, } : {}), @@ -236,6 +237,7 @@ describe("opencode remote execution", () => { `${managedRemoteWorkspace}/.paperclip-runtime/opencode/xdgConfig`, ); expect(modelProbeCall?.[3].remoteExecution?.remoteCwd).toBe("/remote/workspace"); + expect(runCall?.[2].slice(-4)).toEqual(["--dir", managedRemoteWorkspace, "--", "--dir"]); const call = runCall as | [string, string, string[], { env: Record; remoteExecution?: { remoteCwd: string } | null }] | undefined; @@ -335,7 +337,7 @@ describe("opencode remote execution", () => { expect(startAdapterExecutionTargetPaperclipBridge).not.toHaveBeenCalled(); }); - it("resumes saved OpenCode sessions for remote SSH execution only when the identity matches", async () => { + it("resumes matching remote sessions and preserves an explicit run directory", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-remote-resume-")); cleanupDirs.push(rootDir); const workspaceDir = path.join(rootDir, "workspace"); @@ -370,6 +372,7 @@ describe("opencode remote execution", () => { config: { command: "opencode", model: "opencode/gpt-5-nano", + extraArgs: ["--dir", "/operator-supplied"], }, context: { paperclipWorkspace: { @@ -397,5 +400,7 @@ describe("opencode remote execution", () => { | undefined; expect(call?.[2]).toContain("--session"); expect(call?.[2]).toContain("session-123"); + expect(call?.[2].slice(-2)).toEqual(["--dir", "/operator-supplied"]); + expect(call?.[2].filter((arg) => arg === "--dir" || arg.startsWith("--dir="))).toHaveLength(1); }); }); diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index a7458fd3d9..7171067d00 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -621,6 +621,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise arg === "--dir" || arg.startsWith("--dir="))) { + args.push("--dir", effectiveExecutionCwd); + } if (extraArgs.length > 0) args.push(...extraArgs); return args; };