From cc22920c2ea21375efcc53cfa2efaefc3c01fb79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Sun, 15 Mar 2026 15:21:04 -0600 Subject: [PATCH] fix(gitignore): self-heal blanket .gsd/ ignore from pre-v2.14.0 projects (#515) ensureGitignore() now detects and removes standalone ".gsd/" lines that blanket-ignore the entire directory. Replaces with explicit runtime-only patterns so .gsd/milestones/ planning artifacts are tracked in git. Without this, existing projects keep the old blanket ignore forever. New worktrees start with zero planning state because artifacts aren't in git, causing auto-mode to re-execute completed work. Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/gitignore.ts | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/gitignore.ts b/src/resources/extensions/gsd/gitignore.ts index 008ce7dcd..afde88d66 100644 --- a/src/resources/extensions/gsd/gitignore.ts +++ b/src/resources/extensions/gsd/gitignore.ts @@ -87,6 +87,28 @@ export function ensureGitignore(basePath: string): boolean { existing = readFileSync(gitignorePath, "utf-8"); } + // Self-heal: remove blanket ".gsd/" lines from pre-v2.14.0 projects. + // The blanket ignore prevented planning artifacts (.gsd/milestones/) from + // being tracked in git, causing artifacts to vanish in worktrees and + // triggering loop detection failures. Replace with explicit runtime-only + // ignores so planning files are tracked naturally. + let modified = false; + const lines = existing.split("\n"); + const filteredLines = lines.filter(line => { + const trimmed = line.trim(); + // Remove standalone ".gsd/" lines (blanket ignore) but keep specific + // .gsd/ subpath patterns like ".gsd/activity/" or ".gsd/auto.lock" + if (trimmed === ".gsd/" || trimmed === ".gsd") { + modified = true; + return false; + } + return true; + }); + if (modified) { + existing = filteredLines.join("\n"); + writeFileSync(gitignorePath, existing, "utf-8"); + } + // Parse existing lines (trimmed, ignoring comments and blanks) const existingLines = new Set( existing @@ -98,7 +120,7 @@ export function ensureGitignore(basePath: string): boolean { // Find patterns not yet present const missing = BASELINE_PATTERNS.filter((p) => !existingLines.has(p)); - if (missing.length === 0) return false; + if (missing.length === 0) return modified; // Build the block to append const block = [