From 87ee3a674eb4fe7a1a5c64e898c9f9ad61d1d8a6 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 21 Nov 2025 23:44:26 -0800 Subject: [PATCH] Improve bd doctor JSONL checks to focus on real problems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously bd doctor warned about using beads.jsonl vs issues.jsonl, but users should be free to configure any name they want. The real problems are: 1. Having multiple JSONL files (sync/merge conflicts) 2. Configuration not matching reality Changes: - Rewrote CheckLegacyJSONLFilename to scan for ALL .jsonl files - Now filters out merge artifacts (backup, .orig, .bak, etc.) - Warns only when multiple real JSONL files exist - Added CheckDatabaseConfig to detect when configured paths do not match what actually exists on disk - Updated tests to verify backup files are ignored - Added test cases for custom JSONL filenames šŸ¤– Generated with Claude Code Co-Authored-By: Claude --- .beads/beads.jsonl | 2 +- cmd/bd/doctor.go | 13 ++- cmd/bd/doctor/legacy.go | 158 +++++++++++++++++++++++++++++------ cmd/bd/doctor/legacy_test.go | 26 +++++- 4 files changed, 164 insertions(+), 35 deletions(-) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 4d8bff0d..d3021d25 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -534,7 +534,7 @@ {"id":"bd-o43","content_hash":"4caa0f14a58127378a533362ec0292833b6d59195e503fab7505180c9c5c0438","title":"Add richer query capabilities to bd list","description":"Current bd list filters are limited to basic field matching (status, priority, type, assignee, label). This forces users to resort to piping through jq for common queries.\n\nMissing query capabilities:\n- Pattern matching: --title-contains, --desc-contains\n- Date ranges: --created-after, --updated-before, --closed-after\n- Empty/null checks: --empty-description, --no-assignee, --no-labels\n- Numeric ranges: --priority-min, --priority-max\n- Complex boolean logic: --and, --or operators\n- Full-text search: --search across all text fields\n- Negation: --not-status, --exclude-label\n\nExample use cases:\n- Find issues with empty descriptions\n- Find stale issues not updated in 30 days\n- Find high-priority bugs with no assignee\n- Search for keyword across title/description/notes\n\nImplementation approach:\n- Add query builder pattern to storage layer\n- Support --query DSL for complex queries\n- Keep simple flags for common cases\n- Add --json output for programmatic use","notes":"## Progress Update\n\n**Completed:**\n- āœ… Extended IssueFilter struct with new fields (pattern matching, date ranges, empty/null checks, priority ranges)\n- āœ… Updated SQLite SearchIssues implementation \n- āœ… Added CLI flags to list.go\n- āœ… Added parseTimeFlag helper\n- āœ… Comprehensive tests added - all passing\n\n**Remaining:**\n- āš ļø RPC layer needs updating (internal/rpc/protocol.go ListArgs)\n- āš ļø Daemon handler needs to forward new filters\n- āš ļø End-to-end testing with daemon mode\n- šŸ“ Documentation updates\n\n**Files Modified:**\n- internal/types/types.go\n- internal/storage/sqlite/sqlite.go \n- cmd/bd/list.go\n- cmd/bd/list_test.go\n\n**Next Steps:**\n1. Update RPC protocol\n2. Update daemon handler \n3. Test with daemon mode\n4. Update docs","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-05T00:17:48.677493-08:00","updated_at":"2025-11-05T00:33:38.998433-08:00","closed_at":"2025-11-05T00:33:38.998433-08:00","source_repo":"."} {"id":"bd-o4qy","content_hash":"28304d9e414cbc475b89dc2d1474f110d8f9d90d50fdbdca8f15961db540806b","title":"Improve CheckStaleness error handling","description":"## Problem\n\nCheckStaleness returns 'false' (not stale) for multiple error conditions instead of returning errors. This masks problems.\n\n**Location:** internal/autoimport/autoimport.go:253-285\n\n## Edge Cases That Return False\n\n1. **Invalid last_import_time format** (line 259-262)\n2. **No JSONL file found** (line 267-277) \n3. **JSONL stat fails** (line 279-282)\n\n## Fix\n\nReturn errors for abnormal conditions:\n\n```go\nlastImportTime, err := time.Parse(time.RFC3339, lastImportStr)\nif err != nil {\n return false, fmt.Errorf(\"corrupted last_import_time: %w\", err)\n}\n\nif jsonlPath == \"\" {\n return false, fmt.Errorf(\"no JSONL file found\")\n}\n\nstat, err := os.Stat(jsonlPath)\nif err != nil {\n return false, fmt.Errorf(\"cannot stat JSONL: %w\", err)\n}\n```\n\n## Impact\nMedium - edge cases are rare but should be handled\n\n## Effort \n30 minutes - requires updating callers in RPC server","status":"open","priority":2,"issue_type":"bug","created_at":"2025-11-20T20:17:27.606219-05:00","updated_at":"2025-11-20T20:17:27.606219-05:00","source_repo":".","dependencies":[{"issue_id":"bd-o4qy","depends_on_id":"bd-2q6d","type":"blocks","created_at":"2025-11-20T20:18:26.81065-05:00","created_by":"stevey"}]} {"id":"bd-o78","content_hash":"0d61c7a8e687c16c262db4ba4e6c7f63861fbeb524ec014000c68a0552df7965","title":"Enhance `bd doctor` to verify Claude Code integration","description":"Add checks to `bd doctor` that verify Claude Code integration is properly set up when .claude/ directory or Claude environment is detected.","design":"## Checks to Add\n\n### 1. Claude Environment Detection\n```go\nif isClaudeDetected() {\n fmt.Println(\"āœ“ Claude Code environment detected\")\n \n // Check what's installed\n hasMCP := isMCPServerInstalled()\n hasHooks := isClaudeSetupComplete()\n \n if hasMCP \u0026\u0026 hasHooks {\n fmt.Println(\"āœ“ MCP server installed\")\n fmt.Println(\"āœ“ Claude hooks installed (provides workflow reminders)\")\n } else if !hasMCP \u0026\u0026 hasHooks {\n fmt.Println(\"āœ“ Claude hooks installed (provides CLI reference)\")\n fmt.Println(\" Note: MCP server not detected (using CLI mode)\")\n } else if hasMCP \u0026\u0026 !hasHooks {\n fmt.Println(\"āœ“ MCP server installed\")\n fmt.Println(\"⚠ Claude hooks not installed\")\n fmt.Println(\" Hooks prevent agent from forgetting bd workflow after compaction\")\n fmt.Println(\" Run: bd setup claude\")\n } else {\n fmt.Println(\"⚠ Claude integration not set up\")\n fmt.Println(\" Run: bd setup claude\")\n // TODO (future): Recommend CLI approach once proven in production\n // fmt.Println(\" Tip: CLI mode (without MCP) uses fewer tokens\")\n }\n}\n\nfunc isMCPServerInstalled() bool {\n // Check ~/.claude/settings.json for beads MCP server\n home, _ := os.UserHomeDir()\n settingsPath := filepath.Join(home, \".claude/settings.json\")\n \n data, err := os.ReadFile(settingsPath)\n if err != nil {\n return false\n }\n \n var settings map[string]interface{}\n if err := json.Unmarshal(data, \u0026settings); err != nil {\n return false\n }\n \n // Check mcpServers section for beads\n mcpServers, ok := settings[\"mcpServers\"].(map[string]interface{})\n if !ok {\n return false\n }\n \n // Look for beads server (any key containing \"beads\")\n for key := range mcpServers {\n if strings.Contains(strings.ToLower(key), \"beads\") {\n return true\n }\n }\n \n return false\n}\n```\n\n### 2. Hook Installation Verification (MCP-Aware)\n\n```go\nfunc checkClaudeHooks() {\n home, _ := os.UserHomeDir()\n globalSettings := filepath.Join(home, \".claude/settings.json\")\n projectSettings := \".claude/settings.local.json\"\n \n globalHooks := hasBeadsHooks(globalSettings)\n projectHooks := hasBeadsHooks(projectSettings)\n \n if globalHooks {\n fmt.Println(\"āœ“ Global hooks installed\")\n } else if projectHooks {\n fmt.Println(\"āœ“ Project hooks installed\")\n } else {\n fmt.Println(\"⚠ No hooks installed\")\n fmt.Println(\" Run: bd setup claude\")\n return\n }\n \n // Check if hooks will work\n if !commandExists(\"bd\") {\n fmt.Println(\"⚠ 'bd' command not in PATH\")\n fmt.Println(\" Hooks won't work - ensure bd is installed globally\")\n }\n}\n\nfunc hasBeadsHooks(settingsPath string) bool {\n data, err := os.ReadFile(settingsPath)\n if err != nil {\n return false\n }\n \n var settings map[string]interface{}\n if err := json.Unmarshal(data, \u0026settings); err != nil {\n return false\n }\n \n hooks, ok := settings[\"hooks\"].(map[string]interface{})\n if !ok {\n return false\n }\n \n // Check SessionStart and PreCompact for \"bd prime\"\n for _, event := range []string{\"SessionStart\", \"PreCompact\"} {\n eventHooks, ok := hooks[event].([]interface{})\n if !ok {\n continue\n }\n \n for _, hook := range eventHooks {\n hookMap, _ := hook.(map[string]interface{})\n commands, _ := hookMap[\"hooks\"].([]interface{})\n for _, cmd := range commands {\n cmdMap, _ := cmd.(map[string]interface{})\n if cmdMap[\"command\"] == \"bd prime\" {\n return true\n }\n }\n }\n }\n \n return false\n}\n```\n\n### 3. AGENTS.md/CLAUDE.md Reference Check\n```go\n// Check if documentation references bd prime\nagentsContent := readFileIfExists(\"AGENTS.md\")\nclaudeContent := readFileIfExists(\"CLAUDE.md\")\n\nif strings.Contains(agentsContent, \"bd prime\") || strings.Contains(claudeContent, \"bd prime\") {\n // Verify bd prime command exists in current version\n if !commandExists(\"prime\") {\n fmt.Println(\"⚠ Documentation references 'bd prime' but command not found\")\n fmt.Println(\" Upgrade bd or remove references\")\n } else {\n fmt.Println(\"āœ“ Documentation references match installed features\")\n }\n}\n```\n\n### 4. Context Priming Test\n```go\n// Verify bd prime actually works\ncmd := exec.Command(\"bd\", \"prime\")\noutput, err := cmd.CombinedOutput()\n\nif err != nil {\n fmt.Println(\"⚠ 'bd prime' failed to execute\")\n fmt.Println(\" Error:\", err)\n} else if len(output) == 0 {\n fmt.Println(\"⚠ 'bd prime' produced no output\")\n fmt.Println(\" Expected workflow context markdown\")\n} else {\n // Check if output adapts to MCP mode\n hasMCP := isMCPServerInstalled()\n outputStr := string(output)\n \n if hasMCP \u0026\u0026 strings.Contains(outputStr, \"mcp__plugin_beads_beads__\") {\n fmt.Println(\"āœ“ bd prime detected MCP mode (workflow reminders)\")\n } else if !hasMCP \u0026\u0026 strings.Contains(outputStr, \"bd ready\") {\n fmt.Println(\"āœ“ bd prime using CLI mode (full command reference)\")\n } else {\n fmt.Println(\"⚠ bd prime output may not be adapting to environment\")\n }\n}\n```\n\n## Output Format Examples\n\n### With MCP and Hooks\n```\nbd doctor\n\nDatabase:\nāœ“ Database found at .beads/beads.db\nāœ“ Git hooks installed\n\nClaude Code Integration:\nāœ“ Claude Code environment detected\nāœ“ MCP server installed\nāœ“ Claude hooks installed (provides workflow reminders)\nāœ“ bd prime detected MCP mode (workflow reminders)\nāœ“ Documentation references match installed features\n\nSync Status:\nāœ“ No sync issues detected\n```\n\n### Without MCP, With Hooks\n```\nbd doctor\n\nDatabase:\nāœ“ Database found at .beads/beads.db\nāœ“ Git hooks installed\n\nClaude Code Integration:\nāœ“ Claude Code environment detected\nāœ“ Claude hooks installed (provides CLI reference)\n Note: MCP server not detected (using CLI mode)\nāœ“ bd prime using CLI mode (full command reference)\n\nSync Status:\nāœ“ No sync issues detected\n```\n\n### No Integration\n```\nbd doctor\n\nDatabase:\nāœ“ Database found at .beads/beads.db\nāœ“ Git hooks installed\n\nClaude Code Integration:\nāœ“ Claude Code environment detected\n⚠ Claude integration not set up\n Run: bd setup claude\n\nSync Status:\nāœ“ No sync issues detected\n```\n\n## Future Enhancement (Post-Production Validation)\n\nOnce CLI mode is proven in production, add recommendation:\n\n```go\nif isClaudeDetected() \u0026\u0026 !hasMCP \u0026\u0026 !hasHooks {\n fmt.Println(\"⚠ Claude integration not set up\")\n fmt.Println(\" Run: bd setup claude\")\n fmt.Println(\" Tip: CLI mode (without MCP) uses fewer tokens than MCP server\")\n fmt.Println(\" Both approaches work equally well - choose based on preference\")\n}\n```\n\nThis recommendation should only be added after CLI mode with `bd prime` is validated in real-world usage.","acceptance_criteria":"- bd doctor checks for Claude environment\n- Verifies hook installation if .claude/ exists\n- Checks AGENTS.md/CLAUDE.md for bd prime references\n- Detects version mismatches between docs and installed bd\n- Provides actionable suggestions (bd setup claude, upgrade)\n- Tests cover detection logic","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-11T23:30:05.782406-08:00","updated_at":"2025-11-12T00:12:07.717579-08:00","source_repo":".","dependencies":[{"issue_id":"bd-o78","depends_on_id":"bd-rpn","type":"blocks","created_at":"2025-11-11T23:30:05.783234-08:00","created_by":"daemon"},{"issue_id":"bd-o78","depends_on_id":"bd-br8","type":"blocks","created_at":"2025-11-11T23:30:05.783647-08:00","created_by":"daemon"},{"issue_id":"bd-o78","depends_on_id":"bd-90v","type":"parent-child","created_at":"2025-11-11T23:31:27.886095-08:00","created_by":"daemon"}]} -{"id":"bd-obxt","content_hash":"264ee37622f4c2e76631f616b888f85c1f36787ce9e165e614fde3b76c77b1c4","title":"Fix bd doctor to recommend issues.jsonl as canonical (not beads.jsonl)","description":"","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-21T23:27:02.008716-08:00","updated_at":"2025-11-21T23:28:03.373252-08:00","closed_at":"2025-11-21T23:28:03.373252-08:00","source_repo":"."} +{"id":"bd-obxt","content_hash":"264ee37622f4c2e76631f616b888f85c1f36787ce9e165e614fde3b76c77b1c4","title":"Fix bd doctor to recommend issues.jsonl as canonical (not beads.jsonl)","description":"","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-21T23:27:02.008716-08:00","updated_at":"2025-11-21T23:44:06.081448-08:00","closed_at":"2025-11-21T23:44:06.081448-08:00","source_repo":"."} {"id":"bd-oif6","content_hash":"5732dcbfd354e39ae9249cbae70f08ec1ccf026a812129519dfda5a8588e5ad1","title":"Vendor beads-merge Go code into internal/merge/","description":"Copy beads-merge source code from @neongreen's repo into bd codebase.\n\n**Tasks**:\n- Create `internal/merge/` package\n- Copy merge algorithm code\n- Add attribution header to all files\n- Update imports to use bd's internal types\n- Add LICENSE/ATTRIBUTION file crediting @neongreen\n- Keep original algorithm intact\n\n**Source**: https://github.com/neongreen/mono/tree/main/beads-merge","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-05T18:42:20.405283-08:00","updated_at":"2025-11-05T18:52:53.71713-08:00","closed_at":"2025-11-05T18:52:53.71713-08:00","source_repo":".","dependencies":[{"issue_id":"bd-oif6","depends_on_id":"bd-qqvw","type":"parent-child","created_at":"2025-11-05T18:42:28.69196-08:00","created_by":"daemon"}]} {"id":"bd-ola6","content_hash":"79461888e8a7875bf3623b8db44ea004f73a2374daa52ae9cb3fc9d3ce5e6a8b","title":"Implement transaction retry logic for SQLITE_BUSY","description":"BEGIN IMMEDIATE fails immediately on SQLITE_BUSY instead of retrying with exponential backoff.\n\nLocation: internal/storage/sqlite/sqlite.go:223-225\n\nProblem:\n- Under concurrent write load, BEGIN IMMEDIATE can fail with SQLITE_BUSY\n- Current implementation fails immediately instead of retrying\n- Results in spurious failures under normal concurrent usage\n\nSolution: Implement exponential backoff retry:\n- Retry up to N times (e.g., 5)\n- Backoff: 10ms, 20ms, 40ms, 80ms, 160ms\n- Check for context cancellation between retries\n- Only retry on SQLITE_BUSY/database locked errors\n\nImpact: Spurious failures under concurrent write load\n\nEffort: 3 hours","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-16T14:51:31.247147-08:00","updated_at":"2025-11-16T14:51:31.247147-08:00","source_repo":"."} {"id":"bd-omx1","content_hash":"e61d74adb03fc8275c97242df8ce0e4146db7e49271e4e86c3379b4a3fbab0d8","title":"Add `bd merge` command wrapping 3-way merge logic","description":"Implement CLI command to invoke beads-merge functionality.\n\n**Interface**:\n```bash\nbd merge \u003coutput\u003e \u003cbase\u003e \u003cleft\u003e \u003cright\u003e\nbd merge --debug \u003coutput\u003e \u003cbase\u003e \u003cleft\u003e \u003cright\u003e\n```\n\n**Behavior**:\n- Exit code 0 on clean merge\n- Exit code 1 if conflicts (write conflict markers)\n- Support --debug flag for verbose output\n- Match beads-merge's existing behavior\n\n**File**: `cmd/bd/merge.go`","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-05T18:42:20.427429-08:00","updated_at":"2025-11-05T19:01:29.071365-08:00","closed_at":"2025-11-05T19:01:29.071365-08:00","source_repo":".","dependencies":[{"issue_id":"bd-omx1","depends_on_id":"bd-qqvw","type":"parent-child","created_at":"2025-11-05T18:42:28.709123-08:00","created_by":"daemon"},{"issue_id":"bd-omx1","depends_on_id":"bd-oif6","type":"blocks","created_at":"2025-11-05T18:42:35.436444-08:00","created_by":"daemon"}]} diff --git a/cmd/bd/doctor.go b/cmd/bd/doctor.go index 0bf5a148..55c403a2 100644 --- a/cmd/bd/doctor.go +++ b/cmd/bd/doctor.go @@ -275,21 +275,28 @@ func runDiagnostics(path string) doctorResult { result.OverallOK = false } - // Check 6: Legacy JSONL filename (issues.jsonl vs beads.jsonl) + // Check 6: Multiple JSONL files (excluding merge artifacts) jsonlCheck := convertDoctorCheck(doctor.CheckLegacyJSONLFilename(path)) result.Checks = append(result.Checks, jsonlCheck) if jsonlCheck.Status == statusWarning || jsonlCheck.Status == statusError { result.OverallOK = false } - // Check 7: Daemon health + // Check 7: Database/JSONL configuration mismatch + configCheck := convertDoctorCheck(doctor.CheckDatabaseConfig(path)) + result.Checks = append(result.Checks, configCheck) + if configCheck.Status == statusWarning || configCheck.Status == statusError { + result.OverallOK = false + } + + // Check 8: Daemon health daemonCheck := checkDaemonStatus(path) result.Checks = append(result.Checks, daemonCheck) if daemonCheck.Status == statusWarning || daemonCheck.Status == statusError { result.OverallOK = false } - // Check 8: Database-JSONL sync + // Check 9: Database-JSONL sync syncCheck := checkDatabaseJSONLSync(path) result.Checks = append(result.Checks, syncCheck) if syncCheck.Status == statusWarning || syncCheck.Status == statusError { diff --git a/cmd/bd/doctor/legacy.go b/cmd/bd/doctor/legacy.go index be2e2afa..6ad7f631 100644 --- a/cmd/bd/doctor/legacy.go +++ b/cmd/bd/doctor/legacy.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/steveyegge/beads/internal/configfile" ) // CheckLegacyBeadsSlashCommands detects old /beads:* slash commands in documentation @@ -103,25 +105,47 @@ func CheckAgentDocumentation(repoPath string) DoctorCheck { } } -// CheckLegacyJSONLFilename detects if project is using non-standard beads.jsonl -// instead of the canonical issues.jsonl filename. +// CheckLegacyJSONLFilename detects if there are multiple JSONL files, +// which can cause sync/merge issues. Ignores merge artifacts and backups. func CheckLegacyJSONLFilename(repoPath string) DoctorCheck { beadsDir := filepath.Join(repoPath, ".beads") - var jsonlFiles []string - hasBeadsJSON := false - - for _, name := range []string{"issues.jsonl", "beads.jsonl"} { - jsonlPath := filepath.Join(beadsDir, name) - if _, err := os.Stat(jsonlPath); err == nil { - jsonlFiles = append(jsonlFiles, name) - if name == "beads.jsonl" { - hasBeadsJSON = true - } + // Find all .jsonl files + entries, err := os.ReadDir(beadsDir) + if err != nil { + return DoctorCheck{ + Name: "JSONL Files", + Status: "ok", + Message: "No .beads directory found", } } - if len(jsonlFiles) == 0 { + var realJSONLFiles []string + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + + // Must end with .jsonl + if !strings.HasSuffix(name, ".jsonl") { + continue + } + + // Skip merge artifacts and backups + lowerName := strings.ToLower(name) + if strings.Contains(lowerName, "backup") || + strings.Contains(lowerName, ".orig") || + strings.Contains(lowerName, ".bak") || + strings.Contains(lowerName, "~") || + strings.HasPrefix(lowerName, "backup_") { + continue + } + + realJSONLFiles = append(realJSONLFiles, name) + } + + if len(realJSONLFiles) == 0 { return DoctorCheck{ Name: "JSONL Files", Status: "ok", @@ -129,28 +153,108 @@ func CheckLegacyJSONLFilename(repoPath string) DoctorCheck { } } - if len(jsonlFiles) == 1 { - // Single JSONL file - check if it's the non-standard name - if hasBeadsJSON { - return DoctorCheck{ - Name: "JSONL Files", - Status: "warning", - Message: "Using non-standard JSONL filename: beads.jsonl", - Fix: "Run 'git mv .beads/beads.jsonl .beads/issues.jsonl' to use canonical name", - } - } + if len(realJSONLFiles) == 1 { return DoctorCheck{ Name: "JSONL Files", Status: "ok", - Message: fmt.Sprintf("Using %s", jsonlFiles[0]), + Message: fmt.Sprintf("Using %s", realJSONLFiles[0]), } } - // Multiple JSONL files found + // Multiple JSONL files found - this is a problem! return DoctorCheck{ Name: "JSONL Files", Status: "warning", - Message: fmt.Sprintf("Multiple JSONL files found: %s", strings.Join(jsonlFiles, ", ")), - Fix: "Run 'git rm .beads/beads.jsonl' to standardize on issues.jsonl (canonical name)", + Message: fmt.Sprintf("Multiple JSONL files found: %s", strings.Join(realJSONLFiles, ", ")), + Detail: "Having multiple JSONL files can cause sync and merge conflicts.\n" + + " Only one JSONL file should be used per repository.", + Fix: "Determine which file is current and remove the others:\n" + + " 1. Check 'bd stats' to see which file is being used\n" + + " 2. Verify with 'git log .beads/*.jsonl' to see commit history\n" + + " 3. Remove the unused file(s): git rm .beads/.jsonl\n" + + " 4. Commit the change", + } +} + +// CheckDatabaseConfig verifies that the configured database and JSONL paths +// match what actually exists on disk. +func CheckDatabaseConfig(repoPath string) DoctorCheck { + beadsDir := filepath.Join(repoPath, ".beads") + + // Load config + cfg, err := configfile.Load(beadsDir) + if err != nil || cfg == nil { + // No config or error reading - use defaults + return DoctorCheck{ + Name: "Database Config", + Status: "ok", + Message: "Using default configuration", + } + } + + var issues []string + + // Check if configured database exists + if cfg.Database != "" { + dbPath := cfg.DatabasePath(beadsDir) + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + // Check if other .db files exist + entries, _ := os.ReadDir(beadsDir) + var otherDBs []string + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".db") { + otherDBs = append(otherDBs, entry.Name()) + } + } + if len(otherDBs) > 0 { + issues = append(issues, fmt.Sprintf("Configured database '%s' not found, but found: %s", + cfg.Database, strings.Join(otherDBs, ", "))) + } + } + } + + // Check if configured JSONL exists + if cfg.JSONLExport != "" { + jsonlPath := cfg.JSONLPath(beadsDir) + if _, err := os.Stat(jsonlPath); os.IsNotExist(err) { + // Check if other .jsonl files exist + entries, _ := os.ReadDir(beadsDir) + var otherJSONLs []string + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".jsonl") { + name := entry.Name() + // Skip backups + lowerName := strings.ToLower(name) + if !strings.Contains(lowerName, "backup") && + !strings.Contains(lowerName, ".orig") && + !strings.Contains(lowerName, ".bak") { + otherJSONLs = append(otherJSONLs, name) + } + } + } + if len(otherJSONLs) > 0 { + issues = append(issues, fmt.Sprintf("Configured JSONL '%s' not found, but found: %s", + cfg.JSONLExport, strings.Join(otherJSONLs, ", "))) + } + } + } + + if len(issues) == 0 { + return DoctorCheck{ + Name: "Database Config", + Status: "ok", + Message: "Configuration matches existing files", + } + } + + return DoctorCheck{ + Name: "Database Config", + Status: "warning", + Message: "Configuration mismatch detected", + Detail: strings.Join(issues, "\n "), + Fix: "Update configuration in .beads/metadata.json:\n" + + " 1. Check which files are actually being used\n" + + " 2. Update metadata.json to match the actual filenames\n" + + " 3. Or rename the files to match the configuration", } } diff --git a/cmd/bd/doctor/legacy_test.go b/cmd/bd/doctor/legacy_test.go index 4c04522b..6b6d63ab 100644 --- a/cmd/bd/doctor/legacy_test.go +++ b/cmd/bd/doctor/legacy_test.go @@ -190,20 +190,38 @@ func TestCheckLegacyJSONLFilename(t *testing.T) { expectWarning: false, }, { - name: "canonical issues.jsonl", + name: "single issues.jsonl", files: []string{"issues.jsonl"}, expectedStatus: "ok", expectWarning: false, }, { - name: "non-standard beads.jsonl", + name: "single beads.jsonl is ok", files: []string{"beads.jsonl"}, + expectedStatus: "ok", + expectWarning: false, + }, + { + name: "custom name is ok", + files: []string{"my-issues.jsonl"}, + expectedStatus: "ok", + expectWarning: false, + }, + { + name: "multiple JSONL files warning", + files: []string{"beads.jsonl", "issues.jsonl"}, expectedStatus: "warning", expectWarning: true, }, { - name: "both files present", - files: []string{"beads.jsonl", "issues.jsonl"}, + name: "backup files ignored", + files: []string{"issues.jsonl", "issues.jsonl.backup", "BACKUP_issues.jsonl"}, + expectedStatus: "ok", + expectWarning: false, + }, + { + name: "multiple real files with backups", + files: []string{"issues.jsonl", "beads.jsonl", "issues.jsonl.backup"}, expectedStatus: "warning", expectWarning: true, },