From bf6fefa16eee88624ed74b60b45ec48fb4e5495b Mon Sep 17 00:00:00 2001 From: jonathancostin <66714927+jonathancostin@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:44:57 -0500 Subject: [PATCH] fix: register dynamic-cwd write/read/edit tools for worktree support (#72) The built-in write, read, and edit tools capture process.cwd() once at startup. When /worktree switch calls process.chdir() into a worktree, these tools still resolve relative paths against the original launch directory. This caused GSD auto-mode to write .gsd/ artifacts to the main project instead of the worktree. The bash tool was already patched with a spawnHook for dynamic CWD. Apply the same pattern to write, read, and edit: each execute() call creates a fresh tool instance with the current process.cwd(), so relative paths always resolve against the active working directory. --- src/resources/extensions/gsd/index.ts | 55 ++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/index.ts b/src/resources/extensions/gsd/index.ts index bf52720b2..37c255fd0 100644 --- a/src/resources/extensions/gsd/index.ts +++ b/src/resources/extensions/gsd/index.ts @@ -22,7 +22,7 @@ import type { ExtensionAPI, ExtensionContext, } from "@mariozechner/pi-coding-agent"; -import { createBashTool } from "@mariozechner/pi-coding-agent"; +import { createBashTool, createWriteTool, createReadTool, createEditTool } from "@mariozechner/pi-coding-agent"; import { registerGSDCommand } from "./commands.js"; import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js"; @@ -102,6 +102,59 @@ export default function (pi: ExtensionAPI) { }; pi.registerTool(dynamicBash as any); + // ── Dynamic-cwd file tools (write, read, edit) ──────────────────────── + // The built-in file tools capture cwd at startup. When process.chdir() + // moves us into a worktree, relative paths still resolve against the + // original launch directory. These replacements delegate to freshly- + // created tools on each call so that process.cwd() is read dynamically. + const baseWrite = createWriteTool(process.cwd()); + const dynamicWrite = { + ...baseWrite, + execute: async ( + toolCallId: string, + params: { path: string; content: string }, + signal?: AbortSignal, + onUpdate?: any, + ctx?: any, + ) => { + const fresh = createWriteTool(process.cwd()); + return fresh.execute(toolCallId, params, signal, onUpdate, ctx); + }, + }; + pi.registerTool(dynamicWrite as any); + + const baseRead = createReadTool(process.cwd()); + const dynamicRead = { + ...baseRead, + execute: async ( + toolCallId: string, + params: { path: string; offset?: number; limit?: number }, + signal?: AbortSignal, + onUpdate?: any, + ctx?: any, + ) => { + const fresh = createReadTool(process.cwd()); + return fresh.execute(toolCallId, params, signal, onUpdate, ctx); + }, + }; + pi.registerTool(dynamicRead as any); + + const baseEdit = createEditTool(process.cwd()); + const dynamicEdit = { + ...baseEdit, + execute: async ( + toolCallId: string, + params: { path: string; oldText: string; newText: string }, + signal?: AbortSignal, + onUpdate?: any, + ctx?: any, + ) => { + const fresh = createEditTool(process.cwd()); + return fresh.execute(toolCallId, params, signal, onUpdate, ctx); + }, + }; + pi.registerTool(dynamicEdit as any); + // ── session_start: render branded GSD header + remote channel status ── pi.on("session_start", async (_event, ctx) => { const theme = ctx.ui.theme;