Make auto-flush debounce duration configurable

flushDebounce is hardcoded to 5 seconds. Make it configurable via environment variable BEADS_FLUSH_DEBOUNCE (e.g., '500ms', '10s'). Current 5-second value is reasonable for interactive use, but CI/automated scenarios might want faster flush. Add getDebounceDuration() helper function. Located in cmd/bd/main.go:31.


Make beads reusable as a Go library for external projects like vc

Currently beads is only usable as a CLI tool. We want to use beads as a library in other Go projects like ~/src/vc so they can programmatically manage issues without shelling out to the bd CLI.

Goals:
- Export public API from internal packages
- Document Go package usage
- Provide examples of programmatic usage
- Ensure vc can import and use beads storage layer directly

Use case: The vc project needs issue tracking and wants to use beads as an embedded library rather than as a separate CLI tool.

UnderlyingDB() method implemented and tested. Core functionality complete. Still needs documentation updates (bd-17) and lifecycle safety enhancements (bd-16).


GH#146: No color showing in terminal for some users

User reports color not working in macOS (Taho 26.0.1) with iTerm 3.6.4 and Terminal.app, despite color working elsewhere in terminal. Python rich and printf escape codes work.

Need to investigate:
- Is NO_COLOR env var set?
- Terminal type detection?
- fatih/color library configuration
- Does bd list show colors? bd ready? bd init?
- What's the output of: echo $TERM, echo $NO_COLOR


Fix nil pointer crash in bd reopen command

bd reopen crashes with SIGSEGV at reopen.go:30. Nil pointer dereference when trying to reopen an issue.

Fixed by adding daemon RPC support to reopen command. Pattern: check daemonClient != nil first, use RPC UpdateArgs with Status=open, fall back to direct store if daemon unavailable.


Address gosec security warnings (102 issues)

Security linter warnings: file permissions (0755 should be 0750), G304 file inclusion via variable, G204 subprocess launches. Many are false positives but should be reviewed.

Review each gosec warning. Add exclusions for legitimate cases to .golangci.yml. Fix real security issues (overly permissive file modes).


