package doctor import ( "fmt" "os" "path/filepath" "strings" "github.com/steveyegge/gastown/internal/git" ) // SparseCheckoutCheck verifies that git clones/worktrees have sparse checkout configured // to exclude Claude Code context files from source repos. This ensures source repo settings // and instructions don't override Gas Town agent configuration. // Excluded files: .claude/, CLAUDE.md, CLAUDE.local.md, .mcp.json type SparseCheckoutCheck struct { FixableCheck rigPath string affectedRepos []string // repos missing sparse checkout configuration } // NewSparseCheckoutCheck creates a new sparse checkout check. func NewSparseCheckoutCheck() *SparseCheckoutCheck { return &SparseCheckoutCheck{ FixableCheck: FixableCheck{ BaseCheck: BaseCheck{ CheckName: "sparse-checkout", CheckDescription: "Verify sparse checkout excludes Claude context files (.claude/, CLAUDE.md, etc.)", CheckCategory: CategoryRig, }, }, } } // Run checks if sparse checkout is configured for all git repos in the rig. func (c *SparseCheckoutCheck) Run(ctx *CheckContext) *CheckResult { c.rigPath = ctx.RigPath() if c.rigPath == "" { return &CheckResult{ Name: c.Name(), Status: StatusError, Message: "No rig specified", } } c.affectedRepos = nil // Check all git repo locations repoPaths := []string{ filepath.Join(c.rigPath, "mayor", "rig"), filepath.Join(c.rigPath, "refinery", "rig"), } // Add crew clones crewDir := filepath.Join(c.rigPath, "crew") if entries, err := os.ReadDir(crewDir); err == nil { for _, entry := range entries { if entry.IsDir() && entry.Name() != "README.md" { repoPaths = append(repoPaths, filepath.Join(crewDir, entry.Name())) } } } // Add polecat worktrees polecatDir := filepath.Join(c.rigPath, "polecats") if entries, err := os.ReadDir(polecatDir); err == nil { for _, entry := range entries { if entry.IsDir() { repoPaths = append(repoPaths, filepath.Join(polecatDir, entry.Name())) } } } for _, repoPath := range repoPaths { // Skip if not a git repo if _, err := os.Stat(filepath.Join(repoPath, ".git")); os.IsNotExist(err) { continue } // Check if sparse checkout is configured (not just if .claude/ exists) if !git.IsSparseCheckoutConfigured(repoPath) { c.affectedRepos = append(c.affectedRepos, repoPath) } } if len(c.affectedRepos) == 0 { return &CheckResult{ Name: c.Name(), Status: StatusOK, Message: "All repos have sparse checkout configured to exclude Claude context files", } } // Build details with relative paths var details []string for _, repoPath := range c.affectedRepos { relPath, _ := filepath.Rel(c.rigPath, repoPath) if relPath == "" { relPath = repoPath } details = append(details, relPath) } return &CheckResult{ Name: c.Name(), Status: StatusError, Message: fmt.Sprintf("%d repo(s) missing sparse checkout configuration", len(c.affectedRepos)), Details: details, FixHint: "Run 'gt doctor --fix' to configure sparse checkout", } } // Fix configures sparse checkout for affected repos to exclude Claude context files. func (c *SparseCheckoutCheck) Fix(ctx *CheckContext) error { for _, repoPath := range c.affectedRepos { if err := git.ConfigureSparseCheckout(repoPath); err != nil { relPath, _ := filepath.Rel(c.rigPath, repoPath) return fmt.Errorf("failed to configure sparse checkout for %s: %w", relPath, err) } // Check if any excluded files remain (untracked or modified files won't be removed by git read-tree) if remaining := git.CheckExcludedFilesExist(repoPath); len(remaining) > 0 { relPath, _ := filepath.Rel(c.rigPath, repoPath) return fmt.Errorf("sparse checkout configured for %s but these files still exist: %s\n"+ "These files are untracked or modified and were not removed by git.\n"+ "Please manually remove or revert these files in %s", relPath, strings.Join(remaining, ", "), repoPath) } } return nil }