Add three-way merge conflict detection and resolution

## Problem

When version tracking detects a conflict (two clones modified same issue), we need intelligent merge logic instead of just picking a winner.

Depends on: bd-167 (version tracking)

## Solution

Implement three-way merge algorithm using git merge-base concept:
- Base: Last common version
- Local: Current database state
- Remote: Incoming JSONL state

## Three-Way Merge Algorithm

### 1. Find Merge Base

**File**: internal/merge/merge.go (create new)

```go
package merge

import (
    "github.com/steveyegge/beads/internal/types"
)

// FindMergeBase finds the last common version between local and remote
func FindMergeBase(store storage.Storage, issueID string, localVersion, remoteVersion int) (*types.Issue, error) {
    // Get version history from issue_versions table
    baseVersion := min(localVersion, remoteVersion)

    var snapshot []byte
    err := db.QueryRow(`
        SELECT snapshot FROM issue_versions
        WHERE issue_id = ? AND version = ?
        ORDER BY version DESC LIMIT 1
    `, issueID, baseVersion).Scan(&snapshot)

    if err != nil {
        return nil, fmt.Errorf("merge base not found: %w", err)
    }

    var base types.Issue
    json.Unmarshal(snapshot, &base)
    return &base, nil
}
```

### 2. Detect Conflict Type

```go
type ConflictType int

const (
    NoConflict ConflictType = iota
    ModifyModify  // Both modified same field
    ModifyDelete  // One modified, one deleted
    CreateCreate  // Both created same ID (ID collision)
    AutoMergeable // Different fields modified
)

type FieldConflict struct {
    Field       string
    BaseValue   interface{}
    LocalValue  interface{}
    RemoteValue interface{}
}

type MergeResult struct {
    Type      ConflictType
    Merged    *types.Issue
    Conflicts []FieldConflict
}

func ThreeWayMerge(base, local, remote *types.Issue) (*MergeResult, error) {
    result := &MergeResult{
        Type:   NoConflict,
        Merged: &types.Issue{},
    }

    // Copy base as starting point
    *result.Merged = *base

    // Check each field
    conflicts := []FieldConflict{}

    // Title
    if local.Title != base.Title && remote.Title != base.Title {
        if local.Title != remote.Title {
            conflicts = append(conflicts, FieldConflict{
                Field:       "title",
                BaseValue:   base.Title,
                LocalValue:  local.Title,
                RemoteValue: remote.Title,
            })
        } else {
            result.Merged.Title = local.Title  // Same change
        }
    } else if local.Title != base.Title {
        result.Merged.Title = local.Title  // Only local changed
    } else if remote.Title != base.Title {
        result.Merged.Title = remote.Title  // Only remote changed
    }

    // Description
    if local.Description != base.Description && remote.Description != base.Description {
        if local.Description != remote.Description {
            // Try smart merge for text fields
            merged, conflict := mergeText(base.Description, local.Description, remote.Description)
            if conflict {
                conflicts = append(conflicts, FieldConflict{
                    Field:       "description",
                    BaseValue:   base.Description,
                    LocalValue:  local.Description,
                    RemoteValue: remote.Description,
                })
            } else {
                result.Merged.Description = merged
            }
        }
    } else if local.Description != base.Description {
        result.Merged.Description = local.Description
    } else if remote.Description != base.Description {
        result.Merged.Description = remote.Description
    }

    // Status
    if local.Status != base.Status && remote.Status != base.Status {
        if local.Status != remote.Status {
            conflicts = append(conflicts, FieldConflict{
                Field:       "status",
                BaseValue:   base.Status,
                LocalValue:  local.Status,
                RemoteValue: remote.Status,
            })
        }
    } else if local.Status != base.Status {
        result.Merged.Status = local.Status
    } else if remote.Status != base.Status {
        result.Merged.Status = remote.Status
    }

    // Priority (numeric: take higher)
    if local.Priority != base.Priority && remote.Priority != base.Priority {
        result.Merged.Priority = min(local.Priority, remote.Priority)  // Lower number = higher priority
    } else if local.Priority != base.Priority {
        result.Merged.Priority = local.Priority
    } else if remote.Priority != base.Priority {
        result.Merged.Priority = remote.Priority
    }

    // Set conflict type
    if len(conflicts) > 0 {
        result.Type = ModifyModify
        result.Conflicts = conflicts
    } else if hasChanges(base, result.Merged) {
        result.Type = AutoMergeable
    }

    return result, nil
}
```

### 3. Smart Text Merging

```go
// mergeText attempts to merge text changes using line-based diff
func mergeText(base, local, remote string) (string, bool) {
    // If one side didn't change, use the other
    if local == base {
        return remote, false
    }
    if remote == base {
        return local, false
    }

    // Both changed - try line-based merge
    baseLines := strings.Split(base, "\n")
    localLines := strings.Split(local, "\n")
    remoteLines := strings.Split(remote, "\n")

    // Simple merge: if changes are in different lines, combine them
    merged, conflict := mergeLines(baseLines, localLines, remoteLines)
    return strings.Join(merged, "\n"), conflict
}

func mergeLines(base, local, remote []string) ([]string, bool) {
    // Use Myers diff algorithm or simple LCS
    // For MVP, use simple strategy:
    // - If local added lines, keep them
    // - If remote added lines, keep them
    // - If both modified same line, conflict

    // This is a simplified implementation
    // Production would use a proper diff library

    if reflect.DeepEqual(local, remote) {
        return local, false  // Same changes
    }

    // Different changes - conflict
    return local, true
}
```

### 4. Conflict Resolution UI

**File**: cmd/bd/import_core.go

```go
func handleMergeConflict(result *merge.MergeResult) (*types.Issue, error) {
    fmt.Fprintf(os.Stderr, "\n=== MERGE CONFLICT ===\n")
    fmt.Fprintf(os.Stderr, "Issue: %s\n", result.Merged.ID)
    fmt.Fprintf(os.Stderr, "Conflicts in %d field(s):\n\n", len(result.Conflicts))

    for _, conflict := range result.Conflicts {
        fmt.Fprintf(os.Stderr, "Field: %s\n", conflict.Field)
        fmt.Fprintf(os.Stderr, "  Base:   %v\n", conflict.BaseValue)
        fmt.Fprintf(os.Stderr, "  Local:  %v\n", conflict.LocalValue)
        fmt.Fprintf(os.Stderr, "  Remote: %v\n", conflict.RemoteValue)

        fmt.Fprintf(os.Stderr, "\nChoose resolution:\n")
        fmt.Fprintf(os.Stderr, "  1) Use local\n")
        fmt.Fprintf(os.Stderr, "  2) Use remote\n")
        fmt.Fprintf(os.Stderr, "  3) Edit manually\n")
        fmt.Fprintf(os.Stderr, "Choice: ")

        var choice int
        fmt.Scanln(&choice)

        switch choice {
        case 1:
            setField(result.Merged, conflict.Field, conflict.LocalValue)
        case 2:
            setField(result.Merged, conflict.Field, conflict.RemoteValue)
        case 3:
            // Open editor with conflict markers
            edited := editWithConflictMarkers(conflict)
            setField(result.Merged, conflict.Field, edited)
        }
    }

    return result.Merged, nil
}
```

### 5. Auto-Merge Strategy

For non-interactive mode (daemon):

```go
func autoResolveConflict(result *merge.MergeResult, strategy string) *types.Issue {
    switch strategy {
    case "local-wins":
        for _, c := range result.Conflicts {
            setField(result.Merged, c.Field, c.LocalValue)
        }

    case "remote-wins":
        for _, c := range result.Conflicts {
            setField(result.Merged, c.Field, c.RemoteValue)
        }

    case "newest-wins":
        // Use ModifiedAt timestamp
        for _, c := range result.Conflicts {
            if result.Merged.ModifiedAt.After(remoteModifiedAt) {
                setField(result.Merged, c.Field, c.LocalValue)
            } else {
                setField(result.Merged, c.Field, c.RemoteValue)
            }
        }
    }

    return result.Merged
}
```

## Integration with Import

**File**: cmd/bd/import_core.go

```go
func importIssuesCore(ctx context.Context, dbPath string, store storage.Storage, issues []*types.Issue, opts ImportOptions) (*ImportResult, error) {
    // ...

    for _, remoteIssue := range issues {
        localIssue, err := store.GetIssue(ctx, remoteIssue.ID)

        if err == nil {
            // Issue exists - check for conflict
            if localIssue.Version != remoteIssue.Version {
                // Get merge base
                base, err := merge.FindMergeBase(store, remoteIssue.ID,
                    localIssue.Version, remoteIssue.Version)

                if err != nil {
                    // No merge base - use LWW
                    if localIssue.ModifiedAt.After(remoteIssue.ModifiedAt) {
                        continue  // Keep local
                    } else {
                        store.UpdateIssue(ctx, remoteIssue)  // Use remote
                    }
                } else {
                    // Three-way merge
                    result, err := merge.ThreeWayMerge(base, localIssue, remoteIssue)
                    if err != nil {
                        return nil, err
                    }

                    if result.Type == merge.AutoMergeable {
                        // Clean merge
                        store.UpdateIssue(ctx, result.Merged)
                        result.Updated++
                    } else if result.Type == merge.ModifyModify {
                        // Conflict - need resolution
                        if opts.Interactive {
                            merged, _ := handleMergeConflict(result)
                            store.UpdateIssue(ctx, merged)
                        } else {
                            // Auto-resolve
                            merged := autoResolveConflict(result, "newest-wins")
                            store.UpdateIssue(ctx, merged)
                            result.Conflicts++
                        }
                    }
                }
            }
        }
    }

    return result, nil
}
```

## Testing

```bash
# Test auto-merge (different fields)
cd repo-a && bd update bd-1 --title "New title"
cd repo-b && bd update bd-1 --description "New desc"
cd repo-a && bd sync && cd ../repo-b && bd sync
# Expected: Both changes merged

# Test conflict (same field)
cd repo-a && bd update bd-2 --title "A's title"
cd repo-b && bd update bd-2 --title "B's title"
cd repo-a && bd sync && cd ../repo-b && bd sync
# Expected: Conflict detected, newest wins
```

## Success Criteria

- Different field changes auto-merge successfully
- Same field conflicts detected
- Interactive resolution for manual sync
- Auto-resolution for daemon (newest-wins)
- All merges preserve version history

## Estimated Effort

6-8 hours

## Priority

P1 - Enables intelligent conflict resolution


Multi-clone integration tests and stress testing

## Problem

Multi-clone sync is complex with many race conditions and edge cases. Need comprehensive integration tests to validate all scenarios.

## Test Scenarios

### 1. Basic Two-Clone Sync

**File**: test/integration/multi_clone_test.go

```go
func TestBasicTwoCloneSync(t *testing.T) {
    // Setup
    repo1 := setupTestRepo(t, "repo1")
    defer cleanup(repo1)

    // Create initial issue
    bd(repo1, "create", "Issue A")
    bd(repo1, "sync")

    // Clone to repo2
    repo2 := cloneRepo(t, repo1, "repo2")
    defer cleanup(repo2)

    // Verify issue synced
    issues := listIssues(repo2)
    assert.Len(t, issues, 1)
    assert.Equal(t, "Issue A", issues[0].Title)

    // Make change in repo1
    bd(repo1, "create", "Issue B")
    bd(repo1, "sync")

    // Pull in repo2
    bd(repo2, "sync")

    // Verify both issues present
    issues = listIssues(repo2)
    assert.Len(t, issues, 2)
}
```

