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 = [