From 5e86bc8eeb735b6afe39db4d741e51d33068b0ba Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:10:34 +0900 Subject: [PATCH] fix(runtime): write edit tool replacements literally Without replace_all, the edit tool called String.prototype.replace with new_string as the replacement string, so `$$`, `$&`, `` $` `` and `$'` in new_string were expanded instead of written. An edit that inserts `$$HOME` into a Makefile or a `$$` math block into Markdown wrote different text than the model asked for. The replace_all path already wrote new_string verbatim. Pass a replacer function so both paths agree. Co-Authored-By: Claude Opus 5 --- packages/runtime/src/tools/built-in/fs.ts | 3 ++- .../runtime/tests/tools/built-in/fs.test.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/tools/built-in/fs.ts b/packages/runtime/src/tools/built-in/fs.ts index 0025e05f..2ed212be 100644 --- a/packages/runtime/src/tools/built-in/fs.ts +++ b/packages/runtime/src/tools/built-in/fs.ts @@ -309,7 +309,8 @@ export async function edit( } const updated = replaceAll ? content.split(oldString).join(newString) - : content.replace(oldString, newString); + : // A replacer function keeps `$$`, `$&`, `` $` `` and `$'` literal. + content.replace(oldString, () => newString); await fs.writeFile(filePath, updated, "utf8"); const totalReplaced = replaceAll ? occurrences : 1; return `Replaced ${totalReplaced} occurrence${totalReplaced === 1 ? "" : "s"} in ${filePath}`; diff --git a/packages/runtime/tests/tools/built-in/fs.test.ts b/packages/runtime/tests/tools/built-in/fs.test.ts index 05af89ad..df96b920 100644 --- a/packages/runtime/tests/tools/built-in/fs.test.ts +++ b/packages/runtime/tests/tools/built-in/fs.test.ts @@ -39,6 +39,26 @@ describe("filesystem built-in paths", () => { expect(await fs.readFile(absolutePath, "utf8")).toBe("after"); }); + test("edit writes $ sequences in new_string literally", async () => { + const directory = await fs.mkdtemp( + path.join(os.tmpdir(), "llm-space-fs-test-") + ); + testDirectories.push(directory); + const newString = "echo $$HOME $& $' $`"; + const allPath = path.join(directory, "all.mk"); + const singlePath = path.join(directory, "single.mk"); + + await write(allPath, "run:\n\techo OLD\n"); + await edit(allPath, "echo OLD", newString, true); + expect(await fs.readFile(allPath, "utf8")).toBe(`run:\n\t${newString}\n`); + + await write(singlePath, "run:\n\techo OLD\n"); + await edit(singlePath, "echo OLD", newString); + expect(await fs.readFile(singlePath, "utf8")).toBe( + `run:\n\t${newString}\n` + ); + }); + test("write and edit expand a leading home shortcut", async () => { const directory = await fs.mkdtemp( path.join(os.homedir(), ".llm-space-fs-test-")