### 2. Concurrent Non-Overlapping Changes

```go
func TestConcurrentNonOverlappingChanges(t *testing.T) {
    repo1, repo2 := setupTwoClones(t)

    // Repo1: Create bd-1
    bd(repo1, "create", "Issue 1")

    // Repo2: Create bd-2
    bd(repo2, "create", "Issue 2")

    // Both sync
    bd(repo1, "sync")
    bd(repo2, "sync")
    bd(repo1, "sync")  // Pull repo2's changes

    // Verify both repos have both issues
    assert.Len(t, listIssues(repo1), 2)
    assert.Len(t, listIssues(repo2), 2)
}
```

### 3. Concurrent Modification Same Issue (Different Fields)

```go
func TestConcurrentModificationDifferentFields(t *testing.T) {
    repo1, repo2 := setupTwoClones(t)

    // Create initial issue
    bd(repo1, "create", "Issue 1", "--description", "Base description")
    bd(repo1, "sync")
    bd(repo2, "sync")

    // Repo1: Update title
    bd(repo1, "update", "bd-1", "--title", "New title")

    // Repo2: Update description
    bd(repo2, "update", "bd-1", "--description", "New description")

    // Both sync
    bd(repo1, "sync")
    bd(repo2, "sync")
    bd(repo1, "sync")

    // Verify auto-merge happened
    issue1 := getIssue(repo1, "bd-1")
    issue2 := getIssue(repo2, "bd-1")

    assert.Equal(t, "New title", issue1.Title)
    assert.Equal(t, "New description", issue1.Description)
    assert.Equal(t, issue1, issue2)  // Both repos converged
}
```

### 4. Concurrent Modification Same Field (Conflict)

```go
func TestConcurrentModificationSameField(t *testing.T) {
    repo1, repo2 := setupTwoClones(t)

    // Create initial issue
    bd(repo1, "create", "Issue 1")
    bd(repo1, "sync")
    bd(repo2, "sync")

    // Both update title
    bd(repo1, "update", "bd-1", "--title", "Repo1 title")
    bd(repo2, "update", "bd-1", "--title", "Repo2 title")

    // Sync
    bd(repo1, "sync")
    bd(repo2, "sync")
    bd(repo1, "sync")

    // Verify conflict resolved (newest wins)
    issue1 := getIssue(repo1, "bd-1")
    issue2 := getIssue(repo2, "bd-1")
    assert.Equal(t, issue1.Title, issue2.Title)  // Converged to same value
}
```

### 5. Create-Create Collision

```go
func TestCreateCreateCollision(t *testing.T) {
    repo1, repo2 := setupTwoClones(t)

    // Both create with same ID
    bd(repo1, "create", "Issue from repo1", "--id", "bd-100")
    bd(repo2, "create", "Issue from repo2", "--id", "bd-100")

    // Sync with collision resolution
    bd(repo1, "sync")
    bd(repo2, "sync", "--rename-on-import")
    bd(repo1, "sync")

    // Verify one was remapped
    issues := listIssues(repo1)
    assert.Len(t, issues, 2)

    ids := []string{issues[0].ID, issues[1].ID}
    assert.Contains(t, ids, "bd-100")
    assert.Contains(t, ids, "bd-101")  // One remapped
}
```

### 6. Delete-Modify Conflict

```go
func TestDeleteModifyConflict(t *testing.T) {
    repo1, repo2 := setupTwoClones(t)

    // Create issue
    bd(repo1, "create", "Issue 1")
    bd(repo1, "sync")
    bd(repo2, "sync")

    // Repo1: Delete
    bd(repo1, "delete", "bd-1")

    // Repo2: Modify
    bd(repo2, "update", "bd-1", "--title", "Modified")

    // Sync
    bd(repo1, "sync")
    bd(repo2, "sync")
    bd(repo1, "sync")

    // Verify resolution (modification wins over deletion)
    issues1 := listIssues(repo1)
    issues2 := listIssues(repo2)
    assert.Len(t, issues1, 1)  // Deletion reverted
    assert.Len(t, issues2, 1)
}
```

### 7. Three-Clone Convergence

```go
func TestThreeCloneConvergence(t *testing.T) {
    repo1, repo2, repo3 := setupThreeClones(t)

    // Each creates an issue
    bd(repo1, "create", "Issue 1")
    bd(repo2, "create", "Issue 2")
    bd(repo3, "create", "Issue 3")

    // Sync all (multiple rounds)
    for i := 0; i < 3; i++ {
        bd(repo1, "sync")
        bd(repo2, "sync")
        bd(repo3, "sync")
    }

    // Verify all have all issues
    assert.Len(t, listIssues(repo1), 3)
    assert.Len(t, listIssues(repo2), 3)
    assert.Len(t, listIssues(repo3), 3)
}
```

### 8. Daemon Auto-Sync Test

```go
func TestDaemonAutoSync(t *testing.T) {
    repo1, repo2 := setupTwoClones(t)

    // Start daemons
    daemon1 := startDaemon(repo1, "--auto-commit", "--auto-push", "--interval", "2s")
    defer daemon1.Stop()

    daemon2 := startDaemon(repo2, "--auto-commit", "--auto-push", "--interval", "2s")
    defer daemon2.Stop()

    // Create issue in repo1
    bd(repo1, "create", "Issue 1")

    // Wait for sync
    time.Sleep(10 * time.Second)

    // Verify appeared in repo2
    issues := listIssues(repo2)
    assert.Len(t, issues, 1)
    assert.Equal(t, "Issue 1", issues[0].Title)
}
```

### 9. Database Divergence Recovery

```go
func TestDatabaseDivergenceRecovery(t *testing.T) {
    repo1, repo2 := setupTwoClones(t)

    // Create divergence (simulate the bd-160 bug)
    for i := 0; i < 10; i++ {
        bd(repo1, "create", fmt.Sprintf("Issue %d", i))
    }

    // Force divergence (don't sync)
    for i := 0; i < 10; i++ {
        bd(repo2, "create", fmt.Sprintf("Other %d", i))
    }

    // Now sync and verify recovery
    bd(repo1, "sync")
    bd(repo2, "sync")
    bd(repo1, "sync")

    // Should converge
    count1 := len(listIssues(repo1))
    count2 := len(listIssues(repo2))
    assert.Equal(t, count1, count2)
    assert.Equal(t, 20, count1)
}
```

### 10. Timestamp-Only Changes Don't Export

```go
func TestTimestampOnlyNoExport(t *testing.T) {
    repo := setupTestRepo(t)

    // Create issue
    bd(repo, "create", "Issue 1")
    bd(repo, "sync")

    // Get commit count
    commits1 := gitCommitCount(repo)

    // Touch database (simulate timestamp update)
    touchDatabase(repo)

    // Wait for daemon cycle
    time.Sleep(10 * time.Second)

    // Verify no new commit
    commits2 := gitCommitCount(repo)
    assert.Equal(t, commits1, commits2)
}
```

## Test Infrastructure

**File**: test/integration/helpers.go

```go
func setupTestRepo(t *testing.T, name string) string {
    dir := t.TempDir()
    run(dir, "git", "init")
    run(dir, "bd", "init")
    return dir
}

func cloneRepo(t *testing.T, source, name string) string {
    dir := t.TempDir()
    run(dir, "git", "clone", source, name)
    return filepath.Join(dir, name)
}

func bd(repo string, args ...string) {
    cmd := exec.Command("bd", args...)
    cmd.Dir = repo
    output, err := cmd.CombinedOutput()
    if err != nil {
        panic(fmt.Sprintf("bd command failed: %v\n%s", err, output))
    }
}

func listIssues(repo string) []*types.Issue {
    cmd := exec.Command("bd", "list", "--json")
    cmd.Dir = repo
    output, _ := cmd.Output()

    var issues []*types.Issue
    json.Unmarshal(output, &issues)
    return issues
}
```

## CI Integration

**File**: .github/workflows/multi-clone-tests.yml

```yaml
name: Multi-Clone Integration Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'

      - name: Run multi-clone tests
        run: |
          go test -v ./test/integration/... -tags=integration

      - name: Run stress tests
        run: |
          go test -v ./test/stress/... -race -timeout=30m
```

## Success Criteria

- All 10 test scenarios pass
- Tests run in CI on every commit
- Zero flaky tests
- Coverage >80% for sync-related code
- Stress tests validate 100+ concurrent operations

## Estimated Effort

8-10 hours

## Priority

P1 - Required to validate bd-160 fixes


Update EXTENDING.md with UnderlyingDB() usage and best practices

EXTENDING.md currently shows how to use direct sql.Open() to access the database, but doesn't mention the new UnderlyingDB() method that's the recommended way for extensions.

**Update needed:**
1. Add section showing UnderlyingDB() usage:
   ```go
   store, err := beads.NewSQLiteStorage(dbPath)
   db := store.UnderlyingDB()
   // Create extension tables using db
   ```

2. Document when to use UnderlyingDB() vs direct sql.Open():
   - Use UnderlyingDB() when you want to share the storage connection
   - Use sql.Open() when you need independent connection management

3. Add safety warnings (cross-reference from UnderlyingDB() docs):
   - Don't close the DB
   - Don't modify pool settings
   - Keep transactions short

4. Update the VC example to show UnderlyingDB() pattern

5. Explain beads.Storage.UnderlyingDB() in the API section


Documentation for multi-clone workflows and conflict resolution

## Problem

Multi-clone sync is now fixed (bd-160), but users need documentation on:
- How to set up multi-clone workflows
- What to do when conflicts occur
- Best practices for team collaboration
- Recovery procedures for divergence

## Documentation Sections

### 1. Multi-Clone Setup Guide

**File**: docs/multi-clone-setup.md

```markdown
# Multi-Clone Workflow Setup

## Prerequisites

- Git repository with remote configured
- Beads v0.x.x or later (includes bd-160 fixes)

## Setup Steps

### 1. Initialize First Clone

```bash
cd ~/projects/myapp
git init
bd init
bd create "First issue"
bd sync
git remote add origin git@github.com:user/myapp.git
git push -u origin main
```

### 2. Clone to Another Location

```bash
cd ~/work/myapp
git clone git@github.com:user/myapp.git
cd myapp
bd init  # Initializes database from JSONL
```

### 3. Start Daemons (Optional)

For automatic sync:

```bash
# In first clone
cd ~/projects/myapp
bd daemon --auto-commit --auto-push

# In second clone
cd ~/work/myapp
bd daemon --auto-commit --auto-push
```

## Verification

Check both clones have same issues:

```bash
cd ~/projects/myapp && bd list
cd ~/work/myapp && bd list
# Should show identical output
```

## Troubleshooting

If clones diverge:
1. Stop daemons: `bd daemon --stop`
2. Force sync: `bd sync --force`
3. Check diff: `git diff origin/main .beads/beads.jsonl`
```

### 2. Conflict Resolution Guide

**File**: docs/conflict-resolution.md

