Improve internal/daemon test coverage (currently 22.5%)

Daemon functionality needs better coverage:
- Auto-start behavior
- Lock file management
- Discovery mechanisms
- Connection handling
- Error recovery

Current coverage: 58.3% (improved from 22.5% as of Nov 2025)


bd init should refuse when JSONL already has issues (safety guard)

When running `bd init` in a directory with an existing JSONL containing issues, bd should refuse and suggest the correct action instead of proceeding.

## The Problem

Current behavior when database is missing but JSONL exists:
```
$ bd create "test"
Error: no beads database found
Hint: run 'bd init' to create a database...
```

This leads users (and AI agents) to reflexively run `bd init`, which can cause:
- Prefix mismatch if wrong prefix specified
- Data corruption if JSONL is damaged
- Confusion about what actually happened

## Proposed Behavior

```
$ bd init --prefix bd

⚠ Found existing .beads/issues.jsonl with 76 issues.

This appears to be a fresh clone, not a new project.

To hydrate the database from existing JSONL:
  bd doctor --fix

To force re-initialization (may cause data loss):
  bd init --prefix bd --force

Aborting.
```

## Trigger Conditions

- `.beads/issues.jsonl` or `.beads/beads.jsonl` exists
- File contains > 0 valid issue lines
- No `--force` flag provided

## Edge Cases

- Empty JSONL (0 issues) → allow init (new project)
- Corrupted JSONL → warn but allow with confirmation
- Existing `.db` file → definitely refuse (weird state)

## Related

- bd-dmb: Fresh clone should suggest hydration (better error messages)
- bd-4ew: bd doctor should detect fresh clone

This issue is about the safety guard on `bd init` itself, not the error messages from other commands.


Add programmatic tip injection API

Allow tips to be programmatically injected at runtime based on detected conditions. This enables dynamic tips (not just pre-defined ones) to be shown with custom priority and frequency.


Debug test


Git history fallback doesn't escape regex special chars in IDs

## Problem

In `batchCheckGitHistory`, IDs are directly interpolated into a regex pattern:

```go
patterns = append(patterns, fmt.Sprintf(\`"id":"%s"\`, id))
searchPattern := strings.Join(patterns, "|")
cmd := exec.Command("git", "log", "--all", "-G", searchPattern, ...)
```

If an ID contains regex special characters (e.g., `bd-foo.bar` or `bd-test+1`), the pattern will be malformed or match unintended strings.

## Location
`internal/importer/importer.go:923-926`

## Impact
- False positives: IDs with `.` could match any character
- Regex errors: IDs with `[` or `(` could cause git to fail
- Security: potential for regex injection (low risk since IDs are validated)

## Fix
Escape regex special characters:

```go
import "regexp"

escapedID := regexp.QuoteMeta(id)
patterns = append(patterns, fmt.Sprintf(\`"id":"%s"\`, escapedID))
```


Upgrade notification persists after running suggested commands

The upgrade notification kept showing even after running 'bd upgrade review' and 'bd doctor' as suggested.

Expected: Notification should be dismissed after running either/both commands
Actual: Notification persisted on every bd command

Investigation findings:
- metadata.json correctly shows last_bd_version=0.28.0
- The notification stopped appearing after a few more invocations
- May be related to daemon caching or a race condition
- The 'upgradeAcknowledged' flag in version_tracking.go:115 only persists per-session (in-memory)

Possible causes:
1. Daemon vs direct mode inconsistency - daemon may have stale version info
2. The version is checked/updated before daemon connection, but daemon has cached state
3. Race between trackBdVersion() and daemon startup

Status: May have resolved itself, but root cause unclear. Close if no longer reproducible.


Blocked issue


Auto-detect issue prefix from existing JSONL in 'bd init'

When running `bd init` in a fresh clone with existing JSONL, it should auto-detect the issue prefix from the JSONL file instead of requiring `--prefix`.

Currently you must specify `--prefix ef` manually. But the JSONL file already contains issues like `ef-1it`, `ef-1jp` etc., so the prefix is known.

**Ideal UX**:
```
$ bd init
Detected issue prefix 'ef' from existing JSONL (38 issues).
✓ Database initialized...
```

This would make fresh clone hydration a single command: `bd init` with no flags.


fix: bd onboard hangs on Windows


Jira import script (jira2jsonl.py)

Create a Python script to import Jira issues into beads JSONL format.

**Pattern**: Follow examples/github-import/gh2jsonl.py

**Features needed**:
- Fetch issues via Jira REST API with JQL queries
- Pagination handling (100 issues per request)
- Map Jira fields to beads fields (see schema mapping in epic)
- Set external_ref to Jira issue URL for re-sync
- Support both Jira Cloud and Server/Data Center APIs
- Read config from bd config (jira.url, jira.project, jira.api_token)

**Config-driven mapping**:
- jira.status_map.* for status conversion
- jira.type_map.* for issue type conversion
- jira.priority_map.* for priority conversion

**Usage**:
```bash
python jira2jsonl.py --project PROJ | bd import
# or
python jira2jsonl.py --jql 'project=PROJ AND status\!=Done' | bd import
```

**Output**: JSONL with external_ref set to Jira issue URL


Jira export script (jsonl2jira.py)

Create a Python script to push beads issues to Jira.

**Requires**: Jira import script to be complete first (need external_ref matching logic working)

**Features needed**:
- Create new Jira issues from beads issues without external_ref
- Update existing Jira issues matched by external_ref
- Map beads fields back to Jira fields
- Handle Jira workflow transitions (status changes may need transitions)
- Support custom field mapping for design/acceptance_criteria/notes

**Challenges**:
- Jira status changes often require workflow transitions, not direct updates
- Need to discover valid transitions via API
- Custom fields vary by Jira instance

**Usage**:
```bash
bd export | python jsonl2jira.py --create-only  # Only create, don't update
bd export | python jsonl2jira.py                 # Create and update
```

**After creation**: Sets external_ref on beads issue to link back


Implement alias conflict resolution

Handle alias conflicts when multiple clones assign same alias to different issues.

## Scenario
```
Clone A: Creates bd-a1b2c3d4, assigns alias #42
Clone B: Creates bd-e5f6a7b8, assigns alias #42
After sync: Conflict! Which issue gets #42?
```

## Resolution Strategy: Content-Hash Ordering
Deterministic, same result on all clones:
```go
func ResolveAliasConflicts(conflicts []AliasConflict) []AliasRemapping {
    for _, conflict := range conflicts {
        // Sort by hash ID (lexicographic)
        sort.Strings(conflict.IssueIDs)

        // Winner: lowest hash ID (arbitrary but deterministic)
        winner := conflict.IssueIDs[0]

        // Losers: reassign to next available aliases
        for _, loser := range conflict.IssueIDs[1:] {
            newAlias := getNextAlias()
            remappings = append(remappings, AliasRemapping{
                IssueID:  loser,
                OldAlias: conflict.Alias,
                NewAlias: newAlias,
            })
        }
    }
    return remappings
}
```

## Detection During Import
File: internal/importer/importer.go
```go
func handleAliasConflicts(imported []Issue, existing []Issue) error {
    // Build alias map from imported issues
    aliasMap := make(map[int][]string)  // alias → issue IDs

    for _, issue := range imported {
        aliasMap[issue.Alias] = append(aliasMap[issue.Alias], issue.ID)
    }

    // Check against existing aliases
    for alias, importedIDs := range aliasMap {
        existingID := storage.GetIssueIDByAlias(alias)
        if existingID != "" {
            // Conflict! Resolve it
            allIDs := append(importedIDs, existingID)
            conflicts = append(conflicts, AliasConflict{
                Alias:    alias,
                IssueIDs: allIDs,
            })
        }
    }

    // Resolve and apply
    remappings := ResolveAliasConflicts(conflicts)
    applyAliasRemappings(remappings)
}
```

## Alternative Strategies (For Future Consideration)

### Priority-Based
```go
// Higher priority keeps alias
if issueA.Priority < issueB.Priority {
    winner = issueA
}
```

### Timestamp-Based (Last-Write-Wins)
```go
// Newer issue keeps alias
if issueA.UpdatedAt.After(issueB.UpdatedAt) {
    winner = issueA
}
```

### Manual Resolution
```bash
bd resolve-aliases --manual
# Interactive prompt for each conflict
```

## User Notification
```bash
$ bd sync
✓ Synced 5 issues
⚠ Alias conflicts resolved:
  - Issue bd-e5f6a7b8: alias changed from #42 to #100
  - Issue bd-9a8b7c6d: alias changed from #15 to #101
```

## Files to Create/Modify
- internal/storage/sqlite/alias_conflicts.go (new)
- internal/importer/importer.go (detect conflicts)
- cmd/bd/sync.go (show conflict notifications)

## Testing
- Test two clones assign same alias to different issues
- Test conflict resolution is deterministic (same on all clones)
- Test loser gets new alias
- Test winner keeps original alias
- Test multiple conflicts resolved in one import


Enhance `bd doctor` to verify Claude Code integration

Add checks to `bd doctor` that verify Claude Code integration is properly set up when .claude/ directory or Claude environment is detected.


Client self-heal: remove stale pid when lock free + socket missing

When client detects:
- Socket is missing AND
- tryDaemonLock shows lock NOT held

Then automatically:
1. Remove stale daemon.pid file
2. Optionally auto-start daemon (behind BEADS_AUTO_START_DAEMON=1 env var)

This prevents stale artifacts from accumulating after daemon crashes.


User Diagnostics (bd doctor --perf)

Extend cmd/bd/doctor.go to add --perf flag for user performance diagnostics.

Functionality:
- Add --perf flag to existing bd doctor command
- Collect system info (OS, arch, Go version, SQLite version)
- Collect database stats (size, issue counts, dependency counts)
- Time key operations on user's actual database:
  * bd ready
  * bd list --status=open
  * bd show <random-issue>
  * bd create (with rollback)
  * Search with filters
- Generate CPU profile automatically (timestamped filename)
- Output simple report with platform info, timings, profile location

Output example:
  Beads Performance Diagnostics
  Platform: darwin/arm64
  Database: 8,234 issues (4,123 open)

  Operation Performance:
    bd ready                  42ms
    bd list --status=open     15ms

  Profile saved: beads-perf-2025-11-13.prof
  View: go tool pprof -http=:8080 beads-perf-2025-11-13.prof

Implementation:
- Extend cmd/bd/doctor.go (~100 lines)
- Use runtime/pprof for CPU profiling
- Use time.Now()/time.Since() for timing
- Rollback test operations (don't modify user's database)


Extract daemon sync functions to daemon_sync.go


Fix SyncJSONLToWorktree to merge instead of overwrite

SyncJSONLToWorktree blindly copies local JSONL to worktree, overwriting remote issues.

## Root Cause (GitHub #464)
When Computer B syncs with fewer issues than remote:
1. preemptiveFetchAndFastForward updates worktree (10 issues)
2. SyncJSONLToWorktree overwrites with local (1 issue)
3. Commit records deletion of 9 issues
4. 3-way merge sees "deletion in left" → deletion wins

## Fix
Before copying, check if worktree has more issues. If so:
- Option A: Merge local into worktree (preferred)
- Option B: Fail with helpful error message
- Option C: Warn and require --force flag

## Location
internal/syncbranch/worktree.go:107 - SyncJSONLToWorktree call

## Test Case
1. Clone repo with sync-branch containing issues
2. bd init (starts empty since JSONL on sync-branch)
3. bd create "test"
4. bd sync
5. Verify all issues preserved, not just "test"


bd detect-pollution - Test pollution detector

Detect test issues that leaked into production DB.

Pattern matching for:
- Titles starting with 'test', 'benchmark', 'sample'
- Sequential numbering (test-1, test-2)
- Generic descriptions
- Created in rapid succession

Optional AI scoring for confidence.

