From a737223b3c1ddb1edd9e5a3a347eb3d05c7698da Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 27 Dec 2025 20:41:41 -0800 Subject: [PATCH] fix: doctor checks follow redirect for Gas Town support (bd-tvus) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extended bd-tvus fix to all doctor check functions that access .beads/ directory. In Gas Town multi-clone setups, crew/polecat clones use .beads/redirect files to point to the shared mayor/rig beads directory. Doctor checks now use resolveBeadsDir() to follow these redirects: - daemon.go: CheckDaemonStatus - git.go: CheckSyncBranchConfig, FindOrphanedIssues, CheckOrphanedIssues - installation.go: CheckMultipleDatabases, CheckPermissions - integrity.go: CheckIDFormat, CheckDependencyCycles, CheckTombstones, CheckDeletionsManifest, CheckRepoFingerprint - jsonl_integrity.go: CheckJSONLIntegrity - legacy.go: CheckFreshClone - maintenance.go: CheckStaleClosedIssues, CheckExpiredTombstones, CheckCompactionCandidates - perf.go: RunPerformanceDiagnostics, CollectPlatformInfo - validation.go: CheckMergeArtifacts, CheckOrphanedDependencies, CheckDuplicateIssues, CheckTestPollution, CheckChildParentDependencies, CheckGitConflicts - version.go: CheckMetadataVersionTracking Intentionally NOT changed (check local files): - CheckInstallation: checks local .beads/ exists - CheckUntrackedBeadsFiles: checks git tracking of local files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/doctor/daemon.go | 3 ++- cmd/bd/doctor/git.go | 9 ++++++--- cmd/bd/doctor/installation.go | 6 ++++-- cmd/bd/doctor/integrity.go | 15 ++++++++++----- cmd/bd/doctor/jsonl_integrity.go | 3 ++- cmd/bd/doctor/legacy.go | 3 ++- cmd/bd/doctor/maintenance.go | 9 ++++++--- cmd/bd/doctor/perf.go | 6 ++++-- cmd/bd/doctor/validation.go | 18 ++++++++++++------ cmd/bd/doctor/version.go | 3 ++- 10 files changed, 50 insertions(+), 25 deletions(-) diff --git a/cmd/bd/doctor/daemon.go b/cmd/bd/doctor/daemon.go index 15b88b4b..02b34596 100644 --- a/cmd/bd/doctor/daemon.go +++ b/cmd/bd/doctor/daemon.go @@ -43,7 +43,8 @@ func CheckDaemonStatus(path string, cliVersion string) DoctorCheck { } // Check for stale socket directly (catches cases where RPC failed so WorkspacePath is empty) - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) socketPath := filepath.Join(beadsDir, "bd.sock") if _, err := os.Stat(socketPath); err == nil { // Socket exists - try to connect diff --git a/cmd/bd/doctor/git.go b/cmd/bd/doctor/git.go index 63367c25..f5d0c598 100644 --- a/cmd/bd/doctor/git.go +++ b/cmd/bd/doctor/git.go @@ -419,7 +419,8 @@ func CheckMergeDriver(path string) DoctorCheck { // CheckSyncBranchConfig checks if sync-branch is properly configured. func CheckSyncBranchConfig(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Skip if .beads doesn't exist if _, err := os.Stat(beadsDir); os.IsNotExist(err) { @@ -686,7 +687,8 @@ func FindOrphanedIssues(path string) ([]OrphanIssue, error) { return []OrphanIssue{}, nil // Not a git repo, return empty list } - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Skip if no .beads directory if _, err := os.Stat(beadsDir); os.IsNotExist(err) { @@ -814,7 +816,8 @@ func CheckOrphanedIssues(path string) DoctorCheck { } } - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Skip if no .beads directory if _, err := os.Stat(beadsDir); os.IsNotExist(err) { diff --git a/cmd/bd/doctor/installation.go b/cmd/bd/doctor/installation.go index 478c1638..a4734049 100644 --- a/cmd/bd/doctor/installation.go +++ b/cmd/bd/doctor/installation.go @@ -40,7 +40,8 @@ func CheckInstallation(path string) DoctorCheck { // CheckMultipleDatabases checks for multiple database files in .beads directory func CheckMultipleDatabases(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Find all .db files (excluding backups and vc.db) files, err := filepath.Glob(filepath.Join(beadsDir, "*.db")) @@ -88,7 +89,8 @@ func CheckMultipleDatabases(path string) DoctorCheck { // CheckPermissions verifies that .beads directory and database are readable/writable func CheckPermissions(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Check if .beads/ is writable testFile := filepath.Join(beadsDir, ".doctor-test-write") diff --git a/cmd/bd/doctor/integrity.go b/cmd/bd/doctor/integrity.go index 35aecabc..223c2ac3 100644 --- a/cmd/bd/doctor/integrity.go +++ b/cmd/bd/doctor/integrity.go @@ -20,7 +20,8 @@ import ( // CheckIDFormat checks whether issues use hash-based or sequential IDs func CheckIDFormat(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Check metadata.json first for custom database name var dbPath string @@ -108,7 +109,8 @@ func CheckIDFormat(path string) DoctorCheck { // CheckDependencyCycles checks for circular dependencies in the issue graph func CheckDependencyCycles(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) // If no database, skip this check @@ -204,7 +206,8 @@ func CheckDependencyCycles(path string) DoctorCheck { // CheckTombstones checks the health of tombstone records // Reports: total tombstones, expiring soon (within 7 days), already expired func CheckTombstones(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) // Skip if database doesn't exist @@ -302,7 +305,8 @@ func CheckTombstones(path string) DoctorCheck { // CheckDeletionsManifest checks the status of deletions.jsonl and suggests migration to tombstones func CheckDeletionsManifest(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Skip if .beads doesn't exist if _, err := os.Stat(beadsDir); os.IsNotExist(err) { @@ -400,7 +404,8 @@ func CheckDeletionsManifest(path string) DoctorCheck { // This detects when a .beads directory was copied from another repo or when // the git remote URL changed. A mismatch can cause data loss during sync. func CheckRepoFingerprint(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Get database path var dbPath string diff --git a/cmd/bd/doctor/jsonl_integrity.go b/cmd/bd/doctor/jsonl_integrity.go index 1c84f862..70994127 100644 --- a/cmd/bd/doctor/jsonl_integrity.go +++ b/cmd/bd/doctor/jsonl_integrity.go @@ -14,7 +14,8 @@ import ( ) func CheckJSONLIntegrity(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Resolve JSONL path. jsonlPath := "" diff --git a/cmd/bd/doctor/legacy.go b/cmd/bd/doctor/legacy.go index 078bdf4c..a1ce0910 100644 --- a/cmd/bd/doctor/legacy.go +++ b/cmd/bd/doctor/legacy.go @@ -369,7 +369,8 @@ func CheckDatabaseConfig(repoPath string) DoctorCheck { // A fresh clone has JSONL with issues but no database file. // bd-4ew: Recommend 'bd init --prefix ' for fresh clones. func CheckFreshClone(repoPath string) DoctorCheck { - beadsDir := filepath.Join(repoPath, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(repoPath, ".beads")) // Check if .beads/ exists if _, err := os.Stat(beadsDir); os.IsNotExist(err) { diff --git a/cmd/bd/doctor/maintenance.go b/cmd/bd/doctor/maintenance.go index c80e6cb2..4debe2ff 100644 --- a/cmd/bd/doctor/maintenance.go +++ b/cmd/bd/doctor/maintenance.go @@ -21,7 +21,8 @@ const DefaultCleanupAgeDays = 30 // CheckStaleClosedIssues detects closed issues that could be cleaned up. // This consolidates the cleanup command into doctor checks. func CheckStaleClosedIssues(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Check metadata.json first for custom database name var dbPath string @@ -99,7 +100,8 @@ func CheckStaleClosedIssues(path string) DoctorCheck { // CheckExpiredTombstones detects tombstones that have exceeded their TTL. func CheckExpiredTombstones(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) jsonlPath := filepath.Join(beadsDir, "issues.jsonl") if _, err := os.Stat(jsonlPath); os.IsNotExist(err) { @@ -241,7 +243,8 @@ func CheckStaleMolecules(path string) DoctorCheck { // CheckCompactionCandidates detects issues eligible for compaction. func CheckCompactionCandidates(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) // Check metadata.json first for custom database name var dbPath string diff --git a/cmd/bd/doctor/perf.go b/cmd/bd/doctor/perf.go index 43a3bfeb..9dfdc79a 100644 --- a/cmd/bd/doctor/perf.go +++ b/cmd/bd/doctor/perf.go @@ -21,7 +21,8 @@ func RunPerformanceDiagnostics(path string) { fmt.Println(strings.Repeat("=", 50)) // Check if .beads directory exists - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) if _, err := os.Stat(beadsDir); os.IsNotExist(err) { fmt.Fprintf(os.Stderr, "Error: No .beads/ directory found at %s\n", path) fmt.Fprintf(os.Stderr, "Run 'bd init' to initialize beads\n") @@ -107,7 +108,8 @@ func CollectPlatformInfo(path string) map[string]string { info["go_version"] = runtime.Version() // SQLite version - try to find database - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro") if err == nil { diff --git a/cmd/bd/doctor/validation.go b/cmd/bd/doctor/validation.go index 17323795..ca4a132c 100644 --- a/cmd/bd/doctor/validation.go +++ b/cmd/bd/doctor/validation.go @@ -17,7 +17,8 @@ import ( // CheckMergeArtifacts detects temporary git merge files in .beads directory. // These are created during git merges and should be cleaned up. func CheckMergeArtifacts(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) if _, err := os.Stat(beadsDir); os.IsNotExist(err) { return DoctorCheck{ @@ -109,7 +110,8 @@ func readMergeArtifactPatterns(beadsDir string) ([]string, error) { // CheckOrphanedDependencies detects dependencies pointing to non-existent issues. func CheckOrphanedDependencies(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) if _, err := os.Stat(dbPath); os.IsNotExist(err) { @@ -180,7 +182,8 @@ func CheckOrphanedDependencies(path string) DoctorCheck { // CheckDuplicateIssues detects issues with identical content. func CheckDuplicateIssues(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) if _, err := os.Stat(dbPath); os.IsNotExist(err) { @@ -250,7 +253,8 @@ func CheckDuplicateIssues(path string) DoctorCheck { // CheckTestPollution detects test issues that may have leaked into the database. func CheckTestPollution(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) if _, err := os.Stat(dbPath); os.IsNotExist(err) { @@ -312,7 +316,8 @@ func CheckTestPollution(path string) DoctorCheck { // These often indicate a modeling mistake (deadlock: child waits for parent, parent waits for children). // However, they may be intentional in some workflows, so removal requires explicit opt-in. func CheckChildParentDependencies(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) if _, err := os.Stat(dbPath); os.IsNotExist(err) { @@ -386,7 +391,8 @@ func CheckChildParentDependencies(path string) DoctorCheck { // CheckGitConflicts detects git conflict markers in JSONL file. func CheckGitConflicts(path string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) jsonlPath := filepath.Join(beadsDir, "issues.jsonl") if _, err := os.Stat(jsonlPath); os.IsNotExist(err) { diff --git a/cmd/bd/doctor/version.go b/cmd/bd/doctor/version.go index 037aed07..e2c7fe53 100644 --- a/cmd/bd/doctor/version.go +++ b/cmd/bd/doctor/version.go @@ -90,7 +90,8 @@ const localVersionFile = ".local_version" // GH#662: This was updated to check .local_version instead of metadata.json:LastBdVersion, // which is now deprecated. func CheckMetadataVersionTracking(path string, currentVersion string) DoctorCheck { - beadsDir := filepath.Join(path, ".beads") + // Follow redirect to resolve actual beads directory (bd-tvus fix) + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) localVersionPath := filepath.Join(beadsDir, localVersionFile) // Read .local_version file