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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
TÂCHES 2026-03-15 13:56:56 -06:00 committed by GitHub
parent 27d07a35d7
commit f59301e4ba

View file

@ -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) {