Fix critical P0 database reinitialization bug (bd-130)

Fixes silent data loss when .beads/ directory removed and daemon auto-starts.

Root cause: checkGitForIssues() hardcoded 'issues.jsonl' but git tracks 'beads.jsonl'

Changes:
- Fix A (bd-131): checkGitForIssues() tries beads.jsonl first, then issues.jsonl
- Fix B (bd-132): Immediate export after import in bd init to prevent daemon race
- Fix C (bd-133): Safety check that fails loudly if import fails
- Fix D (bd-134): Daemon startup auto-import when DB empty but git has issues
- Tests (bd-135): Comprehensive integration test suite

Oracle-recommended improvements:
- Export to exact git-relative path (prevents path drift)
- filepath.ToSlash for Windows git compatibility
- 64MB scanner buffer for large JSONL lines
- Improved safety check messages (only suggest local file if exists)

All tests passing. No regressions.

Amp-Thread-ID: https://ampcode.com/threads/T-0e31dc6a-a0d9-46c6-87b2-cfdebe829a52
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-24 14:52:24 -07:00
parent 3b15b5b259
commit 14895bf97a
5 changed files with 515 additions and 24 deletions

View File

@@ -58,7 +58,7 @@ func checkAndAutoImport(ctx context.Context, store storage.Storage) bool {
return true
}
// checkGitForIssues checks if git has issues in HEAD:.beads/issues.jsonl
// checkGitForIssues checks if git has issues in HEAD:.beads/beads.jsonl or issues.jsonl
// Returns (issue_count, relative_jsonl_path)
func checkGitForIssues() (int, string) {
// Try to find .beads directory
@@ -67,32 +67,37 @@ func checkGitForIssues() (int, string) {
return 0, ""
}
// Construct relative path to issues.jsonl from git root
// Construct relative path from git root
gitRoot := findGitRoot()
if gitRoot == "" {
return 0, ""
}
relPath, err := filepath.Rel(gitRoot, filepath.Join(beadsDir, "issues.jsonl"))
relBeads, err := filepath.Rel(gitRoot, beadsDir)
if err != nil {
return 0, ""
}
// Check if git has this file with content
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", relPath))
output, err := cmd.Output()
if err != nil {
// File doesn't exist in git or other error
return 0, ""
// Try canonical JSONL filenames in precedence order
candidates := []string{
filepath.Join(relBeads, "beads.jsonl"),
filepath.Join(relBeads, "issues.jsonl"),
}
// Count lines (rough estimate of issue count)
lines := bytes.Count(output, []byte("\n"))
if lines == 0 {
return 0, ""
for _, relPath := range candidates {
// Use ToSlash for git path compatibility on Windows
gitPath := filepath.ToSlash(relPath)
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", gitPath))
output, err := cmd.Output()
if err == nil && len(output) > 0 {
lines := bytes.Count(output, []byte("\n"))
if lines > 0 {
return lines, relPath
}
}
}
return lines, relPath
return 0, ""
}
// findBeadsDir finds the .beads directory in current or parent directories
@@ -131,8 +136,9 @@ func findGitRoot() string {
// importFromGit imports issues from git HEAD
func importFromGit(ctx context.Context, dbFilePath string, store storage.Storage, jsonlPath string) error {
// Get content from git
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", jsonlPath))
// Get content from git (use ToSlash for Windows compatibility)
gitPath := filepath.ToSlash(jsonlPath)
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", gitPath))
jsonlData, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to read from git: %w", err)
@@ -140,6 +146,8 @@ func importFromGit(ctx context.Context, dbFilePath string, store storage.Storage
// Parse JSONL data
scanner := bufio.NewScanner(bytes.NewReader(jsonlData))
// Increase buffer size to handle large JSONL lines (e.g., big descriptions)
scanner.Buffer(make([]byte, 0, 1024*1024), 64*1024*1024) // allow up to 64MB per line
var issues []*types.Issue
for scanner.Scan() {