Fix: Change default JSONL filename from beads.jsonl to issues.jsonl

The canonical beads database name is issues.jsonl. Tens of thousands of users
have issues.jsonl, and beads.jsonl was only used by the Beads project itself
due to git history pollution.

Changes:
- Updated bd doctor to warn about beads.jsonl instead of issues.jsonl
- Changed default config from beads.jsonl to issues.jsonl
- Reversed precedence in checkGitForIssues to prefer issues.jsonl
- Updated git merge driver config to use issues.jsonl
- Updated all tests to expect issues.jsonl as the default

issues.jsonl is now the canonical default; beads.jsonl is legacy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-21 23:34:22 -08:00
parent 3573470d6b
commit c4c5c8063a
16 changed files with 99 additions and 85 deletions

View File

@@ -85,10 +85,10 @@ func checkGitForIssues() (int, string) {
return 0, ""
}
// Try canonical JSONL filenames in precedence order
// Try canonical JSONL filenames in precedence order (issues.jsonl is canonical)
candidates := []string{
filepath.Join(relBeads, "beads.jsonl"),
filepath.Join(relBeads, "issues.jsonl"),
filepath.Join(relBeads, "beads.jsonl"),
}
for _, relPath := range candidates {

View File

@@ -19,8 +19,8 @@ func TestMultiWorkspaceDeletionSync(t *testing.T) {
cloneADir := t.TempDir()
cloneBDir := t.TempDir()
cloneAJSONL := filepath.Join(cloneADir, "beads.jsonl")
cloneBJSONL := filepath.Join(cloneBDir, "beads.jsonl")
cloneAJSONL := filepath.Join(cloneADir, "issues.jsonl")
cloneBJSONL := filepath.Join(cloneBDir, "issues.jsonl")
cloneADB := filepath.Join(cloneADir, "beads.db")
cloneBDB := filepath.Join(cloneBDir, "beads.db")
@@ -177,7 +177,7 @@ func TestMultiWorkspaceDeletionSync(t *testing.T) {
// Remote deletes an issue, but local has modified it
func TestDeletionWithLocalModification(t *testing.T) {
dir := t.TempDir()
jsonlPath := filepath.Join(dir, "beads.jsonl")
jsonlPath := filepath.Join(dir, "issues.jsonl")
dbPath := filepath.Join(dir, "beads.db")
ctx := context.Background()
@@ -343,7 +343,7 @@ func TestComputeAcceptedDeletions_LocallyModified(t *testing.T) {
// TestSnapshotManagement tests the snapshot file lifecycle
func TestSnapshotManagement(t *testing.T) {
dir := t.TempDir()
jsonlPath := filepath.Join(dir, "beads.jsonl")
jsonlPath := filepath.Join(dir, "issues.jsonl")
// Write initial JSONL
content := `{"id":"bd-1","title":"Test"}

View File

@@ -103,20 +103,20 @@ func CheckAgentDocumentation(repoPath string) DoctorCheck {
}
}
// CheckLegacyJSONLFilename detects if project is using legacy issues.jsonl
// instead of the canonical beads.jsonl filename.
// CheckLegacyJSONLFilename detects if project is using non-standard beads.jsonl
// instead of the canonical issues.jsonl filename.
func CheckLegacyJSONLFilename(repoPath string) DoctorCheck {
beadsDir := filepath.Join(repoPath, ".beads")
var jsonlFiles []string
hasIssuesJSON := false
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 == "issues.jsonl" {
hasIssuesJSON = true
if name == "beads.jsonl" {
hasBeadsJSON = true
}
}
}
@@ -130,13 +130,13 @@ func CheckLegacyJSONLFilename(repoPath string) DoctorCheck {
}
if len(jsonlFiles) == 1 {
// Single JSONL file - check if it's the legacy name
if hasIssuesJSON {
// Single JSONL file - check if it's the non-standard name
if hasBeadsJSON {
return DoctorCheck{
Name: "JSONL Files",
Status: "warning",
Message: "Using legacy JSONL filename: issues.jsonl",
Fix: "Run 'git mv .beads/issues.jsonl .beads/beads.jsonl' to use canonical name (matches beads.db)",
Message: "Using non-standard JSONL filename: beads.jsonl",
Fix: "Run 'git mv .beads/beads.jsonl .beads/issues.jsonl' to use canonical name",
}
}
return DoctorCheck{
@@ -151,6 +151,6 @@ func CheckLegacyJSONLFilename(repoPath string) DoctorCheck {
Name: "JSONL Files",
Status: "warning",
Message: fmt.Sprintf("Multiple JSONL files found: %s", strings.Join(jsonlFiles, ", ")),
Fix: "Run 'git rm .beads/issues.jsonl' to standardize on beads.jsonl (canonical name)",
Fix: "Run 'git rm .beads/beads.jsonl' to standardize on issues.jsonl (canonical name)",
}
}

View File

@@ -190,14 +190,14 @@ func TestCheckLegacyJSONLFilename(t *testing.T) {
expectWarning: false,
},
{
name: "canonical beads.jsonl",
files: []string{"beads.jsonl"},
name: "canonical issues.jsonl",
files: []string{"issues.jsonl"},
expectedStatus: "ok",
expectWarning: false,
},
{
name: "legacy issues.jsonl",
files: []string{"issues.jsonl"},
name: "non-standard beads.jsonl",
files: []string{"beads.jsonl"},
expectedStatus: "warning",
expectWarning: true,
},

View File

@@ -812,12 +812,14 @@ func installMergeDriver() error {
}
// Check if beads merge driver is already configured
hasBeadsMerge := strings.Contains(existingContent, ".beads/beads.jsonl") &&
// Check for either pattern (issues.jsonl is canonical, beads.jsonl is legacy)
hasBeadsMerge := (strings.Contains(existingContent, ".beads/issues.jsonl") ||
strings.Contains(existingContent, ".beads/beads.jsonl")) &&
strings.Contains(existingContent, "merge=beads")
if !hasBeadsMerge {
// Append beads merge driver configuration
beadsMergeAttr := "\n# Use bd merge for beads JSONL files\n.beads/beads.jsonl merge=beads\n"
// Append beads merge driver configuration (issues.jsonl is canonical)
beadsMergeAttr := "\n# Use bd merge for beads JSONL files\n.beads/issues.jsonl merge=beads\n"
newContent := existingContent
if !strings.HasSuffix(newContent, "\n") && len(newContent) > 0 {

View File

@@ -526,7 +526,7 @@ func TestInitMergeDriverAutoConfiguration(t *testing.T) {
if err != nil {
t.Fatalf("Failed to read .gitattributes: %v", err)
}
if !strings.Contains(string(content), ".beads/beads.jsonl merge=beads") {
if !strings.Contains(string(content), ".beads/issues.jsonl merge=beads") {
t.Error(".gitattributes should contain merge driver configuration")
}
})
@@ -633,7 +633,7 @@ func TestInitMergeDriverAutoConfiguration(t *testing.T) {
// Create .gitattributes with merge driver
gitattrsPath := filepath.Join(tmpDir, ".gitattributes")
initialContent := "# Existing config\n.beads/beads.jsonl merge=beads\n"
initialContent := "# Existing config\n.beads/issues.jsonl merge=beads\n"
if err := os.WriteFile(gitattrsPath, []byte(initialContent), 0644); err != nil {
t.Fatalf("Failed to create .gitattributes: %v", err)
}
@@ -661,7 +661,7 @@ func TestInitMergeDriverAutoConfiguration(t *testing.T) {
contentStr := string(content)
// Count occurrences - should only appear once
count := strings.Count(contentStr, ".beads/beads.jsonl merge=beads")
count := strings.Count(contentStr, ".beads/issues.jsonl merge=beads")
if count != 1 {
t.Errorf("Expected .gitattributes to contain merge config exactly once, found %d times", count)
}
@@ -727,13 +727,13 @@ func TestInitMergeDriverAutoConfiguration(t *testing.T) {
}
// Should contain beads config
if !strings.Contains(contentStr, ".beads/beads.jsonl merge=beads") {
if !strings.Contains(contentStr, ".beads/issues.jsonl merge=beads") {
t.Error(".gitattributes should contain beads merge config")
}
// Beads config should come after existing content
txtIdx := strings.Index(contentStr, "*.txt")
beadsIdx := strings.Index(contentStr, ".beads/beads.jsonl")
beadsIdx := strings.Index(contentStr, ".beads/issues.jsonl")
if txtIdx >= beadsIdx {
t.Error("Beads config should be appended after existing content")
}

View File

@@ -177,8 +177,8 @@ func TestAutoFlushJSONLContent(t *testing.T) {
dbPath = filepath.Join(tmpDir, "test.db")
// The actual JSONL path - findJSONLPath() will determine this
// but in tests it appears to be beads.jsonl in the same directory as the db
expectedJSONLPath := filepath.Join(tmpDir, "beads.jsonl")
// but in tests it appears to be issues.jsonl in the same directory as the db
expectedJSONLPath := filepath.Join(tmpDir, "issues.jsonl")
// Create store
testStore := newTestStore(t, dbPath)

View File

@@ -28,7 +28,7 @@ Designed to work as a git merge driver. Configure with:
git config merge.beads.driver "bd merge %A %O %A %B"
git config merge.beads.name "bd JSONL merge driver"
echo ".beads/beads.jsonl merge=beads" >> .gitattributes
echo ".beads/issues.jsonl merge=beads" >> .gitattributes
Or use 'bd init' which automatically configures the merge driver.

View File

@@ -52,7 +52,7 @@ func TestCleanupMergeArtifacts_CommandInjectionPrevention(t *testing.T) {
},
{
name: "normal backup file",
filename: "beads.jsonl.backup",
filename: "issues.jsonl.backup",
wantSafe: true,
},
{
@@ -89,7 +89,7 @@ func TestCleanupMergeArtifacts_CommandInjectionPrevention(t *testing.T) {
}
// Create output path
outputPath := filepath.Join(beadsDir, "beads.jsonl")
outputPath := filepath.Join(beadsDir, "issues.jsonl")
if err := os.WriteFile(outputPath, []byte("{}"), 0644); err != nil {
t.Fatalf("Failed to create output file: %v", err)
}
@@ -129,14 +129,14 @@ func TestCleanupMergeArtifacts_OnlyBackupFiles(t *testing.T) {
// Create various files
files := map[string]bool{
"beads.jsonl": false, // Should NOT be removed
"issues.jsonl": false, // Should NOT be removed
"beads.db": false, // Should NOT be removed
"backup.jsonl": true, // Should be removed
"beads.jsonl.backup": true, // Should be removed
"issues.jsonl.backup": true, // Should be removed
"BACKUP_FILE": true, // Should be removed (case-insensitive)
"my_backup_2024.txt": true, // Should be removed
"important_data.jsonl": false, // Should NOT be removed
"beads.jsonl.bak": false, // Should NOT be removed (no "backup")
"issues.jsonl.bak": false, // Should NOT be removed (no "backup")
}
for filename := range files {
@@ -147,7 +147,7 @@ func TestCleanupMergeArtifacts_OnlyBackupFiles(t *testing.T) {
}
// Create output path
outputPath := filepath.Join(beadsDir, "beads.jsonl")
outputPath := filepath.Join(beadsDir, "issues.jsonl")
// Run cleanup
cleanupMergeArtifacts(outputPath, false)
@@ -192,7 +192,7 @@ func TestCleanupMergeArtifacts_GitRmSafety(t *testing.T) {
t.Fatalf("Failed to create backup file: %v", err)
}
outputPath := filepath.Join(beadsDir, "beads.jsonl")
outputPath := filepath.Join(beadsDir, "issues.jsonl")
if err := os.WriteFile(outputPath, []byte("{}"), 0644); err != nil {
t.Fatalf("Failed to create output file: %v", err)
}

View File

@@ -47,7 +47,7 @@ func testFreshCloneAutoImport(t *testing.T) {
runCmd(t, dir, "git", "config", "user.email", "test@example.com")
runCmd(t, dir, "git", "config", "user.name", "Test User")
// Create .beads directory with beads.jsonl (use forward slashes for git)
// Create .beads directory with issues.jsonl (canonical name)
beadsDir := filepath.Join(dir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
@@ -63,13 +63,13 @@ func testFreshCloneAutoImport(t *testing.T) {
IssueType: types.TypeTask,
}
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := writeJSONL(jsonlPath, []*types.Issue{issue}); err != nil {
t.Fatalf("Failed to write JSONL: %v", err)
}
// Commit to git (use forward slashes for git path)
runCmd(t, dir, "git", "add", ".beads/beads.jsonl")
runCmd(t, dir, "git", "add", ".beads/issues.jsonl")
runCmd(t, dir, "git", "commit", "-m", "Initial commit")
// Remove database to simulate fresh clone
@@ -88,7 +88,7 @@ func testFreshCloneAutoImport(t *testing.T) {
t.Fatalf("Failed to set prefix: %v", err)
}
// Test checkGitForIssues detects beads.jsonl
// Test checkGitForIssues detects issues.jsonl
originalDir, _ := os.Getwd()
os.Chdir(dir)
defer os.Chdir(originalDir)
@@ -98,7 +98,7 @@ func testFreshCloneAutoImport(t *testing.T) {
t.Errorf("Expected 1 issue in git, got %d", count)
}
// Normalize path for comparison (handle both forward and backslash)
expectedPath := normalizeGitPath(".beads/beads.jsonl")
expectedPath := normalizeGitPath(".beads/issues.jsonl")
if normalizeGitPath(path) != expectedPath {
t.Errorf("Expected path %s, got %s", expectedPath, path)
}
@@ -127,7 +127,7 @@ func testDatabaseRemovalScenario(t *testing.T) {
runCmd(t, dir, "git", "config", "user.email", "test@example.com")
runCmd(t, dir, "git", "config", "user.name", "Test User")
// Create .beads directory with beads.jsonl
// Create .beads directory with issues.jsonl (canonical name)
beadsDir := filepath.Join(dir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
@@ -151,13 +151,13 @@ func testDatabaseRemovalScenario(t *testing.T) {
},
}
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := writeJSONL(jsonlPath, issues); err != nil {
t.Fatalf("Failed to write JSONL: %v", err)
}
// Commit to git
runCmd(t, dir, "git", "add", ".beads/beads.jsonl")
runCmd(t, dir, "git", "add", ".beads/issues.jsonl")
runCmd(t, dir, "git", "commit", "-m", "Add issues")
// Simulate rm -rf .beads/
@@ -169,12 +169,12 @@ func testDatabaseRemovalScenario(t *testing.T) {
os.Chdir(dir)
defer os.Chdir(originalDir)
// Test checkGitForIssues finds beads.jsonl (not issues.jsonl)
// Test checkGitForIssues finds issues.jsonl (canonical name)
count, path := checkGitForIssues()
if count != 2 {
t.Errorf("Expected 2 issues in git, got %d", count)
}
expectedPath := normalizeGitPath(".beads/beads.jsonl")
expectedPath := normalizeGitPath(".beads/issues.jsonl")
if normalizeGitPath(path) != expectedPath {
t.Errorf("Expected %s, got %s", expectedPath, path)
}
@@ -197,8 +197,8 @@ func testDatabaseRemovalScenario(t *testing.T) {
}
// Verify correct filename was detected
if filepath.Base(path) != "beads.jsonl" {
t.Errorf("Should have imported from beads.jsonl, got %s", path)
if filepath.Base(path) != "issues.jsonl" {
t.Errorf("Should have imported from issues.jsonl, got %s", path)
}
// Verify stats show >0 issues
@@ -286,7 +286,7 @@ func testLegacyFilenameSupport(t *testing.T) {
}
}
// testPrecedenceTest verifies beads.jsonl is preferred over issues.jsonl
// testPrecedenceTest verifies issues.jsonl is preferred over beads.jsonl
func testPrecedenceTest(t *testing.T) {
dir := t.TempDir()
@@ -301,21 +301,21 @@ func testPrecedenceTest(t *testing.T) {
t.Fatalf("Failed to create .beads dir: %v", err)
}
// Create beads.jsonl with 2 issues
beadsIssues := []*types.Issue{
{ID: "test-1", Title: "From beads.jsonl", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{ID: "test-2", Title: "Also from beads.jsonl", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
// Create issues.jsonl with 2 issues (canonical, should be preferred)
canonicalIssues := []*types.Issue{
{ID: "test-1", Title: "From issues.jsonl", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{ID: "test-2", Title: "Also from issues.jsonl", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
}
if err := writeJSONL(filepath.Join(beadsDir, "beads.jsonl"), beadsIssues); err != nil {
t.Fatalf("Failed to write beads.jsonl: %v", err)
if err := writeJSONL(filepath.Join(beadsDir, "issues.jsonl"), canonicalIssues); err != nil {
t.Fatalf("Failed to write issues.jsonl: %v", err)
}
// Create issues.jsonl with 1 issue (should be ignored)
// Create beads.jsonl with 1 issue (should be ignored)
legacyIssues := []*types.Issue{
{ID: "test-99", Title: "From issues.jsonl", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{ID: "test-99", Title: "From beads.jsonl", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
}
if err := writeJSONL(filepath.Join(beadsDir, "issues.jsonl"), legacyIssues); err != nil {
t.Fatalf("Failed to write issues.jsonl: %v", err)
if err := writeJSONL(filepath.Join(beadsDir, "beads.jsonl"), legacyIssues); err != nil {
t.Fatalf("Failed to write beads.jsonl: %v", err)
}
// Commit both files
@@ -327,14 +327,14 @@ func testPrecedenceTest(t *testing.T) {
os.Chdir(dir)
defer os.Chdir(originalDir)
// Test checkGitForIssues prefers beads.jsonl
// Test checkGitForIssues prefers issues.jsonl
count, path := checkGitForIssues()
if count != 2 {
t.Errorf("Expected 2 issues (from beads.jsonl), got %d", count)
t.Errorf("Expected 2 issues (from issues.jsonl), got %d", count)
}
expectedPath := normalizeGitPath(".beads/beads.jsonl")
expectedPath := normalizeGitPath(".beads/issues.jsonl")
if normalizeGitPath(path) != expectedPath {
t.Errorf("Expected beads.jsonl to be preferred, got %s", path)
t.Errorf("Expected issues.jsonl to be preferred, got %s", path)
}
}
@@ -347,7 +347,7 @@ func testInitSafetyCheck(t *testing.T) {
runCmd(t, dir, "git", "config", "user.email", "test@example.com")
runCmd(t, dir, "git", "config", "user.name", "Test User")
// Create .beads directory with beads.jsonl
// Create .beads directory with issues.jsonl (canonical name)
beadsDir := filepath.Join(dir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
@@ -361,13 +361,13 @@ func testInitSafetyCheck(t *testing.T) {
IssueType: types.TypeTask,
}
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := writeJSONL(jsonlPath, []*types.Issue{issue}); err != nil {
t.Fatalf("Failed to write JSONL: %v", err)
}
// Commit to git
runCmd(t, dir, "git", "add", ".beads/beads.jsonl")
runCmd(t, dir, "git", "add", ".beads/issues.jsonl")
runCmd(t, dir, "git", "commit", "-m", "Add issue")
// Change to test directory
@@ -398,7 +398,7 @@ func testInitSafetyCheck(t *testing.T) {
if recheck == 0 {
t.Error("Safety check should have detected issues in git")
}
expectedPath := normalizeGitPath(".beads/beads.jsonl")
expectedPath := normalizeGitPath(".beads/issues.jsonl")
if normalizeGitPath(recheckPath) != expectedPath {
t.Errorf("Safety check found wrong path: %s", recheckPath)
}

View File

@@ -33,7 +33,7 @@ func setupTestStore(t *testing.T, dbPath string) *sqlite.SQLiteStorage {
func TestDBNeedsExport_InSync(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "beads.db")
jsonlPath := filepath.Join(tmpDir, "beads.jsonl")
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
store := setupTestStore(t, dbPath)
defer store.Close()
@@ -81,7 +81,7 @@ func TestDBNeedsExport_InSync(t *testing.T) {
func TestDBNeedsExport_DBNewer(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "beads.db")
jsonlPath := filepath.Join(tmpDir, "beads.jsonl")
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
store := setupTestStore(t, dbPath)
defer store.Close()
@@ -132,7 +132,7 @@ func TestDBNeedsExport_DBNewer(t *testing.T) {
func TestDBNeedsExport_CountMismatch(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "beads.db")
jsonlPath := filepath.Join(tmpDir, "beads.jsonl")
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
store := setupTestStore(t, dbPath)
defer store.Close()
@@ -189,7 +189,7 @@ func TestDBNeedsExport_CountMismatch(t *testing.T) {
func TestDBNeedsExport_NoJSONL(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "beads.db")
jsonlPath := filepath.Join(tmpDir, "beads.jsonl")
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
store := setupTestStore(t, dbPath)
defer store.Close()