```markdown
# Handling Sync Conflicts

## Types of Conflicts

### Auto-Mergeable (No Action Needed)

Different fields modified:
```
Clone A: Update title
Clone B: Update description
Result: Both changes merged automatically
```

### Same-Field Conflict (Newest Wins)

Both modify same field:
```
Clone A: Update title to "A" at 14:00
Clone B: Update title to "B" at 14:05
Result: "B" wins (newer timestamp)
```

### Interactive Conflict

When using manual sync (`bd sync`):
```
=== MERGE CONFLICT ===
Issue: bd-42
Field: title
  Local:  "Fix the bug"
  Remote: "Resolve the issue"

Choose resolution:
  1) Use local
  2) Use remote
  3) Edit manually
Choice: _
```

## Manual Resolution

1. Stop daemon: `bd daemon --stop`
2. Sync with interactive mode: `bd sync`
3. Resolve conflicts as prompted
4. Restart daemon: `bd daemon --auto-commit --auto-push`

## Viewing Conflict History

```bash
# Show version history
bd show bd-42 --versions

# Show who modified
bd show bd-42 --json | jq '.modified_by, .version'
```
```

### 3. Best Practices

**File**: docs/best-practices.md

```markdown
# Multi-Clone Best Practices

## Do's

✅ **Always sync before starting work**
```bash
bd sync  # Pull latest changes
```

✅ **Use descriptive commit messages**
```bash
bd sync -m "Close bd-123: Fix memory leak"
```

✅ **Enable daemon for automatic sync**
```bash
bd daemon --auto-commit --auto-push --interval 30s
```

✅ **Use atomic operations**
```bash
# Good: One logical change
bd update bd-42 --title "New title" --description "New desc"

# Avoid: Multiple separate updates
bd update bd-42 --title "New title"
bd update bd-42 --description "New desc"  # Creates conflict window
```

## Don'ts

❌ **Don't copy .beads directories between repos**
```bash
# WRONG - causes database mismatch errors
cp -r ~/project1/.beads ~/project2/
```

❌ **Don't manually edit JSONL files**
```bash
# WRONG - use bd commands instead
vim .beads/beads.jsonl
```

❌ **Don't use different database names**
```bash
# WRONG - all clones must use beads.db
mv .beads/beads.db .beads/custom.db
```

❌ **Don't force push**
```bash
# WRONG - can lose other clones' changes
git push --force
```

## Team Workflows

### Option 1: Daemon Auto-Sync (Recommended)

Everyone runs daemon with auto-commit/auto-push. Changes sync automatically within seconds.

**Pros**: Zero manual intervention, always in sync
**Cons**: Requires stable internet, more git noise

### Option 2: Manual Sync

Everyone runs `bd sync` before/after work sessions.

**Pros**: Fewer git commits, works offline
**Cons**: Must remember to sync, conflicts more likely

### Option 3: Hybrid

Daemon without auto-push, manual push at milestones.

**Pros**: Auto-import, controlled pushes
**Cons**: Requires both daemon and manual sync
```

### 4. Recovery Procedures

**File**: docs/recovery.md

```markdown
# Recovery from Sync Issues

## Scenario 1: Database Diverged (Different Issue Counts)

```bash
# Clone A: 100 issues
# Clone B: 95 issues

# Recovery:
cd clone-a
bd export -o /tmp/clone-a.jsonl

cd clone-b
bd export -o /tmp/clone-b.jsonl

# Compare
diff <(jq -r .id /tmp/clone-a.jsonl | sort) \
     <(jq -r .id /tmp/clone-b.jsonl | sort)

# Import missing issues
cd clone-b
bd import -i /tmp/clone-a.jsonl --resolve-collisions
bd sync
```

## Scenario 2: Database Corruption

```bash
# Rebuild from JSONL
rm .beads/beads.db
bd init  # Recreates database from JSONL
bd list  # Verify issues restored
```

## Scenario 3: Accidental Force Push

```bash
# Restore from reflog
git reflog
git reset --hard HEAD@{1}
bd import  # Re-import from restored JSONL
```

## Scenario 4: Merge Conflict in JSONL

```bash
# After git pull conflict
git status  # Shows .beads/beads.jsonl as conflicted

# Resolve by importing both sides
git show :2:.beads/beads.jsonl > /tmp/ours.jsonl
git show :3:.beads/beads.jsonl > /tmp/theirs.jsonl

# Import both (collision resolution will merge)
bd import -i /tmp/ours.jsonl --resolve-collisions
bd import -i /tmp/theirs.jsonl --resolve-collisions

# Export merged state
bd export -o .beads/beads.jsonl
git add .beads/beads.jsonl
git commit -m "Resolve JSONL merge conflict"
```
```

### 5. Architecture Documentation

**File**: docs/architecture/sync.md

```markdown
# Sync Architecture

## Components

```
┌─────────────┐
│  Git JSONL  │  ← Source of truth
└─────────────┘
       ↕
┌─────────────┐
│  bd sync    │  ← Orchestrator
└─────────────┘
       ↕
┌─────────────┐
│  Database   │  ← Local cache
└─────────────┘
       ↕
┌─────────────┐
│  Daemon     │  ← Auto-sync (optional)
└─────────────┘
```

## Sync Flow

1. **Export**: DB → JSONL
2. **Commit**: JSONL → Git
3. **Pull**: Git remote → Local
4. **Import**: JSONL → DB (with conflict resolution)
5. **Push**: Local Git → Remote

## Conflict Resolution

Uses three-way merge:
- **Base**: Last common version (from issue_versions table)
- **Local**: Current database state
- **Remote**: Incoming JSONL state

Auto-merge strategy:
- Different fields changed → Merge both
- Same field changed → Newest wins (by modified_at)
- Unresolvable → Interactive prompt (manual sync only)

## Version Tracking

Each issue has:
- `version`: Incremented on each update
- `modified_by`: Clone ID that made change
- `modified_at`: Timestamp of change

Enables:
- Conflict detection
- History tracking
- LWW resolution
```

## Update Existing Docs

**Files to update**:
- README.md - Add multi-clone section
- docs/commands.md - Document sync flags
- docs/daemon.md - Update with conflict resolution
- TROUBLESHOOTING.md - Add sync issues section

## Examples Repository

**File**: examples/multi-clone-demo.sh

```bash
#!/bin/bash
# Multi-clone demonstration script

set -e

echo "=== Multi-Clone Sync Demo ==="

# Setup
mkdir -p /tmp/bd-demo && cd /tmp/bd-demo
rm -rf clone1 clone2

# Create first clone
mkdir clone1 && cd clone1
git init
bd init
bd create "First issue"
bd sync
cd ..

# Create second clone
git clone clone1 clone2
cd clone2
bd init
bd list  # Shows first issue

# Make concurrent changes
cd ../clone1
bd create "Issue from clone1"
bd sync

cd ../clone2
bd create "Issue from clone2"
bd sync

# Sync both
cd ../clone1
bd sync

# Verify convergence
echo "Clone 1 issues:"
bd list

cd ../clone2
echo "Clone 2 issues:"
bd list

echo "✓ Demo complete - both clones have 3 issues"
```

## Success Criteria

- All documentation sections complete
- Examples tested and working
- Troubleshooting covers common scenarios
- Clear diagrams for sync flow
- Published to docs site

## Estimated Effort

4-6 hours

## Priority

P1 - Critical for user adoption of multi-clone workflows


Consider adding UnderlyingConn(ctx) for safer scoped DB access

Currently UnderlyingDB() returns *sql.DB which is correct for most uses, but for extension migrations/DDL, a scoped connection might be safer.

**Proposal:** Add optional UnderlyingConn(ctx) (*sql.Conn, error) method that:
- Returns a scoped connection via s.db.Conn(ctx)
- Encourages lifetime-bounded usage
- Reduces temptation to tune global pool settings
- Better for one-time DDL operations like CREATE TABLE

**Implementation:**
```go
// UnderlyingConn returns a single connection from the pool for scoped use
// Useful for migrations and DDL. Close the connection when done.
func (s *SQLiteStorage) UnderlyingConn(ctx context.Context) (*sql.Conn, error) {
    return s.db.Conn(ctx)
}
```

**Benefits:**
- Safer for migrations (explicit scope)
- Complements UnderlyingDB() for different use cases
- Low implementation cost

**Trade-off:** Adds another method to maintain, but Oracle considers this balanced compromise between safety and flexibility.

**Decision:** This is optional - evaluate based on VC's actual usage patterns.


MCP close tool method signature error - takes 1 positional argument but 2 were given

The close approval routing fix in beads-mcp v0.11.0 works correctly and successfully routes update(status="closed") calls to close() tool. However, the close() tool has a Python method signature bug that prevents execution.

Impact: All MCP-based close operations are broken. Workaround: Use bd CLI directly.

Error: BdDaemonClient.close() takes 1 positional argument but 2 were given

Root cause: BdDaemonClient.close() only accepts self, but MCP tool passes issue_id and reason.

Additional issue: CLI close has FOREIGN KEY constraint error when recording reason parameter.

See GitHub issue #107 for full details.


Improve error handling in dependency removal during remapping

In updateDependencyReferences(), RemoveDependency errors are caught and ignored with continue (line 392). Comment says 'if dependency doesn't exist' but this catches ALL errors including real failures. Should check error type with errors.Is(err, ErrDependencyNotFound) and only ignore not-found errors, returning other errors properly.


Fix pre-existing MCP test failures - show/update return arrays not dicts

9 tests fail in beads-mcp because bd CLI commands return arrays but MCP client expects dicts:

Failing tests:
- test_create_and_show_issue: show returns array, expects dict
- test_update_issue: update returns array, expects dict
- test_add_dependency: show returns array, expects dict
- test_invalid_issue_id: show returns empty dict instead of error
- test_dependency_types: show returns array, expects dict
- test_show_issue_tool: show returns array, expects dict
- test_update_issue_tool: update returns array, expects dict
- test_update_partial_fields: update returns array, expects dict
- test_client_lazy_initialization: BdClient import issue

Root cause: bd CLI commands like 'bd show' and 'bd update' output JSON arrays, but BdCliClient.show() and BdCliClient.update() expect single dict objects.

Example:
```bash
bd show test-1 --json
[{"id":"test-1",...}]  # Array, not dict
```

Fix needed: Update bd_client.py to handle array responses and extract first element, or change CLI to return single object for single-ID operations.


Fix bd sync prefix mismatch error message suggesting non-existent flag

GH #103: bd sync suggests using --rename-on-import flag that doesn't exist. Need to either implement the flag or fix the error message to suggest the correct workflow.


Fix MCP close tool method signature error

GH #107: MCP close() tool fails with "BdDaemonClient.close() takes 1 positional argument but 2 were given". Need to fix method signature in beads-mcp server.


Update Claude Code marketplace plugin