Multi-project MCP context switching (GH#145)

Enable MCP server to manage multiple beads projects in a single session with per-request workspace_root parameter.

Current bug: set_context(workspace_root) doesn't actually switch sockets - all operations hit first initialized socket.

Use case: Managing tasks across multiple organizations with different permission models (internal, partners, open source).

Architecture: Connection pool keyed by workspace_root, each maintaining its own daemon socket connection. Request-scoped routing using ContextVar to avoid global state races.

See GH#145 for full requirements and user context.

✅ APPROVED WITH MODIFICATIONS by architectural review (2025-10-25)

LSP-style model is correct: Single MCP server → per-project daemons → isolated databases.

CRITICAL CHANGES from review:
1. **Simplify connection pool**: No LRU eviction initially (typical user: 2-5 projects)
2. **Add asyncio.Lock**: Prevent race conditions in pool access
3. **Defer health checks**: Only retry on failure, not preemptive pings
4. **Handle submodules**: Check local .beads BEFORE git toplevel
5. **Path canonicalization**: realpath + git toplevel with caching

RISKS MITIGATED:
- Global _client bug: Replace with connection pool keyed by canonical path
- Race conditions: Add asyncio.Lock for pool mutations
- Submodule edge case: Check .beads directory first
- Stale sockets: Retry once on connection failure

Estimated effort: 2.5-3.5 days (simplified from 2.5-4.5 days)
Confidence: 8/10

- Multiple projects can be accessed in single MCP session
- Per-request workspace_root parameter works on all tools
- No cross-project data leakage
- Concurrent calls to different projects work correctly
- Stale sockets auto-reconnect with retry/backoff
- Integration tests verify isolation across 2+ temp repos
- set_context() still works as default fallback

Review doc: docs/bd-103-architectural-review.md

Implementation order:
1. bd-108 (connection manager) - Foundation with pool + lock
2. bd-104 (ContextVar routing) - Per-request workspace
3. bd-106 (require_context) - Validation
4. bd-105 (tests) - Concurrency + edge cases
5. bd-109 (docs) - Usage guide
6. [deleted:bd-142] (health checks) - DEFERRED to Phase 2


Implement request-scoped routing with ContextVar

Add ContextVar-based routing to avoid global state races during concurrent multi-project calls.

Approach:
- Define current_workspace: ContextVar[str|None] in server.py
- Add @with_workspace decorator that resolves workspace_root (via _resolve_workspace_root + realpath)
- Set ContextVar for duration of tool call, reset after
- Falls back to set_context default (BEADS_WORKING_DIR) if workspace_root not provided
- beads_mcp.tools.get_client() reads current_workspace from ContextVar

Blocks: bd-103 (connection manager must exist first)

Decorator pattern with ContextVar for request-scoped workspace routing.

@with_workspace decorator:
- Extract workspace_root parameter from tool call
- Resolve via _resolve_workspace_root + realpath
- Set current_workspace ContextVar for request duration
- Falls back to BEADS_WORKING_DIR if workspace_root not provided
- Reset ContextVar after tool completes

Applied to all tools in server.py. _get_client() reads current_workspace.

⚠ CONCURRENCY GOTCHA (from architectural review):
- ContextVar doesn't propagate to asyncio.create_task() spawned tasks
- SOLUTION: Keep tool calls synchronous, no background task spawning
- If background tasks needed: use contextvars.copy_context()

Document this limitation in bd-109.

Blocks on bd-108 (connection pool must exist first).

CRITICAL: Do NOT spawn background tasks within tool implementations.
ContextVar propagation to spawned tasks is unreliable.


Add integration tests for multi-project MCP switching

Comprehensive tests to verify multi-project isolation, concurrency, and edge cases.

EXPANDED TEST COVERAGE (per architectural review):

**Concurrency tests (CRITICAL):**
- asyncio.gather() with calls to different workspace_root values
- Verify no cross-project data leakage
- Verify pool lock prevents race conditions

**Edge case tests:**
- Submodule handling: Parent repo vs submodule with own .beads
- Symlink deduplication: Same physical path via different symlinks
- Stale socket recovery: Kill daemon, verify retry on failure
- Missing .beads directory handling

**Isolation tests:**
- Create 2+ temp repos with bd init
- Verify operations in project A don't affect project B
- Stress test: many parallel calls across 3-5 repos

Estimated effort: M (1-2 days) including fixtures for temp repos and daemon process management

Test structure:

1. test_concurrent_multi_project.py:
   - asyncio.gather with 2+ projects
   - Verify pool lock prevents corruption

2. test_path_canonicalization.py:
   - Submodule edge case (check .beads first)
   - Symlink deduplication (realpath normalization)

3. test_stale_socket_recovery.py:
   - Kill daemon mid-session
   - Verify retry-on-failure works

4. test_cross_project_isolation.py:
   - Create issues in project A
   - List from project B, verify empty
   - No data leakage

Use pytest fixtures for temp repos and daemon lifecycle.

- All concurrency tests pass with asyncio.gather
- Submodule edge case handled correctly
- Symlinks deduplicated to same connection
- Stale socket retry works
- No cross-project data leakage in stress tests


Fix require_context to support per-request workspace_root

Update require_context decorator to pass if either:
- workspace_root was provided on the tool call (via ContextVar), OR
- BEADS_WORKING_DIR is set (from set_context)

Stop using BEADS_DB as router - treat it only as CLI fallback.

Ensures backward compatibility with set_context() while supporting new per-request routing.


Add health checks and reconnection logic for stale sockets

Handle stale/broken socket connections after daemon restarts or upgrades.

Features:
- ping/health_check method on client
- Check before use or periodic health check
- On failure: drop from pool, attempt reconnect with exponential backoff
- Clear error propagation if reconnect fails after retries
- Handle version mismatch after daemon upgrade
- Handle long-idle connections closed by daemon

Add async ping() to DaemonClient. ConnectionManager.get_client() pings before returning cached client. On failure, del from pool and retry connect. Bounded retry (3-5 attempts) with backoff.


Add connection manager for per-project daemon sockets

Create connection pool for managing multiple daemon socket connections in beads_mcp.tools.

SIMPLIFIED APPROACH (per architectural review):
- Connection pool dict[str, BdClientBase] keyed by canonical path
- Add asyncio.Lock for thread-safe pool access (CRITICAL)
- Path canonicalization with @lru_cache for performance
- NO LRU eviction (start simple, add if monitoring shows memory issues)
- NO preemptive health checks (only retry on failure)

Features:
- get_client(workspace_root) → returns client for that project
- Canonicalization: realpath + git toplevel (handles symlinks)
- Submodule-aware: Check local .beads BEFORE git toplevel
- Auto-start daemon if socket missing (existing behavior)
- Validate version on connect (existing behavior)

CRITICAL: Replace global _client singleton with pool to fix set_context() bug.

Implementation in tools.py:

```python
from contextvars import ContextVar
import asyncio
from functools import lru_cache

current_workspace: ContextVar[str | None] = ContextVar('workspace', default=None)
_connection_pool: Dict[str, BdClientBase] = {}
_pool_lock = asyncio.Lock()  # Prevent race conditions

@lru_cache(maxsize=128)
def _canonicalize_path(path: str) -> str:
    # 1. realpath to resolve symlinks
    real = os.path.realpath(path)

    # 2. Check local .beads (submodule edge case)
    if os.path.exists(os.path.join(real, ".beads")):
        return real

    # 3. Try git toplevel
    return _resolve_workspace_root(real)

async def _get_client() -> BdClientBase:
    workspace = current_workspace.get() or os.environ.get("BEADS_WORKING_DIR")
    if not workspace:
        raise BdError("No workspace set")

    canonical = _canonicalize_path(workspace)

    async with _pool_lock:  # CRITICAL: prevent concurrent mutations
        if canonical not in _connection_pool:
            _connection_pool[canonical] = create_bd_client(
                prefer_daemon=True,
                working_dir=canonical
            )
        return _connection_pool[canonical]
```

~200 LOC total (simplified from ~400-500)


Update MCP multi-project documentation

Update documentation for multi-project workflow in README.md, AGENTS.md, and MCP integration docs.

EXPANDED SECTIONS (per architectural review):

**Usage examples:**
- Per-request workspace_root parameter usage
- Concurrent multi-project queries with asyncio.gather
- Migration from set_context() to workspace_root parameter

**Architecture notes:**
- Connection pooling behavior (no limits initially)
- set_context() as default fallback (still supported)
- Library users NOT affected (all changes in MCP layer)

**Concurrency gotchas (CRITICAL):**
- ContextVar doesn't propagate to asyncio.create_task()
- Do NOT spawn background tasks in tool implementations
- All tool calls should be synchronous/sequential

**Troubleshooting:**
- Stale sockets (retry once on failure)
- Version mismatches (auto-detected since v0.16.0)
- Path aliasing via symlinks (deduplicated by realpath)
- Submodules with own .beads (handled correctly)

**Use cases:**
- Multi-organization collaboration (GH#145)
- Parallel project management scripts
- Cross-project queries

Documentation structure:

1. integrations/beads-mcp/README.md:
   - Add "Multi-Project Support" section
   - workspace_root parameter examples
   - Connection pool behavior

2. AGENTS.md:
   - Update MCP section with workspace_root usage
   - Add concurrency warning (no spawned tasks)
   - Document library non-impact

3. New: docs/MCP_MULTI_PROJECT.md:
   - Detailed architecture explanation
   - Migration guide from set_context()
   - Troubleshooting guide
   - Edge cases (submodules, symlinks)


Beads Library Integration

Migrate from custom SQLite implementation to using Beads as a library dependency. This eliminates ~3000 lines of duplicated code, reduces schema drift risk, and automatically inherits new Beads features.

**Key Benefits:**
- Remove 3000+ lines of duplicated SQLite code
- Eliminate schema drift between bd and vc CLIs
- Inherit Beads improvements automatically
- Stronger type safety with Beads error types
- Faster development velocity for new features
- Clean separation: Beads stays general-purpose, VC extends via wrapper

**Architecture Principle:**
Beads remains 100% standalone with NO VC dependencies. VC imports Beads (VC → Beads dependency) and wraps it with VC-specific storage methods. Both share the same database but maintain separate table namespaces.

**Current Pain Points:**
1. Code Duplication: Issue CRUD, dependency graphs, labels, status transitions all reimplemented
2. Schema Drift Risk: VC schema manually defined, could diverge from Beads
3. Lost Features: Can't leverage Beads query optimizer or advanced features without process spawning
4. Atomic Operations: Hand-rolled 100+ line transaction management
5. Maintenance Burden: Every Beads feature must be manually replicated

**Concrete Example:**
When Beads adds a new field (e.g., estimated_hours):
- Current: 4-6 hours of manual work (update types, 6+ SQL queries, migration, testing)
- With Library: 5 minutes (go get -u github.com/steveyegge/beads)

**Phased Approach:**
1. Phase 1: Add Beads dependency (non-breaking, feature flag)
2. Phase 2: Implement VCStorage wrapper (embeds beads.Storage)
3. Phase 3: Migration script for existing databases
4. Phase 4: Gradual cutover, deprecate SQLite code

**Related Analysis:**
See BEADS_INTEGRATION_ANALYSIS.md for detailed current state analysis and BEADS_LIBRARY_INTEGRATION_EPIC.md for full design document (both to be archived after issue creation).

**Estimated Effort:** 3-4 sprints
**Priority:** P2 (Medium-High - architectural improvement, high ROI)

Phase 1 (bd-12) complete! Beads can now be used as a Go library. VC can import github.com/steveyegge/beads and use beads.Storage directly instead of spawning CLI processes. No custom tables needed - VC uses pure Beads primitives.


bd daemon auto-sync can wipe out issues.jsonl when database is empty

During dogfooding session, bd daemon auto-sync exported empty database to JSONL, losing all 177 issues. Had to git restore to recover.

Root cause: bd export doesn't check if database is empty before exporting. When daemon has empty/wrong database, it wipes out valid JSONL file.

Impact: DATA LOSS

Add safeguard in bd export:
1. Count total issues in database before export
2. If count is 0, refuse to export and show error
3. Provide --force flag to override if truly want empty export

Alternative: Check if target JSONL exists and has issues, warn if about to replace with empty export

- bd export refuses to export when database has 0 issues
- Clear error message: "Refusing to export empty database (0 issues). Use --force to override."
- --force flag allows override for intentional empty exports
- Test: export with empty db fails, export with --force succeeds


bd import doesn't update database modification time (WAL mode)

When running bd import in WAL mode, the -wal file is updated but main .db file timestamp stays old. This breaks staleness detection which only checks main .db file.

Discovered during dogfooding when import didn't trigger staleness refresh.

Impact: Staleness checks fail to detect that database is newer than expected

Two options:
1. Checkpoint WAL after import to flush changes to main .db file
2. Update staleness detection to check both .db and -wal file timestamps

Option 1 is simpler and safer - just add PRAGMA wal_checkpoint(FULL) after import completes

- After bd import, main .db file modification time is updated
- Staleness detection correctly sees database as fresh
- Test: import, check .db mtime, verify it's recent


bd should show which database file it's using

During dogfooding, bd showed "0 issues" when correct database had 177 issues. Confusion arose from which database path was being used (daemon default vs explicit --db flag).

Users need clear feedback about which database file bd is actually using, especially when daemon is involved.

Impact: User confusion, working with wrong database unknowingly

Add database path to verbose output or as a bd info command:
1. bd info shows current database path, daemon status
2. OR: bd ready/list/etc --verbose shows "Using database: /path/to/.beads/beads.db"
3. Consider adding to bd status output

When database path differs from expected, show warning

- User can easily determine which database file bd is using
- bd info or similar command shows full database path
- When using unexpected database (e.g., daemon vs explicit --db), show clear indication
- Documentation updated with how to check database path


Implement configurable sort policy for GetReadyWork

Add SortPolicy field to WorkFilter to support different sorting strategies:
- hybrid (default): Recent issues by priority, old by age
- priority: Strict priority ordering for autonomous execution
- oldest: Backlog clearing mode

Solves issue where VC executor selects low-priority work instead of critical P0 work.

See SORT_POLICY_DESIGN.md for complete design spec including:
- Type definitions and constants
- SQL ORDER BY clause generation
- Testing strategy with test cases
- CLI integration with --sort flag
- Migration plan and backwards compatibility

- SortPolicy type added to types.go
- buildOrderByClause() implemented in ready.go
- Unit tests for all 3 policies pass
- Integration tests verify priority selection order
- bd ready --sort priority|oldest|hybrid works
- Empty SortPolicy defaults to hybrid (backwards compatible)
- Documentation updated in README.md and WORKFLOW.md


Add configurable SortPolicy to GetReadyWork

Add SortPolicy field to WorkFilter to support different ordering strategies: hybrid (default), priority-first, oldest-first. See SORT_POLICY_DESIGN.md for full specification.

See SORT_POLICY_DESIGN.md for complete design.

Implementation summary:
1. Add SortPolicy type and constants (hybrid, priority, oldest)
2. Add SortPolicy field to WorkFilter
3. Implement buildOrderByClause() to generate SQL based on policy
4. Default to hybrid for backwards compatibility
5. Add --sort flag to bd ready command

This enables autonomous execution systems (like VC) to use strict priority ordering while preserving the current hybrid behavior for interactive use.

Unit tests verify each policy generates correct ORDER BY. Integration tests verify priority, hybrid, and oldest policies select issues in expected order. CLI bd ready --sort priority works. Empty SortPolicy defaults to hybrid (backwards compatible).


Implement exclusive lock protocol for daemon/external tool coexistence

VC (VibeCoder) cannot coexist with bd daemon managing the same database. VC needs deterministic execution and directly manages JSONL sync, but daemon's auto-flush creates timing conflicts and nondeterministic behavior.

Current workaround (vc-195): VC kills ALL bd daemon processes with pkill, which is too aggressive and prevents developers from using daemon for their own workflow.

Solution: Implement lock file protocol (.beads/.exclusive-lock) that allows applications to claim exclusive management of a database. bd daemon respects these locks and skips locked databases.

See VC_DAEMON_EXCLUSION_PROTOCOL.md for full specification.

Lock File Protocol:
- File: .beads/.exclusive-lock (JSON format)
- Contains: holder name, PID, hostname, timestamp, version
- Created when external tool (VC) starts
- Removed on clean shutdown
- Stale lock detection: if PID doesn't exist, lock is removed

bd daemon behavior:
1. Before touching any database, check for .exclusive-lock
2. If lock exists and valid (PID alive): skip database in sync cycle
3. If lock is stale (PID dead): remove lock and proceed
4. If lock is malformed: log warning and skip (fail-safe)

External tool behavior:
1. On startup: create .exclusive-lock with process info
2. If lock exists, check if stale; fail if another process is running
3. On shutdown: remove .exclusive-lock

Benefits:
- Clean separation, no process killing
- Developer-friendly (can run daemon for other projects)
- Fail-safe with stale lock detection
- Extensible for other tools beyond VC

1. bd daemon skips databases with valid exclusive locks
2. bd daemon cleans up stale locks automatically
3. Lock file format documented and implemented
4. Integration tests show daemon + external tool coexistence
5. Documentation updated with protocol specification


Add ExclusiveLock struct and JSON marshaling

Create Go struct to represent exclusive lock file format with JSON marshaling support.

Fields needed:
- Holder (string): name of lock holder (e.g., "vc-executor")
- PID (int): process ID
- Hostname (string): hostname where process is running
- StartedAt (time.Time): when lock was acquired
- Version (string): version of lock holder

Should support both Marshal and Unmarshal for reading/writing lock files.


Implement isProcessAlive() helper for PID validation

Implement helper function to check if a process is alive given PID and hostname.

Logic:
- If hostname != current host: return true (can't verify remote, assume alive)
- If hostname == current host: check if PID exists using os.FindProcess + Signal(0)
- Return true if process exists, false otherwise

Needed for stale lock detection.


Implement shouldSkipDatabase() to check for exclusive locks

Add shouldSkipDatabase() function to daemon that checks for .beads/.exclusive-lock before processing a database.

Logic:
1. Check if .beads/.exclusive-lock exists
2. If not: return false (proceed with database)
3. If yes: read and parse JSON
4. If malformed: log warning and skip (fail-safe)
5. If valid: check if PID is alive
6. If stale (PID dead): remove lock file and proceed
7. If active (PID alive): log skip message and return true

Depends on ExclusiveLock struct and isProcessAlive() helper.


Integrate exclusive lock check into daemon sync cycle

Integrate shouldSkipDatabase() into daemon's sync cycle to respect exclusive locks.

Before processing each database:
1. Call shouldSkipDatabase(beadsDir)
2. If true: skip all operations (export, import, commit, push)
3. If false: proceed with normal sync

Add appropriate logging:
- "Skipping .beads/foo.db (locked by vc-executor, PID 12345)" when skipping
- "Removed stale lock at .beads/foo.db (PID 12345 not found)" when cleaning up

Depends on shouldSkipDatabase() implementation.


Phase 1: Add Beads Dependency (Non-Breaking)

Introduce Beads library alongside existing SQLite code without breaking production.

**Goal:** Add Beads as optional dependency with feature flag, establish compatibility baseline.

**Key Tasks:**
1. Add github.com/steveyegge/beads to go.mod
2. Create compatibility test suite comparing VC SQLite vs Beads schema
3. Identify schema differences and document migration requirements
4. Create internal/storage/beads/adapter.go implementing Storage interface
5. Add feature flag: VC_USE_BEADS_LIBRARY=true (disabled by default)

**Acceptance Criteria:**
- Beads library imported successfully
- Compatibility tests pass identifying all schema differences
- Both implementations coexist without conflicts
- No production impact (feature flag disabled by default)
- Documentation of schema differences and migration needs

**Technical Details:**
- Use feature flag to allow A/B testing
- Compatibility tests must cover: issues, dependencies, labels, status transitions, ID generation
- Adapter must implement full Storage interface
- Zero changes to existing production code paths

**Blockers:** None - can start immediately

**Estimated Effort:** 1 sprint


Add unit tests for exclusive lock detection

Add unit tests for exclusive lock functionality:

Test cases:
1. No lock file exists -> shouldSkipDatabase returns false
2. Valid lock with alive PID -> shouldSkipDatabase returns true
3. Stale lock with dead PID -> removes lock, returns false
4. Malformed lock JSON -> logs warning, returns true (fail-safe)
5. Lock with different hostname -> assumes alive, returns true
6. isProcessAlive() correctly detects running/dead processes

Use test helpers to create lock files with specific PIDs.


Add integration tests for daemon + external tool coexistence

Add integration tests simulating real-world daemon + external tool usage:

Test scenarios:
1. Start daemon, create lock file, verify daemon skips database
2. Create stale lock (with dead PID), start daemon, verify lock cleanup
3. Multiple lock/unlock cycles
4. Daemon resumes managing database after lock is removed
5. Lock file created/removed during daemon operation

Should verify:
- Daemon log messages correct
- No JSONL auto-flush when database is locked
- Daemon resumes normal operation after lock removed


Document exclusive lock protocol in README and new doc file

Update documentation to explain exclusive lock protocol:

1. Create EXCLUSIVE_LOCK.md with:
   - Protocol specification
   - Lock file format (JSON schema)
   - Usage examples for external tools
   - Stale lock behavior
   - Edge cases and limitations

2. Update README.md:
   - Mention exclusive lock support in daemon section
   - Link to EXCLUSIVE_LOCK.md for details

3. Update AGENTS.md:
   - Note that external tools can use exclusive locks
   - Explain daemon skips locked databases

Target audience: developers integrating bd with external tools (like VC).


Add staleness check to non-daemon mode

Extend staleness detection to non-daemon mode (--no-daemon).

Implementation:
- On database open, check if .beads/issues.jsonl exists
- If JSONL exists and is newer than .db file: auto-import
- Compare JSONL mtime vs .db mtime (both os.Stat)
- Log: "Auto-importing from .beads/issues.jsonl (newer than database)"

This ensures both daemon and non-daemon modes handle git pull correctly.

INVESTIGATION COMPLETE:

The requested feature is already implemented in ensureStoreActive() (cmd/bd/direct_mode.go:79-81) which calls autoImportIfNewer() on every database open in non-daemon mode.

The implementation uses hash-based staleness detection via autoimport.AutoImportIfNewer() instead of mtime-based, which is BETTER because:
1. Avoids unnecessary imports when file is merely touched
2. Detects actual content changes reliably
3. Works correctly after git pull

Verified working with BD_DEBUG=1:
```
BD_DEBUG=1 ./bd --no-daemon stats
Debug: auto-import skipped, JSONL unchanged (hash match)
```

The issue description requested mtime-based approach like daemon mode, but hash-based is superior for non-daemon usage. Both modes now have auto-import after git pull.


Add 'bd sync' command for explicit synchronization

Add explicit `bd sync` command as fallback for manual synchronization after git pull.

Behavior:
- Import from .beads/issues.jsonl
- If daemon mode: send RPC command to daemon to re-import
- If non-daemon: directly import to local db
- Show summary: "Imported N issues, updated M issues"

Usage:
```bash
git pull
bd sync  # Force immediate sync
```

This complements auto-detection but gives users manual control.

IMPLEMENTED:

Added `bd sync --import-only` flag that:
- Imports from .beads/issues.jsonl automatically (no need to specify path)
- Works in both daemon and non-daemon modes
- Shows summary: "Import complete: X created, Y updated, Z unchanged, N remapped"
- Handles collisions automatically with --resolve-collisions

Usage:
```bash
git pull
bd sync --import-only  # Force immediate sync
```

The existing `bd sync` command does full git workflow (export, commit, pull, import, push). The new --import-only flag complements --flush-only for granular control.

Implementation in cmd/bd/sync.go


Add integration test for git pull sync scenario

Add integration test simulating the git pull sync issue.

Test scenario:
1. Create temp git repo with beads initialized
2. Clone 1: Create and close issue, export, commit, push
3. Clone 2: Start daemon, git pull
4. Clone 2: Verify bd show <issue> reflects closed status immediately
5. Verify no manual import or daemon restart needed

Also test:
- Non-daemon mode (--no-daemon) handles git pull correctly
- bd sync command works in both modes
- Performance: staleness check adds <10ms overhead

Depends on staleness detection implementation.


Add optional post-merge git hook example for bd sync

Create example git hook that auto-runs bd sync after git pull/merge.

Add to examples/git-hooks/:
- post-merge hook that checks if .beads/issues.jsonl changed
- If changed: run `bd sync` automatically
- Make it optional/documented (not auto-installed)

Benefits:
- Zero-friction sync after git pull
- Complements auto-detection as belt-and-suspenders

Note: post-merge hook already exists for pre-commit/post-merge. Extend it to support sync.


Update documentation for auto-sync behavior

Update documentation to explain auto-sync after git pull.

Files to update:
1. README.md - Add section on git workflow and auto-sync
2. AGENTS.md - Note that bd auto-detects JSONL changes after git pull
3. WORKFLOW.md - Update git pull workflow to remove manual import step
4. FAQ.md - Add Q&A about sync behavior and staleness

Key points:
- bd automatically detects when JSONL is newer than database
- No manual import needed after git pull
- bd sync command available for manual control
- Optional git hook for guaranteed sync


Refactor autoImportIfNewer to be callable from daemon

The staleness check in [deleted:bd-160] detects when JSONL is newer than last import, but can't trigger the actual import because autoImportIfNewer() is in cmd/bd and uses global variables.

Need to:
1. Extract core import logic from autoImportIfNewer() into importable function
2. Move to internal/autoimport or similar package
3. Make it callable from daemon (no global state dependency)
4. Update staleness check in server.go to call actual import instead of just logging

This completes the auto-sync feature - daemon will truly auto-import after git pull.


Enforce daemon singleton per workspace with file locking

Agent in ~/src/wyvern discovered 4 simultaneous daemon processes running, causing infinite directory recursion (.beads/.beads/.beads/...). Each daemon used relative paths and created nested .beads/ directories.

Root cause: No singleton enforcement. Multiple `bd daemon` processes can start in same workspace.

Expected: One daemon per workspace (each workspace = separate .beads/ dir with bd.sock)
Actual: Multiple daemons can run simultaneously in same workspace

Note: Separate git clones = separate workspaces = separate daemons (correct). Git worktrees share .beads/ and have known limitations (documented, use --no-daemon).

Use flock (file locking) on daemon socket or database file to enforce singleton:

1. On daemon start, attempt exclusive lock on .beads/bd.sock or .beads/daemon.lock
2. If lock held by another process, refuse to start (exit with clear error)
3. Hold lock for lifetime of daemon process
4. Release lock on daemon shutdown

Alternative: Use PID file with stale detection (check if PID is still running)

Implementation location: Daemon startup code in cmd/bd/ or internal/daemon/

1. Starting second daemon process in same workspace fails with clear error
2. Test: Start daemon, attempt second start, verify failure
3. Killing daemon releases lock, allowing new daemon to start
4. No infinite .beads/ directory recursion possible
5. Works correctly with auto-start mechanism


Phase 2: Implement VCStorage Wrapper

Create VCStorage wrapper that embeds beads.Storage and adds VC-specific operations.

**Goal:** Build clean abstraction layer where VC extends Beads without modifying Beads library.

**Architecture:**
- VCStorage embeds beads.Storage (delegates core operations)
- VCStorage adds VC-specific methods (executor instances, events)
- Same database, separate table namespaces (Beads tables + VC tables)
- Zero changes to Beads library code

**Key Tasks:**
1. Create VCStorage struct that embeds beads.Storage
2. Implement VC-specific methods: CreateExecutorInstance(), GetStaleExecutors(), LogEvent(), UpdateExecutionState()
3. Create VC table schemas (executor_instances, issue_execution_state, agent_events)
4. Verify type compatibility between VC types.Issue and Beads Issue
5. Create MockVCStorage for testing
6. Write unit tests for VC-specific methods
7. Write integration tests (end-to-end with Beads)
8. Benchmark performance vs current SQLite
9. Verify NO changes needed to Beads library

**Acceptance Criteria:**
- VCStorage successfully wraps Beads storage (embedding works)
- VC-specific tables created and accessible via foreign keys to Beads tables
- VC-specific methods work (executor instances, events)
- Core operations delegate to Beads correctly
- Tests pass with >90% coverage
- Performance benchmark shows no regression
- Beads library remains unmodified and standalone

**Technical Details:**
- Use beadsStore.DB() to get underlying database connection
- Create VC tables with FOREIGN KEY references to Beads issues table
- Schema separation: Beads owns (issues, dependencies, labels), VC owns (executor_instances, agent_events)
- Testing: Embed MockBeadsStorage in MockVCStorage

**Dependencies:**
- Blocked by Phase 1 (need Beads library imported)

**Estimated Effort:** 1.5 sprints


Track last JSONL import timestamp in daemon state

Add state tracking to daemon to record when .beads/issues.jsonl was last imported.

Implementation:
- Add lastImportTime field to daemon state (time.Time)
- Update timestamp after successful import
- Persist across RPC requests (in-memory state)
- Initialize on daemon startup

Needed for staleness detection to compare against JSONL mtime.


Implement staleness check before serving daemon requests

Add staleness detection to daemon RPC handler that checks if JSONL is newer than last import.

Logic:
1. Before processing any RPC request (show, list, ready, etc.)
2. Get .beads/issues.jsonl mtime (os.Stat)
3. Compare with lastImportTime
4. If JSONL mtime > lastImportTime: auto-import before processing request
5. If import fails: log error but continue with existing data

Performance: mtime check is fast (<1ms). Only imports if needed.

Depends on lastImportTime tracking.


Daemon fails to auto-import after git pull updates JSONL

After git pull updates .beads/issues.jsonl, daemon doesn't automatically re-import changes, causing stale data to be shown until next sync cycle (up to 5 minutes).

Reproduction:
1. Repo A: Close issues, export, commit, push
2. Repo B: git pull (successfully updates .beads/issues.jsonl)
3. bd show <issue> shows OLD status from daemon's SQLite db
4. JSONL on disk has correct new status

Root cause: Daemon sync cycle runs on timer (5min). When user manually runs git pull, daemon doesn't detect JSONL was updated externally and continues serving stale data from SQLite.

Impact:
- High for AI agents using beads in git workflows
- Breaks fundamental git-as-source-of-truth model
- Confusing UX: git log shows commit, bd shows old state
- Data consistency issues between JSONL and daemon

See WYVERN_SYNC_ISSUE.md for full analysis.

Three possible solutions:

Option 1: Auto-detect and re-import (recommended)
- Before serving any bd command, check if .beads/issues.jsonl mtime > last import time
- If newer, auto-import before processing request
- Fast check, minimal overhead

Option 2: File watcher in daemon
- Daemon watches .beads/issues.jsonl for mtime changes
- Auto-imports when file changes
- More complex, requires file watching infrastructure

Option 3: Explicit sync command
- User runs `bd sync` after git pull
- Manual, error-prone, defeats automation

Recommended: Option 1 (auto-detect) + Option 3 (explicit sync) as fallback.

1. After git pull updates .beads/issues.jsonl, next bd command sees fresh data
2. No manual import or daemon restart required
3. Performance impact < 10ms per command (mtime check is fast)
4. Works in both daemon and non-daemon modes
5. Test: Two repo clones, update in one, pull in other, verify immediate sync

**Current Status (2025-10-26):**

✅ **Completed (bd-128):**
- Created internal/autoimport package with staleness detection
- Daemon can detect when JSONL is newer than last import
- Infrastructure exists to call import logic

❌ **Remaining Work:**
The daemon's importFunc in server.go (line 2096-2102) is a stub that just logs a notice. It needs to actually import the issues.

**Problem:**
- importIssuesCore is in cmd/bd package, not accessible from internal/rpc
- daemon's handleImport() returns 'not yet implemented' error

**Two approaches:**
1. Move importIssuesCore to internal/import package (shares with daemon)
2. Use storage layer directly in daemon (create/update issues via Storage interface)

**Blocker:**
This is the critical bug causing data corruption:
- Agent A pushes changes
- Agent B does git pull
- Agent B's daemon serves stale SQLite data
- Agent B exports stale data back to JSONL, overwriting Agent A's changes
- Agent B pushes, losing Agent A's work

**Next Steps:**
1. Choose approach (probably #1 - move importIssuesCore to internal/import)
2. Implement real importFunc in daemon's checkAndAutoImportIfStale()
3. Test with two-repo scenario (push from A, pull in B, verify B sees changes)
4. Ensure no data corruption in multi-agent workflows


Add configurable SortPolicy to GetReadyWork

Add SortPolicy field to WorkFilter to support different ordering strategies: hybrid (default), priority-first, oldest-first. See SORT_POLICY_DESIGN.md for full specification.


Phase 3: Migration Path & Database Schema Alignment

Enable existing .beads/vc.db files to work with Beads library through automated migration.

**Goal:** Provide safe, tested migration path from SQLite implementation to Beads library.

**Key Tasks:**
1. Run compatibility tests against production databases
2. Identify schema differences (columns, indexes, constraints)
3. Document required migrations
4. Create migration CLI command: 'vc migrate --from sqlite --to beads'
5. Add dry-run mode for preview
6. Add backup/restore capability
7. Implement rollback mechanism
8. Add auto-detection of schema version on startup
9. Add auto-migrate with user prompt

**Acceptance Criteria:**
- Existing databases migrate successfully
- Data integrity preserved (zero data loss verified via checksums)
- Rollback works if migration fails
- Migration tested on real production VC databases
- Dry-run mode shows exactly what will change
- Backup created before migration
- Feature flag: VC_FORCE_SQLITE=true provides escape hatch

**Technical Details:**
- Compare current SQLite schema with Beads schema
- Handle version detection (read schema_version or detect from structure)
- Migration should be idempotent (safe to run multiple times)
- Backup strategy: Copy .beads/vc.db to .beads/vc.db.backup-<timestamp>
- Verify foreign key integrity after migration

**Safety Measures:**
- Require executor shutdown before migration (check for running executors)
- Atomic migration (BEGIN IMMEDIATE transaction)
- Comprehensive pre/post migration validation
- Clear error messages with recovery instructions

**Dependencies:**
- Blocked by Phase 2 (need VCStorage implementation)

**Estimated Effort:** 0.5 sprint


Clarify that humans should run bd init, not agents

Current docs are ambiguous about who runs `bd init`. This leads to agents running it incorrectly, causing daemon issues, sync problems, and confusion.

**Problem:** Agents struggle with:
- Interactive prompts (git hooks)
- Directory detection
- Understanding when to import existing JSONL
- Daemon startup timing

**Solution:** Make it crystal clear that `bd init` is a human setup task, like `npm install` or `git init`.

Update docs to emphasize:
1. README.md - Rewrite Quick Start to show human does `bd init` first
2. AGENTS.md - Add clear "Human does init, agent uses bd" section
3. QUICKSTART.md - Update tutorial flow
4. FAQ.md - Add Q&A about who runs init


Make bd init --quiet skip interactive prompts

Currently `bd init --quiet` flag exists but doesn't actually skip the git hooks prompt. This makes it impossible for agents to run init non-interactively.

**Problem:** Agents setting up repos can't use `bd init` because it prompts for user input.

**Solution:** Make `--quiet` flag actually work:
- Skip git hooks prompt
- Auto-install hooks by default in quiet mode (safest)
- No output except errors

In cmd/bd/init.go, check the quiet flag before prompting:

```go
if isGitRepo() && !hooksInstalled() {
    if quiet {
        // Auto-install hooks silently in quiet mode
        _ = installGitHooks()  // Ignore errors
    } else {
        // Show prompt for interactive mode
        fmt.Printf("Install git hooks now? [Y/n] ")
        // ... existing prompt logic
    }
}
```


Local testing of bd init --quiet and auto-sync improvements

Before doing official version bump, test the new changes locally across multiple agent projects:

**Changes to test:**
- bd init --quiet (non-interactive for agents)
- Auto-sync documentation updates
- Git hooks auto-install in quiet mode

**Test scenarios:**
1. Fresh repo clone with existing .beads/issues.jsonl (bd init --quiet)
2. Agent setting up new project (bd init --quiet)
3. Verify git hooks install automatically in quiet mode
4. Test auto-import after git pull
5. Verify no daemon conflicts across projects

**Projects/agents to test:**
- ~/src/vc (waiting on exclusive lock protocol)
- Other agent projects with fresh clones
- Multiple agents on same machine

From RELEASING.md, for local testing:

1. Build local binary: `go build -o bd ./cmd/bd`
2. Use `./bd` directly (don't install via brew yet)
3. Optional: `alias bd="$PWD/bd"` to test across projects
4. Kill all daemons first: `pkill -f "bd.*daemon"`

**Testing workflow:**
- Build latest bd binary in ~/src/beads
- Create alias: `alias bd="$HOME/src/beads/bd"`
- Test with multiple agents in different repos
- Verify version: `bd version` shows latest
- Check daemon compatibility after restart

Found and fixed bug: bd init --quiet was returning before hooks installation.

Fixed by:
1. Moving hooks check/install before quiet mode return
2. Embedding hooks inline instead of using external install.sh
3. This makes bd init --quiet fully self-contained

Tested successfully in /tmp/bd-test-quiet - hooks installed correctly.


Review and respond to new GitHub PRs

Check for new pull requests on GitHub and review/respond to them.


Document bd edit command and verify MCP exclusion

Follow-up from PR #152:
1. Add "bd edit" to AGENTS.md with "Humans only" note
2. Verify MCP server doesn't expose bd edit command
3. Consider adding test for command registration


Add "bd daemons" command for multi-daemon management

Add a new "bd daemons" command with subcommands to manage daemon processes across all beads repositories/worktrees. Should show all running daemons with metadata (version, workspace, uptime, last sync), allow stopping/restarting individual daemons, auto-clean stale processes, view logs, and show exclusive lock status.

Subcommands:
- list: Show all running daemons with metadata (workspace, PID, version, socket path, uptime, last activity, exclusive lock status)
- stop <path|pid>: Gracefully stop a specific daemon
- restart <path|pid>: Stop and restart daemon
- killall: Emergency stop all daemons
- health: Verify each daemon responds to ping
- logs <path>: View daemon logs

Features:
- Auto-clean stale sockets/dead processes
- Discovery: Scan for .beads/bd.sock files + running processes
- Communication: Use existing socket protocol, add GET /status endpoint for metadata


Implement daemon discovery mechanism

Build the core discovery logic to find all running bd daemons. Scan filesystem for .beads/bd.sock files, check if processes are alive, and collect metadata.


Implement "bd daemons list" subcommand

Create the "bd daemons list" command that displays all running daemons in a table with: workspace path, PID, version, socket path, uptime, last activity, exclusive lock status. Include --json flag.


Add GET /status endpoint to daemon HTTP server

Add a new HTTP endpoint that returns daemon metadata: version, workspace path, PID, uptime, last activity timestamp, exclusive lock status.


Add auto-cleanup of stale sockets and dead processes

When discovering daemons, automatically detect and clean up stale socket files (where process is dead) and orphaned PID files. Should be safe and only remove confirmed-dead processes.


Phase 4: Gradual Cutover & Production Rollout

Replace SQLite implementation with Beads library in production and remove legacy code.

**Goal:** Complete transition to Beads library, deprecate and remove custom SQLite implementation.

**Key Tasks:**
1. Run VC executor with Beads library in CI
2. Dogfood: Use Beads library for VC's own development
3. Monitor for regressions and performance issues
4. Flip feature flag: VC_USE_BEADS_LIBRARY=true by default
5. Monitor production logs for errors
6. Collect user feedback
7. Add deprecation notice to CLAUDE.md
8. Provide migration guide for users
9. Remove legacy code: internal/storage/sqlite/sqlite.go (~1500 lines)
10. Remove migration framework: internal/storage/migrations/
11. Remove manual transaction management code
12. Update all documentation

**Acceptance Criteria:**
- Beads library enabled by default in production
- Zero production incidents related to migration
- Performance meets or exceeds SQLite implementation
- All tests passing with Beads library
- Legacy SQLite code removed
- Documentation updated
- Celebration documented 🎉

**Rollout Strategy:**
1. Week 1: Enable for CI/testing environments
2. Week 2: Dogfood on VC development
3. Week 3: Enable for 50% of production (canary)
4. Week 4: Enable for 100% of production
5. Week 5: Remove legacy code

**Monitoring:**
- Track error rates before/after cutover
- Monitor database query performance
- Track issue creation/update latency
- Monitor executor claim performance

**Rollback Plan:**
- Keep VC_FORCE_SQLITE=true escape hatch for 2 weeks post-cutover
- Keep legacy code for 1 sprint after cutover
- Document rollback procedure

**Success Metrics:**
- Zero data loss
- No performance regression (< 5% latency increase acceptable)
- Reduced maintenance burden (code LOC reduction)
- Positive developer feedback

**Dependencies:**
- Blocked by Phase 3 (need migration tooling)

**Estimated Effort:** 1 sprint


Update AGENTS.md and README.md with "bd daemons" documentation

Document the new "bd daemons" command and all subcommands in AGENTS.md and README.md. Include examples and troubleshooting guidance.


Implement "bd daemons health" subcommand

Add health check command that pings each daemon and reports responsiveness. Should detect and report stale sockets, version mismatches, unresponsive daemons.


Implement "bd daemons logs" subcommand

Add command to view daemon logs for a specific workspace. Requires daemon logging to file (may need separate issue for log infrastructure).


Implement "bd daemons killall" subcommand

Add emergency command to stop all running bd daemons. Should discover all daemons and stop them gracefully (with timeout fallback to SIGKILL).


Implement "bd daemons stop" and "bd daemons restart" subcommands

Add commands to stop and restart individual daemons by path or PID. Should send graceful shutdown signal via socket, with fallback to SIGTERM.


Daemon auto-import creates race condition with deletions

When a user deletes an issue while the daemon is running, the daemon's periodic import from JSONL can immediately re-add the deleted issue before the deletion is flushed to the JSONL file.

## Steps to reproduce:
1. Start daemon: bd daemon --interval 30s --auto-commit --auto-push
2. Delete an issue: bd delete wy-XX --force
3. Wait for daemon sync cycle (observe daemon log showing "Imported from JSONL")
4. Run bd show wy-XX - issue still exists despite successful deletion

## Expected behavior:
The deletion should be immediately flushed to JSONL before the next import cycle, or imports should respect deletions in the database.

## Actual behavior:
The daemon imports from JSONL and re-adds the deleted issue, overwriting the deletion. The user sees "✓ Deleted wy-XX" but the issue persists.

## Workaround:
1. Stop daemon: bd daemon --stop
2. Delete issue: bd delete wy-XX --force
3. Export to JSONL: bd export -o .beads/issues.jsonl
4. Commit and push manually
5. Restart daemon

## Suggested fixes:
1. Flush pending changes to JSONL before each import cycle
2. Track deletions separately and don't re-import deleted issues
3. Make delete operation immediately flush to JSONL when daemon is running
4. Add a "dirty" flag that prevents import if there are pending exports


bd create files issues in wrong project when multiple beads databases exist

When working in a directory with a beads database (e.g., /Users/stevey/src/wyvern/.beads/wy.db), bd create can file issues in a different project's database instead of the current directory's database.

## Steps to reproduce:
1. Have multiple beads projects (e.g., ~/src/wyvern with wy.db, ~/vibecoder with vc.db)
2. cd ~/src/wyvern
3. Run bd create --title "Test" --type bug
4. Observe issue created with wrong prefix (e.g., vc-1 instead of wy-1)

## Expected behavior:
bd create should respect the current working directory and use the beads database in that directory (.beads/ folder).

## Actual behavior:
bd create appears to use a different project's database, possibly the last accessed or a global default.

## Impact:
This can cause issues to be filed in completely wrong projects, polluting unrelated issue trackers.

## Suggested fix:
- Always check for .beads/ directory in current working directory first
- Add --project flag to explicitly specify which database to use
- Show which project/database is being used in command output
- Add validation/confirmation when creating issues if current directory doesn't match database project


Implement "bd daemons list" subcommand


Timestamp-only changes still being exported despite dedup logic

User observed timestamp-only changes in .beads/beads.jsonl causing dirty working tree. Example: bd-128's updated_at changed from 2025-10-25T23:51:09.811006-07:00 to 2025-10-26T14:12:45.207573-07:00 with no other field changes.

This should have been prevented by the export deduplication logic that's supposed to skip timestamp-only updates.

Need to investigate why timestamp-only changes are still being exported and fix the dedup logic.


Add lifecycle safety docs and tests for UnderlyingDB() method

The new UnderlyingDB() method exposes the raw *sql.DB connection for extensions like VC to create their own tables. While database/sql is concurrency-safe, there are lifecycle and misuse risks that need documentation and testing.

**What needs to be done:**

1. **Enhanced documentation** - Expand UnderlyingDB() comments to warn:
   - Callers MUST NOT call Close() on returned DB
   - Do NOT change pool/driver settings (SetMaxOpenConns, SetConnMaxIdleTime)
   - Do NOT modify SQLite PRAGMAs (WAL mode, journal, etc.)
   - Expect errors after Storage.Close() - use contexts
   - Keep write transactions short to avoid blocking core storage

2. **Add lifecycle tracking** - Implement closed flag:
   - Add atomic.Bool closed field to SQLiteStorage
   - Set flag in Close(), clear in New()
   - Optional: Add IsClosed() bool method

3. **Add safety tests** (run with -race):
   - TestUnderlyingDB_ConcurrentAccess - N goroutines using UnderlyingDB() during normal storage ops
   - TestUnderlyingDB_AfterClose - Verify operations fail cleanly after storage closed
   - TestUnderlyingDB_CreateExtensionTables - Create VC table with FK to issues, verify FK enforcement
   - TestUnderlyingDB_LongTxDoesNotCorrupt - Ensure long read tx doesn't block writes indefinitely

**Why this matters:**
VC will use this to create tables in the same database. Need to ensure production-ready safety without over-engineering.

**Estimated effort:** S+S+S = M total (1-3h)

Oracle recommends "simple path": enhanced docs + minimal guardrails + focused tests. See oracle output for detailed rationale on concurrency safety, lifecycle risks, and when to consider advanced path (wrapping interface).


Critical: Multi-clone sync is fundamentally broken

## Problem

Two clones of the same beads repo working on non-overlapping issues cannot stay in sync. The JSONL export/import mechanism creates catastrophic divergence instead of keeping databases synchronized.

## What Happened (2025-10-26)

Two repos working simultaneously:
- ~/src/beads (bd.db, 159 issues) - worked on bd-153, bd-152, bd-150, closed them
- ~/src/fred/beads (beads.db, 165 issues) - worked on bd-159, bd-160-164

Result after attempting sync:
- Databases completely diverged (159 vs 165 issues)
- JSONL files contain conflicting state
- Database corruption in fred/beads
- bd-150/152/153 show as closed in one repo, open in the other
- No clear recovery path without manual database copying
- git pull + bd sync does NOT synchronize state

## Root Cause Analysis

### SMOKING GUN: Daemon Import is a NO-OP
**Location**: cmd/bd/daemon.go:791-797

The daemon's importToJSONLWithStore() function returns nil without actually importing.
This means the daemon exports DB to JSONL, commits, pulls from remote, but NEVER imports remote changes back into the database.

Result: Remote changes are pulled but never imported, daemon keeps exporting stale state.

### Other Root Causes

1. **Database naming inconsistency**: One repo uses bd.db, other uses beads.db - no enforcement
2. **Daemon state divergence**: Each repo's daemon maintains separate state, never converges
3. **JSONL import/export race conditions**: Auto-import can overwrite local changes before export
4. **No conflict resolution**: When databases diverge, there's no merge strategy
5. **Timestamp-only changes**: bd-159 - exports trigger even with no real changes
6. **Multiple daemons**: No coordination between daemon instances

## Impact

**Beads is unusable for multi-developer or multi-agent workflows**. The core promise - git-based sync via JSONL - is broken.

## Fix Strategy (Epic)

This issue is tracked as an EPIC with child issues:

### Phase 1: Stop the Bleeding (P0)
- Implement daemon JSONL import (fixes the NO-OP)
- Add database integrity checks
- Fix timestamp-only exports (bd-159)

### Phase 2: Database Consistency (P0)
- Enforce canonical database naming
- Add database fingerprinting
- Migration tooling

### Phase 3: Conflict Resolution (P1)
- Implement version tracking
- Three-way merge detection
- Interactive conflict resolution

### Phase 4: Testing & Validation (P1)
- Multi-clone integration tests
- Stress tests
- Documentation

## Severity

P0 - This breaks the fundamental use case of beads. Without reliable sync, the tool is unusable for any multi-agent or team scenario.


Implement daemon JSONL import (fix NO-OP stub)

## Critical Bug

The daemon's sync loop calls importToJSONLWithStore() but this function is a NO-OP stub that returns nil without importing any changes.

## Location

**File**: cmd/bd/daemon.go:791-797

Current implementation:
```go
func importToJSONLWithStore(ctx context.Context, store storage.Storage, jsonlPath string) error {
    // TODO Phase 4: Implement direct import for daemon
    // Currently a no-op - daemon doesn't import git changes into DB
    return nil
}
```

## Impact

This is the PRIMARY cause of bd-160. When the daemon:
1. Exports DB → JSONL
2. Commits changes
3. Pulls from remote (gets other clone's changes)
4. Calls importToJSONLWithStore() ← **Does nothing!**
5. Pushes commits (overwrites remote with stale state)

Result: Perpetual divergence between clones.

## Implementation Approach

Replace the NO-OP with actual import logic:

```go
func importToJSONLWithStore(ctx context.Context, store storage.Storage, jsonlPath string) error {
    // Read JSONL file
    file, err := os.Open(jsonlPath)
    if err != nil {
        return fmt.Errorf("failed to open JSONL: %w", err)
    }
    defer file.Close()

    // Parse all issues
    var issues []*types.Issue
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        var issue types.Issue
        if err := json.Unmarshal(scanner.Bytes(), &issue); err != nil {
            return fmt.Errorf("failed to parse issue: %w", err)
        }
        issues = append(issues, &issue)
    }

    if err := scanner.Err(); err != nil {
        return fmt.Errorf("failed to read JSONL: %w", err)
    }

    // Use existing import logic with auto-conflict resolution
    opts := ImportOptions{
        ResolveCollisions: true,  // Auto-resolve ID conflicts
        DryRun:           false,
        SkipUpdate:       false,
        Strict:           false,
    }

    _, err = importIssuesCore(ctx, "", store, issues, opts)
    return err
}
```

## Testing

After implementation, test with:
```bash
# Create two clones
git init repo1 && cd repo1 && bd init && bd daemon
bd new "Issue A"
git add . && git commit -m "init"

cd .. && git clone repo1 repo2 && cd repo2 && bd init && bd daemon

# Make changes in repo1
cd ../repo1 && bd new "Issue B"

# Wait for daemon sync, then check repo2
sleep 10
cd ../repo2 && bd list  # Should show both Issue A and B
```

## Success Criteria

- Daemon imports remote changes after git pull
- Issue count converges across clones within one sync cycle
- No manual intervention needed
- Existing collision resolution logic handles conflicts

## Estimated Effort

30-60 minutes

## Priority

P0 - This is the critical path fix for bd-160


Add database integrity checks to sync operations

## Problem

When databases diverge (due to the import NO-OP bug or race conditions), there are no safety checks to detect or prevent catastrophic data loss.

Need integrity checks before/after sync operations to catch divergence early.

## Implementation Locations

**Pre-export checks** (cmd/bd/daemon.go:948, sync.go:108):
- Before exportToJSONLWithStore()
- Before exportToJSONL()

**Post-import checks** (cmd/bd/daemon.go:985):
- After importToJSONLWithStore()

## Checks to Implement

### 1. Database vs JSONL Count Divergence

Before export:
```go
func validatePreExport(store storage.Storage, jsonlPath string) error {
    dbIssues, _ := store.SearchIssues(ctx, "", types.IssueFilter{})
    dbCount := len(dbIssues)

    jsonlCount, _ := countIssuesInJSONL(jsonlPath)

    if dbCount == 0 && jsonlCount > 0 {
        return fmt.Errorf("refusing to export empty DB over %d issues in JSONL", jsonlCount)
    }

    divergencePercent := math.Abs(float64(dbCount-jsonlCount)) / float64(jsonlCount) * 100
    if divergencePercent > 50 {
        log.Printf("WARNING: DB has %d issues, JSONL has %d (%.1f%% divergence)",
            dbCount, jsonlCount, divergencePercent)
        log.Printf("This suggests sync failure - investigate before proceeding")
    }

    return nil
}
```

### 2. Duplicate ID Detection

```go
func checkDuplicateIDs(store storage.Storage) error {
    // Query for duplicate IDs
    rows, _ := db.Query(`
        SELECT id, COUNT(*) as cnt
        FROM issues
        GROUP BY id
        HAVING cnt > 1
    `)

    var duplicates []string
    for rows.Next() {
        var id string
        var count int
        rows.Scan(&id, &count)
        duplicates = append(duplicates, fmt.Sprintf("%s (x%d)", id, count))
    }

    if len(duplicates) > 0 {
        return fmt.Errorf("database corruption: duplicate IDs: %v", duplicates)
    }
    return nil
}
```

### 3. Orphaned Dependencies

```go
func checkOrphanedDeps(store storage.Storage) ([]string, error) {
    // Find dependencies pointing to non-existent issues
    rows, _ := db.Query(`
        SELECT DISTINCT d.depends_on_id
        FROM dependencies d
        LEFT JOIN issues i ON d.depends_on_id = i.id
        WHERE i.id IS NULL
    `)

    var orphaned []string
    for rows.Next() {
        var id string
        rows.Scan(&id)
        orphaned = append(orphaned, id)
    }

    if len(orphaned) > 0 {
        log.Printf("WARNING: Found %d orphaned dependencies: %v", len(orphaned), orphaned)
    }

    return orphaned, nil
}
```

### 4. Post-Import Validation

After import, verify:
```go
func validatePostImport(before, after int) error {
    if after < before {
        return fmt.Errorf("import reduced issue count: %d → %d (data loss!)", before, after)
    }
    if after == before {
        log.Printf("Import complete: no changes")
    } else {
        log.Printf("Import complete: %d → %d issues (+%d)", before, after, after-before)
    }
    return nil
}
```

## Integration Points

Add to daemon sync loop (daemon.go:920-999):
```go
// Before export
if err := validatePreExport(store, jsonlPath); err != nil {
    log.log("Pre-export validation failed: %v", err)
    return
}

// Export...

// Before import
beforeCount := countDBIssues(store)

// Import...

// After import
afterCount := countDBIssues(store)
if err := validatePostImport(beforeCount, afterCount); err != nil {
    log.log("Post-import validation failed: %v", err)
}
```

## Testing

Create test scenarios:
1. Empty DB, non-empty JSONL → should error
2. Duplicate IDs in DB → should error
3. Orphaned dependencies → should warn
4. Import reduces count → should error

## Success Criteria

- Catches divergence >50% before export
- Detects duplicate IDs
- Reports orphaned dependencies
- Validates import doesn't lose data
- All checks logged clearly

## Estimated Effort

2-3 hours

## Priority

P0 - Safety checks prevent data loss during sync


Fix timestamp-only export deduplication (bd-159)

## Problem

Export deduplication logic is supposed to skip timestamp-only changes, but it's not working. This causes:
- Spurious git commits every sync cycle
- Increased race condition window
- Harder to detect real changes
- Amplifies bd-160 sync issues

Related to bd-159.

## Location

**File**: cmd/bd/export.go:236-246

Current code clears dirty flags for all exported issues:
```go
if output == "" || output == findJSONLPath() {
    if err := store.ClearDirtyIssuesByID(ctx, exportedIDs); err != nil {
        fmt.Fprintf(os.Stderr, "Warning: failed to clear dirty issues: %v\n", err)
    }
    clearAutoFlushState()
}
```

Problem: No check whether issue actually changed (beyond timestamps).

## Root Cause

Issues are marked dirty on ANY update, including:
- Timestamp updates (UpdatedAt field)
- No-op updates (same values written)
- Database reopens (sqlite WAL journal replays)

## Implementation Approach

### 1. Add Content Hash to dirty_issues Table

```sql
ALTER TABLE dirty_issues ADD COLUMN content_hash TEXT;
```

The hash should exclude timestamp fields:
```go
func computeIssueContentHash(issue *types.Issue) string {
    // Clone issue and zero out timestamps
    normalized := *issue
    normalized.CreatedAt = time.Time{}
    normalized.UpdatedAt = time.Time{}

    // Serialize to JSON
    data, _ := json.Marshal(normalized)

    // SHA256 hash
    hash := sha256.Sum256(data)
    return hex.EncodeToString(hash[:])
}
```

### 2. Track Previous Export State

Store issue snapshots in issue_snapshots table (already exists):
```go
func saveExportSnapshot(ctx context.Context, store storage.Storage, issue *types.Issue) error {
    snapshot := &types.IssueSnapshot{
        IssueID:     issue.ID,
        SnapshotAt:  time.Now(),
        Title:       issue.Title,
        Description: issue.Description,
        Status:      issue.Status,
        // ... all fields except timestamps
    }
    return store.SaveSnapshot(ctx, snapshot)
}
```

### 3. Deduplicate During Export

In export.go:
```go
// Before encoding each issue
if shouldSkipExport(ctx, store, issue) {
    skippedCount++
    continue
}

func shouldSkipExport(ctx context.Context, store storage.Storage, issue *types.Issue) bool {
    // Get last exported snapshot
    snapshot, err := store.GetLatestSnapshot(ctx, issue.ID)
    if err != nil || snapshot == nil {
        return false  // No snapshot, must export
    }

    // Compare content hash
    currentHash := computeIssueContentHash(issue)
    snapshotHash := computeSnapshotHash(snapshot)

    if currentHash == snapshotHash {
        // Timestamp-only change, skip
        log.Printf("Skipping %s (timestamp-only change)", issue.ID)
        return true
    }

    return false
}
```

### 4. Update on Real Export

Only save snapshot when actually exporting:
```go
for _, issue := range issues {
    if shouldSkipExport(ctx, store, issue) {
        continue
    }

    if err := encoder.Encode(issue); err != nil {
        return err
    }

    // Save snapshot of exported state
    saveExportSnapshot(ctx, store, issue)
    exportedIDs = append(exportedIDs, issue.ID)
}
```

## Alternative: Simpler Approach

If snapshot complexity is too much, use a simpler hash:

```go
// In dirty_issues table, store hash when marking dirty
func markIssueDirty(ctx context.Context, issueID string, issue *types.Issue) error {
    hash := computeIssueContentHash(issue)

    _, err := db.Exec(`
        INSERT INTO dirty_issues (issue_id, content_hash)
        VALUES (?, ?)
        ON CONFLICT(issue_id) DO UPDATE SET content_hash = ?
    `, issueID, hash, hash)

    return err
}

// During export, check if hash changed
func hasRealChanges(ctx context.Context, store storage.Storage, issue *types.Issue) bool {
    var storedHash string
    err := db.QueryRow("SELECT content_hash FROM dirty_issues WHERE issue_id = ?", issue.ID).Scan(&storedHash)
    if err != nil {
        return true  // No stored hash, export it
    }

    currentHash := computeIssueContentHash(issue)
    return currentHash != storedHash
}
```

## Testing

Test cases:
1. Update issue timestamp only → no export
2. Update issue title → export
3. Multiple timestamp updates → single export
4. Database reopen → no spurious exports

Validation:
```bash
# Start daemon, wait 1 hour
bd daemon --interval 5s
sleep 3600

# Check git log - should be 0 commits
git log --since="1 hour ago" --oneline | wc -l  # expect: 0
```

## Success Criteria

- Zero spurious exports for timestamp-only changes
- Real changes still exported immediately
- No performance regression
- bd-159 resolved

## Estimated Effort

2-3 hours

## Priority

P0 - Prevents noise that amplifies bd-160 sync issues


Enforce canonical database naming (beads.db)

## Problem

Currently, different clones can use different database filenames (bd.db, beads.db, issues.db), causing incompatibility when attempting to sync.

Example from bd-160:
- ~/src/beads uses bd.db
- ~/src/fred/beads uses beads.db
- Sync fails because they're fundamentally different databases

## Solution

Enforce a single canonical database name: **beads.db**

## Implementation

### 1. Define Canonical Name

**File**: beads.go or constants.go (create if needed)

```go
package beads

// CanonicalDatabaseName is the required database filename for all beads repositories
const CanonicalDatabaseName = "beads.db"

// LegacyDatabaseNames are old names that should be migrated
var LegacyDatabaseNames = []string{"bd.db", "issues.db", "bugs.db"}
```

### 2. Update bd init Command

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

```go
func runInit(cmd *cobra.Command, args []string) error {
    beadsDir := ".beads"
    os.MkdirAll(beadsDir, 0755)

    dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)

    // Check for legacy databases
    for _, legacy := range beads.LegacyDatabaseNames {
        legacyPath := filepath.Join(beadsDir, legacy)
        if exists(legacyPath) {
            fmt.Printf("Found legacy database: %s\n", legacy)
            fmt.Printf("Migrating to canonical name: %s\n", beads.CanonicalDatabaseName)

            // Rename to canonical
            if err := os.Rename(legacyPath, dbPath); err != nil {
                return fmt.Errorf("migration failed: %w", err)
            }
            fmt.Printf("✓ Migrated %s → %s\n", legacy, beads.CanonicalDatabaseName)
        }
    }

    // Create new database if doesn't exist
    if !exists(dbPath) {
        store, err := sqlite.New(dbPath)
        if err != nil {
            return err
        }
        defer store.Close()

        // Initialize with version metadata
        store.SetMetadata(context.Background(), "bd_version", Version)
        store.SetMetadata(context.Background(), "db_name", beads.CanonicalDatabaseName)
    }

    // ... rest of init
}
```

### 3. Validate on Daemon Start

**File**: cmd/bd/daemon.go:1076-1095

Update the existing multiple-DB check:
```go
// Check for multiple .db files
beadsDir := filepath.Dir(daemonDBPath)
matches, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
if err == nil && len(matches) > 1 {
    log.log("Error: Multiple database files found:")
    for _, match := range matches {
        log.log("  - %s", filepath.Base(match))
    }
    log.log("")
    log.log("Beads requires a single canonical database: %s", beads.CanonicalDatabaseName)
    log.log("Run 'bd init' to migrate legacy databases")
    os.Exit(1)
}

// Validate using canonical name
if filepath.Base(daemonDBPath) != beads.CanonicalDatabaseName {
    log.log("Error: Non-canonical database name: %s", filepath.Base(daemonDBPath))
    log.log("Expected: %s", beads.CanonicalDatabaseName)
    log.log("Run 'bd init' to migrate to canonical name")
    os.Exit(1)
}
```

### 4. Add Migration Command

**File**: cmd/bd/migrate.go (create new)

```go
var migrateCmd = &cobra.Command{
    Use:   "migrate",
    Short: "Migrate database to canonical naming and schema",
    Run: func(cmd *cobra.Command, args []string) {
        beadsDir := ".beads"

        // Find current database
        var currentDB string
        for _, name := range append(beads.LegacyDatabaseNames, beads.CanonicalDatabaseName) {
            path := filepath.Join(beadsDir, name)
            if exists(path) {
                currentDB = path
                break
            }
        }

        if currentDB == "" {
            fmt.Println("No database found")
            return
        }

        targetPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)

        if currentDB == targetPath {
            fmt.Println("Database already using canonical name")
            return
        }

        // Backup first
        backupPath := currentDB + ".backup"
        copyFile(currentDB, backupPath)
        fmt.Printf("Created backup: %s\n", backupPath)

        // Rename
        if err := os.Rename(currentDB, targetPath); err != nil {
            fmt.Fprintf(os.Stderr, "Migration failed: %v\n", err)
            os.Exit(1)
        }

        fmt.Printf("✓ Migrated: %s → %s\n", filepath.Base(currentDB), beads.CanonicalDatabaseName)

        // Update metadata
        store, _ := sqlite.New(targetPath)
        defer store.Close()
        store.SetMetadata(context.Background(), "db_name", beads.CanonicalDatabaseName)
    },
}
```

### 5. Update FindDatabasePath

**File**: beads.go (or wherever FindDatabasePath is defined)

```go
func FindDatabasePath() string {
    beadsDir := findBeadsDir()
    if beadsDir == "" {
        return ""
    }

    // First try canonical name
    canonical := filepath.Join(beadsDir, CanonicalDatabaseName)
    if exists(canonical) {
        return canonical
    }

    // Check for legacy names (warn user)
    for _, legacy := range LegacyDatabaseNames {
        path := filepath.Join(beadsDir, legacy)
        if exists(path) {
            fmt.Fprintf(os.Stderr, "WARNING: Using legacy database name: %s\n", legacy)
            fmt.Fprintf(os.Stderr, "Run 'bd migrate' to upgrade to canonical name: %s\n", CanonicalDatabaseName)
            return path
        }
    }

    return ""
}
```

## Testing

```bash
# Test migration
mkdir test-repo && cd test-repo && git init
mkdir .beads
sqlite3 .beads/bd.db "CREATE TABLE test (id int);"

bd init  # Should detect and migrate bd.db → beads.db

# Verify
ls .beads/*.db  # Should only show beads.db

# Test daemon rejection
sqlite3 .beads/old.db "CREATE TABLE test (id int);"
bd daemon  # Should error: multiple databases found

# Test clean init
rm -rf test-repo2 && mkdir test-repo2 && cd test-repo2
bd init  # Should create .beads/beads.db directly
```

## Rollout Strategy

1. Add migration logic to bd init
2. Update FindDatabasePath to warn on legacy names
3. Add 'bd migrate' command for manual migration
4. Update docs to specify canonical name
5. Add daemon validation after 2 releases

## Success Criteria

- All new repositories use beads.db
- bd init auto-migrates legacy names
- bd daemon rejects non-canonical names
- Clear migration path for existing users
- No data loss during migration

## Estimated Effort

3-4 hours

## Priority

P0 - Critical for multi-clone compatibility


Add database fingerprinting and validation

## Problem

When multiple clones exist, there's no validation that they're actually clones of the same repository. Different repos can accidentally share databases, causing data corruption.

Need database fingerprinting to ensure clones belong to the same logical repository.

## Solution

Add repository fingerprint to database metadata and validate on daemon start.

## Implementation

### 1. Compute Repository ID

**File**: pkg/fingerprint.go (create new)

```go
package beads

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "os/exec"
)

// ComputeRepoID generates a unique identifier for this git repository
func ComputeRepoID() (string, error) {
    // Get git remote URL (canonical repo identifier)
    cmd := exec.Command("git", "config", "--get", "remote.origin.url")
    output, err := cmd.Output()
    if err != nil {
        // No remote configured, use local path
        cmd = exec.Command("git", "rev-parse", "--show-toplevel")
        output, err = cmd.Output()
        if err != nil {
            return "", fmt.Errorf("not a git repository")
        }
    }

    repoURL := strings.TrimSpace(string(output))

    // Normalize URL (remove .git suffix, https vs git@, etc.)
    repoURL = normalizeGitURL(repoURL)

    // SHA256 hash for privacy (don't expose repo URL in database)
    hash := sha256.Sum256([]byte(repoURL))
    return hex.EncodeToString(hash[:16]), nil  // Use first 16 bytes
}

func normalizeGitURL(url string) string {
    // Convert git@github.com:user/repo.git → github.com/user/repo
    // Convert https://github.com/user/repo.git → github.com/user/repo
    url = strings.TrimSuffix(url, ".git")
    url = strings.ReplaceAll(url, "git@", "")
    url = strings.ReplaceAll(url, "https://", "")
    url = strings.ReplaceAll(url, "http://", "")
    url = strings.ReplaceAll(url, ":", "/")
    return url
}

// GetCloneID generates a unique ID for this specific clone (not shared with other clones)
func GetCloneID() string {
    // Use hostname + path for uniqueness
    hostname, _ := os.Hostname()
    path, _ := os.Getwd()
    hash := sha256.Sum256([]byte(hostname + ":" + path))
    return hex.EncodeToString(hash[:8])
}
```

### 2. Store Fingerprint on Init

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

```go
func runInit(cmd *cobra.Command, args []string) error {
    // ... create database ...

    // Compute and store repo ID
    repoID, err := beads.ComputeRepoID()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Warning: could not compute repo ID: %v\n", err)
    } else {
        if err := store.SetMetadata(ctx, "repo_id", repoID); err != nil {
            return fmt.Errorf("failed to set repo_id: %w", err)
        }
        fmt.Printf("Repository ID: %s\n", repoID[:8])
    }

    // Store clone ID
    cloneID := beads.GetCloneID()
    if err := store.SetMetadata(ctx, "clone_id", cloneID); err != nil {
        return fmt.Errorf("failed to set clone_id: %w", err)
    }
    fmt.Printf("Clone ID: %s\n", cloneID)

    // Store creation timestamp
    if err := store.SetMetadata(ctx, "created_at", time.Now().Format(time.RFC3339)); err != nil {
        return fmt.Errorf("failed to set created_at: %w", err)
    }

    return nil
}
```

### 3. Validate on Database Open

**File**: cmd/bd/daemon.go (in runDaemonLoop)

```go
func validateDatabaseFingerprint(store storage.Storage) error {
    ctx := context.Background()

    // Get stored repo ID
    storedRepoID, err := store.GetMetadata(ctx, "repo_id")
    if err != nil && err.Error() != "metadata key not found: repo_id" {
        return fmt.Errorf("failed to read repo_id: %w", err)
    }

    // If no repo_id, this is a legacy database - set it now
    if storedRepoID == "" {
        repoID, err := beads.ComputeRepoID()
        if err != nil {
            log.log("Warning: could not compute repo ID: %v", err)
            return nil  // Non-fatal for backward compat
        }

        log.log("Legacy database detected, setting repo_id: %s", repoID[:8])
        if err := store.SetMetadata(ctx, "repo_id", repoID); err != nil {
            return fmt.Errorf("failed to set repo_id: %w", err)
        }
        return nil
    }

    // Validate repo ID matches
    currentRepoID, err := beads.ComputeRepoID()
    if err != nil {
        log.log("Warning: could not compute current repo ID: %v", err)
        return nil  // Non-fatal
    }

    if storedRepoID != currentRepoID {
        return fmt.Errorf(`
DATABASE MISMATCH DETECTED!

This database belongs to a different repository:
  Database repo ID:  %s
  Current repo ID:   %s

This usually means:
  1. You copied a .beads directory from another repo (don't do this!)
  2. Git remote URL changed (run 'bd migrate' to update)
  3. Database corruption

Solutions:
  - If remote URL changed: bd migrate --update-repo-id
  - If wrong database: rm -rf .beads && bd init
  - If correct database: BEADS_IGNORE_REPO_MISMATCH=1 bd daemon
`, storedRepoID[:8], currentRepoID[:8])
    }

    return nil
}

// In runDaemonLoop, after opening database:
if err := validateDatabaseFingerprint(store); err != nil {
    if os.Getenv("BEADS_IGNORE_REPO_MISMATCH") != "1" {
        log.log("Error: %v", err)
        os.Exit(1)
    }
    log.log("Warning: repo mismatch ignored (BEADS_IGNORE_REPO_MISMATCH=1)")
}
```

### 4. Add Update Command for Remote Changes

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

```go
var updateRepoID bool

func init() {
    migrateCmd.Flags().BoolVar(&updateRepoID, "update-repo-id", false,
        "Update repository ID (use after changing git remote)")
}

// In migrate command:
if updateRepoID {
    newRepoID, err := beads.ComputeRepoID()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }

    oldRepoID, _ := store.GetMetadata(ctx, "repo_id")

    fmt.Printf("Updating repository ID:\n")
    fmt.Printf("  Old: %s\n", oldRepoID[:8])
    fmt.Printf("  New: %s\n", newRepoID[:8])

    if err := store.SetMetadata(ctx, "repo_id", newRepoID); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }

    fmt.Println("✓ Repository ID updated")
}
```

## Metadata Schema

Add to `metadata` table:

| Key | Value | Description |
|-----|-------|-------------|
| repo_id | sha256(git_remote)[..16] | Repository fingerprint |
| clone_id | sha256(hostname:path)[..8] | Clone-specific ID |
| created_at | RFC3339 timestamp | Database creation time |
| db_name | "beads.db" | Canonical database name |
| bd_version | "v0.x.x" | Schema version |

## Testing

```bash
# Test repo ID generation
cd /tmp/test-repo && git init
git remote add origin https://github.com/user/repo.git
bd init
bd show-meta repo_id  # Should show consistent hash

# Test mismatch detection
cd /tmp/other-repo && git init
git remote add origin https://github.com/other/repo.git
cp -r /tmp/test-repo/.beads /tmp/other-repo/
bd daemon  # Should error: repo mismatch

# Test migration
git remote set-url origin https://github.com/user/new-repo.git
bd migrate --update-repo-id  # Should update successfully
```

## Success Criteria

- New databases automatically get repo_id
- Daemon validates repo_id on start
- Clear error messages on mismatch
- Migration path for remote URL changes
- Legacy databases automatically fingerprinted

## Estimated Effort

3-4 hours

## Priority

P0 - Prevents accidental database mixing across repos


Implement version tracking for issues

## Problem

When two clones modify the same issue concurrently, there's no way to detect or handle the conflict properly. Last writer wins arbitrarily, losing data.

Need version tracking to implement proper conflict detection and resolution.

## Solution

Add version counter and last-modified metadata to issues for Last-Writer-Wins (LWW) conflict resolution.

## Database Schema Changes

**File**: internal/storage/sqlite/schema.go

```sql
-- Add version tracking columns to issues table
ALTER TABLE issues ADD COLUMN version INTEGER DEFAULT 1 NOT NULL;
ALTER TABLE issues ADD COLUMN modified_by TEXT DEFAULT '' NOT NULL;
ALTER TABLE issues ADD COLUMN modified_at DATETIME;

-- Create index for version-based queries
CREATE INDEX IF NOT EXISTS idx_issues_version ON issues(id, version);

-- Store modification history
CREATE TABLE IF NOT EXISTS issue_versions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    issue_id TEXT NOT NULL,
    version INTEGER NOT NULL,
    modified_by TEXT NOT NULL,
    modified_at DATETIME NOT NULL,
    snapshot BLOB NOT NULL,  -- JSON snapshot of issue at this version
    FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
    UNIQUE(issue_id, version)
);

CREATE INDEX IF NOT EXISTS idx_issue_versions_lookup
    ON issue_versions(issue_id, version);
```

## Implementation

### 1. Update Issue Type

**File**: internal/types/issue.go

```go
type Issue struct {
    ID           string    `json:"id"`
    Title        string    `json:"title"`
    Description  string    `json:"description"`
    Status       Status    `json:"status"`
    Priority     int       `json:"priority"`
    CreatedAt    time.Time `json:"created_at"`
    UpdatedAt    time.Time `json:"updated_at"`

    // Version tracking (new fields)
    Version      int       `json:"version"`       // Incremented on each update
    ModifiedBy   string    `json:"modified_by"`   // Clone ID that made the change
    ModifiedAt   time.Time `json:"modified_at"`   // When the change was made

    // ... rest of fields
}
```

### 2. Increment Version on Update

**File**: internal/storage/sqlite/sqlite.go

```go
func (s *SQLiteStorage) UpdateIssue(ctx context.Context, issue *types.Issue) error {
    // Get current version from database
    var currentVersion int
    var currentModifiedAt time.Time
    err := s.db.QueryRowContext(ctx, `
        SELECT version, modified_at
        FROM issues
        WHERE id = ?
    `, issue.ID).Scan(&currentVersion, &currentModifiedAt)

    if err != nil && err != sql.ErrNoRows {
        return fmt.Errorf("failed to get current version: %w", err)
    }

    // Detect conflict: incoming version is stale
    if issue.Version > 0 && issue.Version < currentVersion {
        return &ConflictError{
            IssueID:        issue.ID,
            LocalVersion:   currentVersion,
            RemoteVersion:  issue.Version,
            LocalModified:  currentModifiedAt,
            RemoteModified: issue.ModifiedAt,
        }
    }

    // No conflict or local is newer: increment version
    issue.Version = currentVersion + 1
    issue.ModifiedBy = getCloneID()  // From fingerprinting
    issue.ModifiedAt = time.Now()

    // Save version snapshot before updating
    if err := s.saveVersionSnapshot(ctx, issue); err != nil {
        // Non-fatal warning
        log.Printf("Warning: failed to save version snapshot: %v", err)
    }

    // Perform update
    _, err = s.db.ExecContext(ctx, `
        UPDATE issues SET
            title = ?,
            description = ?,
            status = ?,
            priority = ?,
            updated_at = ?,
            version = ?,
            modified_by = ?,
            modified_at = ?
        WHERE id = ?
    `, issue.Title, issue.Description, issue.Status, issue.Priority,
       issue.UpdatedAt, issue.Version, issue.ModifiedBy, issue.ModifiedAt,
       issue.ID)

    return err
}

func (s *SQLiteStorage) saveVersionSnapshot(ctx context.Context, issue *types.Issue) error {
    snapshot, _ := json.Marshal(issue)

    _, err := s.db.ExecContext(ctx, `
        INSERT INTO issue_versions (issue_id, version, modified_by, modified_at, snapshot)
        VALUES (?, ?, ?, ?, ?)
    `, issue.ID, issue.Version, issue.ModifiedBy, issue.ModifiedAt, snapshot)

    return err
}
```

### 3. Conflict Detection on Import

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

```go
type ConflictError struct {
    IssueID        string
    LocalVersion   int
    RemoteVersion  int
    LocalModified  time.Time
    RemoteModified time.Time
    LocalIssue     *types.Issue
    RemoteIssue    *types.Issue
}

func (e *ConflictError) Error() string {
    return fmt.Sprintf("conflict on %s: local v%d (modified %s) vs remote v%d (modified %s)",
        e.IssueID, e.LocalVersion, e.LocalModified, e.RemoteVersion, e.RemoteModified)
}

func detectVersionConflict(local, remote *types.Issue) *ConflictError {
    // No conflict if same version
    if local.Version == remote.Version {
        return nil
    }

    // Remote is newer - no conflict
    if remote.Version > local.Version {
        return nil
    }

    // Local is newer - remote is stale
    if remote.Version < local.Version {
        // Check if concurrent modification (both diverged from same base)
        if local.ModifiedAt.Sub(remote.ModifiedAt).Abs() < 1*time.Minute {
            return &ConflictError{
                IssueID:        local.ID,
                LocalVersion:   local.Version,
                RemoteVersion:  remote.Version,
                LocalModified:  local.ModifiedAt,
                RemoteModified: remote.ModifiedAt,
                LocalIssue:     local,
                RemoteIssue:    remote,
            }
        }
    }

    return nil
}
```

### 4. Conflict Resolution Strategies

```go
type ConflictStrategy int

const (
    StrategyLWW           ConflictStrategy = iota // Last Writer Wins (use newest modified_at)
    StrategyHighestVersion                        // Use highest version number
    StrategyInteractive                           // Prompt user
    StrategyMerge                                 // Three-way merge (future)
)

func resolveConflict(conflict *ConflictError, strategy ConflictStrategy) (*types.Issue, error) {
    switch strategy {
    case StrategyLWW:
        if conflict.RemoteModified.After(conflict.LocalModified) {
            return conflict.RemoteIssue, nil
        }
        return conflict.LocalIssue, nil

    case StrategyHighestVersion:
        if conflict.RemoteVersion > conflict.LocalVersion {
            return conflict.RemoteIssue, nil
        }
        return conflict.LocalIssue, nil

    case StrategyInteractive:
        return promptUserForResolution(conflict)

    default:
        return nil, fmt.Errorf("unknown conflict strategy: %v", strategy)
    }
}
```

## Migration for Existing Databases

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

```go
func migrateToVersionTracking(store storage.Storage) error {
    ctx := context.Background()

    // Add columns if not exist
    _, err := db.Exec(`
        ALTER TABLE issues ADD COLUMN IF NOT EXISTS version INTEGER DEFAULT 1 NOT NULL
    `)
    if err != nil {
        return err
    }

    _, err = db.Exec(`
        ALTER TABLE issues ADD COLUMN IF NOT EXISTS modified_by TEXT DEFAULT ''
    `)
    if err != nil {
        return err
    }

    _, err = db.Exec(`
        ALTER TABLE issues ADD COLUMN IF NOT EXISTS modified_at DATETIME
    `)
    if err != nil {
        return err
    }

    // Backfill modified_at from updated_at
    _, err = db.Exec(`
        UPDATE issues SET modified_at = updated_at WHERE modified_at IS NULL
    `)

    return err
}
```

## Testing

```bash
# Test version increment
bd create "Test issue"
bd show bd-1 --json | jq .version  # Should be 1
bd update bd-1 --title "Updated"
bd show bd-1 --json | jq .version  # Should be 2

# Test conflict detection
# Clone A: modify bd-1
cd repo-a && bd update bd-1 --title "A's version"
# Clone B: modify bd-1
cd repo-b && bd update bd-1 --title "B's version"

# Sync
cd repo-a && bd sync  # Should detect conflict
```

## Success Criteria

- All issues have version numbers
- Version increments on each update
- Conflicts detected when importing stale versions
- Version history preserved in issue_versions table
- Migration works for existing databases

## Estimated Effort

4-5 hours

## Priority

P1 - Enables proper conflict detection (required before three-way merge)