Files: cmd/bd/detect_pollution.go (new)


Complete remaining sync branch daemon tests

4 remaining test scenarios in daemon_sync_branch_test.go need completion:

⚠ MINOR FIXES (apply same pattern as TestSyncBranchCommitAndPush_Success):
1. TestSyncBranchCommitAndPush_NoChanges
   - Reorder: call initMainBranch() BEFORE creating JSONL
   - Pattern: init branch → create issue → export JSONL → test

2. TestSyncBranchCommitAndPush_WorktreeHealthCheck
   - Same reordering needed
   - Verify worktree corruption detection and auto-repair

🔧 MORE WORK NEEDED (remote branch setup):
3. TestSyncBranchPull_Success
   - Issue: remote doesn't have sync branch after push
   - Need to verify branch is pushed to remote correctly
   - Then test pull from clone2

4. TestSyncBranchIntegration_EndToEnd
   - Full workflow: Agent A commits → Agent B pulls → Agent B commits → Agent A pulls
   - Same remote branch issue

Pattern to apply (from TestSyncBranchCommitAndPush_Success):
- Call initMainBranch(t, dir) BEFORE creating issues/JSONL
- This ensures sync branch worktree has changes to commit

Acceptance:
- All 7 tests pass
- go test -v -run TestSyncBranch ./cmd/bd/ succeeds


Daemon gets stuck when auto-import blocked by git conflicts

CRITICAL: The daemon enters a corrupt state that breaks RPC commands when auto-import is triggered but git pull fails due to uncommitted changes.

Impact: This is a data integrity and usability issue that could cause users to lose trust in Beads. The daemon silently fails for certain commands while appearing healthy.

Reproduction:
1. Make local changes to issues (creates uncommitted .beads/beads.jsonl)
2. Remote has updates (JSONL newer, triggers auto-import)
3. Daemon tries to pull but fails: 'cannot pull with rebase: You have unstaged changes'
4. Daemon enters bad state - 'bd show' and other commands return EOF
5. 'bd list' still works, daemon process is running, no errors logged

Technical details:
- Auto-import check runs in handleRequest() before processing RPC commands
- When import is blocked, it appears to corrupt daemon state
- Likely: deadlock, unclosed transaction, or storage handle corruption
- Panic recovery (server_lifecycle_conn.go:183) didn't catch anything - not a panic

Required fix:
- Auto-import must not block RPC command execution
- Handle git pull failures gracefully without corrupting state
- Consider: skip auto-import if git is dirty, queue import for later, or use separate goroutine
- Add timeout/circuit breaker for import operations
- Log clear warnings when auto-import is skipped

Without this fix, users in collaborative environments will frequently encounter mysterious EOF errors that require daemon restarts.


Test lowercase p0


Debug MCP Agent Mail tool execution errors

**EXTERNAL WORK**: Debug the standalone MCP Agent Mail server (separate from beads integration).

The Agent Mail server runs as an independent service at ~/src/mcp_agent_mail. This is NOT beads code - it's a separate GitHub project we're evaluating for optional coordination features.

Current Issue:
- MCP API endpoint returns errors when calling ensure_project tool
- Error: "Server encountered an unexpected error while executing tool"
- Core HTTP server works, web UI functional, but tool wrapper layer fails

Server Details:
- Location: ~/src/mcp_agent_mail (separate repo)
- Repository: https://github.com/Dicklesworthstone/mcp_agent_mail
- Runs on: http://127.0.0.1:8765
- Bearer token: In .env file

Investigation Steps:
1. Check tool execution logs for full stack trace
2. Verify Git storage initialization at ~/.mcp_agent_mail_git_mailbox_repo
3. Review database setup (storage.sqlite3)
4. Test with simpler MCP tools if available
5. Compare with working test cases in tests/

Why This Matters:
- Blocks [deleted:bd-6hji] (testing file reservations)
- Need working MCP API to validate Agent Mail benefits
- Proof of concept for lightweight beads integration later

Note: The actual beads integration (bd-wfmw) will be lightweight HTTP client code only.


Debug test 2


Add tests for helper functions (GetDirtyIssueHash, GetAllDependencyRecords, export hashes)

Several utility functions have 0% coverage:
- GetDirtyIssueHash (dirty.go)
- GetAllDependencyRecords (dependencies.go)
- GetExportHash, SetExportHash, ClearAllExportHashes (hash.go)

These are lower priority but should have basic coverage.


Extract daemon server functions to daemon_server.go


Add unit tests for getLocalSyncBranch

The new getLocalSyncBranch() function in autoimport.go (bd-0is fix) has no dedicated unit tests.

Should test:
- Returns empty string when no config.yaml exists
- Returns empty string when config.yaml has no sync-branch key
- Returns value when sync-branch is set with double quotes
- Returns value when sync-branch is set with single quotes
- Returns value when sync-branch is set without quotes
- Returns BEADS_SYNC_BRANCH env var when set (takes precedence)
- Handles edge cases: empty file, whitespace-only lines, comments

Location: cmd/bd/autoimport_test.go


Extract validation functions to validators.go

Move validatePriority, validateStatus, validateIssueType, validateTitle, validateEstimatedMinutes, validateFieldUpdate to validators.go


Doctor: exclude merge artifacts from 'multiple JSONL' warning

## Problem
`bd doctor` warns about 'Multiple JSONL files found' when merge artifact files exist:
```
JSONL Files: Multiple JSONL files found: beads.base.jsonl, beads.left.jsonl, issues.jsonl ⚠
```

This is confusing because these aren't real issue JSONL files - they're temporary snapshots for deletion tracking.

## Fix
Exclude known merge artifact patterns from the multiple-JSONL warning:

```go
// In doctor JSONL check
skipPatterns := map[string]bool{
    "beads.base.jsonl": true,
    "beads.left.jsonl": true,
    "beads.right.jsonl": true,
}
```

## Files
- cmd/bd/doctor/ (JSONL check logic)


bd doctor --json flag not working

The --json flag on bd doctor command doesn't produce JSON output. It continues to show human-readable output instead. The flag is registered locally on doctorCmd but the code uses the global jsonOutput variable set by PersistentPreRun. Need to investigate why the flag isn't being honored.


sync tries to create worktree in .git file

example: bd sync --no-daemon
→ Exporting pending changes to JSONL...
→ Committing changes to sync branch 'worktree-db-fail'...
Error committing to sync branch: failed to create worktree: failed to create worktree parent directory: mkdir /var/home/matt/dev/beads/worktree-db-fail/.git: not a directory


Fix import failure on missing parent issues

Import process fails atomically when JSONL references deleted parent issues. Implement hybrid solution: topological sorting + parent resurrection to handle deleted parents gracefully while maintaining referential integrity. See docs/import-bug-analysis-bd-3xq.md for full analysis.


bd message: Fix inefficient client-side filtering for --unread-only


bd sync fails with orphaned issues and duplicate ID conflict

After fixing the deleted_at TEXT column scanning bug (commit 18b1eb2), bd sync still fails with two issues:

1. Orphan Detection Warning: 12 orphaned child issues whose parents no longer exist (bd-cb64c226.* and bd-cbed9619.*)

2. Import Failure: UNIQUE constraint failed for bd-360 - this tombstone exists in both DB and JSONL

Error: "Import failed: error creating depth-0 issues: bulk insert issues: failed to insert issue bd-360: sqlite3: constraint failed: UNIQUE constraint failed: issues.id"

Fix options:
- Delete orphaned child issues with bd delete
- Resolve bd-360 duplicate (in deletions.jsonl vs tombstone in DB)
- Reset sync branch: git branch -f beads-sync main && git push --force-with-lease origin beads-sync

Fixed tombstone constraint violation bug. When deleting closed issues, the CHECK constraint (status = 'closed') = (closed_at IS NOT NULL) was violated because CreateTombstone didn't clear closed_at. Fix: set closed_at = NULL in tombstone creation SQL.

The sync data corruption (orphaned issues in beads-sync branch) requires manual cleanup: reset sync branch with 'git branch -f beads-sync main && git push --force-with-lease origin beads-sync'


Migration tool: sequential → hash IDs

Create migration tool to convert existing sequential-ID databases to hash-ID format.

## Command: bd migrate --hash-ids

```bash
bd migrate --hash-ids [--dry-run]
```

## Migration Process

### 1. Backup Database
```bash
cp .beads/beads.db .beads/beads.db.backup-1761798384
echo "✓ Backup created: .beads/beads.db.backup-1234567890"
```

### 2. Generate Hash IDs for Existing Issues
```go
func migrateToHashIDs(db *SQLiteStorage) error {
    // Read all issues
    issues, err := db.ListIssues(ctx, ListOptions{Status: "all"})

    // Generate mapping: old ID → new hash ID
    mapping := make(map[string]string)
    for _, issue := range issues {
        hashID := GenerateHashID(
            issue.Title,
            issue.Description,
            issue.CreatedAt,
            db.workspaceID,
        )
        mapping[issue.ID] = hashID
    }

    // Detect hash collisions (extremely rare)
    if hasCollisions(mapping) {
        return fmt.Errorf("hash collision detected, aborting")
    }

    return nil
}
```

### 3. Update Database Schema
```sql
-- Add alias column
ALTER TABLE issues ADD COLUMN alias INTEGER UNIQUE;

-- Populate aliases from old IDs
UPDATE issues SET alias = CAST(SUBSTR(id, 4) AS INTEGER)
  WHERE id LIKE 'bd-%' AND SUBSTR(id, 4) GLOB '[0-9]*';

-- Create new issues_new table with hash IDs
CREATE TABLE issues_new (
    id TEXT PRIMARY KEY,  -- Hash IDs now
    alias INTEGER UNIQUE,
    title TEXT NOT NULL,
    -- ... rest of schema
);

-- Copy data with ID mapping
INSERT INTO issues_new SELECT
    <new_hash_id>,  -- From mapping
    alias,
    title,
    -- ...
FROM issues;

-- Drop old table, rename new
DROP TABLE issues;
ALTER TABLE issues_new RENAME TO issues;
```

### 4. Update Dependencies
```sql
-- Update depends_on_id using mapping
UPDATE dependencies
SET issue_id = <new_hash_id>,
    depends_on_id = <new_depends_on_hash_id>
FROM mapping;
```

### 5. Update Text References
```go
// Update all text fields that mention old IDs
func updateTextReferences(db *SQLiteStorage, mapping map[string]string) error {
    for oldID, newID := range mapping {
        // Update description, notes, design, acceptance_criteria
        db.Exec(`UPDATE issues SET
            description = REPLACE(description, ?, ?),
            notes = REPLACE(notes, ?, ?),
            design = REPLACE(design, ?, ?),
            acceptance_criteria = REPLACE(acceptance_criteria, ?, ?)
        `, oldID, newID, oldID, newID, oldID, newID, oldID, newID)
    }
}
```

### 6. Export to JSONL
```bash
bd export  # Writes hash IDs to .beads/issues.jsonl
git add .beads/issues.jsonl
git commit -m "Migrate to hash-based IDs"
```

## Output
```bash
$ bd migrate --hash-ids
Migrating to hash-based IDs...
✓ Backup created: .beads/beads.db.backup-1730246400
✓ Generated 150 hash IDs
✓ No hash collisions detected
✓ Updated issues table schema
✓ Updated 150 issue IDs
✓ Updated 87 dependencies
✓ Updated 234 text references
✓ Exported to .beads/issues.jsonl
✓ Migration complete!

Next steps:
  1. Test: bd list, bd show #1, etc.
  2. Commit: git commit -m "Migrate to hash-based IDs"
  3. Push: git push origin main
  4. Notify collaborators to pull and re-init
```

