diff --git a/docs/notes/2025.10.30.13.09.02.md b/docs/notes/2025.10.30.13.09.02.md new file mode 100644 index 00000000000..88d9bcfc113 --- /dev/null +++ b/docs/notes/2025.10.30.13.09.02.md @@ -0,0 +1,5 @@ +[4:08 PM]AKTK: And now I've just started getting this too, did you find a cause/solution at all? +[4:10 PM]shuv: what was the prompt/session that triggered it? based on the token counts, it looks like it was trying to print the entire previous context back to you again +[4:13 PM]AKTK: Sent out via a subagent. It was running fine for a while, then just randomly goes 💩 and errors. Which then means it delegates the task back out and leaves the repo in an awful state (That /undo didn't fix the first time) +Image +Image diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index a3ccfc397f2..734089ddb6a 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -43,6 +43,12 @@ export const BashTool = Tool.define("bash", { parameters: z.object({ command: z.string().describe("The command to execute"), timeout: z.number().describe("Optional timeout in milliseconds").optional(), + cwd: z + .string() + .describe( + "The working directory for the command. Must be within the project directory. If not specified, uses the project root directory.", + ) + .optional(), description: z .string() .describe( @@ -54,6 +60,28 @@ export const BashTool = Tool.define("bash", { throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`) } const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) + + // Validate and resolve cwd parameter + let workingDirectory = Instance.directory + if (params.cwd) { + const resolvedCwd = await $`realpath ${params.cwd}` + .cwd(Instance.directory) + .quiet() + .nothrow() + .text() + .then((x) => x.trim()) + + if (!resolvedCwd) { + throw new Error(`Invalid working directory: ${params.cwd}`) + } + + if (!Filesystem.contains(Instance.directory, resolvedCwd)) { + throw new Error(`Working directory ${resolvedCwd} is outside of project directory ${Instance.directory}`) + } + + workingDirectory = resolvedCwd + } + const tree = await parser().then((p) => p.parse(params.command)) if (!tree) { throw new Error("Failed to parse command") @@ -84,6 +112,7 @@ export const BashTool = Tool.define("bash", { for (const arg of command.slice(1)) { if (arg.startsWith("-") || (command[0] === "chmod" && arg.startsWith("+"))) continue const resolved = await $`realpath ${arg}` + .cwd(workingDirectory) .quiet() .nothrow() .text() @@ -138,7 +167,7 @@ export const BashTool = Tool.define("bash", { const proc = spawn(params.command, { shell: true, - cwd: Instance.directory, + cwd: workingDirectory, env: { ...process.env, }, diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index 67c54677fce..4e36432e5fb 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -19,15 +19,19 @@ Before executing the command, please follow these steps: Usage notes: - The command argument is required. - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes). + - You can specify an optional working directory with the cwd parameter. The directory must be within the project directory. If not specified, uses the project root directory. - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - If the output exceeds 30000 characters, output will be truncated before being returned to you. - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and List to read files. - If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed. - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings). - - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it. + - Try to maintain your current working directory throughout the session by using absolute paths, the cwd parameter, and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it. pytest /foo/bar/tests + + Run command in specific directory: command="npm test" cwd="/foo/bar" + cd /foo/bar && pytest tests diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 2919ccb0245..9524f5c9339 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -49,4 +49,75 @@ describe("tool.bash", () => { }, }) }) + + test("cwd parameter with valid directory", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const result = await bash.execute( + { + command: "pwd", + cwd: projectRoot, + description: "Get current working directory", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + expect(result.metadata.output).toContain(projectRoot) + }, + }) + }) + + test("cwd parameter outside project should fail", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + expect( + bash.execute( + { + command: "pwd", + cwd: "/tmp", + description: "Try to use cwd outside project", + }, + ctx, + ), + ).rejects.toThrow("Working directory") + }, + }) + }) + + test("cwd parameter with relative path", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const result = await bash.execute( + { + command: "pwd", + cwd: path.join(projectRoot, "src"), + description: "Use absolute path for cwd", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + expect(result.metadata.output).toContain("/src") + }, + }) + }) + + test("default behavior without cwd parameter", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const result = await bash.execute( + { + command: "pwd", + description: "Get default working directory", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + expect(result.metadata.output).toContain(projectRoot) + }, + }) + }) })