From 03caf9c958dd084be8c67f4d887ff5302989be7e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 10:23:39 -0400 Subject: [PATCH] fix(auto-worktree): auto-commit project root dirty state before milestone merge (#1130) --- src/resources/extensions/gsd/auto-worktree.ts | 5 +++++ .../gsd/tests/all-milestones-complete-merge.test.ts | 3 +++ .../gsd/tests/auto-worktree-milestone-merge.test.ts | 3 +++ .../gsd/tests/feature-branch-lifecycle-integration.test.ts | 3 +++ .../gsd/tests/milestone-transition-worktree.test.ts | 3 +++ src/resources/extensions/gsd/tests/parallel-merge.test.ts | 3 +++ .../extensions/gsd/tests/stale-worktree-cwd.test.ts | 3 +++ src/resources/extensions/gsd/tests/worktree-e2e.test.ts | 3 +++ 8 files changed, 26 insertions(+) diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index ff9938945..a4775fe9c 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -512,6 +512,11 @@ export function mergeMilestoneToMain( const previousCwd = process.cwd(); process.chdir(originalBasePath_); + // 3a. Auto-commit any dirty state in the project root that syncStateToProjectRoot + // wrote during execution. Without this, the squash merge can fail with + // "Your local changes to the following files would be overwritten by merge" (#1127). + autoCommitDirtyState(originalBasePath_); + // 4. Resolve integration branch — prefer milestone metadata, fall back to preferences / "main" const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {}; const integrationBranch = readIntegrationBranch(originalBasePath_, milestoneId); diff --git a/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts b/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts index 59114c912..a35303eb0 100644 --- a/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +++ b/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts @@ -37,6 +37,9 @@ function createTempRepo(): string { run("git config user.email test@test.com", dir); run("git config user.name Test", dir); writeFileSync(join(dir, "README.md"), "# test\n"); + // Mirror production: .gsd/worktrees/ is gitignored so autoCommitDirtyState + // doesn't pick up the worktrees directory as dirty state (#1127 fix). + writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n"); run("git add .", dir); run("git commit -m init", dir); run("git branch -M main", dir); diff --git a/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts b/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts index 806f56097..385476902 100644 --- a/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +++ b/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts @@ -32,6 +32,9 @@ function createTempRepo(): string { run("git config user.email test@test.com", dir); run("git config user.name Test", dir); writeFileSync(join(dir, "README.md"), "# test\n"); + // Mirror production: GSD runtime dirs are gitignored so autoCommitDirtyState + // doesn't pick up the worktrees directory as dirty state (#1127 fix). + writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n"); mkdirSync(join(dir, ".gsd"), { recursive: true }); writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n"); run("git add .", dir); diff --git a/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts b/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts index 5c4fc929e..26be12465 100644 --- a/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +++ b/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts @@ -75,6 +75,9 @@ function createFeatureBranchRepo(featureBranch: string): string { // Initial commit on main writeFileSync(join(dir, "README.md"), "# project\n"); + // Mirror production: GSD runtime dirs are gitignored so autoCommitDirtyState + // doesn't pick up the worktrees directory as dirty state (#1127 fix). + writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n"); mkdirSync(join(dir, ".gsd"), { recursive: true }); writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n"); run("git add .", dir); diff --git a/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts b/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts index 66ff99ab7..332e1b685 100644 --- a/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +++ b/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts @@ -38,6 +38,9 @@ function createTempRepo(): string { run("git config user.email test@test.com", dir); run("git config user.name Test", dir); writeFileSync(join(dir, "README.md"), "# test\n"); + // Mirror production: .gsd/worktrees/ is gitignored so autoCommitDirtyState + // doesn't pick up the worktrees directory as dirty state (#1127 fix). + writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n"); run("git add .", dir); run("git commit -m init", dir); run("git branch -M main", dir); diff --git a/src/resources/extensions/gsd/tests/parallel-merge.test.ts b/src/resources/extensions/gsd/tests/parallel-merge.test.ts index a1b9c96b3..0e8ddcfd3 100644 --- a/src/resources/extensions/gsd/tests/parallel-merge.test.ts +++ b/src/resources/extensions/gsd/tests/parallel-merge.test.ts @@ -51,6 +51,9 @@ function createTempRepo(): string { run("git config user.email test@test.com", dir); run("git config user.name Test", dir); writeFileSync(join(dir, "README.md"), "# test\n"); + // Mirror production: .gsd/worktrees/ is gitignored so autoCommitDirtyState + // doesn't pick up the worktrees directory as dirty state (#1127 fix). + writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n"); mkdirSync(join(dir, ".gsd"), { recursive: true }); writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n"); run("git add .", dir); diff --git a/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts b/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts index 163b0a804..aa696853c 100644 --- a/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +++ b/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts @@ -28,6 +28,9 @@ function createTempRepo(): string { run("git config user.email test@test.com", dir); run("git config user.name Test", dir); writeFileSync(join(dir, "README.md"), "# test\n"); + // Mirror production: .gsd/worktrees/ is gitignored so autoCommitDirtyState + // doesn't pick up the worktrees directory as dirty state (#1127 fix). + writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n"); run("git add .", dir); run("git commit -m init", dir); run("git branch -M main", dir); diff --git a/src/resources/extensions/gsd/tests/worktree-e2e.test.ts b/src/resources/extensions/gsd/tests/worktree-e2e.test.ts index 865813e07..6014682aa 100644 --- a/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-e2e.test.ts @@ -38,6 +38,9 @@ function createTempRepo(): string { run("git config user.email test@test.com", dir); run("git config user.name Test", dir); writeFileSync(join(dir, "README.md"), "# test\n"); + // Mirror production: .gsd/worktrees/ is gitignored so autoCommitDirtyState + // doesn't pick up the worktrees directory as dirty state (#1127 fix). + writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n"); mkdirSync(join(dir, ".gsd"), { recursive: true }); writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n"); run("git add .", dir);