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.
This commit is contained in:
jonathancostin 2026-03-11 18:44:57 -05:00 committed by GitHub
parent 2b9451dfd4
commit bf6fefa16e

View file

@ -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;