Update the beads plugin in the Claude Code marketplace to the latest version. This may help resolve some of the open GitHub issues related to marketplace installation and compatibility (#54, #112).

Should include:
- Latest beads version
- Updated documentation
- Any new features or bug fixes


Git worktree support - ensure --no-daemon works correctly

Users working with git worktrees report that beads MCP commits to the wrong branch (main instead of worktree branch).

**Root cause:** Git worktrees share the same .beads directory, so the daemon/MCP server doesn't know which branch the worktree has checked out.

**Current state:**
- Daemon mode: Cannot work properly with worktrees (fundamental limitation - shared .beads, unknown branch)
- CLI with --no-daemon: Should work but needs verification

**Action items:**
1. Test and verify --no-daemon works correctly in worktrees
2. Document the limitation in README/AGENTS.md
3. Add clear error/warning when using daemon with worktrees
4. Consider if MCP server can detect worktree usage and auto-disable daemon

**Workaround for users:**
Use --no-daemon flag when working in worktrees, or set BEADS_AUTO_DAEMON=false

**Related:** GH issue #55

**Implementation completed with oracle review improvements:**

1. **Robust detection** - Changed from substring check to canonical git-dir vs git-common-dir comparison
2. **Correct warning gate** - Now only checks isGitWorktree() and only called when daemon is actually connected
3. **Complete coverage** - Added warning to `bd daemon` command start path
4. **Better UX** - Shows shared database path and clarifies BEADS_AUTO_START_DAEMON behavior
5. **Improved tests** - Validates canonical detection method and path truncation

**Files changed:**
- cmd/bd/worktree.go - Improved detection and warning
- cmd/bd/worktree_test.go - Better test coverage
- cmd/bd/main.go - Warning after daemon connection (2 places)
- cmd/bd/daemon.go - Warning when starting daemon
- README.md & AGENTS.md - Documented limitations and solutions


Investigate jujutsu integration for beads

Research and document how beads could integrate with jujutsu (jj), the next-generation VCS. Key areas to explore:
- How jj's operation model differs from git (immutable operations, working-copy-as-commit)
- JSONL sync strategy with jj's conflict resolution model
- Daemon compatibility with jj's more frequent rewrites
- Whether auto-import/export needs changes for jj workflows
- Example configurations and documentation updates needed


Add description parameter to bd update command and MCP server

Issue #114 and #122 show that agents expect to update issue descriptions. Currently only create supports description. Need to add --description flag to CLI update command and description parameter to MCP update_issue tool.


Add integration tests for worktree workflow with separate databases

Created test_worktree_separate_dbs.py that verifies the recommended workflow: one beads database per worktree with daemon-less MCP mode (BEADS_USE_DAEMON=0). Tests confirm isolation, git syncing, MCP operations, and --no-daemon flag.


Migrate to Viper for unified configuration management

Consolidate all config, flags, and environment variables into a singleton using spf13/viper. Benefits: unified config precedence, auto-env binding, live reloading, better integration with Cobra. Current state: manual env var handling, global flags, and new bd config command (GH #124).

Hybrid architecture:
- Viper: Tool-level user preferences (global defaults for --json, --no-daemon, etc) from ~/.config/bd/config.yaml, env vars, flags
- bd config: Project-level integration data (jira.url, linear.token) in database - version-controlled, team-shared

This separation is correct: tool settings are user-specific, project config is team-shared. Agents benefit from bd config's structured interface vs manual YAML editing.


Add viper dependency and initialize singleton

go get github.com/spf13/viper, create viper instance in main, set up config file paths (.beads/config.yaml, ~/.config/bd/config.yaml)


Make maxDepth configurable in bd dep tree command

Currently maxDepth is hardcoded to 50 in GetDependencyTree. Add --max-depth flag to bd dep tree command to allow users to control recursion depth. Default should remain 50 for safety, but users with very deep trees or wanting shallow views should be able to configure it.


Bind all global flags to viper

Migrate --db, --actor, --json, --no-daemon, --no-auto-flush, --no-auto-import to viper with automatic env binding (BD_DB, BD_ACTOR, etc)


Replace manual env var handling with viper

Remove os.Getenv() calls for BD_* variables, use viper.GetString() etc. Update CONFIG.md with viper precedence order


Keep bd config independent from Viper

Update config.go to use viper for get/set/list operations. Consider whether to keep db-stored config or migrate fully to file-based

Keep bd config independent from Viper. Two separate systems:
- Viper: tool-level preferences (global flags, env vars, ~/.config/bd/config.yaml) - user settings like default --json, --no-daemon
- bd config: project-level integration config (jira.url, linear.token) - stored in database, version-controlled, team-shared

Rationale: Agents need structured commands with validation/discoverability. Direct YAML editing is error-prone for agents. This separation is architecturally correct: tool settings vs project data.


Add tests for viper configuration

Test config precedence (defaults < config file < env vars < flags), test config file discovery, test env binding


Auto-import updates all issue timestamps causing perpetually dirty JSONL

**Problem:**
Every bd command ends sessions with dirty .beads/beads.jsonl because auto-import unnecessarily updates `updated_at` timestamps on ALL issues, even when issue data is unchanged.

**Root Cause:**
Auto-import (autoImportIfNewer) imports issues and the import process updates `updated_at` timestamps on every issue, even when the issue data is identical. This causes:
1. Import updates all 83 issues' timestamps (e.g., 2025-10-23T11:01:13.xxx)
2. Export writes JSONL with new timestamps
3. Git shows JSONL as modified with 200+ line changes
4. User commits changes
5. Next command triggers auto-import again
6. Cycle repeats

**Evidence:**
```bash
git diff .beads/beads.jsonl | grep 'updated_at' | wc -l
# Shows 200+ lines changed (every issue touched)

git diff .beads/beads.jsonl | grep 'updated_at' | sed 's/.*updated_at[^:]*:\s*"\([^"]*\)".*/\1/' | sort -u
# All timestamps identical, proving bulk update
```

**Expected Behavior:**
Import should only update `updated_at` when issue data actually changes. Idempotent imports should not modify timestamps.

**Impact:**
- Frustrating UX: "always conclude sessions with dirty beads.jsonl"
- Pollutes git history with timestamp-only changes
- Makes actual changes hard to detect in git diff
- Wastes CI/CD time on false changes

**Related:**
- bd-17 fixed import to preserve timestamps, but doesn't address this issue
- May be related to how UpdateIssue works vs just inserting/replacing


Add GoReleaser workflow for cross-platform binary releases

GitHub issue #89 requests pre-compiled binaries for vendoring.

Currently users must have Go installed to use beads via 'go install'. Publishing release binaries would:
- Enable vendoring (user's use case)
- Support users without Go
- Enable version pinning
- Simplify CI/CD integration

Implementation:
1. Add .goreleaser.yml config
2. Add .github/workflows/release.yml for tag pushes
3. Build matrix: darwin (amd64/arm64), linux (amd64/arm64), windows (amd64)
4. Generate checksums
5. Create GitHub releases automatically
6. Update install.sh to download from releases

Reference: https://github.com/steveyegge/beads/issues/89


Add RPC support for epic commands in daemon mode

Epic status and close-eligible commands currently error out in daemon mode with a message to use --no-daemon. These commands should work with daemon RPC like other commands.

Locations:
- cmd/bd/epic.go:26 (epic status command)
- cmd/bd/epic.go:106 (epic close-eligible command)


bd import reports "0 created, 0 updated" when successfully importing issues

The `bd import` command successfully imported 125 issues but reported "0 created, 0 updated" in the output. The import actually worked, but the success message is incorrect/misleading.

This appears to be a bug in the reporting logic that counts and displays the number of issues created/updated during import.


Auto-detect and kill old daemon versions

When the client version doesn't match the daemon version, we get confusing behavior (auto-flush race conditions, stale data, etc.). The client should automatically detect version mismatches and handle them gracefully.

**Current behavior:**
- `bd version --daemon` shows mismatch but requires manual intervention
- Old daemons keep running after binary upgrades
- MCP server may connect to old daemon
- Results in dirty working tree after commits, stale data

**Proposed solution:**

Key lifecycle points to check/restart daemon:
1. **On first command after version mismatch**: Check daemon version, auto-restart if incompatible
2. **On daemon start**: Check for existing daemons, kill old ones before starting
3. **After brew upgrade/install**: Add post-install hook to kill old daemons
4. **On `bd init`**: Ensure fresh daemon

**Detection logic:**
```go
// PersistentPreRun: check daemon version
if daemonVersion != clientVersion {
    log.Warn("Daemon version mismatch, restarting...")
    killDaemon()
    startDaemon()
}
```

**Considerations:**
- Should we be aggressive (always kill mismatched) or conservative (warn first)?
- What about multiple workspaces with different bd versions?
- Should this be opt-in via config flag?
- How to handle graceful shutdown vs force kill?

**Related issues:**
- Race condition with auto-flush (see bd-38)
- Version mismatch confusion for users
- Stale daemon after upgrades

## Implementation Summary

Implemented automatic daemon version detection and restart in v0.16.0.

### Changes Made

**1. Auto-restart on version mismatch (main.go PersistentPreRun)**
- Check daemon version during health check
- If incompatible, automatically stop old daemon and start new one
- Falls back to direct mode if restart fails
- Transparent to users - no manual intervention needed

**2. Auto-stop old daemon on startup (daemon.go)**
- When starting daemon, check if existing daemon has compatible version
- If versions are incompatible, auto-stop old daemon before starting new one
- Prevents "daemon already running" errors after upgrades

**3. Robust restart implementation**
- Sets correct working directory so daemon finds right database
- Cleans up stale socket files after force kill
- Properly reaps child process to avoid zombies
- Uses waitForSocketReadiness helper for reliable startup detection
- 5-second readiness timeout

### Key Features

- **Automatic**: No user action required after upgrading bd
- **Transparent**: Works with both MCP server and CLI
- **Safe**: Falls back to direct mode if restart fails
- **Tested**: All existing tests pass

### Related
- Addresses race conditions mentioned in bd-39
- Uses semver compatibility checking from internal/rpc/server.go


Race condition between git commit and auto-flush debounce

When using MCP/daemon mode, operations trigger a 5-second debounced auto-flush to JSONL. This creates a race condition with git commits, leaving the working tree dirty.

**Example scenario:**
1. User closes issue via MCP → daemon schedules flush (5 sec delay)
2. User commits code changes → JSONL appears clean
3. Daemon flush fires → JSONL modified after commit
4. Result: dirty working tree showing JSONL changes

**Root cause:**
- Auto-flush uses 5-second debounce to batch changes
- Git commits happen immediately
- No coordination between flush schedule and git operations

**Possible solutions:**

1. **Immediate flush before git operations**
   - Detect git commands (commit, status, push)
   - Force immediate flush if pending
   - Pros: Clean working tree guaranteed
   - Cons: Requires hooking git, may be slow

2. **Commit includes pending flushes**
   - Add `bd sync` to commit workflow
   - Wait for flush to complete before committing
   - Pros: Simple, explicit
   - Cons: Requires user discipline

3. **Git hooks integration**
   - pre-commit hook: `bd sync --wait`
   - Ensures JSONL is up-to-date before commit
   - Pros: Automatic, reliable
   - Cons: Requires hook installation

4. **Reduce debounce delay**
   - Lower from 5s to 1s or 500ms
   - Pros: Faster sync, less likely to race
   - Cons: More frequent I/O, doesn't eliminate race

5. **Lock-based coordination**
   - Daemon holds lock while flush pending
   - Git operations wait for lock
   - Pros: Guarantees ordering
   - Cons: Complex, may block operations

**Recommended approach:**
Combine #2 and #3:
- Add `bd sync` command to explicitly flush
- Provide git hooks in `examples/git-hooks/`
- Document workflow in AGENTS.md
- Keep 5s debounce for normal operations

**Related:**
- bd-38 (daemon version detection)


Add transaction support to storage layer for atomic multi-operation workflows

Currently each storage method (CreateIssue, UpdateIssue, etc.) starts its own transaction. This makes it impossible to perform atomic multi-step operations like collision resolution. Add support for passing *sql.Tx through the storage interface, or create transaction-aware versions of methods. This would make remapCollisions and other batch operations truly atomic.


Clean up linter errors (914 total issues)

The codebase has 914 linter issues reported by golangci-lint. While many are documented as baseline in LINTING.md, we should clean these up systematically to improve code quality and maintainability.

Break down by linter category, prioritizing high-impact issues:
1. dupl (7) - Code duplication
2. goconst (12) - Repeated strings
3. gocyclo (11) - High complexity functions
4. revive (78) - Style issues
5. gosec (102) - Security warnings
6. errcheck (683) - Unchecked errors (many in tests)

All linter categories reduced to acceptable levels, with remaining baseline documented in LINTING.md

Reduced from 56 to 41 issues locally, then to 0 issues.

**Fixed in commits:**
- c2c7eda: Fixed 15 actual errors (dupl, gosec, revive, staticcheck, unparam)
- 963181d: Configured exclusions to get to 0 issues locally

**Current status:**
- ✅ Local: golangci-lint reports 0 issues
- ❌ CI: Still failing (see bd-50)

**Problem:**
Config v2 format or golangci-lint-action@v8 compatibility issue causing CI to fail despite local success.

**Next:** Debug bd-50 to fix CI/local discrepancy


Fix code duplication in label.go (dupl)

Lines 72-120 duplicate lines 122-170 in cmd/bd/label.go. The add and remove commands have nearly identical structure.

Extract common batch operation logic into a shared helper function that takes the operation type as a parameter.


Refactor high complexity functions (gocyclo)

11 functions exceed cyclomatic complexity threshold (>30): runDaemonLoop (42), importIssuesCore (71), TestLabelCommands (67), issueDataChanged (39), etc.

Break down complex functions into smaller, testable units. Extract validation, error handling, and business logic into separate functions.

Refactored issueDataChanged from complexity 39 → 11 by extracting into fieldComparator struct with methods for each comparison type.

Refactored runDaemonLoop from complexity 42 → 7 by extracting:
- setupDaemonLogger: Logger initialization logic
- setupDaemonLock: Lock and PID file management
- startRPCServer: RPC server startup with error handling
- runGlobalDaemon: Global daemon mode handling
- createSyncFunc: Sync cycle logic (export, commit, pull, import, push)
- runEventLoop: Signal handling and main event loop

Code review fixes:
- Fixed sync overlap: Changed initial sync from `go doSync()` to synchronous `doSync()` to prevent race with ticker
- Fixed resource cleanup: Replaced `os.Exit(1)` with `return` after acquiring locks to ensure defers run and clean up PID files/locks
- Added signal.Stop(sigChan) in runEventLoop and runGlobalDaemon to prevent lingering notifications
- Added server.Stop() in serverErrChan case for consistent cleanup

Refactored TestLabelCommands from complexity 67 → <10 by extracting labelTestHelper with methods:
- createIssue: Issue creation helper
- addLabel/addLabels: Label addition helpers
- removeLabel: Label removal helper
- getLabels: Label retrieval helper
- assertLabelCount/assertHasLabel/assertHasLabels/assertNotHasLabel: Assertion helpers
- assertLabelEvent: Event verification helper

Refactored TestReopenCommand from complexity 37 → <10 by extracting reopenTestHelper with methods:
- createIssue: Issue creation helper
- closeIssue/reopenIssue: State transition helpers
- getIssue: Issue retrieval helper
- addComment: Comment addition helper
- assertStatus/assertClosedAtSet/assertClosedAtNil: Status assertion helpers
- assertCommentEvent: Event verification helper

Refactored tryAutoStartDaemon from complexity 34 → <10 by extracting:
- debugLog: Centralized debug logging helper
- isDaemonHealthy: Fast-path health check
- acquireStartLock: Lock acquisition with wait/retry logic
- handleStaleLock: Stale lock detection and retry
- handleExistingSocket: Socket cleanup and validation
- determineSocketMode: Global vs local daemon logic
- startDaemonProcess: Process spawning and readiness wait
- setupDaemonIO: I/O redirection setup

Refactored DeleteIssues from complexity 37 → <10 by extracting:
- buildIDSet: ID deduplication
- resolveDeleteSet: Cascade/force/validation mode routing
- expandWithDependents: Recursive dependent collection
- validateNoDependents: Dependency validation
- checkSingleIssueValidation: Per-issue dependent check
- trackOrphanedIssues: Force-mode orphan tracking
- collectOrphansForID: Per-issue orphan collection
- buildSQLInClause: SQL placeholder generation
- populateDeleteStats: Dry-run statistics collection
- executeDelete: Actual deletion execution

Code review fix (via oracle):
- Added rows.Err() check in checkSingleIssueValidation to catch iterator errors

Refactored TestLibraryIntegration from complexity 32 → <10 by extracting integrationTestHelper with methods:
- createIssue/createFullIssue: Issue creation helpers
- updateIssue/closeIssue: Issue modification helpers
- addDependency/addLabel/addComment: Relationship helpers
- getIssue/getDependencies/getLabels/getComments: Retrieval helpers
- assertID/assertEqual/assertNotNil/assertCount: Assertion helpers

Refactored TestExportImport from complexity 31 → <10 by extracting exportImportHelper with methods:
- createIssue/createFullIssue: Issue creation helpers
- searchIssues/getIssue/updateIssue: Storage operations
- encodeJSONL/validateJSONLines: JSONL encoding and validation
- assertCount/assertEqual/assertSorted: Assertion helpers

Refactored TestListCommand from complexity 31 → <10 by extracting listTestHelper with methods:
- createTestIssues: Batch test data creation
- addLabel: Label addition
- search: Issue search with filters
- assertCount/assertEqual/assertAtMost: Assertion helpers

Refactored TestGetEpicsEligibleForClosure from complexity 32 → <10 by extracting epicTestHelper with methods:
- createEpic/createTask: Issue creation
- addParentChildDependency/closeIssue: Issue relationships
- getEligibleEpics/findEpic: Epic status queries
- assertEpicStats/assertEpicFound/assertEpicNotFound: Epic-specific assertions

Refactored TestCreateIssues from complexity 35 → <10 by extracting createIssuesTestHelper with methods:
- newIssue: Issue construction helper
- createIssues: Batch issue creation
- assertNoError/assertError: Error assertions
- assertCount/assertIDSet/assertTimestampSet: Field assertions
- assertUniqueIDs/assertEqual/assertNotNil: Validation helpers
- assertNoAutoGenID: Error-case validation

All tests pass after refactoring. ✅

**importIssuesCore was already refactored** (complexity 71 → ~10) using phase-based extraction:
- getOrCreateStore, handlePrefixMismatch, handleCollisions
- upsertIssues, importDependencies, importLabels, importComments

**Status:** All 11 high-complexity functions have been refactored to <10 complexity.


Fix revive style issues (78 issues)

Style violations: unused parameters (many cmd/args in cobra commands), missing exported comments, stuttering names (SQLiteStorage), indent-error-flow issues.

Rename unused params to _, add godoc comments to exported types, fix stuttering names, simplify control flow.

Fixed 19 revive issues:
- 14 unused-parameter (renamed to _)
- 2 redefines-builtin-id (max→maxCount, min→minInt)
- 3 indent-error-flow (gofmt fixed 2, skipped 1 complex nested one)

Remaining issues are acceptable: 11 unused-params in deeper code, 2 empty-blocks with comments, 1 complex indent case, 1 superfluous-else in test.


Handle unchecked errors (errcheck - 683 issues)

683 unchecked error returns, mostly in tests (Close, Rollback, RemoveAll). Many already excluded in config but still showing up.

Review .golangci.yml exclude-rules. Most defer Close/Rollback errors in tests can be ignored. Add systematic exclusions or explicit _ = assignments where appropriate.

Fixed all errcheck warnings in production code:
- Enabled errcheck linter (was disabled)
- Set tests: false in .golangci.yml to focus on production code
- Fixed 27 total errors in production code using Oracle guidance:
  * Database patterns: defer func() { _ = rows.Close() }() and defer func() { _ = tx.Rollback() }()
  * Best-effort closers: _ = store.Close(), _ = client.Close()
  * Proper error handling for file writes, fmt.Scanln(), os.Remove()
- All tests pass
- Only 2 "unused" linter warnings remain (not errcheck)


Update LINTING.md with current baseline

After cleanup, document the remaining acceptable baseline in LINTING.md so we can track regression.


Fix Windows CI test failures (5 failing tests)

Windows CI has 5 flaky/failing tests: TestTryDaemonLockDetectsRunning, TestIsDaemonRunning_CurrentProcess (PID detection issues), TestScripts/import, TestMetricsSnapshot/uptime, TestSocketCleanup (socket in use).

Investigate Windows-specific PID/process detection and socket cleanup. These may be race conditions or platform differences in how Windows handles process IDs and file locks.


Investigate GH#144: FOREIGN KEY regression in v0.16.0

v0.16.0 introduced FOREIGN KEY constraint error when closing issues via MCP (daemon mode). CLI works fine.

**Key Finding**: Only change between v0.15.0 and v0.16.0 that could affect FK behavior is:
- modernc.org/sqlite bumped from 1.38.2 to 1.39.1 (commit fbe63bf)

**Evidence**:
- User reports v0.15.0 worked perfectly (Oct 24, 04:35 UTC)
- v0.16.0 fails with 'constraint failed: FOREIGN KEY constraint failed (787)'
- Error occurs in both close() tool and update(status=closed) smart routing
- CLI 'bd close' works fine, only MCP/daemon fails
- No changes to CloseIssue() implementation between versions
- No changes to schema or RPC server handlers

**Next Steps**:
1. Test downgrading sqlite to 1.38.2
2. Check modernc.org/sqlite changelog for FK-related changes
3. Reproduce locally with MCP
4. If sqlite is the culprit, either fix or pin version

**Related**: GH #144


Test FK regression fix


Investigate and upgrade to modernc.org/sqlite 1.39.1+

We had to pin modernc.org/sqlite to v1.38.2 due to a FOREIGN KEY constraint regression in v1.39.1 (SQLite 3.50.4).

**Issue:** bd-47, GH #144

**Symptom:** CloseIssue fails with "FOREIGN KEY constraint failed (787)" when called via MCP/daemon, but works fine via CLI.

**Root Cause:** Unknown - likely stricter FK enforcement in SQLite 3.50.4 or modernc.org wrapper changes.

**Workaround:** Pinned to v1.38.2 (SQLite 3.49.x)

**TODO:**
1. Monitor modernc.org/sqlite releases for fixes
2. Check SQLite 3.50.5+ changelogs for FK-related fixes
3. Investigate why daemon mode fails but CLI succeeds (connection reuse? transaction isolation?)
4. Consider filing upstream issue with reproducible test case
5. Upgrade when safe


Add validation/warning for malformed issue IDs

getNextID silently ignores non-numeric ID suffixes (e.g., bd-foo). CAST returns NULL for invalid strings. Consider detecting and warning about malformed IDs in database. Location: internal/storage/sqlite/sqlite.go:79-82


CI still failing despite local lint passing - config v2 issue

Despite fixing all lint issues locally (golangci-lint reports 0 issues), CI continues to fail for dependabot PRs and test/lint checks.

**Local environment:**
- golangci-lint v2.5.0
- .golangci.yml version: "2" (v2 config format)
- Result: 0 issues ✅

**CI environment:**
- golangci/golangci-lint-action@v8
- golangci-lint version: v2.5.0
- Result: FAILING ❌

**Root cause suspects:**

1. **Config v2 compatibility**: The golangci-lint-action@v8 may not properly support config version "2" format, causing exclusion rules to be ignored

2. **Environment differences**:
   - CI runs on Ubuntu (linux/amd64)
   - Local runs on macOS (darwin/arm64)
   - Pattern matching in exclusions might behave differently

3. **Default linters**: CI might enable different default linters that aren't covered by our exclusions

4. **Config parsing**: The action might not properly read .golangci.yml or apply settings correctly

**Evidence:**
- Commits 963181d and c2c7eda fixed issues locally
- git push succeeded
- But CI still showing failures on dependabot PRs

**Investigation steps:**

1. Access actual CI logs from failing runs to see exact error messages
2. Test exact CI command locally: `golangci-lint run --timeout=5m ./...`
3. Check if .golangci.yml is being loaded by adding debug output
4. Compare default linters between local and CI: `golangci-lint linters`
5. Test config v1 format vs v2 format

**Potential fixes:**

A. **Switch to config v1**: Revert .golangci.yml to version 1 format (older but more stable)

B. **Explicit linter control**: Use `disable-all: true` and only enable specific linters

C. **Update action version**: Try newer golangci-lint-action or pin different golangci-lint version

D. **Inline exclusions**: Move exclusions from YAML to nolint directives in code

E. **CI override**: Add `args: --disable=gocyclo,gosec` to CI workflow

- CI passes lint checks with 0 issues
- Dependabot PRs can merge successfully
- Local and CI environments report same lint results
- Config is maintainable and exclusions work reliably

Fixed: added 'version: "2"' to .golangci.yml and upgraded golangci-lint-action from v6 to v8 with version: latest


Add findByExternalRef query and database index

Implement storage layer support for looking up issues by external_ref field.

## Tasks
- Add findByExternalRef(ctx, externalRef) method to SQLiteStorage
- Add database index: CREATE INDEX IF NOT EXISTS idx_issues_external_ref ON issues(external_ref)
- Handle nil/empty external_ref cases properly
- Add unit tests for the new query method


Modify collision detection to check external_ref first

Update DetectCollisions in collision.go to use external_ref as primary matching key.

## Changes
- In DetectCollisions: before checking issue ID, check if incoming has external_ref
- If external_ref is set, call findByExternalRef to find existing issue
- If found by external_ref, compare content:
  - If matches: add to ExactMatches
  - If differs: treat as UPDATE (not collision)
- Only fall back to ID-based matching if no external_ref
- Add comprehensive tests for all matching scenarios


Update import flow to handle external_ref updates

Modify importIssuesCore in import_shared.go to handle external_ref-based updates.

## Changes
- Phase 4: When processing issues, check collision result for external_ref matches
- Track external_ref updates separately from ID-based updates
- Ensure local issues (no external_ref) are never overwritten by imports
- Handle ID conflicts when external issue ID matches local issue ID (auto-remap)
- Update result reporting to show external_ref updates vs ID updates


Write comprehensive tests for external_ref import scenarios

Create test suite covering all external_ref import behaviors.

## Test Cases
1. Import with external_ref → creates new issue
2. Re-import same external_ref with changes → updates existing
3. Re-import same external_ref unchanged → idempotent (no update)
4. Import external issue with ID collision → auto-remaps external issue
5. Import without external_ref → uses current ID-based logic
6. Mixed batch: some with external_ref, some without
7. External_ref match + ID mismatch → updates by external_ref
8. Local issue (no external_ref) never matched by external import
9. Performance test with external_ref index

Add tests to:
- cmd/bd/import_shared_test.go
- internal/storage/sqlite/collision_test.go


Update documentation for external_ref import behavior

Document the new external_ref-based import matching behavior.

## Files to Update
- README.md: Add external_ref import section
- QUICKSTART.md: Add example of Jira/GitHub integration workflow
- AGENTS.md: Document external_ref import behavior for AI agents
- FAQ.md: Add Q&A about external system integration
- Add examples/ directory entry showing Jira/GitHub/Linear integration

## Key Points to Document
- How external_ref matching works
- That local issues are protected from external imports
- Example workflow: import → add local tasks → re-import updates
- ID conflict resolution behavior


Code review: external_ref import feature

Comprehensive code review of external_ref import implementation.

## Review Checklist
- [ ] Storage layer changes are correct and efficient
- [ ] Index created properly on external_ref column
- [ ] Collision detection logic handles all edge cases
- [ ] Import flow correctly distinguishes external vs local issues
- [ ] No breaking changes to existing workflows
- [ ] Tests cover all scenarios (see bd-54)
- [ ] Documentation is clear and complete (see bd-55)
- [ ] Performance impact is acceptable
- [ ] Error messages are helpful
- [ ] Logging is appropriate for debugging

## Oracle Review
Use oracle tool to analyze implementation for correctness, edge cases, and potential issues.


Warn when multiple beads databases detected in workspace hierarchy

Add detection and warning when multiple .beads directories exist in the workspace hierarchy to prevent confusion and database pollution.

## Problem
Users can accidentally work in the wrong beads database when multiple exist (e.g., ~/src/beads and ~/src/fred/beads), leading to:
- Database pollution from cross-contamination
- Lost work when changes go to wrong database
- Confusion about which database is active

## Solution
On startup, scan parent directories for additional .beads directories and:
- Warn if multiple found
- Show which database is currently active
- Suggest consolidation or cleanup

## Detection Strategy
1. Walk up directory tree from CWD
2. Check for .beads/bd.db in each level
3. If multiple found, show warning with paths
4. Optionally: check common sibling directories

## Example Warning
```
⚠  Multiple beads databases detected:
  - /Users/stevey/src/beads/.beads (ACTIVE - 111 issues)
  - /Users/stevey/src/fred/beads/.beads (202 issues)

Consider consolidating or removing unused databases.
```


Optimize auto-flush to use incremental updates

Every flush exports ALL issues and ALL dependencies, even if only one issue changed. For large projects (1000+ issues), this could be expensive. Current approach guarantees consistency, which is fine for MVP, but future optimization could track which issues changed and use incremental updates. Located in cmd/bd/main.go:255-276.


Fix flaky TestMetricsSnapshot/memory_stats on Linux

Linux CI test TestMetricsSnapshot/memory_stats fails with "Expected non-zero memory allocation". Appears to be a flaky test - metrics_test.go:168.

Add retry logic or wait for GC stats to populate, or adjust test expectations for timing.


Simplify getNextID SQL query parameters

Query passes prefix four times to same SQL query. Works but fragile if query changes. Consider simplifying SQL to require fewer parameters. Location: internal/storage/sqlite/sqlite.go:73-78


Add merged_into field to database schema

Add merged_into field to Issue struct and update database schema to support merge tracking

Simplified: no schema field needed. Close merged issues with reason 'Merged into bd-X'. See bd-29 design.


Update export/import for merge fields

Include merged_into in JSONL export format. Handle merge relationships on import.


Implement text reference scanning and replacement

Scan all issues for text references to merged IDs (bd-X patterns) and update to target ID. Reuse logic from import collision resolution.


Add CLI merge command and flags

Implement bd merge command with: multiple sources, --into target, --dry-run, --json flags. Add interactive confirmation.


Document label best practices and use cases

Create documentation covering:
- When to use labels vs structured fields
- Common label sets (coding agents, open source, product dev, SRE)
- Naming conventions (kebab-case, specificity, present tense)
- Anti-patterns (too many labels, overlapping, personal labels)
- Label lifecycle management

Content from LABELS.md analysis document.


Implement dependency migration for merge

Migrate all dependencies from source issue(s) to target issue during merge, removing duplicates and preserving graph integrity


Fix flaky CI tests in compactor and daemon modules

Multiple test failures observed in CI:
- TestCompactTier1_DryRun
- TestCompactTier1Batch_DryRun
- TestCompactTier1Batch_WithIneligible
- TestMockAPI_CompactTier1
- TestBatchOperations_ErrorHandling
- TestCommentOperationsViaRPC
- TestMemoryPressureDetection
- TestPing

All failures related to 'issue has open dependents or not closed long enough' eligibility checks.

See CI run: https://github.com/steveyegge/beads/actions/runs/18688772658

Fixed race condition in compactor_test.go. The createClosedIssue helper was setting ClosedAt to time.Now(), but the eligibility check uses '<= datetime(now, -0 days)'. Changed to now.Add(-1*time.Second) to ensure issues are always eligible. All tests now passing.


Compact command fails with daemon - requires --no-daemon workaround

The 'bd compact' command fails with 'Error: compact requires SQLite storage' when used with the daemon (default mode), but works correctly with the '--no-daemon' flag.

The daemon RPC interface doesn't properly expose the compact command, even though the daemon itself uses SQLite storage.

Reproduction:
1. Ensure daemon is running (bd daemon status)
2. Run: bd compact --stats
   Result: Error: compact requires SQLite storage
3. Run: bd compact --stats --no-daemon
   Result: Works correctly, shows statistics

Expected behavior:
The compact command should work through the daemon RPC interface just like other commands (list, create, update, delete, renumber, etc.)

Impact:
Users cannot use compact operations in the normal workflow. They must use --no-daemon which bypasses the daemon entirely.

Suggested fix:
Add compact operation support to the daemon RPC interface, similar to how renumber and other operations are exposed.


Investigate stress test database pollution (vc-248)

Investigation of stress tests polluting production database with 1,600+ test issues on Oct 21 at 20:24-20:25. Root cause analysis completed. Tests now verified to work correctly with proper isolation.

Bug confirmed! Tests DO pollute production DB. 1,000 test issues created at 20:46:01-20:46:02 during TestStressNoUniqueConstraintViolations. Root cause: test goroutines connect to production daemon at .beads/bd.sock instead of test daemon.


Consider implementing pre-commit hooks for Storage interface changes

The documentation (INTERFACE_CHANGES.md) suggests adding pre-commit hooks that automatically check for Storage interface changes and verify all mocks are updated. This would prevent similar issues in the future where interface changes break mock implementations.

Discovered during execution of vc-228 (dogfooding run #14/15).

Implement a pre-commit hook that:
1. Detects changes to internal/storage/storage.go
2. Runs scripts/find-storage-mocks.sh to find all mock implementations
3. Attempts to compile all test files with mocks
4. Blocks commit if compilation fails

Tools: husky, pre-commit framework, or simple .git/hooks/pre-commit script

- Pre-commit hook installed and documented
- Hook detects Storage interface changes
- Hook validates all mocks compile
- Hook can be bypassed with --no-verify if needed
- Documentation updated with installation instructions


Refactor parseMarkdownFile to reduce cyclomatic complexity

The parseMarkdownFile function in cmd/bd/markdown.go has a cyclomatic complexity of 38, which exceeds the recommended threshold of 30. This makes the function harder to understand, test, and maintain.

Split the function into smaller, focused units:

1. parseMarkdownFile(filepath) - Main entry point, handles file I/O
2. parseMarkdownContent(scanner) - Core parsing logic
3. processIssueSection(issue, section, content) - Handle section finalization (current switch statement)
4. parseLabels(content) []string - Extract labels from content
5. parseDependencies(content) []string - Extract dependencies from content
6. parsePriority(content) int - Parse and validate priority

Benefits:
- Each function has a single responsibility
- Easier to test individual components
- Lower cognitive load when reading code
- Better encapsulation of parsing logic

- parseMarkdownFile complexity < 15
- New helper functions each have complexity < 10
- All existing tests still pass
- No change in functionality or behavior
- Code coverage maintained or improved


Add customizable time threshold for compact command

Currently compact uses fixed 30-day and 90-day tiers. Add support for custom time thresholds like '--older-than 60h' or '--older-than 2.5d' to allow more flexible compaction policies.

Examples:
  bd compact --all --older-than 60h
  bd compact --all --older-than 2.5d
  bd compact --all --tier 1 --age 48h

This would allow users to set their own compaction schedules based on their workflow needs.


Add --id flag to bd list for filtering by specific issue IDs

Add --id flag accepting comma-separated IDs. Usage: bd list --id wy-11,wy-12. Combines with other filters. From filter-flag-design.md.


bd sync crashes with nil pointer when daemon is running

The 'bd sync' command crashes with a nil pointer dereference when the daemon is running.

**Reproduction:**
```bash
# With daemon running
./bd sync
```

**Error:**
```
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x120 pc=0x1012314ac]

goroutine 1 [running]:
main.exportToJSONL({0x1014ec2e0, 0x101a49900}, {0x14000028db0, 0x30})
        /Users/stevey/src/fred/beads/cmd/bd/sync.go:245 +0x4c
```

**Root cause:**
The sync command's `exportToJSONL` function directly accesses `store.SearchIssues()` at line 245, but when daemon mode is active, the global `store` variable is nil. The sync command should either:
1. Use daemon RPC when daemon is running, or
2. Force direct mode for sync operations

**Workaround:**
Use `--no-daemon` flag: `bd sync --no-daemon`


Optimize export dependency queries (N+1 problem)

Export triggers separate GetDependencyRecords() per issue. For large DBs (1000+ issues), this is N+1 queries. Add GetAllDependencyRecords() to fetch all dependencies in one query. Location: cmd/bd/export.go:52-59, import.go:138-142


Add workspace config file for multi-repo management (optional enhancement)

For users who want explicit control over multi-repo setup without daemon, add optional workspace config file.

Config file: ~/.beads/workspaces.toml

Example:
[workspaces]
current = "global"

[workspace.global]
db = "~/.beads/global.db"
description = "System-wide tasks"

[workspace.project1]
db = "~/src/project1/.beads/db.sqlite"
description = "Main product"

[workspace.project2]
db = "~/src/project2/.beads/db.sqlite"
description = "Internal tools"

Commands:
bd workspace list                    # Show all workspaces
bd workspace add NAME PATH           # Add workspace
bd workspace remove NAME             # Remove workspace
bd workspace use NAME                # Switch active workspace
bd workspace current                 # Show current workspace
bd --workspace NAME <command>        # Override for single command

Implementation:
- Load config in PersistentPreRun
- Override dbPath based on current workspace
- Store workspace state in config file
- Support both workspace config AND auto-discovery
- Workspace config takes precedence over auto-discovery

Priority rationale:
- Priority 3 (low) because daemon approach already solves this
- Only implement if users request explicit workspace management
- Adds complexity vs daemon's automatic discovery

Alternative: Users can use BEADS_DB env var for manual workspace switching today.


Use safer placeholder pattern in replaceIDReferences

Currently uses bd-313 which could theoretically collide with user text. Use a truly unique placeholder like null bytes: \x00REMAP\x00_0_\x00 which are unlikely to appear in normal text. Located in collision.go:324. Very low probability issue but worth fixing for completeness.


Implement full cross-type cycle prevention in AddDependency

Expand cycle prevention in AddDependency to check for cycles across ALL dependency types, not just 'blocks'. Currently only 'blocks' type dependencies are checked for cycles, allowing cross-type circular dependencies to form (e.g., A blocks B, B parent-child A). This can cause semantic confusion and is a maintenance hazard for future operations that traverse dependencies.

Implementation approach:
1. Modify the cycle check in AddDependency (postgres.go:559-599)
2. Remove the 'type = blocks' filter from the recursive CTE
3. Check for cycles regardless of dependency type being added
4. Return a clear error message indicating which types form the cycle

Trade-offs to consider:
- This is more mathematically correct (no cycles in dependency DAG)
- May break legitimate use cases where cross-type cycles are intentional
- Need to evaluate whether ANY cross-type cycles are valid in practice
- Alternative: make this configurable with a --allow-cycle flag

Before implementing, should investigate:
- Are there legitimate reasons for cross-type cycles?
- What's the performance impact on large graphs (1000+ issues)?
- Should certain type combinations be allowed to cycle?

- AddDependency prevents cycles across all dependency types, not just 'blocks'
- Clear error message when cycle would be created, including dependency types
- All existing tests pass
- Performance benchmarked on large dependency graphs (100+ issues)
- Decision documented on whether to add --allow-cycle flag or exception rules


Refactor duplicate flush logic in PersistentPostRun

PersistentPostRun contains a complete copy of the flush logic instead of calling flushToJSONL(). This violates DRY principle and makes maintenance harder. Refactor to use flushToJSONL() with a force parameter to bypass isDirty check, or extract shared logic into a helper function. Located in cmd/bd/main.go:104-138.


Add compact --dry-run that shows size savings estimates

When running 'bd compact --dry-run', show estimated database size reduction in KB/MB and percentage, similar to what 'du -h' would show.

Example output:
  Tier 1 candidates: 15 issues
  Current size: 2.4 MB
  After compaction: ~1.7 MB (70% reduction, 0.7 MB saved)

This helps users understand impact before compacting.


Add EXPLAIN QUERY PLAN tests for ready work query

Verify that the hierarchical blocking query uses proper indexes and doesn't do full table scans.

**Queries to analyze:**
1. The recursive CTE (both base case and recursive case)
2. The final SELECT with NOT EXISTS
3. Impact of various filters (status, priority, assignee)

**Implementation:**
Add test function that:
- Runs EXPLAIN QUERY PLAN on GetReadyWork query
- Parses output to verify no SCAN TABLE operations
- Documents expected query plan in comments
- Fails if query plan degrades

**Benefits:**
- Catch performance regressions in tests
- Document expected query behavior
- Ensure indexes are being used

Related to: bd-36 (composite index on depends_on_id, type)


Add --show-all-paths flag to bd dep tree

Currently bd dep tree deduplicates nodes when multiple paths exist (diamond dependencies). Add optional --show-all-paths flag to display the full graph with all paths, showing duplicates. Useful for debugging complex dependency structures and understanding all relationships.


Add performance benchmarks document

Document actual performance metrics with hyperfine tests


Investigate vector/semantic search for issue discovery

From GH issue #2 RFC discussion: Evaluate if vector/semantic search over issues would provide value for beads.

**Use case:** Find semantically related issues (e.g., 'login broken' finds 'authentication failure', 'session expired').

**Questions to answer:**
1. What workflows would this enable that we can't do now?
2. Is dataset size (typically 50-200 issues) large enough to benefit?
3. Do structured features (deps, tags, types) already provide better relationships?
4. What's the maintenance cost (embeddings, storage, recomputation)?

**Alternatives to consider:**
- Improve 'bd list' filtering with regex/boolean queries
- Add 'bd related <id>' showing deps + mentions + same tags
- Export to JSON and pipe to external AI tools

**Decision:** Only implement if clear use case emerges. Don't add complexity for theoretical benefits.

**Context:** Part of evaluating Turso RFC ideas (GH #2). Vector search was proposed but unclear if needed for typical beads usage.


Add visual indicators for nodes with multiple parents in dep tree

When a node appears in the dependency tree via multiple paths (diamond dependencies), add a visual indicator like (*) or (multiple parents) to help users understand the graph structure. This would make it clear when deduplication has occurred. Example: 'bd-503: Shared dependency (*) [P1] (open)'


Write tests for merge functionality

Unit tests: validation, merge logic, data integrity. Integration tests: end-to-end workflow, export/import. Edge case tests: chains, circular refs, epics.


Add godoc comments for auto-flush functions

Add comprehensive godoc comments for findJSONLPath(), markDirtyAndScheduleFlush(), and flushToJSONL() explaining behavior, concurrency considerations, and error handling. Include notes about debouncing behavior (timer resets on each write, flush occurs 5s after LAST operation) and flush-on-exit guarantees. Located in cmd/bd/main.go:188-307.


Document merge command and AI integration

Update README, AGENTS.md with merge command examples. Document AI agent duplicate detection workflow.


Stress tests pollute production database with test issues

TestStressNoUniqueConstraintViolations and other stress tests in internal/rpc/stress_test.go create issues in production database instead of test database. Confirmed: 1,000 test issues (Agent X Issue Y) created at 20:46:01 during test run. Root cause: test goroutines connect to production daemon at .beads/bd.sock instead of isolated test daemon in temp directory.


Add cross-repo issue references (future enhancement)

Support referencing issues across different beads repositories. Useful for tracking dependencies between separate projects.

Proposed syntax:
- Local reference: bd-28 (current behavior)
- Cross-repo by path: ~/src/other-project#bd-456
- Cross-repo by workspace name: @project2:bd-789

Use cases:
1. Frontend project depends on backend API issue
2. Shared library changes blocking multiple projects
3. System administrator tracking work across machines
4. Monorepo with separate beads databases per component

Implementation challenges:
- Storage layer needs to query external databases
- Dependency resolution across repos
- What if external repo not available?
- How to handle in JSONL export/import?
- Security: should repos be able to read others?

Design questions to resolve first:
1. Read-only references vs full cross-repo dependencies?
2. How to handle repo renames/moves?
3. Absolute paths vs workspace names vs git remotes?
4. Should bd-27 auto-discover related repos?

Recommendation:
- Gather user feedback first
- Start with read-only references
- Implement as plugin/extension?

Context: This is mentioned in bd-27 as approach #2. Much more complex than daemon multi-repo approach. Only implement if there's strong user demand.

Priority: Backlog (4) - wait for user feedback before designing


Add rule-based compaction (e.g., compact children of closed epics)

Support semantic compaction rules beyond just time-based, such as:
- Compact all children of closed epics
- Compact by priority level (e.g., all P3/P4 closed issues)
- Compact by label (e.g., all issues labeled 'archive')
- Compact by type (e.g., all closed chores)

This would allow smarter database size management based on semantic meaning rather than just age.


Add automated duplicate detection tool for post-import cleanup

After collision resolution with --resolve-collisions, we can end up with content duplicates (different IDs, identical content) when parallel work creates the same issues.

## Problem
Current situation:
- `deduplicateIncomingIssues()` only deduplicates within the import batch
- Doesn't detect duplicates between DB and incoming issues
- Doesn't detect duplicates across the entire database post-import
- Manual detection: `bd list --json | jq 'group_by(.title) | map(select(length > 1))'`

## Real Example
After import with collision resolution, we had 7 duplicate pairs:
--196/bd-67: Same external_ref epic
--196-196: Same findByExternalRef task
--196-196: Same collision detection task
--196-196: Same import flow task
--196-196: Same test writing task
--196-196: Same documentation task
--196-196: Same code review task

## Proposed Solution
Add `bd duplicates` command that:
1. Groups all issues by content hash (title + description + design + acceptance_criteria)
2. Reports duplicate groups with suggested merge target (lowest ID or most references)
3. Optionally auto-merge with `--auto-merge` flag
4. Respects status (don't merge open with closed)

## Example Output
```
🔍 Found 7 duplicate groups:

Group 1: "Feature: Use external_ref as primary matching key"
  --196 (open, P1, 0 references)
  - bd-67 (open, P1, 0 references)
  Suggested merge: bd merge-196 --into bd-67

Group 2: "Add findByExternalRef query"
  --196 (open, P1, 0 references)
  --196 (open, P1, 0 references)
  Suggested merge: bd merge-196 --into-196
...

Run with --auto-merge to execute all suggested merges
```

## Implementation Notes
- Use same content hashing as deduplicateIncomingIssues
- Consider reference counts when choosing merge target
- Skip duplicates with different status (open vs closed)
- Add --dry-run mode
- Integration with import: `bd import --resolve-collisions --dedupe-after`


Remove unused issueMap in scoreCollisions

scoreCollisions() creates issueMap and populates it (lines 135-138) but never uses it. Either remove it or add a TODO comment explaining future use. Located in collision.go:135-138. Cosmetic cleanup.


Make merge command idempotent for safe retry after partial failures

The merge command currently performs 3 operations without an outer transaction:
1. Migrate dependencies from source → target
2. Update text references across all issues
3. Close source issues

If merge fails mid-operation (network issue, daemon crash, etc.), a retry will fail or produce incorrect results because some operations already succeeded.

**Goal:** Make merge idempotent so retrying after partial failure is safe and completes the remaining work.

**Idempotency checks needed:**
- Skip dependency migration if target already has the dependency
- Skip text reference updates if already updated
- Skip closing source issues if already closed
- Report which operations were skipped vs performed

**Example output:**
```
✓ Merged 2 issue(s) into bd-48
  - Dependencies: 3 migrated, 2 already existed
  - Text references: 5 updated, 0 already correct
  - Source issues: 1 closed, 1 already closed
```

**Related:** bd-1 originally requested transaction support, but idempotency is a better solution for this use case since individual operations are already atomic.

Current merge code already has some idempotency:
- Dependency migration checks `alreadyExists` before adding (line ~145-151 in merge.go)
- Text reference updates are naturally idempotent (replacing bd-X with bd-Y twice has same result)

Missing idempotency:
- CloseIssue fails if source already closed
- Error messages don't distinguish "already done" from "real failure"

Implementation:
1. Check source issue status before closing - skip if already closed
2. Track which operations succeeded/skipped
3. Return detailed results for user visibility
4. Consider adding --dry-run output showing what would be done vs skipped


Daemon storage cache doesn't detect external database modifications

When bd commands bypass the daemon and directly modify the database (e.g., `bd import` with direct file access, or deleting/recreating bd.db), the daemon's cached storage connection becomes stale and serves outdated data.

**Reproduction**:
1. Start daemon: `bd daemon`
2. Run bd stats → shows N issues
3. Delete database: `rm .beads/bd.db`
4. Reinit and import: `bd init && bd import -i .beads/issues.jsonl`
5. Run bd stats → shows 0 issues (wrong!)
6. Direct query: `sqlite3 .beads/bd.db 'SELECT COUNT(*) FROM issues'` → shows correct count
7. Restart daemon: `bd daemon --stop` then retry stats → now shows correct count

**Root cause**:
- server.go:1410-1414 retrieves cached storage without checking if DB file changed
- Cache only evicts based on TTL (30min) or LRU, never on external modifications
- Direct file operations bypass daemon, leaving cache stale

**Impact**:
- Users see incorrect/stale data after external DB operations
- Confusing behavior with no clear indication cache is stale
- Requires daemon restart to fix

**Proposed fixes**:
1. Check mtime on cache hit, invalidate if file changed
2. Add cache eviction API (bd cache --clear)
3. Use file locking to prevent external modifications while daemon running
4. SQLite WAL mode change notifications

**Better approach: Check DB file mtime on cache lookup**

Too many commands bypass the daemon (import, init, renumber, compact, delete, dep tree, export, stale). Notifying from each would be error-prone and easy to forget when adding new commands.

**Implementation:**

1. Add `dbMtime time.Time` field to `StorageCacheEntry`
2. In `getStorageForRequest()` on cache hit:
   - Stat the DB file to get current mtime
   - If mtime changed since cached, evict entry and reopen
   - Otherwise return cached connection
3. Store mtime when initially caching

**Code location:**
- `internal/rpc/server.go:1410-1414` (cache hit path)
- `internal/rpc/server.go:49-52` (StorageCacheEntry struct)

**Benefits:**
- Simple, centralized check
- Works for all commands that bypass daemon
- Works for external tools modifying DB
- No need to update every command
- Minimal performance overhead (one stat() call on cache hit)

**Trade-offs:**
- Small overhead on every cache hit (negligible - stat is fast)
- mtime granularity may miss rapid changes (unlikely in practice)


Implement bd quickstart command

Add bd quickstart command to show context-aware repo information: recent issues, database location, configured prefix, example queries. Helps AI agents understand current project state. Companion to bd onboard.

After review, we already have good context tools: bd stats, bd list, bd ready, and the AGENTS.md onboarding section. Adding bd quickstart would be redundant and doesn't add enough value to justify maintenance cost. Closing as won't implement.


Global daemon should warn/reject --auto-commit and --auto-push

When user runs 'bd daemon --global --auto-commit', it's unclear which repo the daemon will commit to (especially after fixing bd-27 where global daemon won't open a DB).

Options:
1. Warn and ignore the flags in global mode
2. Error out with clear message

Line 87-91 already checks autoPush, but should skip check entirely for global mode. Add user-friendly messaging about flag incompatibility.


Convert repeated strings to constants (goconst)

12 instances of repeated strings that should be constants: "alice", "windows", "bd-2", "daemon", "import", "healthy", "unhealthy", "1.0.0", "custom-1", "custom-2"

Create package-level or test-level constants for frequently used test strings and command names.


Auto-flush writes test pollution and session work to git-tracked issues.jsonl

Auto-flush exports ALL issues from DB to issues.jsonl every 5 seconds, including:
- Test issues (bd-4053 through bd-4059 were version test junk)
- Issues created during debugging sessions
- Test pollution from stress tests
- Temporary diagnostic issues

This pollutes the git-tracked issues.jsonl with garbage that shouldn't be committed.

Example from today:
- Git had 49 clean issues
- Our DB grew to 100+ with test junk and session work
- Auto-flush wrote all 100+ to issues.jsonl
- Git status showed modified issues.jsonl with 50+ unwanted issues

Impact:
- Pollutes git history with test/debug garbage
- Makes code review difficult (noise in diffs)
- Can't distinguish real work from session artifacts
- Other team members pull polluted issues

Solutions to consider:
1. Disable auto-flush by default (require explicit --enable-auto-flush)
2. Add .beadsignore to exclude issue ID patterns
3. Make auto-flush only export 'real' issues (exclude test-*)
4. Require manual 'bd sync' for git commit
5. Auto-flush to separate file (.beads/session.jsonl vs issues.jsonl)

Related: bd-8 (test pollution), isolation_test.go (test DB separation)


Issue counter gets out of sync with actual issues

The issue counter in issue_counters table frequently desyncs from actual max issue ID, causing:
- Import from JSONL leaves counter at old high value
- Test pollution increments counter but cleanup doesn't decrement it
- Delete issues doesn't update counter
- Only fix is 'rm -rf .beads' which is destructive

Examples from today's session:
- Had 48 issues but counter at 7714 after test pollution
- Import from git didn't reset counter
- Next new issue would be bd-7715 instead of bd-8

Proposed fixes:
1. Auto-recalculate counter from max(issue_id) on import
2. Add 'bd fix-counter' command
3. Make counter lazy (always compute from DB, don't store)
4. Import should reset counter to match imported data

Related:-254 (test isolation), bd-12 (init timestamp bug)


Counter not synced after import on existing DB with populated issue_counters table

The counter sync fix in counter_sync_test.go only syncs during initial migration when issue_counters table is empty (migrateIssueCountersTable checks count==0). For existing databases with stale counters:

- Import doesn't resync the counter
- Delete doesn't update counter
- Renumber doesn't fix counter
- Counter remains stuck at old high value

Example from today:
- Had 49 issues after clean import
- Counter stuck at 4106 from previous test pollution
- Next issue would be bd-4107 instead of bd-12
- Even after renumber, counter stayed at 4106

Root cause: Migration only syncs if table is empty (line 182 in sqlite.go). Once populated, never resyncs.

Fix needed:
1. Sync counter after import operations (not just empty table)
2. Add counter resync after renumber
3. Daemon caches counter value - needs to reload after external changes

Related: bd-8 (original counter sync fix), bd-9 (daemon cache staleness)

## Investigation Results

After thorough code review, all the fixes mentioned in the issue description have ALREADY been implemented:

### ✅ Fixes Already in Place:

1. **Import DOES resync counters**
   - `cmd/bd/import_shared.go:253` calls `SyncAllCounters()` after batch import
   - Verified with new test `TestCounterSyncAfterImport`

2. **Delete DOES update counters**
   - `internal/storage/sqlite/sqlite.go:1424` calls `SyncAllCounters()` after deletion
   - Both single delete and batch delete sync properly
   - Verified with existing tests: `TestCounterSyncAfterDelete`, `TestCounterSyncAfterBatchDelete`

3. **Renumber DOES fix counters**
   - `cmd/bd/renumber.go:298-304` calls `ResetCounter()` then `SyncAllCounters()`
   - Forces counter to actual max ID (not just MAX with stale value)

4. **Daemon cache DOES detect external changes**
   - `internal/rpc/server.go:1466-1487` checks file mtime and evicts stale cache
   - When DB file changes externally, cached storage is evicted and reopened

### Tests Added:

- `TestCounterSyncAfterImport`: Confirms import syncs counters from stale value (4106) to actual max (49)
- `TestCounterNotSyncedWithoutExplicitSync`: Documents what would happen without the fix (bd-4107 instead of bd-12)

### Conclusion:

The issue described in bd-12 has been **fully resolved**. All operations (import, delete, renumber) now properly sync counters. The daemon correctly detects external DB changes via file modification time.

The root cause (migration only syncing empty tables) was fixed by adding explicit `SyncAllCounters()` calls after import, delete, and renumber operations.


Re-land TestDatabaseReinitialization after fixing Windows/Nix issues

TestDatabaseReinitialization test was reverted due to CI failures:
- Windows: JSON parse errors, missing files
- Nix: git not available in build environment

Need to fix and re-land:
1. Make test work on Windows (path separators, file handling)
2. Skip test in Nix environment or mock git
3. Fix JSON parsing issues


Feature: Use external_ref as primary matching key for import updates

Implement external_ref-based matching for imports to enable hybrid workflows with external systems (Jira, GitHub, Linear).

## Problem
Current import collision detection treats any content change as a collision, preventing users from syncing updates from external systems without creating duplicates.

## Solution
Use external_ref field as primary matching key during imports. When an incoming issue has external_ref set:
- Search for existing issue with same external_ref
- If found, UPDATE (not collision)
- If not found, create new issue
- Never match local issues (without external_ref)

## Use Cases
- Jira integration: Import backlog, add local tasks, re-sync updates
- GitHub integration: Import issues, track with local subtasks, sync status
- Linear integration: Team coordination with local breakdown

## Reference
GitHub issue #142: https://github.com/steveyegge/beads/issues/142