On Windows, paths embedded in bash command strings have backslashes stripped by the shell (e.g. C:\Users\user becomes C:Useruser), causing cd and other commands to fail silently. This left ~1.4 GB orphaned worktree directories after milestone completion. - Normalize all paths to forward slashes before embedding in the subagent cmux bash script (cd, tee, process args) - Add post-teardown orphan detection: warn and attempt rmSync fallback if the worktree directory persists after removeWorktree - Add regression tests for Windows path normalization Closes #1436 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
333c769e21
commit
e5ae9fd249
3 changed files with 124 additions and 3 deletions
|
|
@ -761,6 +761,24 @@ export function teardownAutoWorktree(
|
|||
branch,
|
||||
deleteBranch: !preserveBranch,
|
||||
});
|
||||
|
||||
// Verify cleanup succeeded — warn if the worktree directory is still on disk.
|
||||
// On Windows, bash-based cleanup can silently fail when paths contain
|
||||
// backslashes (#1436), leaving ~1 GB+ orphaned directories.
|
||||
const wtDir = worktreePath(originalBasePath, milestoneId);
|
||||
if (existsSync(wtDir)) {
|
||||
console.error(
|
||||
`[GSD] WARNING: Worktree directory still exists after teardown: ${wtDir}\n` +
|
||||
` This is likely an orphaned directory consuming disk space.\n` +
|
||||
` Remove it manually with: rm -rf "${wtDir.replaceAll("\\", "/")}"`,
|
||||
);
|
||||
// Attempt a direct filesystem removal as a fallback
|
||||
try {
|
||||
rmSync(wtDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Non-fatal — the warning above tells the user how to clean up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* windows-path-normalization.test.ts — Verify Windows backslash paths are
|
||||
* normalised to forward slashes before embedding in bash command strings.
|
||||
*
|
||||
* Regression test for #1436: on Windows, `cd C:\Users\user\project` in bash
|
||||
* strips backslashes (escape characters), producing `C:Usersuserproject`.
|
||||
*/
|
||||
|
||||
import { createTestContext } from "./test-helpers.ts";
|
||||
|
||||
const { assertEq, assertTrue, report } = createTestContext();
|
||||
|
||||
// ─── shellEscape + path normalization ──────────────────────────────────────
|
||||
|
||||
// Replicate the shellEscape helper from cmux/index.ts
|
||||
function shellEscape(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
// The bashPath pattern used in subagent/index.ts
|
||||
function bashPath(p: string): string {
|
||||
return shellEscape(p.replaceAll("\\", "/"));
|
||||
}
|
||||
|
||||
console.log("\n=== Windows backslash path normalization (#1436) ===");
|
||||
|
||||
// Backslash paths are converted to forward slashes
|
||||
assertEq(
|
||||
bashPath("C:\\Users\\user\\project"),
|
||||
"'C:/Users/user/project'",
|
||||
"backslash path normalised to forward slashes in shell-escaped string",
|
||||
);
|
||||
|
||||
// Unix paths pass through unchanged
|
||||
assertEq(
|
||||
bashPath("/home/user/project"),
|
||||
"'/home/user/project'",
|
||||
"Unix path unchanged",
|
||||
);
|
||||
|
||||
// Mixed separators are normalised
|
||||
assertEq(
|
||||
bashPath("C:\\Users/user\\project/src"),
|
||||
"'C:/Users/user/project/src'",
|
||||
"mixed separators normalised",
|
||||
);
|
||||
|
||||
// Paths with single quotes are still properly escaped
|
||||
assertEq(
|
||||
bashPath("C:\\Users\\o'brien\\project"),
|
||||
"'C:/Users/o'\\''brien/project'",
|
||||
"single quote in path is escaped after normalisation",
|
||||
);
|
||||
|
||||
// UNC paths
|
||||
assertEq(
|
||||
bashPath("\\\\server\\share\\dir"),
|
||||
"'//server/share/dir'",
|
||||
"UNC path normalised",
|
||||
);
|
||||
|
||||
// Empty string
|
||||
assertEq(
|
||||
bashPath(""),
|
||||
"''",
|
||||
"empty string handled",
|
||||
);
|
||||
|
||||
// ─── cd command construction ───────────────────────────────────────────────
|
||||
|
||||
console.log("\n=== cd command construction with normalised paths ===");
|
||||
|
||||
const windowsCwd = "C:\\Users\\user\\project\\.gsd\\worktrees\\M001";
|
||||
const cdCommand = `cd ${bashPath(windowsCwd)}`;
|
||||
assertEq(
|
||||
cdCommand,
|
||||
"cd 'C:/Users/user/project/.gsd/worktrees/M001'",
|
||||
"cd command uses forward slashes for Windows worktree path",
|
||||
);
|
||||
|
||||
// Verify the mangled form from #1436 is NOT produced
|
||||
assertTrue(
|
||||
!cdCommand.includes("C:Users"),
|
||||
"mangled path C:Usersuserproject must not appear",
|
||||
);
|
||||
|
||||
// ─── Worktree teardown orphan detection ────────────────────────────────────
|
||||
|
||||
console.log("\n=== teardown orphan warning path formatting ===");
|
||||
|
||||
const windowsWtDir = "C:\\Users\\user\\project\\.gsd\\worktrees\\M001";
|
||||
const helpCommand = `rm -rf "${windowsWtDir.replaceAll("\\", "/")}"`;
|
||||
assertEq(
|
||||
helpCommand,
|
||||
'rm -rf "C:/Users/user/project/.gsd/worktrees/M001"',
|
||||
"orphan cleanup help command uses forward slashes",
|
||||
);
|
||||
|
||||
report();
|
||||
|
|
@ -516,12 +516,16 @@ async function runSingleAgentInCmuxSplit(
|
|||
const bundledPaths = (process.env.GSD_BUNDLED_EXTENSION_PATHS ?? "").split(path.delimiter).map((s) => s.trim()).filter(Boolean);
|
||||
const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
|
||||
const processArgs = [process.env.GSD_BIN_PATH!, ...extensionArgs, ...buildSubagentProcessArgs(agent, task, tmpPromptPath)];
|
||||
// Normalize all paths to forward slashes before embedding in bash strings.
|
||||
// On Windows, backslashes are interpreted as escape characters by bash,
|
||||
// mangling paths like C:\Users\user into C:Useruser (#1436).
|
||||
const bashPath = (p: string) => shellEscape(p.replaceAll("\\", "/"));
|
||||
const innerScript = [
|
||||
`cd ${shellEscape(cwd ?? defaultCwd)}`,
|
||||
`cd ${bashPath(cwd ?? defaultCwd)}`,
|
||||
"set -o pipefail",
|
||||
`${shellEscape(process.execPath)} ${processArgs.map(shellEscape).join(" ")} 2> >(tee ${shellEscape(stderrPath)} >&2) | tee ${shellEscape(stdoutPath)}`,
|
||||
`${bashPath(process.execPath)} ${processArgs.map(a => bashPath(a)).join(" ")} 2> >(tee ${bashPath(stderrPath)} >&2) | tee ${bashPath(stdoutPath)}`,
|
||||
"status=${PIPESTATUS[0]}",
|
||||
`printf '%s' "$status" > ${shellEscape(exitPath)}`,
|
||||
`printf '%s' "$status" > ${bashPath(exitPath)}`,
|
||||
].join("; ");
|
||||
|
||||
const sent = await cmuxClient.sendSurface(cmuxSurfaceId, `bash -lc ${shellEscape(innerScript)}`);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue