From f59301e4ba5c01fffbb13160844f31e4d76191a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Sun, 15 Mar 2026 13:56:56 -0600 Subject: [PATCH] fix(auto): prevent nested worktree creation inside existing worktrees (#511) * fix(auto): prevent nested worktree creation inside existing worktrees When auto-mode starts inside a manual worktree (e.g., /worktree memory-db), it unconditionally created an auto-worktree for the milestone, nesting .gsd/worktrees/M001 inside the existing worktree. This caused GSD to chdir into the inner worktree, read state from the wrong repo, and report "All milestones complete" or loop on artifact verification. Add detectWorktreeName() guard to both the start and resume paths: if already inside a worktree, skip auto-worktree creation and work directly on the current branch. Co-Authored-By: Claude Opus 4.6 (1M context) * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/resources/extensions/gsd/auto.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index a65c16ae3..bb1227a92 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -69,11 +69,13 @@ import { getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js"; import { dirname, join } from "node:path"; +import { sep as pathSep } from "node:path"; import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs"; import { execSync, execFileSync } from "node:child_process"; import { autoCommitCurrentBranch, captureIntegrationBranch, + detectWorktreeName, getCurrentBranch, getMainBranch, MergeConflictError, @@ -505,7 +507,8 @@ export async function startAuto( if (currentMilestoneId) setActiveMilestoneId(base, currentMilestoneId); // ── Auto-worktree: re-enter worktree on resume if not already inside ── - if (currentMilestoneId && originalBasePath && !isInAutoWorktree(basePath)) { + // Skip if already inside a worktree (manual /worktree) to prevent nesting. + if (currentMilestoneId && originalBasePath && !isInAutoWorktree(basePath) && !detectWorktreeName(basePath) && !detectWorktreeName(originalBasePath)) { try { const existingWtPath = getAutoWorktreePath(originalBasePath, currentMilestoneId); if (existingWtPath) { @@ -668,8 +671,22 @@ export async function startAuto( // ── Auto-worktree: create or enter worktree for the active milestone ── // Store the original project root before any chdir so we can restore on stop. + // Skip if already inside a worktree (manual /worktree or another auto-worktree) + // to prevent nested worktree creation. originalBasePath = base; - if (currentMilestoneId) { + + const isUnderGsdWorktrees = (p: string): boolean => { + // Prevent creating nested auto-worktrees when running from within any + // `.gsd/worktrees/...` directory (including manual worktrees). + const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`; + if (p.includes(marker)) { + return true; + } + const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`; + return p.endsWith(worktreesSuffix); + }; + + if (currentMilestoneId && !detectWorktreeName(base) && !isUnderGsdWorktrees(base)) { try { const existingWtPath = getAutoWorktreePath(base, currentMilestoneId); if (existingWtPath) {