## Dry Run Mode
```bash
$ bd migrate --hash-ids --dry-run
[DRY RUN] Would migrate 150 issues:
  bd-1c63eb84   → bd-af78e9a2  (alias: #1)
  bd-9063acda   → bd-e5f6a7b8  (alias: #2)
  ...
  bd-150 → bd-9a8b7c6d  (alias: #150)

[DRY RUN] Would update:
  - 150 issue IDs
  - 87 dependencies
  - 234 text references in descriptions/notes

No changes made. Run without --dry-run to apply.
```

## Files to Create
- cmd/bd/migrate.go (new command)
- internal/storage/sqlite/migrations/hash_ids.go

## Testing
- Test migration on small database (10 issues)
- Test migration on large database (1000 issues)
- Test hash collision detection (inject collision artificially)
- Test text reference updates
- Test rollback (restore from backup)
- Test migrated database works correctly

## Rollback Procedure
```bash
# If migration fails or has issues
mv .beads/beads.db.backup-1234567890 .beads/beads.db
bd export  # Restore JSONL from backup DB
```

## Multi-Clone Coordination
**Important**: All clones must migrate before syncing:

1. Coordinator sends message: "Migrating to hash IDs on 2025-10-30 at 10:00 UTC"
2. All collaborators pull latest changes
3. All run: `bd migrate --hash-ids`
4. All push changes
5. New work can continue with hash IDs

**Do NOT**:
- Mix sequential and hash IDs in same database
- Sync before all clones migrate


Speed up main_test.go - tests hang indefinitely due to nil rootCtx

## Problem

main_test.go tests are hanging indefinitely, making them unusable and blocking development.

## Root Cause

Tests that call flushToJSONL() or autoImportIfNewer() hang forever because:
- rootCtx is nil in test environment (defined in cmd/bd/main.go:67)
- flushToJSONL() uses rootCtx for DB operations (autoflush.go:503)
- When rootCtx is nil, DB calls timeout/hang indefinitely

## Affected Tests (10+)

- TestAutoFlushOnExit
- TestAutoFlushJSONLContent
- TestAutoFlushErrorHandling
- TestAutoImportIfNewer
- TestAutoImportDisabled
- TestAutoImportWithUpdate
- TestAutoImportNoUpdate
- TestAutoImportMergeConflict
- TestAutoImportConflictMarkerFalsePositive
- TestAutoImportClosedAtInvariant

## Proof

```bash
# Before fix: TestAutoFlushJSONLContent HANGS (killed after 2+ min)
# After adding rootCtx init: Completes in 0.04s
# Speedup: ∞ → 0.04s
```

## Solution

See MAIN_TEST_OPTIMIZATION_PLAN.md for complete fix plan.

**Quick Fix (5 min)**: Add to each hanging test:
```go
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
oldRootCtx := rootCtx
rootCtx = ctx
defer func() { rootCtx = oldRootCtx }()
```

**Full Optimization (40 min total)**:
1. Fix rootCtx (5 min) - unblocks tests
2. Reduce sleep durations 10x (2 min) - saves ~280ms
3. Use in-memory DBs (10 min) - saves ~1s
4. Share test fixtures (15 min) - saves ~1.2s
5. Fix skipped debounce test (5 min)

## Expected Results

- Tests go from hanging → <5s total runtime
- Keep critical integration test coverage
- Tests caught real bugs: bd-270, bd-206, bd-160

## Files

- Analysis: MAIN_TEST_REFACTOR_NOTES.md
- Solution: MAIN_TEST_OPTIMIZATION_PLAN.md
- Tests: cmd/bd/main_test.go (18 tests, 1322 LOC)

## Priority

P1 - Blocking development, tests unusable in current state


Extract SQLite migrations into separate files

The file `internal/storage/sqlite/sqlite.go` is 2,136 lines and contains 11 sequential migrations alongside core storage logic. Extract migrations into a versioned system.

Current issues:
- 11 migration functions mixed with core logic
- Hard to see migration history
- Sequential migrations slow database open
- No clear migration versioning

Migration functions to extract:
- `migrateDirtyIssuesTable()`
- `migrateIssueCountersTable()`
- `migrateExternalRefColumn()`
- `migrateCompositeIndexes()`
- `migrateClosedAtConstraint()`
- `migrateCompactionColumns()`
- `migrateSnapshotsTable()`
- `migrateCompactionConfig()`
- `migrateCompactedAtCommitColumn()`
- `migrateExportHashesTable()`
- Plus 1 more (11 total)

Target structure:
```
internal/storage/sqlite/
├── sqlite.go          # Core storage (~800 lines)
├── schema.go          # Table definitions (~200 lines)
├── migrations.go      # Migration orchestration (~200 lines)
└── migrations/        # Individual migrations
    ├── 001_initial_schema.go
    ├── 002_dirty_issues.go
    ├── 003_issue_counters.go
    [... through 011_export_hashes.go]
```

Benefits:
- Clear migration history
- Each migration self-contained
- Easier to review migration changes in PRs
- Future migrations easier to add


Split monolithic sqlite.go into focused files

internal/storage/sqlite/sqlite.go is 1050 lines containing initialization, 20+ CRUD methods, query building, and schema management.

Split into:
- store.go: Store struct & initialization (150 lines)
- bead_queries.go: Bead CRUD (300 lines)
- work_queries.go: Work queries (200 lines)
- stats_queries.go: Statistics (150 lines)
- schema.go: Schema & migrations (150 lines)
- helpers.go: Common utilities (100 lines)

Impact: Impossible to understand at a glance; hard to find specific functionality; high cognitive load

Effort: 6-8 hours


Add date and priority filters to bd search

Add filter parity with bd list for consistent querying.

**Missing filters to add:**
- --priority, --priority-min, --priority-max
- --created-after, --created-before
- --updated-after, --updated-before
- --closed-after, --closed-before
- --empty-description, --no-assignee, --no-labels
- --desc-contains, --notes-contains

**Files to modify:**
- cmd/bd/search.go
- internal/rpc/protocol.go (SearchArgs)
- internal/storage/storage.go (Search method)

**Testing:**
- All filter combinations
- Date format parsing
- Daemon and direct mode


DetectCycles SQL query has bug preventing cycle detection

The DetectCycles function's SQL query has a bug in the LIKE filter that prevents it from detecting cycles.

Current code (line 571):
```sql
AND p.path NOT LIKE '%' || d.depends_on_id || '→%'
```

This prevents ANY revisit to nodes, including returning to the start node to complete a cycle.

Fix:
```sql
AND (d.depends_on_id = p.start_id OR p.path NOT LIKE '%' || d.depends_on_id || '→%')
```

This allows revisiting the start node (to detect the cycle) while still preventing intermediate node revisits.

Impact: Currently DetectCycles cannot detect any cycles, but this hasn't been noticed because AddDependency prevents cycles from being created. The function would only matter if cycles were manually inserted into the database.


bd init creates nested .beads directories

bd init sometimes creates .beads/.beads/ nested directories, which should never happen. This occurs fairly often and can cause confusion about which .beads directory is active. Need to add validation to detect if already inside a .beads directory and either error or use the parent .beads location.


Test label update feature


Feature P1


Modify EnsureIDs to support parent resurrection

Update internal/storage/sqlite/ids.go:189-202 to call TryResurrectParent before failing on missing parent. Add resurrection mode flag, log resurrected parents for transparency. Maintain backwards compatibility with strict validation mode.


Use TryResurrectParentChain instead of TryResurrectParent in GetNextChildID

## Context
Current bd-dvd fix uses TryResurrectParent, which only resurrects the immediate parent. For deeply nested hierarchies, this might not be sufficient.

## Example Scenario
Creating child with parent "bd-abc.1.2" where:
- bd-abc exists in DB
- bd-abc.1 was deleted (missing from DB, exists in JSONL)
- bd-abc.1.2 was deleted (missing from DB, exists in JSONL)

Current fix: Only tries to resurrect "bd-abc.1.2", fails because its parent "bd-abc.1" is missing.

## Solution
Replace TryResurrectParent with TryResurrectParentChain:

```go
if count == 0 {
    // Try to resurrect entire parent chain from JSONL history (bd-dvd fix)
    // This handles deeply nested hierarchies where intermediate parents are also missing
    resurrected, err := s.TryResurrectParentChain(ctx, parentID)
    if err != nil {
        return "", fmt.Errorf("failed to resurrect parent chain for %s: %w", parentID, err)
    }
    if !resurrected {
        return "", fmt.Errorf("parent issue %s does not exist and could not be resurrected from JSONL history", parentID)
    }
}
```

## Investigation
- Check if this scenario actually happens in practice
- TryResurrectParentChain already exists (resurrection.go:174-209)
- May be overkill if users always create parents before children

## Files
- internal/storage/sqlite/hash_ids.go

## Testing
Add test case for deeply nested resurrection


Q4 Platform Improvements

## Overview

[Describe the high-level goal and scope of this epic]

## Success Criteria

- [ ] Criteria 1
- [ ] Criteria 2
- [ ] Criteria 3

## Background

[Provide context and motivation]

## Scope

**In Scope:**
- Item 1
- Item 2

**Out of Scope:**
- Item 1
- Item 2


Add merge helper commands (bd sync --merge)

Add commands to merge beads branch back to main.

Tasks:
- Implement bd sync --merge command
- Implement bd sync --status command
- Implement bd sync --auto-merge (optional, for automation)
- Detect merge conflicts and provide guidance
- Show commit diff between branches
- Verify main branch is clean before merge
- Push merged changes to remote

Estimated effort: 2-3 days


Test auto-start in fred


Export deduplication breaks when JSONL and export_hashes table diverge

## Problem

The export deduplication feature (timestamp-only skipping) breaks when the JSONL file and export_hashes table get out of sync, causing exports to skip issues that aren't actually in the file.

## Symptoms

- `bd export` reports "Skipped 128 issue(s) with timestamp-only changes"
- JSONL file only has 38 lines but DB has 149 issues
- export_hashes table has 149 entries
- Auto-import doesn't trigger (hash matches despite missing data)
- Two repos on same commit show different issue counts

## Root Cause

shouldSkipExport() in autoflush.go compares current issue hash with stored export_hashes entry. If they match, it skips export assuming the issue is already in the JSONL.

This assumption fails when:
1. Git operations (pull, reset, checkout) change JSONL without clearing export_hashes
2. Manual JSONL edits or corruption
3. Import operations that modify DB but don't update export_hashes
4. Partial exports that update export_hashes but don't complete

## Impact

