From 954e333228b3c2e0d74bead4c85151b7d51b4b6b Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Sun, 15 Mar 2026 09:15:30 -0600 Subject: [PATCH] fix(doctor): Windows path compatibility for worktree/branch detection Three fixes for Windows CI failures: 1. Remove single quotes from git branch --list glob patterns. On Windows, cmd.exe passes single quotes literally to git, preventing glob expansion. Affects shouldUseWorktreeIsolation() and stale branch detection. 2. Extend listWorktrees() branch-name fallback to cover milestone/* branches, not just worktree/* branches. On Windows, path normalization can prevent path-based worktree matching; the branch-name fallback is the safety net. 3. Use path.sep instead of hardcoded "/" in CWD prefix checks (doctor.ts orphan fix guard, worktree-manager.ts removeWorktree guard). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/auto-worktree.ts | 4 +++- src/resources/extensions/gsd/doctor.ts | 8 +++++--- src/resources/extensions/gsd/worktree-manager.ts | 10 +++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index 7dd510d60..e45ae0544 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -54,7 +54,9 @@ export function shouldUseWorktreeIsolation(basePath: string, overridePrefs?: { i // Legacy detection: check for existing gsd/*/* branches (branch-per-slice pattern) try { - const output = execSync("git branch --list 'gsd/*/*'", { + // Use unquoted glob pattern — single quotes are not interpreted by cmd.exe on Windows, + // causing the pattern to match literally instead of as a glob. + const output = execSync("git branch --list gsd/*/*", { cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", diff --git a/src/resources/extensions/gsd/doctor.ts b/src/resources/extensions/gsd/doctor.ts index 80bf451f1..16cfd4a58 100644 --- a/src/resources/extensions/gsd/doctor.ts +++ b/src/resources/extensions/gsd/doctor.ts @@ -1,6 +1,6 @@ import { execSync } from "node:child_process"; import { existsSync, mkdirSync } from "node:fs"; -import { join } from "node:path"; +import { join, sep } from "node:path"; import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js"; import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js"; @@ -511,7 +511,7 @@ async function checkGitHealth( if (shouldFix("orphaned_auto_worktree")) { // Never remove a worktree matching current working directory const cwd = process.cwd(); - if (wt.path === cwd || cwd.startsWith(wt.path + "/")) { + if (wt.path === cwd || cwd.startsWith(wt.path + sep)) { fixesApplied.push(`skipped removing worktree at ${wt.path} (is cwd)`); } else { try { @@ -527,7 +527,9 @@ async function checkGitHealth( // ── Stale milestone branches ───────────────────────────────────────── try { - const branchOutput = execSync("git branch --list 'milestone/*'", { cwd: basePath, stdio: "pipe" }).toString().trim(); + // Use unquoted glob — single quotes are not interpreted by cmd.exe on Windows, + // causing the pattern to match literally instead of as a glob. + const branchOutput = execSync("git branch --list milestone/*", { cwd: basePath, stdio: "pipe" }).toString().trim(); if (branchOutput) { const branches = branchOutput.split("\n").map(b => b.trim().replace(/^\*\s*/, "")).filter(Boolean); const worktreeBranches = new Set(milestoneWorktrees.map(wt => wt.branch)); diff --git a/src/resources/extensions/gsd/worktree-manager.ts b/src/resources/extensions/gsd/worktree-manager.ts index 6b08a52bb..6696b7cf8 100644 --- a/src/resources/extensions/gsd/worktree-manager.ts +++ b/src/resources/extensions/gsd/worktree-manager.ts @@ -17,7 +17,7 @@ import { existsSync, mkdirSync, realpathSync } from "node:fs"; import { execSync } from "node:child_process"; -import { join, resolve } from "node:path"; +import { join, resolve, sep } from "node:path"; // ─── Types ───────────────────────────────────────────────────────────────── @@ -213,7 +213,11 @@ export function listWorktrees(basePath: string): WorktreeInfo[] { const entryPath = wtLine.replace("worktree ", ""); const branch = branchLine.replace("branch refs/heads/", ""); - const branchWorktreeName = branch.startsWith("worktree/") ? branch.slice("worktree/".length) : null; + const branchWorktreeName = branch.startsWith("worktree/") + ? branch.slice("worktree/".length) + : branch.startsWith("milestone/") + ? branch.slice("milestone/".length) + : null; const entryVariants = [resolve(entryPath)]; if (existsSync(entryPath)) { entryVariants.push(realpathSync(entryPath)); @@ -272,7 +276,7 @@ export function removeWorktree( // If we're inside the worktree, move out first — git can't remove an in-use directory const cwd = process.cwd(); const resolvedCwd = existsSync(cwd) ? realpathSync(cwd) : cwd; - if (resolvedCwd === resolvedWtPath || resolvedCwd.startsWith(resolvedWtPath + "/")) { + if (resolvedCwd === resolvedWtPath || resolvedCwd.startsWith(resolvedWtPath + sep)) { process.chdir(basePath); }