fix(opencode-local): run in the realized workspace (#11438)

## 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>
This commit is contained in:
Kilianandcommitperclip-bot authored and GitHub committed 2026-10-10 10:46:21 -05:00
1 parent 40bb466d78
commit 28e134873e
2 files changed
+13 -1

No files matched your search

@@ -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<string, string>; 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);
});
});
@@ -621,6 +621,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
if (resumeSessionId) args.push("--session", resumeSessionId);
if (model) args.push("--model", model);
if (variant) args.push("--variant", variant);
// OpenCode prefers the inherited PWD over process.cwd(). Use the realized
// workspace unless the operator already selected a directory explicitly.
const optionTerminator = extraArgs.indexOf("--");
const extraOptionArgs = optionTerminator === -1 ? extraArgs : extraArgs.slice(0, optionTerminator);
if (!extraOptionArgs.some((arg) => arg === "--dir" || arg.startsWith("--dir="))) {
args.push("--dir", effectiveExecutionCwd);
}
if (extraArgs.length > 0) args.push(...extraArgs);
return args;
};