- **Critical data loss risk**: Issues appear to be tracked but aren't persisted to git
- Breaks multi-repo sync (root cause of today's debugging session)
- Auto-import fails to detect staleness (hash matches despite missing data)
- Silent data corruption (no error messages, just missing issues)

## Reproduction

1. Have DB with 149 issues, all in export_hashes table
2. Truncate JSONL to 38 lines (simulate git reset or corruption)
3. Run `bd export` - it skips 128 issues
4. JSONL still has only 38 lines but export thinks it succeeded

## Current Workaround

```bash
sqlite3 .beads/beads.db "DELETE FROM export_hashes"
bd export -o .beads/beads.jsonl
```

## Proposed Solutions

**Option 1: Verify JSONL integrity before skipping**
- Count lines in JSONL, compare with export_hashes count
- If mismatch, clear export_hashes and force full export
- Safe but adds I/O overhead

**Option 2: Hash-based JSONL validation**
- Store hash of entire JSONL file in metadata
- Before export, check if JSONL hash matches
- If mismatch, clear export_hashes
- More efficient, detects any JSONL corruption

**Option 3: Disable timestamp-only deduplication**
- Remove the feature entirely
- Always export all issues
- Simplest and safest, but creates larger git commits

**Option 4: Clear export_hashes on git operations**
- Add post-merge hook to clear export_hashes
- Clear on any import operation
- Defensive approach but may over-clear

## Recommended Fix

Combination of Options 2 + 4:
1. Store JSONL file hash in metadata after export
2. Check hash before export, clear export_hashes if mismatch
3. Clear export_hashes on import operations
4. Add `bd validate` check for JSONL/export_hashes sync

## Files Involved

- cmd/bd/autoflush.go (shouldSkipExport)
- cmd/bd/export.go (export with deduplication)
- internal/storage/sqlite/metadata.go (export_hashes table)

## Recovery Session (2025-10-29 21:30)

### What Happened
- Created 14 new hash ID issues (bd-f8b764c9 through bd-f8b764c9.1)
- bd sync appeared to succeed
- Canonical repo (~/src/beads): 162 issues in DB + JSONL ✓
- Secondary repo (fred/beads): Only 145 issues vs 162 in canonical ✗
- Both repos on same git commit but different issue counts!

### Bug Manifestation During Recovery

1. **Initial state**: fred/beads had 145 issues, 145 lines in JSONL, 145 export_hashes entries

2. **After git reset --hard origin/main**:
   - JSONL: 162 lines (from git)
   - DB: 150 issues (auto-import partially worked)
   - Auto-import failed with UNIQUE constraint error

3. **After manual import --resolve-collisions**:
   - DB: 160 issues
   - JSONL: Still 162 lines
   - export_hashes: 159 entries

4. **After bd export**:
   - **JSONL reduced to 17 lines!** ← The bug in action
   - export_hashes: 159 entries (skipped exporting 142 issues)
   - Silent data loss - no error message

5. **After clearing export_hashes and re-export**:
   - JSONL: 159 lines (missing 3 issues still)
   - DB: 159 issues
   - Still diverged from canonical

### The Bug Loop
Once export_hashes and JSONL diverge:
- Export skips issues already in export_hashes
- But those issues aren't actually in JSONL
- This creates corrupt JSONL with missing issues
- Auto-import can't detect the problem (file hash matches what was exported)
- Data is lost with no error messages

### Recovery Solution
Couldn't break the loop with export alone. Had to:
1. Copy .beads/beads.db from canonical repo
2. Clear export_hashes
3. Full re-export
4. Finally converged to 162 issues

### Key Learnings

1. **The bug is worse than we thought**: It can create corrupt exports (17 lines instead of 162!)

2. **Auto-import can't save you**: Once export is corrupt, auto-import just imports the corrupt data

3. **Silent failure**: No warnings, no errors, just missing issues

4. **Git operations trigger it**: git reset, git pull, etc. change JSONL without clearing export_hashes

5. **Import operations populate export_hashes**: Even manual imports update export_hashes, setting up future export failures

### Immediate Action Required

**DISABLE EXPORT DEDUPLICATION NOW**

This feature is fundamentally broken and causes data loss. Should be disabled until properly fixed.

Quick fix options:
- Set environment variable to disable feature
- Comment out shouldSkipExport check
- Always clear export_hashes before export
- Add validation that DB count == JSONL line count before allowing export

### Long-term Fix

Need Option 2 + 4 from proposed solutions:
1. Store JSONL file hash after every successful export
2. Before export, verify JSONL hash matches expected
3. If mismatch, log WARNING and clear export_hashes
4. Clear export_hashes on every import operation
5. Add git post-merge hook to clear export_hashes
6. Add `bd validate` command to detect divergence


Review and document rarely-used commands

Document use cases or consider deprecation for infrequently-used commands.

**Commands to review:**
1. bd rename-prefix - How often is this used? Document use cases
2. bd detect-pollution - Consider integrating into bd validate
3. bd migrate-hash-ids - One-time migration, keep but document as legacy

**For each command:**
- Document typical use cases
- Add examples to help text
- Consider if it should be a subcommand instead
- Add deprecation warning if appropriate

**Not changing:**
- duplicates ✓ (useful for data quality)
- repair-deps ✓ (useful for fixing broken refs)
- restore ✓ (critical for compacted issues)
- compact ✓ (performance feature)

**Deliverable:**
- Updated help text
- Documentation in ADVANCED.md
- Deprecation plan if needed


Auto-resolve all field conflicts in merge.go

Eliminate manual conflict resolution by adding deterministic auto-resolve rules for all fields:

- Priority: higher priority wins (lower number = more urgent)
- IssueType: local wins
- Notes: concatenate both sides with separator
- Title: side with latest updated_at on the issue wins
- Description: side with latest updated_at on the issue wins

Currently true conflicts (both sides changed same field to different values) fail sync. With this change, NO conflicts ever require manual resolution.

Location: internal/merge/merge.go
Parent issue: bd-3s8


Improve Beads test coverage from 46% to 80%


Benchmark Suite for Critical Operations

Extend existing benchmark suite with comprehensive benchmarks for critical operations at 10K-20K scale.

Existing benchmarks (keep these):
- cycle_bench_test.go - Cycle detection up to 5K issues (linear, tree, dense graphs)
- compact_bench_test.go - Compaction candidate queries (100 issues)
- internal/rpc/bench_test.go - Daemon vs direct mode comparison

New benchmarks to add in sqlite_bench_test.go (~10-12 total):
1. GetReadyWork - Simple, deep hierarchies, cross-linked (CRITICAL - not currently benchmarked)
2. SearchIssues - No filters, complex filters (CRITICAL - not currently benchmarked)
3. CreateIssue - Single issue creation
4. UpdateIssue - Status/priority/assignee changes
5. AddDependency - Extend to 10K/20K scale (currently only up to 5K)
6. JSONL Export - Full export performance
7. JSONL Import - Import performance

Scale levels:
- Large: 10K issues (5K open, 5K closed)
- XLarge: 20K issues (10K open, 10K closed)

Implementation:
- NEW FILE: internal/storage/sqlite/sqlite_bench_test.go
- Keep existing cycle_bench_test.go and compact_bench_test.go unchanged
- Build tag: //go:build bench
- Standard testing.B benchmarks
- b.ReportAllocs() for memory tracking
- Test both SQLite and JSONL-imported databases

Always generates CPU and memory profiles for analysis.


Add bd daemon --stop-all command to kill all daemon processes

Currently there's no easy way to stop all running bd daemon processes. Users must resort to pkill -f 'bd daemon' or similar shell commands.

Add a --stop-all flag to bd daemon that:
1. Finds all running bd daemon processes (not just the current repo's daemon)
2. Gracefully stops them all
3. Reports how many were stopped

This is useful when:
- Multiple daemons are running and causing race conditions
- User wants a clean slate before running bd sync
- Debugging daemon-related issues


Prefix detection treats embedded hyphens as prefix delimiters

The prefix detection logic in bd import incorrectly identifies issues like 'vc-baseline-test' and 'vc-92cl-gate-test' as having different prefixes ('vc-baseline-' and 'vc-92cl-gate-') instead of recognizing them as having the standard 'vc-' prefix with hyphenated suffixes.

This breaks import with error: 'prefix mismatch detected: database uses vc- but found issues with prefixes: [vc-92cl-gate- (1 issues) vc-baseline- (1 issues)]'

The prefix should be determined by the pattern: prefix is everything up to and including the first hyphen. The suffix can contain hyphens without being treated as part of the prefix.

Example problematic IDs:
- vc-baseline-test (detected as prefix: vc-baseline-)
- vc-92cl-gate-test (detected as prefix: vc-92cl-gate-)
- vc-test (correctly detected as prefix: vc-)

Impact: Users cannot use descriptive multi-part IDs without triggering false prefix mismatch errors.


Test worktree sync branch commit


PushSyncBranch may fail if worktree was cleaned up


Auto-invoke 3-way merge for JSONL conflicts

Currently when git pull encounters merge conflicts in .beads/issues.jsonl, the post-merge hook fails with an error message pointing users to manual resolution or the beads-merge tool.

This is a poor user experience - the conflict detection is working, but we should automatically invoke the advanced 3-way merging instead of just telling users about it.

**Current behavior:**
- Detect conflict markers in JSONL
- Display error with manual resolution options
- Exit with failure

**Desired behavior:**
- Detect conflict markers in JSONL
- Automatically invoke beads-merge 3-way merge
- Only fail if automatic merge cannot resolve the conflicts

**Reference:**
- beads-merge tool: https://github.com/neongreen/mono/tree/main/beads-merge
- Error occurs in post-merge hook during bd sync after git pull


Implement tombstone types and schema migration

Add tombstone fields to Issue struct (deleted_at, deleted_by, delete_reason, original_type), add StatusTombstone constant, create SQLite migration for new columns. Per design bd-2m7.


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.

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.


Add integration tests for bd reset command

End-to-end integration tests for the reset command.

## Test Scenarios

### Basic reset
1. Init beads, create some issues
2. Run bd reset --force
3. Verify .beads/ is fresh, issues gone

### Hard reset
1. Init beads, create issues, commit
2. Run bd reset --hard --force
3. Verify git history has reset commits

### Backup functionality
1. Init beads, create issues
2. Run bd reset --backup --force
3. Verify backup exists with correct contents
4. Verify main .beads/ is reset

### Dry run
1. Init beads, create issues
2. Run bd reset --dry-run
3. Verify nothing changed

### Confirmation prompt
1. Init beads
2. Run bd reset (no --force)
3. Verify prompts for confirmation
4. Test both y and n responses

## Location
tests/integration/reset_test.go or similar


Make two-clone workflow actually work (no hacks)

TestTwoCloneCollision proves beads CANNOT handle two independent clones filing issues simultaneously. This is the basic collaborative workflow and it must work cleanly.

Test location: beads_twoclone_test.go

The test creates two git clones, both file issues with same ID (test-1), --resolve-collisions remaps clone B's to test-2, but after sync:
- Clone A has test-1="Issue from clone A", test-2="Issue from clone B"
- Clone B has test-1="Issue from clone B", test-2="Issue from clone A"

The TITLES are swapped! Both clones have 2 issues but with opposite title assignments.

We've tried many fixes (per-project daemons, auto-sync, lamport hashing, precommit hooks) but nothing has made the test pass.

Goal: Make the test pass WITHOUT hacks. The two clones should converge to identical state after sync.

**Major progress achieved!** The two-clone workflow now converges correctly.

**What was fixed:**
--3d844c58: Implemented content-hash based rename detection
- bd-64c05d00.1: Fixed test to compare content not timestamps
- Both clones now converge to identical issue databases
- test-1 and test-2 have correct titles in both clones
- No more title swapping!

**Current status (VERIFIED):**
✅ Acceptance criteria 1: TestTwoCloneCollision passes (confirmed Oct 28)
✅ Acceptance criteria 2: Both clones converge to identical issue database (content matches)
✅ Acceptance criteria 3: No manual conflict resolution required (automatic)
✅ Acceptance criteria 4: Git status clean
✅ Acceptance criteria 5: bd ready output identical (timestamps are expected difference)

**ALL ACCEPTANCE CRITERIA MET!** This issue is complete and can be closed.


Add integration tests for adapter library

Test suite for beads_mail_adapter.py covering all scenarios.

Acceptance Criteria:
- Test enabled mode (server available)
- Test disabled mode (server unavailable)
- Test graceful degradation (server dies mid-operation)
- Test reservation conflicts
- Test message sending/receiving
- Mock HTTP server for testing
- 90%+ code coverage

File: lib/test_beads_mail_adapter.py

Test suite completed with 29 comprehensive tests covering:
- Enabled mode (server available): 10 tests
- Disabled mode (server unavailable): 2 tests
- Graceful degradation: 4 tests
- Reservation conflicts: 2 tests
- Configuration: 5 tests
- Health check scenarios: 3 tests
- HTTP error handling: 3 tests

**Performance**: All tests run in 10ms (fast!)

**Coverage highlights**:
✅ Server health checks (ok, degraded, error, timeout)
✅ All API operations (reserve, release, notify, check_inbox, get_reservations)
✅ HTTP errors (404, 409 conflict, 500, 503)
✅ Network errors (timeout, connection refused)
✅ Malformed responses (bad JSON, empty body, plain text errors)
✅ Environment variable configuration
✅ Graceful degradation when server dies mid-operation
✅ Conflict handling with both JSON and plain text errors
✅ Dict wrapper responses ({"messages": [...]} and {"reservations": [...]})
✅ Custom TTL for reservations
✅ Default agent name fallback

No external dependencies, no slow integration tests, just fast unit tests with mocks.


Auth tokens expire too quickly

## Summary

[Brief description of the bug]

## Steps to Reproduce

1. Step 1
2. Step 2
3. Step 3

## Expected Behavior

[What should happen]

## Actual Behavior

[What actually happens]

## Environment

- OS: [e.g., macOS 15.7.1]
- Version: [e.g., bd 0.20.1]
- Additional context: [any relevant details]

## Additional Context

[Screenshots, logs, or other relevant information]


Add security tests for file path validation in clean command

Test coverage gap identified by automated analysis (vc-217).

**Original Issue:** [deleted:bd-da96-baseline-lint]

In cmd/bd/clean.go:118, os.Open(gitignorePath) is flagged by gosec G304 for potential file inclusion via variable without validation.

Add tests covering:
- Path traversal attempts (../../etc/passwd)
- Absolute paths outside project directory
- Symlink following behavior
- Non-existent file handling
- Validation that only .gitignore files in valid locations are opened

This is a security-sensitive code path that needs validation to prevent unauthorized file access.

_This issue was automatically created by AI test coverage analysis._


Test database naming


Fix MCP tools failing to load in Claude Code (GH#346)

Fix FastMCP schema generation bug by refactoring `Issue` model to avoid recursion.

    - Refactored `Issue` in `models.py` to use `LinkedIssue` for dependencies/dependents.
    - Restored missing `Mail*` models to `models.py`.
    - Fixed `tests/test_mail.py` mock assertion.

    Fixes GH#346.


Detect and warn about outdated git hooks

Users may have outdated git hooks installed that reference removed flags (e.g., --resolve-collisions). bd should detect this and warn users to reinstall.


Optimize --sandbox mode: skip FlushManager

When `--sandbox` mode is enabled, skip FlushManager creation entirely.

## Current Behavior
`main.go:561` always creates FlushManager:
```go
flushManager = NewFlushManager(autoFlushEnabled, getDebounceDuration())
```

Even with `--sandbox` (which sets `noAutoFlush=true`), the FlushManager goroutine is still running.

## Proposed Change
```go
// Only create FlushManager if we might actually need it
if !sandboxMode && autoFlushEnabled {
    flushManager = NewFlushManager(true, getDebounceDuration())
}
```

## Handle nil FlushManager
Update PersistentPostRun to handle nil:
```go
if flushManager != nil && !skipFinalFlush {
    if err := flushManager.Shutdown(); err != nil {
        // ...
    }
}
```

## Synchronous Export Fallback
When sandbox mode + write operation occurs, do synchronous export:
```go
if sandboxMode && isDirty {
    performSynchronousExport()
}
```

## Also: Default lock-timeout to 100ms in sandbox mode
In PersistentPreRun, when sandboxMode is detected:
```go
if sandboxMode {
    noDaemon = true
    noAutoFlush = true
    noAutoImport = true
    if lockTimeout == 30*time.Second { // Not explicitly set
        lockTimeout = 100 * time.Millisecond
    }
}
```

Part of bd-olc1


Document sandbox workarounds for GH #353

Add documentation for sandbox troubleshooting and new flags.

**Tasks:**
1. Create or update TROUBLESHOOTING.md with sandbox section
2. Document new flags in CLI reference
3. Add comment to GH #353 with immediate workarounds

**Content needed:**
- Symptoms of daemon lock issues in sandboxed environments
- Usage guide for --sandbox, --force, and --allow-stale flags
- Step-by-step troubleshooting for Codex users
- Examples of each escape hatch

**Files to update:**
- docs/TROUBLESHOOTING.md (create if needed)
- docs/CLI_REFERENCE.md or README.md
- GitHub issue #353

**References:**
- docs/GH353_INVESTIGATION.md (lines 240-276)


Use RAM disk for Windows CI tests

Windows CI tests time out due to slow filesystem I/O. Even with splitting into parallel jobs, each package set takes >30 minutes.

Implement a RAM disk solution using ImDisk or similar on Windows GitHub Actions runners to speed up tests.

References:
- https://github.com/actions/runner-images/issues/intlharry has examples
- ImDisk can create RAM disks on Windows
- Alternative: use D: drive which may be faster on some runners


Refactor daemon.go into internal/daemonrunner

Extract daemon runtime from daemon.go (1,565 lines) into internal/daemonrunner with focused modules: config.go, daemon.go, process.go, rpc_server.go, sync.go, git.go. Keep cobra command thin.


Update documentation after code health cleanup

Update all documentation to reflect code structure changes after cleanup phases complete.

Documentation to update:
1. **AGENTS.md** - Update file structure references
2. **CONTRIBUTING.md** (if exists) - Update build/test instructions
3. **Code comments** - Update any outdated references
4. **Package documentation** - Update godoc for reorganized packages

New documentation to add:
1. **internal/util/README.md** - Document shared utilities
2. **internal/debug/README.md** - Document debug logging
3. **internal/rpc/README.md** - Document new file organization
4. **internal/storage/sqlite/migrations/README.md** - Migration system docs

Impact: Keeps documentation in sync with code


Add is_tombstone flag to schema

Optionally add is_tombstone boolean field to issues table. Marks resurrected parents that were deleted. Allows distinguishing tombstones from normal deleted issues. Update schema.go and create migration.


Blocking issue


Clarify JSONL size bounds with multi-repo

The contributor-workflow-analysis.md states (line 226): 'Keep beads.jsonl small enough for agents to read (<25k)'

With multi-repo hydration, it's unclear whether this bound applies to:
- Each individual JSONL file (likely intention)
- The total hydrated size across all repos (unclear)
- Both (most conservative)

Clarification needed because:
- VC monitors .beads/issues.jsonl size to stay under limit
- With multi-repo, VC needs to know if each additional repo also has 25k limit
- Agents reading hydrated data need to know total size bounds
- Performance characteristics depend on total vs per-repo limits

Example scenario:
- Primary repo: 20k JSONL
- Planning repo: 15k JSONL
- Total hydrated: 35k
Is this acceptable or does it violate the <25k principle?


Add MCP server functions for repair commands

Expose new repair commands via MCP server for agent access:

Functions to add:
- beads_repair_deps()
- beads_detect_pollution()
- beads_validate()
- beads_resolve_conflicts() (when implemented)

Update integrations/beads-mcp/src/beads_mcp/server.py

See repair_commands.md lines 803-884 for design.


Batch test 1


Refactor sqlite.go into focused modules

Split sqlite.go (2,298 lines) into focused modules: migrations.go, ids.go, issues.go, events.go, dirty.go, db.go. This will improve maintainability and reduce cognitive load.


Add comprehensive tests for show.go commands (show, update, edit, close)

Need to add tests for cmd/bd/show.go which contains show, update, edit, and close commands.

**Challenge**: The existing test patterns use rootCmd.SetArgs() and rootCmd.Execute(), but the global `store` variable needs to match what the commands use. Initial attempt created tests that failed with "no issue found" because the test's store instance wasn't the same as the command's store.

**Files to test**:
- show.go (contains showCmd, updateCmd, editCmd, closeCmd)

**Coverage needed**:
- show command (single issue, multiple issues, JSON output, with dependencies, with labels, with compaction)
- update command (status, priority, title, assignee, description, multiple fields, multiple issues)
- edit command (requires $EDITOR, may need mocking)
- close command (single issue, multiple issues, with reason, JSON output)

**Test approach**:
1. Study working test patterns in init_test.go, list_test.go, etc.
2. Ensure BEADS_NO_DAEMON=1 is set
3. Properly initialize database with bd init
4. Use the command's global store, not a separate instance
5. May need to reset global state between tests

**Success criteria**:
- All test functions pass
- Coverage for show.go increases significantly
- Tests follow existing patterns in cmd/bd/*_test.go


MCP server loses workspace context after Amp restart - causes silent failures

**CRITICAL BUG**: The beads MCP server loses workspace context when Amp restarts, leading to silent failures and potential data corruption.

## Reproduction
1. Start Amp with beads MCP server configured
2. Call `mcp__beads__set_context(workspace_root="/path/to/project")`
3. Use MCP tools successfully (e.g., `mcp__beads__show`, `mcp__beads__list`)
4. Restart Amp (new thread/session)
5. Try to use MCP tools without calling `set_context` again
6. **Result**: "Not connected" or "No workspace set" errors

## Impact
- Amp agents silently fail when trying to read/update beads issues
- May attempt to create duplicate issues because they can't see existing ones
- Potential for data corruption if operating on wrong database
- Breaks multi-session workflows
- Creates confusion: CLI works (`./bd`) but MCP tools don't

## Current Workaround
Manually call `mcp__beads__set_context()` at start of every Amp session.

## Root Cause
MCP server is stateful and doesn't persist workspace context across restarts.

## Proposed Fix
**Option 1 (Best)**: Auto-detect workspace from current working directory
- Match behavior of CLI `./bd` commands
- Check for `.beads/` directory in current dir or parents
- No manual context setting needed

**Option 2**: Persist context in MCP server state file
- Save last workspace_root to `~/.config/beads/mcp_context.json`
- Restore on server startup

**Option 3**: Require explicit context in every MCP call
- Add optional `workspace_root` parameter to all MCP tools
- Fall back to saved context if not provided

Acceptance:
- MCP tools work across Amp restarts without manual set_context()
- Auto-detection matches CLI behavior (walks up from CWD)
- Clear error message when no workspace found
- set_context() still works for explicit override
- BEADS_WORKING_DIR env var support
- Integration test validates restart behavior


Test issue 1


Implement TryResurrectParent function

Create internal/storage/sqlite/resurrection.go with TryResurrectParent(ctx, parentID) function. Parse JSONL history to find deleted parent, create tombstone with status=deleted and is_tombstone=true flag. Handle recursive resurrection for multi-level missing parents (bd-abc.1.2 with missing bd-abc and bd-abc.1).


bd doctor: per-fix confirmation mode

Add an interactive mode where users can approve/reject each fix individually rather than all-or-nothing. Currently --fix prompts once for all fixes. This would give users more control over which repairs to apply, especially useful when some fixes may have side effects.


Improve clean vs cleanup command naming/documentation

Clarify the difference between bd clean and bd cleanup to reduce user confusion.

**Current state:**
- bd clean: Remove temporary artifacts (.beads/bd.sock, logs, etc.)
- bd cleanup: Delete old closed issues from database

**Options:**
1. Rename for clarity:
   - bd clean → bd clean-temp
   - bd cleanup → bd cleanup-issues

2. Keep names but improve help text and documentation

3. Add prominent warnings in help output

**Preferred approach:** Option 2 (improve documentation)
- Update short/long descriptions in commands
- Add examples to help text
- Update README.md
- Add cross-references in help output

**Files to modify:**
- cmd/bd/clean.go
- cmd/bd/cleanup.go
- README.md or ADVANCED.md


Create architecture decision record (ADR)

Document why we chose Agent Mail, alternatives considered, and tradeoffs.

Acceptance Criteria:
- Problem statement (git traffic, no locks)
- Alternatives considered (custom RPC, Redis, etc.)
- Why Agent Mail fits Beads
- Integration principles (optional, graceful degradation)
- Future considerations

File: docs/adr/002-agent-mail-integration.md


Test parent issue


Fix transaction conflict in TryResurrectParent

Integration test TestImportWithDeletedParent fails with 'database is locked' error when resurrection happens inside CreateIssue.

Root cause: TryResurrectParent calls conn.Get() and insertIssue() which conflicts with existing transaction in CreateIssue.

Error: failed to create tombstone for parent bd-parent: failed to insert issue: sqlite3: database is locked

Solution: Refactor resurrection to accept optional transaction parameter, use existing transaction when available instead of creating new connection.

Impact: Blocks resurrection from working in CreateIssue flow, only works in EnsureIDs (which may not have active transaction).


Fix autoimport tests for content-hash collision scoring

## Overview
Three autoimport tests are failing after [deleted:bd-cbed9619.4] because they expect behavior based on the old reference-counting collision resolution, but the system now uses deterministic content-hash scoring.

## Failing Tests
1. `TestAutoImportMultipleCollisionsRemapped` - expects local versions preserved
2. `TestAutoImportAllCollisionsRemapped` - expects local versions preserved
3. `TestAutoImportCollisionRemapMultipleFields` - expects specific collision resolution behavior

## Root Cause
These tests were written when ScoreCollisions used reference counting to determine which version to keep. Now it uses content-hash comparison (introduced in commit 2e87329), which produces different but deterministic results.

## Example
Old behavior: Issue with more references would be kept
New behavior: Issue with lexicographically lower content hash is kept

## Solution
Update each test to:
1. Verify the new content-hash based behavior is correct
2. Check that the remapped issue (not necessarily local/remote) has the expected content
3. Ensure dependencies are preserved on the correct remapped issue

## Acceptance Criteria
- All three autoimport tests pass
- Tests verify content-hash determinism (same collision always resolves the same way)
- Tests check dependency preservation on remapped issues
- Test documentation explains content-hash scoring expectations

## Files to Modify
- `cmd/bd/autoimport_collision_test.go`

## Testing
Run: `go test ./cmd/bd -run "TestAutoImport.*Collision" -v`


Add fsnotify dependency to go.mod


Stress test: event storm handling

Simulate 100+ rapid JSONL writes. Verify debouncer batches to single import. Verify no data loss. Test daemon stability.


Issue with labels


Safety check tests use string(rune()) which only works for single digits


Create Docker image for Agent Mail

Containerize Agent Mail server for easy deployment.

Acceptance Criteria:
- Dockerfile with Python 3.14
- Health check endpoint
- Volume mount for storage
- Environment variable configuration
- Multi-arch builds (amd64, arm64)

File: deployment/agent-mail/Dockerfile


bd-hv01: Deletion tracking production readiness

Epic to track all improvements and fixes needed to make the deletion tracking implementation ([deleted:bd-hv01]) production-ready.

The core 3-way merge algorithm is sound, but there are critical issues around atomicity, error handling, and edge cases that need to be addressed before this can be safely used in production.

Critical path (P1):
- Non-atomic snapshot operations
- Brittle JSON string comparison
- Silent partial deletion failures
- Race conditions in concurrent scenarios

Follow-up work (P2-P3):
- Test coverage for edge cases and multi-repo mode
- Performance optimizations
- Code refactoring and observability

Related commit: 708a81c


Update LINTING.md with current baseline

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


bd doctor --fix needs non-interactive mode (-y/--yes flag)

When running `bd doctor --fix` in non-interactive mode (scripts, CI, Claude Code), it prompts 'Continue? (Y/n):' and fails with EOF.

**Expected**: A `-y` or `--yes` flag to auto-confirm fixes.

**Workaround**: Currently have to run `bd init` instead, but that's not discoverable from the doctor output.


Pre-push hook and daemon export produce different JSONL

After committing and pushing, git status shows .beads/beads.jsonl as dirty. Investigation shows:

1. Pre-push hook ran successfully and exported DB → JSONL
2. Push completed
3. Shortly after, daemon exported DB → JSONL again with different content
4. Diff shows comments added to old issues (bd-23a8, bd-6049, bd-87a0)

Timeline:
- Commit c731c45 "Update beads JSONL"
- Pre-push hook exported JSONL
- Push succeeded
- Daemon PID 33314 exported again with different content

Questions:
1. Did someone run a command between commit and daemon export?
2. Is there a timing issue where pre-push hook doesn't capture all DB changes?
3. Should pre-commit hook flush daemon changes before committing?

The comments appear to be from Nov 5 (created_at: 2025-11-05T08:38:46Z) but are only appearing in JSONL now. This suggests the DB had these comments but they weren't exported during pre-push.

Possible causes:
- Pre-push hook uses BEADS_NO_DAEMON=1 which might skip pending writes
- Daemon has unflushed changes in memory
- Race condition between pre-push export and daemon's periodic export

Improved fix based on oracle code review:
1. Pre-push now flushes pending changes first (prevents debounce race)
2. Uses git status --porcelain to catch all change types
3. Handles both beads.jsonl and issues.jsonl
4. Works even if bd not installed (git-only check)


Reduce sync rounds in multiclone tests

Analyze and reduce the number of sync rounds in hash multiclone tests.

Current state:
- TestHashIDs_MultiCloneConverge: 1 round of syncs across 3 clones
- TestHashIDs_IdenticalContentDedup: 2 rounds across 2 clones

Investigation needed:
- Profile to see how much time each sync takes
- Determine minimum rounds needed for convergence
- Consider making rounds configurable via env var

File: beads_hash_multiclone_test.go:70, :132


Publish official devcontainer configuration

Devcontainer configuration implemented. Manual testing required in actual devcontainer environment (Codespaces or VSCode Remote Containers). All code changes complete, tests pass, linting clean.


CLI accepts both hash IDs and aliases

Update all CLI commands to accept both hash IDs (bd-af78e9a2) and aliases (#42, or just 42).

## Parsing Logic
```go
// internal/utils/id_parser.go
func ParseIssueID(input string) (issueID string, err error) {
    // Hash ID: bd-af78e9a2
    if strings.HasPrefix(input, "bd-") {
        return input, nil
    }

    // Alias: #42 or 42
    aliasStr := strings.TrimPrefix(input, "#")
    alias, err := strconv.Atoi(aliasStr)
    if err != nil {
        return "", fmt.Errorf("invalid issue ID: %s", input)
    }

    // Resolve alias to hash ID
    return storage.GetIssueIDByAlias(alias)
}
```

## Commands to Update
All commands that accept issue IDs:

### 1. bd show
```bash
bd show bd-af78e9a2        # Hash ID
bd show #42                # Alias
bd show 42                 # Alias (shorthand)
bd show bd-af78e9a2 #42    # Mixed (multiple IDs)
```

### 2. bd update
```bash
bd update #42 --status in_progress
bd update bd-af78e9a2 --priority 1
```

### 3. bd close
```bash
bd close #42 --reason "Done"
```

### 4. bd dep add/tree
```bash
bd dep add #42 #1 --type blocks
bd dep tree bd-af78e9a2
```

### 5. bd label add/remove
```bash
bd label add #42 critical
```

### 6. bd merge
```bash
bd merge #42 #43 --into #41
```

## Display Format
Default to showing aliases in output:
```bash
$ bd list
#1   Fix authentication bug          P1  open
#2   Add logging to daemon           P2  open
#42  Investigate jujutsu integration P3  open
```

With `--format=hash` flag:
```bash
$ bd list --format=hash
bd-af78e9a2  Fix authentication bug          P1  open
bd-e5f6a7b8  Add logging to daemon           P2  open
bd-1a2b3c4d  Investigate jujutsu integration P3  open
```

## Files to Modify
- internal/utils/id_parser.go (new)
- cmd/bd/show.go
- cmd/bd/update.go
- cmd/bd/close.go
- cmd/bd/reopen.go
- cmd/bd/dep.go
- cmd/bd/label.go
- cmd/bd/merge.go
- cmd/bd/list.go (add --format flag)

## Testing
- Test hash ID parsing
- Test alias parsing (#42, 42)
- Test mixed IDs in single command
- Test error on invalid ID
- Test alias resolution failure


Daemon race condition: stale export overwrites recent DB changes

**Symptom:**
Merged bd-fc2d into bd-fb05 in ~/src/beads (commit ce4d756), pushed to remote. The ~/src/fred/beads daemon then exported its stale DB state and committed (8cc1bb4), reverting bd-fc2d back to "open" status.

**Timeline:**
1. 21:45:12 - Merge committed from ~/src/beads (ce4d756): bd-fc2d closed
2. 21:49:42 - Daemon in ~/src/fred/beads exported stale state (8cc1bb4): bd-fc2d open again

**Root cause:**
The fred/beads daemon had a stale database (bd-fc2d still open) and didn't auto-import the newer JSONL before exporting. When it exported, it overwrote the merge with its stale state.

**Expected behavior:**
Daemon should detect that JSONL is newer than its last export and import before exporting.

**Actual behavior:**
Daemon exported stale DB state, creating a conflicting commit that reverted upstream changes.

**Impact:**
Multi-workspace setups with daemons can silently lose changes if one daemon has stale state and exports.


Registry has cross-process race condition

The global daemon registry (~/.beads/registry.json) can be corrupted when multiple daemons from different workspaces write simultaneously.

**Root cause:**
- Registry uses an in-process mutex but no file-level locking
- Register() and Unregister() release the mutex between read and write
- Multiple daemon processes can interleave their read-modify-write cycles

**Evidence:**
Found registry.json with double closing bracket: `]]` instead of `]`

**Fix options:**
1. Use file locking (flock/fcntl) around the entire read-modify-write cycle
2. Use atomic write pattern (write to temp file, rename)
3. Both (belt and suspenders)

**Files:**
- internal/daemon/registry.go:46-64 (readEntries)
- internal/daemon/registry.go:67-87 (writeEntries)
- internal/daemon/registry.go:90-108 (Register - the race window)


Complete and commit beads-mcp type safety improvements

Uncommitted type safety work in beads-mcp needs review and completion


Add bd migrate-tombstones command

Create migration command to convert existing deletions.jsonl entries to tombstones. Support --dry-run and --verbose flags. Archive old file with .migrated suffix. Per design bd-dli.


Run bd doctor at end of bd init to verify setup

Run bd doctor diagnostics at end of bd init (after line 398 in init.go). If issues found, warn user immediately: '⚠ Setup incomplete. Run bd doctor --fix to complete setup.' Catches configuration problems before user encounters them in normal workflow.


Test Epic

## Overview

[Describe the high-level goal and scope of this epic]

## Success Criteria

- [ ] Criteria 1
- [ ] Criteria 2
- [ ] Criteria 3

## Background

[Provide context and motivation]

## Scope

**In Scope:**
- Item 1
- Item 2

**Out of Scope:**
- Item 1
- Item 2


Measure notification latency vs git sync

Benchmark end-to-end latency for status updates to propagate between agents using both methods.

Acceptance Criteria:
- Measure git sync latency (commit → push → pull → import)
- Measure Agent Mail latency (send_message → fetch_inbox)
- Document latency distribution (p50, p95, p99)
- Verify <100ms claim for Agent Mail
- Compare against 1-5s baseline for git

Success Metric: Agent Mail latency < 100ms, git sync latency > 1000ms

Latency benchmark completed. Results documented in latency_results.md:
- Git sync: 2000-5000ms (full cycle with network)
- Agent Mail: <100ms (HTTP API round-trip)
- Confirms 20-50x latency reduction claim


Publish @beads/bd package to npm registry

Publish the npm package to the public npm registry:

## Prerequisites
- npm account created
- Organization @beads created (or use different namespace)
- npm login completed locally
- Package tested locally (bd-f282 completed)

## Publishing steps
1. Verify package.json version matches current bd version
2. Run npm pack and inspect tarball contents
3. Test installation from tarball one more time
4. Run npm publish --access public
5. Verify package appears on https://www.npmjs.com/package/@beads/bd
6. Test installation from registry: npm install -g @beads/bd

## Post-publish
- Add npm badge to README.md
- Update CHANGELOG.md with npm package release
- Announce in release notes

## Note
- May need to choose different name if @beads namespace unavailable
- Alternative: beads-cli, bd-cli, or unscoped beads-issue-tracker


Auto-detect scenarios and prompt users

Detect when user is in fork/contributor scenario and prompt with helpful suggestions. Check: git remote relationships, existing .beads config, repo ownership. Suggest appropriate wizard.


Prevent test pollution in production database

The bd-vxdr cleanup revealed test issues were created during manual testing in the production workspace (Nov 2-4, template feature development).

**Root cause:** Manual testing with `./bd create "Test issue"` pollutes the production .beads database.

**Prevention strategies:**
1. Use TEST_DB environment variable for manual testing
2. Add warning when creating issues with "Test" prefix
3. Improve developer docs about testing workflow
4. Consider adding `bd test-mode` command for isolated testing

**Implementation completed:**

1. ✅ Added warning when creating issues with "Test" prefix in production database
   - Shows yellow warning with ⚠ symbol
   - Suggests using BEADS_DB for isolated testing
   - Warning appears in create.go after title validation

2. ✅ Documented BEADS_DB testing workflow in AGENTS.md
   - Added "Testing Workflow" section in Development Guidelines
   - Includes manual testing examples with BEADS_DB
   - Includes automated testing examples with t.TempDir()
   - Clear warning about not polluting production database

3. ⚠ Decided against bd test-mode command
   - BEADS_DB already provides simple, flexible isolation
   - Additional command would add complexity without much benefit
   - Current approach follows Unix philosophy (env vars for config)

**Files modified:**
- cmd/bd/create.go - Added Test prefix warning
- AGENTS.md - Added Testing Workflow section

**Testing:**
- Verified warning appears when creating "Test" prefix issues
- Verified BEADS_DB isolation works correctly
- Built successfully with `go build`


Add tests for daemon sync module


Configure database connection pool limits for daemon mode

Database connection pool not configured for file-based databases when running in daemon mode.

Location: internal/storage/sqlite/sqlite.go:108-116

Problem:
- Daemon is a long-running server handling concurrent RPC requests
- Multiple CLI commands hit same daemon simultaneously
- Go default: unlimited connections (MaxOpenConns=0)
- SQLite IMMEDIATE transactions serialize on write lock
- Can have 100+ goroutines blocked waiting, each holding connection
- Results in connection exhaustion and 'database is locked' errors

Current code only limits in-memory DBs:
if isInMemory {
    db.SetMaxOpenConns(1)
    db.SetMaxIdleConns(1)
}
// File DBs: UNLIMITED connections!

Fix:
if !isInMemory {
    maxConns := runtime.NumCPU() + 1  // 1 writer + N readers
    db.SetMaxOpenConns(maxConns)
    db.SetMaxIdleConns(2)
    db.SetConnMaxLifetime(0)
}

Impact: 'database is locked' errors under concurrent load in daemon mode

Note: NOT an issue for direct CLI usage (each process isolated). Only affects daemon mode where multiple CLI commands share one database pool.

Effort: 1 hour


CRITICAL: bd sync exports before pull, allowing stale DB to corrupt JSONL statuses

## Root Cause

The fix in bd-53c (reverse ZFC check) only checks COUNTS, not content. The real corruption happens when:

1. Polecat A has stale DB with old status values (e.g., status=closed for issues that are now open on remote)
2. Polecat A runs bd sync:
   - **Export FIRST**: DB (status=closed) → JSONL (overwrites correct status=open)
   - Commit: Stale JSONL committed
   - Pull: 3-way merge with remote
   - Merge uses 'closed wins' rule → status stays closed
3. Polecat A pushes → Remote now corrupted with status=closed

## Why bd-53c didn't fix it

The reverse ZFC check compares COUNTS:
```go
if jsonlCount > dbCount  // Only catches count mismatch
```

But in the corruption scenario:
- JSONL count = 5, DB count = 5 (same count!)
- Only the STATUS field differs

## The Real Fix

**PULL BEFORE EXPORT**. The sync order must be:
1. Pull from remote (get latest state)
2. Import merged JSONL to DB
3. THEN export DB changes (if any)

Current order is: Export → Commit → Pull → Import → Push

This is a fundamental architecture change to the sync flow.

## Workaround

Use --no-auto-flush and manually control the sync order, or disable daemon auto-export.

## Evidence

From user investigation:
- At 595b7943 (13:20:30): 5 open issues
- At 10239812 (13:28:39): 0 open issues
- All 5 issues had their status changed from open to closed
- Count stayed at 5 (not a deletion issue)


Add TestNWayCollision for 5+ clones

## Overview
Add comprehensive tests for N-way (5+) collision resolution to verify the solution scales beyond 3 clones.

## Purpose
While TestThreeCloneCollision validates the basic N-way case, we need to verify:
1. Solution scales to arbitrary N
2. Performance is acceptable with more clones
3. Convergence time is bounded
4. No edge cases in larger collision groups

## Implementation Tasks

### 1. Create TestFiveCloneCollision
File: beads_twoclone_test.go (or new beads_nway_test.go)

```go
func TestFiveCloneCollision(t *testing.T) {
    // Test with 5 clones creating same ID with different content
    // Verify all 5 clones converge after sync rounds

    t.Run("SequentialSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "A", "B", "C", "D", "E")
    })

    t.Run("ReverseSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "E", "D", "C", "B", "A")
    })

    t.Run("RandomSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "C", "A", "E", "B", "D")
    })
}
```

### 2. Implement generalized testNCloneCollision
Generalize the 3-clone test to handle arbitrary N:

```go
func testNCloneCollision(t *testing.T, numClones int, syncOrder ...string) {
    t.Helper()

    if len(syncOrder) != numClones {
        t.Fatalf("syncOrder length (%d) must match numClones (%d)",
            len(syncOrder), numClones)
    }

    tmpDir := t.TempDir()

    // Setup remote and N clones
    remoteDir := setupBareRepo(t, tmpDir)
    cloneDirs := make(map[string]string)

    for i := 0; i < numClones; i++ {
        name := string(rune('A' + i))
        cloneDirs[name] = setupClone(t, tmpDir, remoteDir, name)
    }

    // Each clone creates issue with same ID but different content
    for name, dir := range cloneDirs {
        createIssue(t, dir, fmt.Sprintf("Issue from clone %s", name))
    }

    // Sync in specified order
    for _, name := range syncOrder {
        syncClone(t, cloneDirs[name], name)
    }

    // Final pull for convergence
    for name, dir := range cloneDirs {
        finalPull(t, dir, name)
    }

    // Verify all clones have all N issues
    expectedTitles := make(map[string]bool)
    for i := 0; i < numClones; i++ {
        name := string(rune('A' + i))
        expectedTitles[fmt.Sprintf("Issue from clone %s", name)] = true
    }

    for name, dir := range cloneDirs {
        titles := getTitles(t, dir)
        if !compareTitleSets(titles, expectedTitles) {
            t.Errorf("Clone %s missing issues: expected %v, got %v",
                name, expectedTitles, titles)
        }
    }

    t.Log("✓ All", numClones, "clones converged successfully")
}
```

### 3. Add performance benchmarks
Test convergence time and memory usage:

```go
func BenchmarkNWayCollision(b *testing.B) {
    for _, n := range []int{3, 5, 10, 20} {
        b.Run(fmt.Sprintf("N=%d", n), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                // Run N-way collision and measure time
                testNCloneCollisionBench(b, n)
            }
        })
    }
}
```

### 4. Add convergence time tests
Verify bounded convergence:

```go
func TestConvergenceTime(t *testing.T) {
    // Test that convergence happens within expected rounds
    // For N clones, should converge in at most N-1 sync rounds

    for n := 3; n <= 10; n++ {
        t.Run(fmt.Sprintf("N=%d", n), func(t *testing.T) {
            rounds := measureConvergenceRounds(t, n)
            maxExpected := n - 1
            if rounds > maxExpected {
                t.Errorf("Convergence took %d rounds, expected ≤ %d",
                    rounds, maxExpected)
            }
        })
    }
}
```

### 5. Add edge case tests
Test boundary conditions:
- All N clones have identical content (dedup works)
- N-1 clones have same content, 1 differs
- All N clones have unique content
- Mix of collisions and non-collisions

## Acceptance Criteria
- TestFiveCloneCollision passes with all sync orders
- All 5 clones converge to identical content
- Performance is acceptable (< 5 seconds for 5 clones)
- Convergence time is bounded (≤ N-1 rounds)
- Edge cases handled correctly
- Benchmarks show scalability to 10+ clones

## Files to Create/Modify
- beads_twoclone_test.go or beads_nway_test.go
- Add helper functions for N-clone setup

## Testing Strategy

### Test Matrix
| N Clones | Sync Orders | Expected Result |
|----------|-------------|-----------------|
| 3        | A→B→C       | Pass            |
| 3        | C→B→A       | Pass            |
| 5        | A→B→C→D→E   | Pass            |
| 5        | E→D→C→B→A   | Pass            |
| 5        | Random      | Pass            |
| 10       | Sequential  | Pass            |

### Performance Targets
- 3 clones: < 2 seconds
- 5 clones: < 5 seconds
- 10 clones: < 15 seconds

## Dependencies
- Requires bd-cbed9619.5, bd-cbed9619.4, bd-cbed9619.3, bd-dcd6f14b to be completed
- TestThreeCloneCollision must pass first

## Success Metrics
- All tests pass for N ∈ {3, 5, 10}
- Convergence time scales linearly (O(N))
- Memory usage reasonable (< 100MB for 10 clones)
- No data corruption or loss in any scenario


Migration and onboarding for multi-repo

Create migration tools, wizards, and documentation to help users adopt multi-repo workflow, with special focus on OSS contributor onboarding and team adoption scenarios.


Create Python adapter library

Create beads_mail_adapter.py library that wraps Agent Mail HTTP calls with health checks and graceful degradation.

Acceptance Criteria:
- AgentMailAdapter class with health check on init
- enabled flag auto-disables if server unreachable
- All methods wrapped in try/catch (non-blocking failures)
- Methods: reserve_issue(), release_issue(), notify(), check_inbox()
- Environment-based configuration (AGENT_MAIL_URL, AGENT_MAIL_TOKEN)
- Unit tests for enabled/disabled modes

File: lib/beads_mail_adapter.py


Second worktree test


Fix inconsistent error handling in multi-repo deletion tracking

From bd-xo6b code review: Multi-repo deletion tracking has mixed failure modes that can leave system in inconsistent state.

**Current behavior (daemon_sync.go):**
- Snapshot capture (L505-514): Hard fail → aborts sync
- Merge/prune (L575-584): Hard fail → aborts sync
- Base snapshot update (L613-619): Soft fail → logs warning, continues

**Critical problem:**
If merge fails on repo 3 of 5:
- Repos 1-2 have already merged and deleted issues (irreversible)
- Repos 3-5 are untouched
- Database is in partially-updated state
- No rollback mechanism

**Real-world scenario:**
```
Sync with repos [A, B, C]:
1. Capture snapshots A ✓, B ✓, C ✗ → ABORT (good)
2. Merge A ✓, B ✗ → ABORT but A already deleted issues (BAD - no rollback)
3. Update base A ⚠, B ⚠ → Warnings only (inconsistent with 1 & 2)
```

**Solution options:**
1. **Two-phase commit:**
   - Phase 1: Validate all repos (check files exist, readable, parseable)
   - Phase 2: Apply changes atomically (or fail entirely before any mutations)

2. **Fail-fast validation:**
   - Before any snapshot/merge operations, validate all repos upfront
   - Abort entire sync if any repo fails validation

3. **Make base snapshot update consistent:**
   - Either make it hard-fail like the others, or make all soft-fail

**Files:**
- cmd/bd/daemon_sync.go:505-514 (snapshot capture)
- cmd/bd/daemon_sync.go:575-584 (merge/prune)
- cmd/bd/daemon_sync.go:613-619 (base snapshot update)

**Recommendation:** Use option 1 (two-phase) or option 2 (fail-fast validation) + fix base snapshot inconsistency.


Add TestNWayCollision for 5+ clones

## Overview
Add comprehensive tests for N-way (5+) collision resolution to verify the solution scales beyond 3 clones.

## Purpose
While TestThreeCloneCollision validates the basic N-way case, we need to verify:
1. Solution scales to arbitrary N
2. Performance is acceptable with more clones
3. Convergence time is bounded
4. No edge cases in larger collision groups

## Implementation Tasks

### 1. Create TestFiveCloneCollision
File: beads_twoclone_test.go (or new beads_nway_test.go)

```go
func TestFiveCloneCollision(t *testing.T) {
    // Test with 5 clones creating same ID with different content
    // Verify all 5 clones converge after sync rounds

    t.Run("SequentialSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "A", "B", "C", "D", "E")
    })

    t.Run("ReverseSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "E", "D", "C", "B", "A")
    })

    t.Run("RandomSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "C", "A", "E", "B", "D")
    })
}
```

### 2. Implement generalized testNCloneCollision
Generalize the 3-clone test to handle arbitrary N:

```go
func testNCloneCollision(t *testing.T, numClones int, syncOrder ...string) {
    t.Helper()

    if len(syncOrder) != numClones {
        t.Fatalf("syncOrder length (%d) must match numClones (%d)",
            len(syncOrder), numClones)
    }

    tmpDir := t.TempDir()

    // Setup remote and N clones
    remoteDir := setupBareRepo(t, tmpDir)
    cloneDirs := make(map[string]string)

    for i := 0; i < numClones; i++ {
        name := string(rune('A' + i))
        cloneDirs[name] = setupClone(t, tmpDir, remoteDir, name)
    }

    // Each clone creates issue with same ID but different content
    for name, dir := range cloneDirs {
        createIssue(t, dir, fmt.Sprintf("Issue from clone %s", name))
    }

    // Sync in specified order
    for _, name := range syncOrder {
        syncClone(t, cloneDirs[name], name)
    }

    // Final pull for convergence
    for name, dir := range cloneDirs {
        finalPull(t, dir, name)
    }

    // Verify all clones have all N issues
    expectedTitles := make(map[string]bool)
    for i := 0; i < numClones; i++ {
        name := string(rune('A' + i))
        expectedTitles[fmt.Sprintf("Issue from clone %s", name)] = true
    }

    for name, dir := range cloneDirs {
        titles := getTitles(t, dir)
        if !compareTitleSets(titles, expectedTitles) {
            t.Errorf("Clone %s missing issues: expected %v, got %v",
                name, expectedTitles, titles)
        }
    }

    t.Log("✓ All", numClones, "clones converged successfully")
}
```

### 3. Add performance benchmarks
Test convergence time and memory usage:

```go
func BenchmarkNWayCollision(b *testing.B) {
    for _, n := range []int{3, 5, 10, 20} {
        b.Run(fmt.Sprintf("N=%d", n), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                // Run N-way collision and measure time
                testNCloneCollisionBench(b, n)
            }
        })
    }
}
```

### 4. Add convergence time tests
Verify bounded convergence:

```go
func TestConvergenceTime(t *testing.T) {
    // Test that convergence happens within expected rounds
    // For N clones, should converge in at most N-1 sync rounds

    for n := 3; n <= 10; n++ {
        t.Run(fmt.Sprintf("N=%d", n), func(t *testing.T) {
            rounds := measureConvergenceRounds(t, n)
            maxExpected := n - 1
            if rounds > maxExpected {
                t.Errorf("Convergence took %d rounds, expected ≤ %d",
                    rounds, maxExpected)
            }
        })
    }
}
```

### 5. Add edge case tests
Test boundary conditions:
- All N clones have identical content (dedup works)
- N-1 clones have same content, 1 differs
- All N clones have unique content
- Mix of collisions and non-collisions

## Acceptance Criteria
- TestFiveCloneCollision passes with all sync orders
- All 5 clones converge to identical content
- Performance is acceptable (< 5 seconds for 5 clones)
- Convergence time is bounded (≤ N-1 rounds)
- Edge cases handled correctly
- Benchmarks show scalability to 10+ clones

## Files to Create/Modify
- beads_twoclone_test.go or beads_nway_test.go
- Add helper functions for N-clone setup

## Testing Strategy

### Test Matrix
| N Clones | Sync Orders | Expected Result |
|----------|-------------|-----------------|
| 3        | A→B→C       | Pass            |
| 3        | C→B→A       | Pass            |
| 5        | A→B→C→D→E   | Pass            |
| 5        | E→D→C→B→A   | Pass            |
| 5        | Random      | Pass            |
| 10       | Sequential  | Pass            |

### Performance Targets
- 3 clones: < 2 seconds
- 5 clones: < 5 seconds
- 10 clones: < 15 seconds

## Dependencies
- Requires bd-cbed9619.5, bd-cbed9619.4, bd-cbed9619.3, bd-cbed9619.2 to be completed
- TestThreeCloneCollision must pass first

## Success Metrics
- All tests pass for N ∈ {3, 5, 10}
- Convergence time scales linearly (O(N))
- Memory usage reasonable (< 100MB for 10 clones)
- No data corruption or loss in any scenario


Fix G304 potential file inclusion in cmd/bd/tips.go:259

Linting issue: G304: Potential file inclusion via variable (gosec) at cmd/bd/tips.go:259:18. Error: if data, err := os.ReadFile(settingsPath); err == nil {


bd resolve-conflicts - Git merge conflict resolver

Automatically resolve JSONL merge conflicts.

Modes:
- Mechanical: ID remapping (no AI)
- AI-assisted: Smart merge/keep decisions
- Interactive: Review each conflict

Handles <<<<<<< conflict markers in .beads/beads.jsonl

Files: cmd/bd/resolve_conflicts.go (new)


Command Set Standardization & Flag Consistency

Comprehensive improvements to bd command set based on 2025 audit findings.

## Background
See docs/command-audit-2025.md for detailed analysis.

## Goals
1. Standardize flag naming and behavior across all commands
2. Add missing flags for feature parity
3. Fix naming confusion
4. Improve consistency in JSON output

## Success Criteria
- All mutating commands support --dry-run (no --preview variants)
- bd update supports label operations
- bd search has filter parity with bd list
- Priority flags accept both int and P0-P4 format everywhere
- JSON output is consistent across all commands


Add BEADS_DAEMON_MODE flag handling

Add environment variable BEADS_DAEMON_MODE (values: poll, events). Default to 'poll' for Phase 1. Wire into daemon startup to select runEventLoop vs runEventDrivenLoop.


bd-hv01: Missing cleanup of .merged temp file on failure

Problem: deletion_tracking.go:49 creates tmpMerged file but does not clean up on failure, causing disk space leak and potential interference with subsequent syncs.

Fix: Add defer os.Remove(tmpMerged) after creating temp file path.

Files: cmd/bd/deletion_tracking.go:38-89


Test issue 1


Rapid 1


Import overwrites newer local issues with older remote versions

## Problem

During git pull + import, local issues with newer updated_at timestamps get overwritten by older versions from remote JSONL.

## What Happened

Timeline:
1. 17:52 - Closed bd-df190564 and bd-b501fcc1 locally (updated_at: 2025-10-31)
2. 17:51 - Remote pushed same issues with status=open (updated_at: 2025-10-30)
3. 17:52 - Local sync pulled remote commit and imported JSONL
4. Result: Issues reverted to open despite local version being newer

## Root Cause

DetectCollisions (internal/storage/sqlite/collision.go:67-79) compares fields but doesn't check timestamps:

```go
conflictingFields := compareIssues(existing, incoming)
if len(conflictingFields) == 0 {
    result.ExactMatches = append(result.ExactMatches, incoming.ID)
} else {
    // Same ID, different content - treats as UPDATE
    result.Collisions = append(result.Collisions, &CollisionDetail{...})
}
```

Import applies incoming version regardless of which is newer.

## Expected Behavior

Import should:
1. Compare updated_at timestamps when collision detected
2. Skip update if local version is newer
3. Apply update only if remote version is newer
4. Warn on timestamp conflicts

## Solution

Add timestamp checking to DetectCollisions or importIssues:

```go
if len(conflictingFields) > 0 {
    // Check timestamps
    if !incoming.UpdatedAt.After(existing.UpdatedAt) {
        // Local is newer or same - skip update
        result.ExactMatches = append(result.ExactMatches, incoming.ID)
        continue
    }
    // Remote is newer - apply update
    result.Collisions = append(result.Collisions, &CollisionDetail{...})
}
```

## Files
- internal/storage/sqlite/collision.go
- internal/importer/importer.go

## References
- Discovered during bd-df190564, bd-b501fcc1 re-opening


sync.branch config lost on clone - should be in config.yaml

## Problem

The sync.branch configuration is stored in the SQLite database, which is gitignored. When contributors clone a repo that uses beads:

1. They get .beads/issues.jsonl (tracked)
2. They don't get .beads/beads.db (gitignored)
3. They lose the sync.branch config

When they run 'bd init' or 'bd doctor --fix', sync.branch gets auto-set to the current branch (usually 'main'), but if the project uses a dedicated sync branch like 'beads-sync', they'll be committing to the wrong branch.

## Root Cause

syncbranch.Get() only checks:
1. BEADS_SYNC_BRANCH env var
2. Database config (store.GetConfig)

It never reads from config.yaml, which IS tracked in git and persists across clones.

## Proposed Solution

Add sync-branch support to config.yaml with this precedence:
1. BEADS_SYNC_BRANCH env var (highest)
2. config.yaml sync-branch setting
3. Database config (legacy/local override)
4. Auto-detect from current branch (lowest)

Changes needed:
1. Update syncbranch.Get() to check config.GetString('sync-branch') between env and database
2. Update config.yaml template in init.go to include sync-branch
3. Update docs to recommend setting sync-branch in config.yaml for team projects
4. Consider: bd init --branch should also write to config.yaml, not just database


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:** [deleted:bd-cb64c226.2], 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


Test issue 2


Fix compact --analyze/--apply error messages to clarify direct mode requirement

**Problem:**
When users run `bd compact --analyze` with daemon running, they get:
```
Error: compact requires SQLite storage
```

This is misleading because they ARE using SQLite (via daemon), but the command needs DIRECT SQLite access.

**Current behavior:**
- Error message suggests they don't have SQLite
- No hint about using --no-daemon flag
- Related to issue #349 item #1

**Proposed fix:**
1. Update error messages in cmd/bd/compact.go lines 106-114 (analyze) and 121-137 (apply)
2. Add explicit hint: "Use --no-daemon flag to bypass daemon"
3. Change SQLite check error from "requires SQLite storage" to "failed to open database in direct mode"

**Files to modify:**
- cmd/bd/compact.go (lines ~106-137)

**Testing:**
- Run with daemon: `bd compact --analyze` should show clear error + hint
- Run with --no-daemon: `bd compact --analyze --no-daemon` should work
- Verify error message is actionable


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:** [deleted: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


bd config set succeeds but doesn't persist to config.toml

Commands like `bd config set daemon.auto_push true` return "Set daemon.auto_push = true" but the config file is never created and `bd info --json | jq '.config'` returns null.

**Steps to reproduce:**
1. Run `bd config set daemon.auto_push true`
2. See success message: "Set daemon.auto_push = true"
3. Check `cat .beads/config.toml` → file doesn't exist
4. Check `bd info --json | jq '.config'` → returns null

**Expected:**
- .beads/config.toml should be created with the setting
- bd info should show the config value

**Impact:**
Users can't enable auto-push/auto-commit via CLI as documented in AGENTS.md