Add 'bd message send' command for Agent Mail messaging

Agent Mail server supports messaging between agents, but bd CLI only uses it for file reservations. Add commands for inter-agent messaging.

## Background
- Agent Mail server running at http://127.0.0.1:8765
- 12 workspaces configured across 3 channels (beads.dev, vc.dev, wyvern.dev)
- Current integration: file reservations only
- Gap: no way to send messages from bd CLI

## Proposed Commands

```bash
# Send message to another agent
bd message send <to-agent> <message> [options]
  --subject <subject>
  --thread-id <thread-id>     # Optional - group related messages
  --project-id <project>      # Defaults to BEADS_PROJECT_ID

# List inbox messages
bd message inbox [options]
  --limit <N>
  --unread-only

# Read specific message
bd message read <message-id>

# Mark message as acknowledged
bd message ack <message-id>
```

## Example Usage

```bash
# Send message to agent in same channel
bd message send cino-beads-stevey-macbook "Working on bd-z0yn, need your review" \
  --subject "Review request" \
  --thread-id bd-z0yn

# Check inbox
bd message inbox --unread-only

# Read and acknowledge
bd message read msg-abc123
bd message ack msg-abc123
```

## Design Notes
- Use same env vars (BEADS_AGENT_MAIL_URL, BEADS_AGENT_NAME, BEADS_PROJECT_ID)
- Graceful degradation if Agent Mail unavailable
- JSON output support for all commands
- Consider integrating with bd update/close (auto-notify on status changes)

## References
- Agent Mail README: ~/src/mcp_agent_mail/README.md
- Beads integration docs: docs/AGENT_MAIL.md

## Implementation Summary

Added four new commands to bd CLI for Agent Mail messaging:

1. `bd message send <to-agent> <message>` - Send messages to other agents
   - Flags: --subject, --thread-id, --importance, --ack-required
   - Supports markdown content
   - Thread conversations by issue ID

2. `bd message inbox` - List inbox messages
   - Flags: --limit, --unread-only, --urgent-only, --json
   - Shows subject, sender, age, importance
   - Highlights unread and ACK-required messages

3. `bd message read <message-id>` - Read and mark message as read
   - Automatically marks message as read
   - Shows message content

4. `bd message ack <message-id>` - Acknowledge a message
   - Marks message as acknowledged
   - Also marks as read if not already

## Implementation Details

- Uses JSON-RPC over HTTP to communicate with Agent Mail server
- Configuration via environment variables (BEADS_AGENT_MAIL_URL, BEADS_AGENT_NAME, BEADS_PROJECT_ID)
- Graceful error messages when Agent Mail not configured
- Full JSON output support for programmatic use
- Follows same patterns as existing bd commands

## Documentation

Updated:
- docs/AGENT_MAIL.md - Added "Messaging Commands" section with examples and best practices
- README.md - Added "Messaging (Agent Mail)" section in Usage

## Testing

- Compiles successfully
- Help output works correctly
- Ready for integration testing with Agent Mail server


Implement transaction retry logic for SQLITE_BUSY

BEGIN IMMEDIATE fails immediately on SQLITE_BUSY instead of retrying with exponential backoff.

Location: internal/storage/sqlite/sqlite.go:223-225

Problem:
- Under concurrent write load, BEGIN IMMEDIATE can fail with SQLITE_BUSY
- Current implementation fails immediately instead of retrying
- Results in spurious failures under normal concurrent usage

Solution: Implement exponential backoff retry:
- Retry up to N times (e.g., 5)
- Backoff: 10ms, 20ms, 40ms, 80ms, 160ms
- Check for context cancellation between retries
- Only retry on SQLITE_BUSY/database locked errors

Impact: Spurious failures under concurrent write load

Effort: 3 hours


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.


Debug test


Create startup hook snippet for upgrade detection

Provide a reusable startup hook that agents can use to detect bd upgrades automatically.

## What to Create
A bash script snippet that:
1. Reads current bd version
2. Compares to last-seen version in metadata.json
3. Shows 'bd info --whats-new' output when version changes
4. Updates metadata.json with new version
5. Auto-runs 'bd hooks install' if hooks outdated

## Where to Document
- Create examples/hooks/startup-version-check.sh
- Document in AGENTS.md with usage instructions
- Add comment header explaining how to integrate with Claude Code, Cursor, etc.

## Key Features
- Works today with zero bd code changes
- Leverages existing bd info --whats-new
- Uses metadata.json for persistence
- Auto-fixes outdated git hooks

## Acceptance Criteria
- Script works standalone
- Clear integration instructions for popular AI environments
- Handles edge cases (no metadata.json, first run, etc.)


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


Create backup and restore procedures

Disaster recovery procedures for Agent Mail data.

Acceptance Criteria:
- Automated daily snapshots (GCP persistent disk)
- SQLite backup script
- Git repository backup
- Restore procedure documentation
- Test restore from backup

File: deployment/agent-mail/backup.sh


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


Implement global N-way collision resolution algorithm

## Overview
Phase 3: Replace pairwise collision resolution with global N-way resolution that produces deterministic results regardless of sync order.

## Current Problem
ScoreCollisions (collision.go:228) compares issues pairwise:
```go
collision.RemapIncoming = existingHash < incomingHash
```

This works for 2-way but fails for 3+ way because:
- Each clone makes local decisions without global context
- No guarantee intermediate states are consistent
- Remapping decisions depend on sync order
- Can't detect transitive remap chains (test-1 → test-2 → test-3)

## Solution
Implement global resolution that:
1. Collects ALL versions of same logical issue
2. Sorts by content hash (deterministic)
3. Assigns sequential IDs based on sorted order
4. All clones converge to same assignments

## Implementation Tasks

### 1. Create ResolveNWayCollisions function
File: internal/storage/sqlite/collision.go

Replace ScoreCollisions with:
```go
// ResolveNWayCollisions handles N-way collisions deterministically.
// Groups all versions with same base ID, sorts by content hash,
// assigns sequential IDs. Returns mapping of old ID → new ID.
func ResolveNWayCollisions(ctx context.Context, s *SQLiteStorage,
    collisions []*CollisionDetail, incoming []*types.Issue) (map[string]string, error) {

    if len(collisions) == 0 {
        return make(map[string]string), nil
    }

    // Group by base ID pattern (e.g., test-1, test-2 → base "test-1")
    groups := groupCollisionsByBaseID(collisions)

    idMapping := make(map[string]string)

    for baseID, versions := range groups {
        // 1. Collect all unique versions by content hash
        uniqueVersions := deduplicateVersionsByContentHash(versions)

        // 2. Sort by content hash (deterministic!)
        sort.Slice(uniqueVersions, func(i, j int) bool {
            return uniqueVersions[i].ContentHash < uniqueVersions[j].ContentHash
        })

        // 3. Assign sequential IDs based on sorted order
        prefix := extractPrefix(baseID)
        baseNum := extractNumber(baseID)

        for i, version := range uniqueVersions {
            targetID := fmt.Sprintf("%s-%d", prefix, baseNum+i)

            // Map this version to its deterministic ID
            if version.ID != targetID {
                idMapping[version.ID] = targetID
            }
        }
    }

    return idMapping, nil
}
```

### 2. Implement helper functions

```go
// groupCollisionsByBaseID groups collisions by their logical base ID
func groupCollisionsByBaseID(collisions []*CollisionDetail) map[string][]*types.Issue {
    groups := make(map[string][]*types.Issue)
    for _, c := range collisions {
        baseID := c.ID  // All share same ID (that's why they collide)
        groups[baseID] = append(groups[baseID], c.ExistingIssue, c.IncomingIssue)
    }
    return groups
}

// deduplicateVersionsByContentHash keeps one issue per unique content hash
func deduplicateVersionsByContentHash(issues []*types.Issue) []*types.Issue {
    seen := make(map[string]*types.Issue)
    for _, issue := range issues {
        if _, found := seen[issue.ContentHash]; !found {
            seen[issue.ContentHash] = issue
        }
    }
    result := make([]*types.Issue, 0, len(seen))
    for _, issue := range seen {
        result = append(result, issue)
    }
    return result
}
```

### 3. Update handleCollisions in importer
File: internal/importer/importer.go

Replace ScoreCollisions call with:
```go
// OLD:
if err := sqlite.ScoreCollisions(ctx, sqliteStore, collisionResult.Collisions, allExistingIssues); err != nil {
    return nil, fmt.Errorf("failed to score collisions: %w", err)
}

// NEW:
idMapping, err := sqlite.ResolveNWayCollisions(ctx, sqliteStore,
    collisionResult.Collisions, issues)
if err != nil {
    return nil, fmt.Errorf("failed to resolve collisions: %w", err)
}
```

### 4. Update RemapCollisions
RemapCollisions currently uses collision.RemapIncoming field. Update to use idMapping directly:
- Remove RemapIncoming logic
- Use idMapping to determine what to remap
- Simplify to just apply the computed mapping

### 5. Add comprehensive tests

Test cases:
1. 3-way collision with different content → 3 sequential IDs
2. 3-way collision with 2 identical content → 2 IDs (dedupe works)
3. Sync order independence (A→B→C vs C→A→B produce same result)
4. Content hash ordering is respected
5. Works with 5+ clones

## Acceptance Criteria
- ResolveNWayCollisions implemented and replaces ScoreCollisions
- Groups all versions of same ID together
- Deduplicates by content hash
- Sorts by content hash deterministically
- Assigns sequential IDs starting from base ID
- Returns complete mapping (old ID → new ID)
- All clones converge to same ID assignments
- Works for arbitrary N-way collisions
- TestThreeCloneCollision passes (or gets much closer)

## Files to Modify
- internal/storage/sqlite/collision.go (new function, helpers)
- internal/importer/importer.go (call new function)
- internal/storage/sqlite/collision_test.go (comprehensive tests)

## Testing Strategy

### Unit Tests
- groupCollisionsByBaseID correctly groups
- deduplicateVersionsByContentHash removes duplicates
- Sorting by hash is stable and deterministic
- Sequential ID assignment is correct

### Integration Tests
- 3-way collision resolves to 3 issues
- Sync order doesn't affect final IDs
- Content hash ordering determines winner

### Property Tests
- For any N clones with same content, all converge to same IDs
- Idempotent: running resolution twice produces same result

## Dependencies
- Requires bd-cbed9619.5 (ContentHash field) to be completed first
- Requires bd-cbed9619.4 (read-only detection) for clean integration

## Notes
This is the core algorithm that enables convergence. The key insight:
**Sort by content hash globally, not pairwise comparison.**


Standardize --dry-run flag across all commands

Replace all instances of --preview with --dry-run for consistency.

**Commands to update:**
- clean: Change --preview to --dry-run

**Commands to verify:**
- cleanup, compact, delete, duplicates, epic close-eligible, import, sync

**Testing:**
- Ensure all mutating commands support --dry-run
- Verify backward compatibility (deprecation warning for --preview?)
- Update tests and documentation

All commands already use --dry-run consistently. Verified:
- cleanup: line 130
- compact: line 873
- delete: line 559
- duplicates: line 158
- import: line 703
- sync: line 333
- epic close-eligible: line 208
- clean: line 162

No --preview flags found in the codebase. This task appears to have been completed before the epic was created.


Research Go WASM compilation and modernc.org/sqlite WASM support

Investigate technical requirements for compiling bd to WASM:
- Verify modernc.org/sqlite has working js/wasm support
- Identify Go stdlib limitations in WASM (syscalls, file I/O, etc.)
- Research wasm_exec.js runtime and Node.js integration
- Document any API differences between native and WASM builds


Add tests for daemon lifecycle module


Extract daemon config functions to daemon_config.go


bd message: Improve type safety with typed parameter structs


GetNextChildID doesn't attempt parent resurrection from JSONL history

When creating a child issue with --parent flag, GetNextChildID fails immediately if parent doesn't exist in DB, without attempting to resurrect it from JSONL history. This breaks the intended resurrection workflow and causes 'parent issue X does not exist' errors even when the parent exists in JSONL.

Related to GH #334 and #278.

Current behavior:
- GetNextChildID checks if parent exists in DB
- If not found, returns error immediately
- No resurrection attempt

Expected behavior:
- GetNextChildID should call TryResurrectParent before failing
- Parent should be restored as tombstone if found in JSONL history
- Child creation should succeed if resurrection succeeds

Impact: Users cannot create child issues for parents that were deleted but exist in JSONL history.


Test RPC mutation event


Create WASM build infrastructure (Makefile, scripts)

Set up build tooling for WASM compilation:
- Add GOOS=js GOARCH=wasm build target
- Copy wasm_exec.js from Go distribution
- Create wrapper script for Node.js execution
- Add build task to Makefile or build script


Message System Improvements

Consolidate improvements to the bd message command including core functionality (message reading), reliability (timeouts), validation, and code quality refactoring


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.


Rapid 3


Beads commands operate on stale database without warning

All beads read operations should validate database is in sync with JSONL before proceeding.

**Current Behavior:**
- Commands can query/read from stale database
- Only mutation operations (like 'bd sync') check if JSONL is newer
- User gets incorrect results without realizing database is out of sync

**Expected Behavior:**
- All beads commands should have pre-flight check for database freshness
- If JSONL is newer than database, refuse to operate with error: "Database out of sync. Run 'bd import' first."
- Same safety check that exists for 'bd sync' should apply to ALL operations

**Impact:**
- Users make decisions based on incomplete/outdated data
- Silent failures lead to confusion (e.g., thinking issues don't exist when they do)
- Similar to running git commands on stale repo without being warned to pull

**Example:**
- Searched for bd-g9eu issue file: not found
- Issue exists in .beads/issues.jsonl (in git)
- Database was stale, but no warning was given
- Led to incorrect conclusion that issue was already closed/deleted

## Implementation Complete

**Phase 1: Created staleness check (cmd/bd/staleness.go)**
- ensureDatabaseFresh() function checks JSONL mtime vs last_import_time
- Returns error with helpful message when database is stale
- Auto-skips in daemon mode (daemon has auto-import)

**Phase 2: Added to all read commands**
- list, show, ready, status, stale, info, duplicates, validate
- Check runs before database queries in direct mode
- Daemon mode already protected via checkAndAutoImportIfStale()

**Phase 3: Code Review Findings**
See follow-up issues:
- bd-XXXX: Add warning when staleness check errors
- bd-YYYY: Improve CheckStaleness error handling
- bd-ZZZZ: Refactor redundant daemon checks (low priority)

**Testing:**
- Build successful: go build ./cmd/bd
- Binary works: ./bd --version
- Ready for manual testing

**Next Steps:**
1. Test with stale database scenario
2. Implement review improvements
3. Close issue when tests pass


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`


Document which files to commit after bd init --branch

GH #312 reported confusion about which files should be committed after running 'bd init --branch beads-metadata'. Updated PROTECTED_BRANCHES.md to clearly document:

1. Files that should be committed to protected branch (main):
   - .beads/.gitignore
   - .gitattributes

2. Files that are automatically gitignored
3. Files that live in the sync branch (beads-metadata)

Changes:
- Added step-by-step instructions in Quick Start section
- Added 'What lives in each branch' section to How It Works
- Clarified the directory structure diagram

Fixes: https://github.com/steveyegge/beads/issues/312


Add label-based filtering to bd ready command

Allow filtering ready work by labels to help organize work by sprint, week, or category.

Example usage:
  bd ready --label week1-2
  bd ready --label frontend,high-priority

This helps teams organize work into batches and makes it easier for agents to focus on specific categories of work.

Implementation notes:
- Add --label flag to ready command
- Support comma-separated labels (AND logic)
- Should work with existing ready work logic (unblocked issues)


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.


Test simple issue


Add schema compatibility probe to prevent silent migration failures

Issue #262 revealed a serious bug: migrations may fail silently, causing UNIQUE constraint errors later.

Root cause:
- sqlite.New() runs migrations once on open
- checkVersionMismatch() prints 'database will be upgraded automatically' but only updates metadata
- If migrations fail or daemon runs older version, queries expecting new columns fail with 'no such column'
- Import logic misinterprets this as 'not found' and tries INSERT on existing ID
- Result: UNIQUE constraint failed: issues.id

Fix strategy (minimal):
1. Add schema probe in sqlite.New() after RunMigrations
   - SELECT all expected columns from all tables with LIMIT 0
   - If fails, retry RunMigrations and probe again
   - If still fails, return fatal error with clear message
2. Fix checkVersionMismatch to not claim 'will upgrade' unless probe passes
3. Only update bd_version after successful migration probe
4. Add schema verification before import operations
5. Map 'no such column' errors to clear actionable message

Related: #262


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.


Add tests for daemon config module


Make DetectCollisions read-only (separate detection from modification)

## Overview
Phase 2: Separate collision detection from state modification to enable safe, composable collision resolution.

## Current Problem
DetectCollisions (collision.go:38-111) modifies database state during detection:
- Line 83-86: Deletes issues when content matches but ID differs
- This violates separation of concerns
- Causes race conditions when processing multiple issues
- Makes contentToDBIssue map stale after first deletion
- Partial failures leave DB in inconsistent state

## Solution
Make DetectCollisions purely read-only. Move all modifications to a separate ApplyCollisionResolution function.

## Implementation Tasks

### 1. Add RenameDetail to CollisionResult
File: internal/storage/sqlite/collision.go
```go
type CollisionResult struct {
    ExactMatches []string
    Collisions   []*CollisionDetail
    NewIssues    []string
    Renames      []*RenameDetail  // NEW
}

type RenameDetail struct {
    OldID string        // ID in database
    NewID string        // ID in incoming
    Issue *types.Issue  // The issue with new ID
}
```

### 2. Remove deletion from DetectCollisions
Replace lines 83-86:
```go
// OLD (DELETE THIS):
if err := s.DeleteIssue(ctx, dbMatch.ID); err != nil {
    return nil, fmt.Errorf("failed to delete renamed issue...")
}

// NEW (ADD THIS):
result.Renames = append(result.Renames, &RenameDetail{
    OldID: dbMatch.ID,
    NewID: incoming.ID,
    Issue: incoming,
})
continue  // Don't mark as NewIssue yet
```

### 3. Create ApplyCollisionResolution function
New function to apply all modifications atomically:
```go
func ApplyCollisionResolution(ctx context.Context, s *SQLiteStorage,
    result *CollisionResult, mapping map[string]string) error {

    // Phase 1: Handle renames (delete old IDs)
    for _, rename := range result.Renames {
        if err := s.DeleteIssue(ctx, rename.OldID); err != nil {
            return fmt.Errorf("failed to delete renamed issue %s: %w",
                rename.OldID, err)
        }
    }

    // Phase 2: Create new IDs (from mapping)
    // Phase 3: Update references
    return nil
}
```

### 4. Update callers to use two-phase approach
File: internal/importer/importer.go (handleCollisions)
```go
// Phase 1: Detect (read-only)
collisionResult, err := sqlite.DetectCollisions(ctx, sqliteStore, issues)

// Phase 2: Resolve (compute mapping)
mapping, err := sqlite.ResolveNWayCollisions(ctx, sqliteStore, collisionResult)

// Phase 3: Apply (modify DB)
err = sqlite.ApplyCollisionResolution(ctx, sqliteStore, collisionResult, mapping)
```

### 5. Update tests
- Verify DetectCollisions doesn't modify DB
- Test ApplyCollisionResolution separately
- Add test for rename detection without modification

## Acceptance Criteria
- DetectCollisions performs zero writes to database
- DetectCollisions returns RenameDetail entries for content matches
- ApplyCollisionResolution handles all modifications
- All existing tests still pass
- New test verifies read-only detection
- contentToDBIssue map stays consistent throughout detection

## Files to Modify
- internal/storage/sqlite/collision.go (DetectCollisions, new function)
- internal/importer/importer.go (handleCollisions caller)
- internal/storage/sqlite/collision_test.go (add tests)

## Testing
- Unit test: DetectCollisions with content match doesn't delete DB issue
- Unit test: RenameDetail correctly populated
- Unit test: ApplyCollisionResolution applies renames
- Integration test: Full flow still works end-to-end

## Risk Mitigation
This is a significant refactor of core collision logic. Recommend:
1. Add comprehensive tests before modifying
2. Use feature flag to enable/disable new behavior
3. Test thoroughly with TestTwoCloneCollision first


Blocking issue


Test child issue


Add 'bd doctor --rebuild-cache' command for cache recovery

Add admin command to manually rebuild the blocked_issues_cache table for recovery scenarios.

## Use cases

1. **Cache corruption recovery**
   - If cache gets out of sync with actual dependencies
   - Manual rebuild as safety measure

2. **Post-migration verification**
   - After database migrations that affect dependencies
   - Verify cache is correctly populated

3. **Debugging**
   - Compare cache state with actual blocking calculations
   - Troubleshoot performance issues

## Implementation

Add flag to bd doctor command:

```bash
bd doctor --rebuild-cache
```

Should:
- Call `SQLiteStorage.invalidateBlockedCache()` directly
- Print before/after cache statistics
- Show timing information
- Exit code 0 on success

Example output:
```
Rebuilding blocked_issues_cache...
Before: 15 blocked issues
After: 15 blocked issues
Rebuilt in 12ms
✓ Cache rebuild complete
```

## Files to modify

- cmd/bd/doctor.go - add --rebuild-cache flag
- internal/storage/sqlite/blocked_cache.go - maybe add public RebuildBlockedCache() method

## Related

- [deleted:bd-5qim]: GetReadyWork performance optimization
- bd-13gm: Add cache validation tests


bd validate - Comprehensive health check

Run all validation checks in one command.

Checks:
- Duplicates
- Orphaned dependencies
- Test pollution
- Git conflicts

Supports --fix-all for auto-repair.

Depends on bd-cbed9619.1, bd-0dcea000, bd-31aab707, bd-9826b69a.

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


Fix bd doctor to recommend issues.jsonl as canonical (not beads.jsonl)


Event-driven daemon architecture

Replace 5-second polling sync loop with event-driven architecture that reacts instantly to changes. Eliminates stale data issues while reducing CPU ~60%. Key components: FileWatcher (fsnotify), Debouncer (500ms), RPC mutation events, optional git hooks. Target latency: <500ms (vs 5000ms). See event_driven_daemon.md for full design.

## Implementation Progress

**Completed:**
1. ✅ Mutation events infrastructure (bd-143 equivalent)
   - MutationEvent channel in RPC server
   - Events emitted for all write operations: create, update, close, label add/remove, dep add/remove, comment add
   - Non-blocking emission with dropped event counter

2. ✅ FileWatcher with fsnotify (bd-b0c7f7ef related)
   - Watches .beads/issues.jsonl and .git/refs/heads
   - 500ms debounce
   - Polling fallback if fsnotify unavailable

3. ✅ Debouncer (bd-144 equivalent)
   - 500ms debounce for both export and import triggers
   - Thread-safe trigger/cancel

4. ✅ Separate export-only and import-only functions
   - createExportFunc(): exports + optional commit/push (no pull/import)
   - createAutoImportFunc(): pull + import (no export)
   - Target latency <500ms achieved by avoiding full sync

5. ✅ Dropped events safety net (bd-eef03e0a related)
   - Atomic counter tracks dropped mutation events
   - 60-second health check triggers export if events were dropped
   - Prevents silent data loss from event storms

**Still Needed:**
- Platform-specific tests (bd-69bce74a)
- Integration test for mutation→export latency (bd-140)
- Unit tests for FileWatcher (bd-b0c7f7ef)
- Unit tests for Debouncer (bd-144)
- Event storm stress test (bd-eef03e0a)
- Documentation update (bd-142)

**Next Steps:**
Add comprehensive test coverage before enabling events mode by default.


Widespread double JSON encoding bug in daemon mode RPC calls

Multiple CLI commands had the same double JSON encoding bug found in bd-1048. All commands that called ResolveID via RPC used string(resp.Data) instead of properly unmarshaling the JSON response. This caused IDs to retain JSON quotes ("bd-1048" instead of bd-1048), which then got double-encoded when passed to subsequent RPC calls.

Affected commands:
- bd show (3 instances)
- bd dep add/remove/tree (5 instances)
- bd label add/remove/list (3 instances)
- bd reopen (1 instance)

Root cause: resp.Data is json.RawMessage (already JSON-encoded), so string() conversion preserves quotes.

Fix: Replace all string(resp.Data) with json.Unmarshal(resp.Data, &id) for proper deserialization.

All commands now tested and working correctly with daemon mode.


Run final validation and cleanup checks

Final validation pass to ensure all cleanup objectives met and no regressions introduced.

Validation checklist:
1. Dead code verification: `go run golang.org/x/tools/cmd/deadcode@latest -test ./...`
2. Test coverage: `go test -cover ./...`
3. Build verification: `go build ./cmd/bd/`
4. Linting: `golangci-lint run`
5. Integration tests
6. Metrics verification
7. Git clean check

Final metrics to report:
- LOC removed: ~____
- Files deleted: ____
- Files created: ____
- Test coverage: ____%
- Build time: ____ (before/after)
- Test run time: ____ (before/after)

Impact: Confirms all cleanup objectives achieved successfully

## Validation Results

**Dead Code:** ✅ Found and removed 1 unreachable function (`DroppedEventsCount`)
**Tests:** ✅ All pass
**Coverage:**
- Main: 39.6%
- cmd/bd: 20.2%
- Created follow-up issues (bd-85487065 through bd-bc2c6191) to improve coverage

**Build:** ✅ Clean
**Linting:** 73 issues (up from 34 baseline)
- Increase due to unused functions from refactoring
- Need cleanup in separate issue

**Integration Tests:** ✅ All pass
**Metrics:** 56,464 LOC across 193 Go files
**Git:** 2 files modified (deadcode fix + auto-synced JSONL)

## Follow-up Issues Created
- bd-85487065: Add tests for internal/autoimport (0% coverage)
- bd-0dcea000: Add tests for internal/importer (0% coverage)
- bd-4d7fca8a: Add tests for internal/utils (0% coverage)
- bd-6221bdcd: Improve cmd/bd coverage (20.2% -> target higher)
- bd-bc2c6191: Improve internal/daemon coverage (22.5% -> target higher)


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).

## Fix Summary

Successfully prevented the nested .beads/.beads/ recursion bug by implementing two safeguards:

1. **Path Canonicalization in FindDatabasePath()** (beads.go):
   - Added filepath.Abs() + filepath.EvalSymlinks() to normalize all database paths
   - Prevents relative path edge cases that create nested directories
   - Ensures all daemons see the same canonical path

2. **Nested Directory Detection** (daemon_lifecycle.go):
   - Added explicit check for ".beads/.beads" pattern in setupDaemonLock()
   - Fails fast with clear error message if nested structure detected
   - Provides user hints about proper usage

## Root Cause

The daemon lock (added Oct 22, 2025) correctly prevents simultaneous daemons in the SAME workspace. However, when BEADS_DB used a relative path (e.g., ".beads/beads.db") from inside the .beads directory, FindDatabasePath() would resolve it to a nested path creating a separate workspace:
- First daemon: /workspace/.beads/beads.db
- Second daemon from .beads/: /workspace/.beads/.beads/beads.db ← Different lock file!

## Testing

All acceptance criteria passed:
✅ 1. Second daemon start fails with "daemon already running" error
✅ 2. Killing daemon releases lock, new daemon can start
✅ 3. No infinite .beads/ recursion possible (tested nested BEADS_DB path)
✅ 4. Works with auto-start mechanism

The fix addresses the edge case while maintaining the existing lock mechanism's correctness.


Fix monitor-webui status filter


bd doctor should validate metadata.json version tracking

bd doctor should check that metadata.json has the LastBdVersion field and that it's valid.

Checks to add:
- metadata.json exists and is valid JSON
- LastBdVersion field is present
- LastBdVersion is a valid semver-like string (or empty on first run)
- Optionally warn if LastBdVersion is very old (> 10 versions behind)

This helps ensure version tracking (bd-loka) is working correctly.

Related: bd-loka


Channel isolation test - beads

Resetting stale in_progress status from old executor run (13 days old)


Add unit tests for import ordering

Test topological sort: import [child, parent] should succeed, import [parent.1.2, parent, parent.1] should sort correctly. Verify depth-based batching works. Test max depth limits.


Add import.orphan_handling config option

Add configuration option to control orphan handling behavior: 'strict' (fail on missing parent, current behavior), 'resurrect' (auto-resurrect from JSONL, recommended default), 'skip' (skip orphaned issues with warning), 'allow' (import orphans without validation). Update CONFIG.md documentation.


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


Fix TestFallbackToDirectModeEnablesFlush deadlock causing 10min test timeout

## Problem

TestFallbackToDirectModeEnablesFlush in direct_mode_test.go deadlocks for 9m59s before timing out, causing the entire test suite to take 10+ minutes instead of <10 seconds.

## Root Cause

Database lock contention between test cleanup and flushToJSONL():
- Test cleanup (line 36) tries to close DB via defer
- flushToJSONL() (line 132) is still accessing DB
- Results in deadlock: database/sql.(*DB).Close() waits for mutex while GetJSONLFileHash() holds it

## Stack Trace Evidence

```
goroutine 512 [sync.Mutex.Lock, 9 minutes]:
database/sql.(*DB).Close(0x14000643790)
  .../database/sql/sql.go:927 +0x84
github.com/steveyegge/beads/cmd/bd.TestFallbackToDirectModeEnablesFlush.func1()
  .../direct_mode_test.go:36 +0xf4

While goroutine running flushToJSONL() holds DB connection via GetJSONLFileHash()
```

## Impact

- Test suite: 10+ minutes → should be <10 seconds
- ALL other tests pass in ~4 seconds
- This ONE test accounts for 99.9% of test runtime

## Related

This is the EXACT same issue documented in MAIN_TEST_REFACTOR_NOTES.md for why main_test.go refactoring was deferred - global state manipulation + DB cleanup = deadlock.

## Fix Approaches

1. **Add proper cleanup sequencing** - stop flush goroutines BEFORE closing DB
2. **Use test-specific DB lifecycle** - ensure flush completes before cleanup
3. **Mock the flush mechanism** - avoid real DB for testing this code path
4. **Add explicit timeout handling** - fail fast with clear error instead of hanging

## Files

- cmd/bd/direct_mode_test.go:36-132
- cmd/bd/autoflush.go:353 (validateJSONLIntegrity)
- cmd/bd/autoflush.go:508 (flushToJSONLWithState)

## Acceptance

- Test passes without timeout
- Test suite completes in <10 seconds
- No deadlock between cleanup and flush operations


Remove Cache-Related Tests

Delete or update tests that assume multi-repo caching


VCS-agnostic sync support

Make bd sync work with multiple VCS types (git, jujutsu, mercurial, sapling) by detecting VCS per repo and using appropriate sync commands, supporting mixed-VCS multi-repo configs.


Create config.go with config and metadata methods


Create cmd/bd/daemon_watcher.go (~150 LOC)

Implement FileWatcher using fsnotify to watch JSONL file and git refs. Handle platform differences (inotify/FSEvents/ReadDirectoryChangesW). Include edge case handling for file rename, event storm, watcher failure.


Repair Commands & AI-Assisted Tooling

Add specialized repair tools to reduce agent repair burden:
1. Git merge conflicts in JSONL
2. Duplicate issues from parallel work
3. Semantic inconsistencies
4. Orphaned references

See ~/src/fred/beads/repair_commands.md for full design doc.

Reduces agent repair time from 5-10 minutes to <30 seconds per repair.


Add tests for merge driver auto-config in bd init

Add comprehensive tests for the merge driver auto-configuration functionality in `bd init`.

**Test cases needed:**
- Auto-install in quiet mode
- Skip with --skip-merge-driver flag
- Detect already-installed merge driver
- Append to existing .gitattributes
- Interactive prompt behavior (if feasible)

**File:** `cmd/bd/init_test.go`


Issue to close


Write Agent Mail integration guide

Comprehensive guide for setting up and using Agent Mail with Beads.

Acceptance Criteria:
- Installation instructions
- Configuration (environment variables)
- Architecture diagram
- Benefits and tradeoffs
- When to use vs not use
- Troubleshooting section
- Migration from git-only mode

File: docs/AGENT_MAIL.md

Sections:
- Quick start
- How it works
- Integration points
- Graceful degradation
- Multi-machine deployment
- FAQ


Batch test 2


Fix bd doctor hash ID detection for short all-numeric hashes

bd doctor incorrectly flags hash-based IDs as sequential when they are short (3-4 chars) and all-numeric (e.g., pf-158).

Root cause: isHashID() in cmd/bd/migrate_hash_ids.go:328-358 uses faulty heuristic:
- For IDs < 5 chars, only returns true if contains letters
- But base36 hash IDs can be 3+ chars and all-numeric (MinLength: 3)
- Example: pf-158 is valid hash ID but flagged as sequential

Fix: Check multiple IDs (10-20 samples) instead of single-ID pattern matching:
- Sample IDs across database
- Check majority pattern (sequential vs hash format)
- Sequential: 1-4 digits (bd-1, bd-2...)
- Hash: 3-8 chars base36 (pf-158, pf-3s9...)

Impact: False positive warnings in bd doctor output


Test integration issue

This is a real integration test


Code review follow-up for bd-dvd and bd-ymj fixes

Track improvements and issues identified during code review of parent resurrection (bd-dvd) and export metadata (bd-ymj) bug fixes.

## Context
Code review identified several areas for improvement:
- Code duplication in metadata updates
- Missing multi-repo support
- Test coverage gaps
- Potential race conditions

## Related Issues
Original bugs fixed: bd-dvd, bd-ymj

## Goals
- Eliminate code duplication
- Add multi-repo support where needed
- Improve test coverage
- Address edge cases


Add integration tests for multi-repo sync

Test: Clone A deletes issue, Clone B imports Clone A's JSONL. Verify Clone B handles deletion gracefully with resurrection. Test concurrent imports with same orphans (should be idempotent). Test round-trip fidelity (export→delete parent→import→verify structure).


Extract duplicated metadata update code in daemon_sync.go

## Problem
The same 22-line metadata update block appears identically in both:
- createExportFunc (lines 309-328)
- createSyncFunc (lines 520-539)

This violates DRY principle and makes maintenance harder.

## Solution
Extract to helper function:

```go
// updateExportMetadata updates last_import_hash and related metadata after a successful export.
// This prevents "JSONL content has changed since last import" errors on subsequent exports (bd-ymj fix).
func updateExportMetadata(ctx context.Context, store storage.Storage, jsonlPath string, log daemonLogger) {
    currentHash, err := computeJSONLHash(jsonlPath)
    if err != nil {
        log.log("Warning: failed to compute JSONL hash for metadata update: %v", err)
        return
    }

    if err := store.SetMetadata(ctx, "last_import_hash", currentHash); err != nil {
        log.log("Warning: failed to update last_import_hash: %v", err)
    }

    exportTime := time.Now().Format(time.RFC3339)
    if err := store.SetMetadata(ctx, "last_import_time", exportTime); err != nil {
        log.log("Warning: failed to update last_import_time: %v", err)
    }

    // Store mtime for fast-path optimization
    if jsonlInfo, statErr := os.Stat(jsonlPath); statErr == nil {
        mtimeStr := fmt.Sprintf("%d", jsonlInfo.ModTime().Unix())
        if err := store.SetMetadata(ctx, "last_import_mtime", mtimeStr); err != nil {
            log.log("Warning: failed to update last_import_mtime: %v", err)
        }
    }
}
```

## Files
- cmd/bd/daemon_sync.go

## Benefits
- Easier maintenance
- Single source of truth
- Consistent behavior


Daemons don't exit when parent process dies, causing accumulation and race conditions

Multiple daemon processes accumulate over time because daemons don't automatically stop when their parent process (e.g., coding agent) is killed. This causes:

1. Race conditions: 8+ daemons watching same .beads/beads.db, each with own 30s debounce timer
2. Git conflicts: Multiple daemons racing to commit/push .beads/issues.jsonl
3. Resource waste: Orphaned daemons from sessions days/hours old still running

Example: User had 8 daemons from multiple sessions (12:37AM, 7:20PM, 7:22PM, 7:47PM, 9:19PM yesterday + 9:54AM, 10:55AM today).

Solutions to consider:
1. Track parent PID and exit when parent dies
2. Use single global daemon instead of per-session
3. Document manual cleanup: pkill -f "bd daemon"
4. Add daemon lifecycle management (auto-cleanup of stale daemons)

Implementation complete:

1. Added ParentPID field to DaemonLockInfo struct (stored in daemon.lock JSON)
2. Daemon now tracks parent PID via os.Getppid() at startup
3. Both event loops (polling and event-driven) check parent process every 10 seconds
4. Daemon gracefully exits if parent process dies (detected via isProcessRunning check)
5. Handles edge cases:
   - ParentPID=0: Older daemons without tracking (ignored)
   - ParentPID=1: Adopted by init means parent died (exits)
   - Otherwise checks if parent process is still running

The fix prevents daemon accumulation by ensuring orphaned daemons automatically exit within 10 seconds of parent death.


Auto-migrate database on CLI version bump

When CLI is upgraded (e.g., 0.24.0 → 0.24.1), database version becomes stale. Add auto-migration in PersistentPreRun or daemon startup. Check dbVersion != CLIVersion and run bd migrate automatically. Fixes recurring UX issue where bd doctor shows version mismatch after every CLI upgrade.


Update Metrics and Health Endpoints

Remove cache-related metrics from health/metrics endpoints


Test auto-start in fred


Update python-agent example with Agent Mail integration

Modify examples/python-agent/agent.py to use Agent Mail adapter at 4 integration points.

Acceptance Criteria:
- Import and initialize adapter
- Check inbox before find_ready_work()
- Reserve issue before claim_task()
- Notify on status changes
- Release reservation on complete_task()
- Works identically when Agent Mail disabled
- No changes required to core Beads CLI

File: examples/python-agent/agent.py


Migration guide documentation

Write comprehensive migration guide covering: OSS contributor workflow, team workflow, multi-phase development, multiple personas. Include step-by-step instructions, troubleshooting, and backward compatibility notes.


Fix goroutine leak in daemon restart

Fire-and-forget goroutine in daemon restart leaks on every restart.

Location: cmd/bd/daemons.go:251

Problem:
go func() { _ = daemonCmd.Wait() }()

- Spawns goroutine without timeout or cancellation
- If daemon command never completes, goroutine leaks forever
- Each daemon restart leaks one more goroutine

Solution: Add timeout and cleanup:
go func() {
    done := make(chan struct{})
    go func() {
        _ = daemonCmd.Wait()
        close(done)
    }()

    select {
    case <-done:
        // Exited normally
    case <-time.After(10 * time.Second):
        // Timeout - daemon should have forked by now
        _ = daemonCmd.Process.Kill()
    }
}()

Impact: Goroutine leak on every daemon restart

Effort: 2 hours


Add t.Parallel() to slow hash multiclone tests

Add t.Parallel() to TestHashIDs_MultiCloneConverge and TestHashIDs_IdenticalContentDedup so they run concurrently.

Expected savings: ~10 seconds (from 20s to ~11s)

Implementation:
- Add t.Parallel() call at start of each test function
- Verify tests don't share resources that would cause conflicts
- Run tests to confirm they work in parallel

File: beads_hash_multiclone_test.go:34, :101


Batch test 5


Verify MCP Server Compatibility

Ensure MCP server works with cache-free daemon


Add WaitGroup tracking to FileWatcher goroutines

FileWatcher spawns goroutines without WaitGroup tracking, causing race condition on shutdown.

Location: cmd/bd/daemon_watcher.go:123-182, 215-291

Problem:
- Goroutines spawned without sync.WaitGroup
- Close() cancels context but doesn't wait for goroutines to exit
- Race condition: goroutine may access fw.debouncer during Close() cleanup
- No guarantee goroutine stopped before fw.watcher.Close() is called

Solution:
- Add sync.WaitGroup field to FileWatcher
- Track goroutines with wg.Add(1) and defer wg.Done()
- Call wg.Wait() in Close() before cleanup

Impact: Race condition on daemon shutdown; potential panic

Effort: 2 hours


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


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


bd-hv01: Refactor snapshot management into dedicated module

Problem: Snapshot logic is scattered across deletion_tracking.go. Would benefit from abstraction with SnapshotManager type.

Benefits: cleaner separation of concerns, easier to test in isolation, better encapsulation, could add observability/metrics.

Suggested improvements: add magic constants, track merge statistics, better error messages.

Files: cmd/bd/deletion_tracking.go (refactor into new snapshot_manager.go)


Unit tests for FileWatcher

Test watcher detects JSONL changes. Test git ref changes trigger import. Test debounce integration. Test watcher recovery from file removal/rename.


Import reports misleading '0 created, 0 updated' when actually importing all issues

When running 'bd import' on a fresh database (no existing issues), the command reports 'Import complete: 0 created, 0 updated' even though it successfully imported all issues from the JSONL file.

**Steps to reproduce:**
1. Delete .beads/beads.db
2. Run: bd import .beads/issues.jsonl
3. Observe output: 'Import complete: 0 created, 0 updated'
4. Run: bd list
5. Confirm: All issues are actually present in the database

**Expected behavior:**
Report the actual number of issues imported, e.g., 'Import complete: 523 created, 0 updated'

**Actual behavior:**
'Import complete: 0 created, 0 updated' (misleading - makes user think import failed)

**Impact:**
- Users think import failed when it succeeded
- Confusing during database sync operations (e.g., after git pull)
- Makes debugging harder (can't tell if import actually worked)

**Context:**
Discovered during VC session when syncing database after git pull. The misleading message caused confusion about whether the database was properly synced with the canonical JSONL file.


Event-driven daemon architecture

Replace 5-second polling sync loop with event-driven architecture that reacts instantly to changes. Eliminates stale data issues while reducing CPU ~60%. Key components: FileWatcher (fsnotify), Debouncer (500ms), RPC mutation events, optional git hooks. Target latency: <500ms (vs 5000ms). See event_driven_daemon.md for full design.

Production-ready after 3 critical fixes (commit 349b892):
- Skip redundant imports (mtime check prevents self-trigger loops)
- Add server.Stop() in serverErrChan case (clean shutdown)
- Fallback ticker (60s) when watcher unavailable (ensures remote sync)

Ready to make default after integration test (bd-1f4086c5.1) passes.


Integrate beads-merge tool by @neongreen

**Context**: @neongreen built a production-ready 3-way merge tool for JSONL files that works with both Git and Jujutsu. This is superior to our planned bd resolve-conflicts because it prevents conflicts proactively instead of resolving them after the fact.

**Tool**: https://github.com/neongreen/mono/tree/main/beads-merge

**What it does**:
- 3-way merge of JSONL files (base, left, right)
- Field-level merging (titles, status, priority, etc.)
- Smart dependency merging (union + dedup)
- Conflict markers for unresolvable conflicts
- Exit code 1 for conflicts (standard)

**Integration options**:

1. **Recommend (minimal effort)** - Document in AGENTS.md + TROUBLESHOOTING.md
2. **Bundle binary** - Include in releases (cross-platform builds)
3. **Port to Go** - Reimplement in bd codebase
4. **Auto-install hook** - During bd init, offer to install merge driver

**Recommendation**: Start with option 1 (document), then option 2 (bundle) once proven.

**Related**: bd-5f483051 (bd resolve-conflicts - can close as superseded)

Created GitHub issue to discuss integration approach with @neongreen: https://github.com/neongreen/mono/issues/240

Awaiting their preference on:
1. Vendor with attribution (fastest)
2. Extract as importable module (best long-term)
3. Keep as separate tool (current state)

Next: Wait for response before proceeding with integration.

UPDATE 2025-11-06: @neongreen gave permission to vendor! Quote: "I switched from beads to my own thing (tk) so I'm very happy to give beads-merge away — feel free to move it into the beads repo and I will point mono's readme to beads"

Next: Vendor beads-merge with full attribution


Fix :memory: database connection pool issue causing "no such table" errors

Critical bug in v0.21.6 where :memory: databases with cache=shared create multiple connections in the pool, causing intermittent "no such table" errors. SQLite's shared cache for in-memory databases only works reliably with a single connection.

Root cause: Missing db.SetMaxOpenConns(1) after sql.Open() for :memory: databases.

Impact: 37 test failures in VC project, affects all consumers using :memory: for testing.


Add internal/ai package for LLM integration

Shared AI client for repair commands.

Providers:
- Anthropic (Claude)
- OpenAI (GPT)
- Ollama (local)

Env vars:
- BEADS_AI_PROVIDER
- BEADS_AI_API_KEY
- BEADS_AI_MODEL

Files: internal/ai/client.go (new)


Help agents understand version changes with bd info --whats-new

**Problem** (from GH Discussion #239 by @maphew):
Weekly major versions mean agents need to adapt workflows, but currently there's no efficient way to communicate "what changed that affects you."

**Proposed solutions:**

1. **bd info --whats-new** - Show agent-actionable changes since last version
   ```
   Since v0.20.1:
   • Hash IDs eliminate collisions - remove ID coordination workarounds
   • Event-driven daemon (opt-in) - add BEADS_DAEMON_MODE=events
   • Merge driver auto-configured - conflicts rarer
   ```

2. **Version-aware bd onboard** - Detect version changes and show diff of agent-relevant changes

3. **AGENTS.md top section** - "🆕 Recent Changes (Last 3 Versions)" with workflow impacts

**Why agents need this:**
- Raw CHANGELOG is token-heavy and buried in release details
- Full bd onboard re-run wasteful if only 2-3 things changed
- Currently requires user to manually explain updates

**Related:** https://github.com/steveyegge/beads/discussions/239


MCP server: "No workspace set" and "chunk longer than limit" errors

Two related errors reported in beads-mcp v0.21:

**Error 1: "No workspace set" after successful set_context**
```
✓ Set beads context
✗ list
  Error calling tool 'list': No workspace set. Either provide workspace_root
  parameter or call set_context() first.
```

Hypothesis: Environment variable persistence issue between MCP tool calls, or ContextVar not being set correctly by @with_workspace decorator.

**Error 2: "Separator is found, but chunk is longer than limit"**
```
✗ list
  Error calling tool 'list': Separator is found, but chunk is longer than limit
```

Hypothesis: MCP protocol output size limit exceeded. Large issue databases may produce JSON output that exceeds MCP stdio buffer limits.

Platform: Fedora 43, using copilot-cli with Sonnet 4.5

Workaround: CLI works fine (`bd list --status open --json`)

## Fixes Implemented

**Issue 1: "No workspace set" after successful set_context** ✅ FIXED

Root cause: os.environ doesn't persist across MCP tool calls. When set_context() set BEADS_WORKING_DIR in os.environ, that change was lost on the next tool call.

Solution:
- Added module-level _workspace_context dict for persistent storage (server.py:51)
- Modified set_context() to store in both persistent dict and os.environ (server.py:265-287)
- Modified with_workspace() decorator to check persistent context first (server.py:129-133)
- Updated where_am_i() to check persistent context (server.py:302-330)

**Issue 2: "chunk longer than limit"** ✅ FIXED

Root cause: MCP stdio protocol has buffer limits. Large issue lists with full dependencies/dependents exceed this.

Solution:
- Reduced default list limit from 50 to 20 (server.py:356, models.py:122)
- Reduced max list limit from 1000 to 100 (models.py:122)
- Strip dependencies/dependents from list() and ready() responses (server.py:343-350, 368-373)
- Full dependency details still available via show() command

## Testing

✅ Python syntax validated with py_compile
✅ Changes are backward compatible
✅ Persistent context falls back to os.environ for compatibility

Users should now be able to call set_context() once and have it persist across all subsequent tool calls. Large databases will no longer cause buffer overflow errors.


Run final validation and cleanup checks

Final validation pass to ensure all cleanup objectives met and no regressions introduced.

Validation checklist:
1. Dead code verification: `go run golang.org/x/tools/cmd/deadcode@latest -test ./...`
2. Test coverage: `go test -cover ./...`
3. Build verification: `go build ./cmd/bd/`
4. Linting: `golangci-lint run`
5. Integration tests
6. Metrics verification
7. Git clean check

Final metrics to report:
- LOC removed: ~____
- Files deleted: ____
- Files created: ____
- Test coverage: ____%
- Build time: ____ (before/after)
- Test run time: ____ (before/after)

Impact: Confirms all cleanup objectives achieved successfully

Validation completed:
- LOC: 52,372 lines total
- Dead code: 4 functions in import_shared.go (tracked in bd-6fe4622f)
- Build: ✓ Successful
- Test coverage: ~20-82% across packages
- Test failure: TestTwoCloneCollision (timeout issue)
- Linting: errcheck warnings present (defer close, fmt errors)
- Test time: ~20s

Issues found:
1. bd-6fe4622f: Remove unreachable import functions (renameImportedIssuePrefixes, etc)
2. TestTwoCloneCollision: Daemon killall timeout causing test failure
3. Linting: errcheck violations need fixing


Windows binaries lack SQLite support (GH #253)

Windows users installing via install.ps1 get "sql: unknown driver sqlite" error. Root cause: GoReleaser was building with CGO_ENABLED=0, which excludes SQLite driver.

Fixed by:
1. Enabling CGO in .goreleaser.yml
2. Installing MinGW cross-compiler in release workflow
3. Splitting builds per platform to set correct CC for Windows

Needs new release to fix for users.


Event-driven daemon architecture


Update LINTING.md with current baseline

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


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).


Improve timestamp comparison readability in import

The timestamp comparison logic uses double-negative which can be confusing:

Current code:
if !incoming.UpdatedAt.After(existing.UpdatedAt) {
    // skip update
}

More readable:
if incoming.UpdatedAt.After(existing.UpdatedAt) {
    // perform update
} else {
    // skip (local is newer)
}

This is a minor refactor for code clarity.

Related: bd-1022
Files: internal/importer/importer.go:411, 488


Add comprehensive filters to bd export

Enhance bd export with filtering options for selective exports.

**Currently only has:**
- --status

**Add filters:**
- --label, --label-any
- --assignee
- --type
- --priority, --priority-min, --priority-max
- --created-after, --created-before
- --updated-after, --updated-before

**Use case:**
- Export only open issues: bd export --status open
- Export high-priority bugs: bd export --type bug --priority-max 1
- Export recent issues: bd export --created-after 2025-01-01

**Files to modify:**
- cmd/bd/export.go
- Reuse filter logic from list.go


Investigation & Proof of Concept

Validate that MCP Agent Mail works as expected and delivers promised benefits before committing to full integration.

POC completed successfully:
✅ bd-muls: Server installed and tested
✅ bd-27xm: MCP tool execution issues resolved
✅ [deleted:bd-6hji]: File reservation collision prevention validated
✅ bd-htfk: Latency benchmarking shows 20-50x improvement
✅ bd-pmuu: ADR 002 created documenting integration decision

Results validate Agent Mail benefits:
- Collision prevention works (exclusive file reservations)
- Latency: <100ms (vs 2000-5000ms git sync)
- Lightweight deployment (<50MB memory)
- Optional/non-intrusive integration approach validated

Next: bd-wfmw (Integration Layer Implementation)


Extract database search logic into helper function

The logic for finding a database in a beads directory is duplicated:
- FindDatabasePath() BEADS_DIR section (beads.go:141-169)
- findDatabaseInTree() (beads.go:248-280)

Both implement the same search order:
1. Check config.json first (single source of truth)
2. Fall back to canonical beads.db
3. Search for *.db files, filtering backups and vc.db

Refactoring suggestion:
Extract to a helper function like:
  func findDatabaseInBeadsDir(beadsDir string) string

Benefits:
- Single source of truth for database search logic
- Easier to maintain and update search order
- Reduces code duplication

Related to [deleted:[deleted:[deleted:[deleted:bd-e16b]]]] implementation.


Add embedding generation for duplicate detection

Use embeddings for scalable duplicate detection.

Model: text-embedding-3-small (OpenAI) or all-MiniLM-L6-v2 (local)
Storage: SQLite vector extension or in-memory
Cost: ~/bin/bash.0002 per 100 issues

Much cheaper than LLM comparisons for large databases.

Files: internal/embeddings/ (new package)


Backend task


Add AI planning docs management guidance to bd onboard (GH-196)

Enhanced bd onboard command to provide guidance for managing AI-generated planning documents (Claude slop).

Addresses GitHub issue #196: https://github.com/steveyegge/beads/issues/196

Changes:
- Added Managing AI-Generated Planning Documents section to bd onboard
- Recommends using history/ directory for ephemeral planning files
- Updated AGENTS.md to demonstrate the pattern
- Added comprehensive tests

Commit: d46177d


Update documentation for merge driver auto-config

Update documentation to reflect the new merge driver auto-configuration during `bd init`.

**Files to update:**
- README.md - Mention merge driver setup in initialization section
- AGENTS.md - Update onboarding section about merge driver
- Possibly QUICKSTART.md

**Content:**
- Explain what the merge driver does
- Show --skip-merge-driver flag usage
- Manual installation steps for post-init setup


Implement topological sort for import ordering

Refactor upsertIssues() to sort issues by hierarchy depth before batch creation. Ensures parents are created before children, fixing latent bug where parent-child pairs in same batch can fail if ordered wrong. Sort by dot count, create in depth-order batches (0→1→2→3).


Add mermaid output format for bd dep tree

Add visual dependency graph output using Mermaid format for better visualization of issue relationships.

Example usage:
  bd dep tree --format mermaid <issue-id>
  bd dep tree --format mermaid bd-42 > graph.md

This would output Mermaid syntax that can be rendered in GitHub, documentation sites, or Mermaid live editor.

Implementation notes:
- Add --format flag to dep tree command
- Support 'text' (default) and 'mermaid' formats
- Mermaid graph should show issue IDs, titles, and dependency types
- Consider using flowchart LR or graph TD syntax


Convert magic numbers to named constants in FlushManager


Add executable bit validation for git hooks in init.go

**Location:** cmd/bd/init.go:388-415

**Issue:** hooksInstalled() checks for hook marker string but doesn't verify:
- Hook file is actually executable
- Hook has correct shebang line
- Hook hasn't been corrupted/truncated

**Current Code:**
```go
preCommitContent, err := os.ReadFile(preCommit)
if err \!= nil || \!strings.Contains(string(preCommitContent), "bd (beads) pre-commit hook") {
    return false
}
// No executable bit check\!
```

**Impact:** Hooks may appear installed but fail silently because they're not executable.

**Fix:** Add executable bit check:
```go
fileInfo, err := os.Stat(preCommit)
if err \!= nil {
    return false
}
if fileInfo.Mode().Perm() & 0111 == 0 {
    return false  // Not executable
}
```


Log errors from timer-triggered flushes instead of discarding


bd-hv01: Multi-repo mode not tested with deletion tracking

Problem: Test suite has no coverage for multi-repo mode. ExportToMultiRepo creates multiple JSONL files but snapshot files are hardcoded to single JSONL location.

Impact: Deletion tracking likely silently broken for multi-repo users, could cause data loss.

Fix: Add test and update snapshot logic to handle multiple JSONL files.

Files: cmd/bd/deletion_tracking_test.go, cmd/bd/deletion_tracking.go, cmd/bd/daemon_sync.go:24-34


Daemon crash recovery: panic handler + socket cleanup

Improve daemon cleanup on unexpected exit:
1. Add top-level recover() in runDaemonLoop to capture panics
2. Write daemon-error file with stack trace on panic
3. Prefer return over os.Exit where possible (so defers run)
4. In stopDaemon forced-kill path, also remove stale socket if present

This ensures better diagnostics and cleaner state after crashes.


Fix Windows test failure: TestFindAllDatabases returns nil

On Windows, TestFindAllDatabases fails with 'expected non-nil slice'.

Test failure: beads_test.go:63: expected non-nil slice

The FindAllDatabases() function is returning nil instead of an empty slice on Windows. Need to investigate why the function returns nil and ensure it always returns a non-nil slice (even if empty).


Fix race condition in client socket cleanup (client.go)

**Location:** internal/rpc/client.go:44-76

**Issue:** TryConnectWithTimeout() has race window between socket existence check and lock file check.

**Problematic Sequence:**
```go
socketExists := endpointExists(socketPath)  // line 56
if !socketExists {
    beadsDir := filepath.Dir(socketPath)
    running, _ := lockfile.TryDaemonLock(beadsDir)  // line 61
    // RACE: socket could've been deleted between lines 56 and 61
}
```

**Impact:** Could incorrectly report daemon as running when it crashed. Socket gets deleted during cleanup but lock file still held temporarily.

**Fix:** Combine checks atomically or use transactional approach with proper locking.


Multi-repo hydration layer

Build core infrastructure to hydrate database from N repos (N≥1), with smart caching via file mtime tracking and routing writes to correct JSONL based on source_repo metadata.


Add validation to require description when creating issues

## Solution: Code-level validation

Add validation in bd create to warn or require a description when creating non-test issues.

## Implementation Options

### Option 1: Warning (Non-blocking)
- Print a warning when description is empty
- Similar to existing 'Test' prefix warning in create.go:62-66
- Allows users to proceed but raises awareness

### Option 2: Strict validation with bypass
- Require description for all issues except:
  - Issues with 'test' in title (case-insensitive)
  - When --no-description-check flag is provided
- Exit with error if description is empty and no bypass

### Option 3: Interactive prompt (CLI only)
- If description empty, prompt user to enter one
- Only works in CLI, not via MCP/daemon mode
- May confuse AI agents expecting non-interactive behavior

## Recommendation

Start with Option 1 (warning) to gather feedback, then potentially move to Option 2 if problem persists.

## Files to modify

- cmd/bd/create.go - Add validation after line 80 (where description is retrieved)
- Consider adding to RPC handler in daemon mode as well


Create multi-agent race condition test

Automated test that runs 2+ agents simultaneously to verify collision prevention.

Acceptance Criteria:
- Script spawns 2 agents in parallel
- Both try to claim same issue
- Only one succeeds (via reservation)
- Other agent skips to different work
- Verify in JSONL that no duplicate claims
- Test with Agent Mail enabled/disabled

File: tests/integration/test_agent_race.py

Success Metric: Zero duplicate claims with Agent Mail, collisions without it


Auto-configure git merge driver during `bd init`

Enhance `bd init` to optionally set up beads-merge as git merge driver.

**Tasks**:
- Prompt user to install git merge driver
- Configure `.git/config`: `merge.beads.driver "bd merge %A %O %L %R"`
- Create/update `.gitattributes`: `.beads/beads.jsonl merge=beads`
- Add `--skip-merge-driver` flag for non-interactive use
- Update AGENTS.md onboarding section

**Files**:
- `cmd/bd/init.go`
- `.gitattributes` template


Baseline quality gate failure: test

The test quality gate is failing on the baseline (main branch).

This blocks the executor from claiming work until fixed.

Error: go test failed: exit status 1

Output:
```
?   	github.com/steveyegge/beads	[no test files]
# github.com/steveyegge/beads/internal/beads_test [github.com/steveyegge/beads/internal/beads.test]
internal/beads/routing_integration_test.go:142:27: not enough arguments in call to sqlite.New
	have (string)
	want (context.Context, string)
# github.com/steveyegge/beads/internal/compact [github.com/steveyegge/beads/internal/compact.test]
internal/compact/compactor_test.go:17:27: not enough arguments in call to sqlite.New
	have (string)
	want (context.Context, string)
# github.com/steveyegge/beads/cmd/bd [github.com/steveyegge/beads/cmd/bd.test]
cmd/bd/integrity_content_test.go:31:32: undefined: ctx
cmd/bd/integrity_content_test.go:183:32: undefined: ctx
FAIL	github.com/steveyegge/beads/cmd/bd [build failed]
# github.com/steveyegge/beads/internal/daemon [github.com/steveyegge/beads/internal/daemon.test]
internal/daemon/discovery_test.go:21:27: not enough arguments in call to sqlite.New
	have (string)
	want (context.Context, string)
internal/daemon/discovery_test.go:59:27: not enough arguments in call to sqlite.New
	have (string)
	want (context.Context, string)
internal/daemon/discovery_test.go:232:27: not enough arguments in call to sqlite.New
	have (string)
	want (context.Context, string)
# github.com/steveyegge/beads/internal/importer [github.com/steveyegge/beads/internal/importer.test]
internal/importer/external_ref_test.go:22:27: not enough arguments in call to sqlite.New
	have (string)
	want (context.Context, string)
internal/importer/external_ref_test.go:106:27: not enough arguments in call to sqlite.New
	have (string)
	want (context.Context, string)
internal/importer/external_ref_test.go:197:27: not enough arguments in call to sqlite.New
	have (string)
	want (context.Context, string)
internal/importer/external_ref_test.go:295:27: not enough arguments in call to sqlite.New
	have (string)
	want (context.Context, string)
internal/importer/importer_test.go:566:27: not enough arguments in call to sqlite.New
	have (st
... (truncated, see full output in logs)
```

Released by executor after budget limit hit. Tests were already fixed manually. Issue can be closed when tests are verified to pass.


bd delete fails to find closed issues

## Steps to Reproduce
1. Close some issues with `bd close`
2. Try to delete them with `bd delete <ids> --force`
3. Get error "issues not found"

## Expected Behavior
Should delete the closed issues

## Actual Behavior
```
Error: issues not found: bd-74ee, bd-9b13, bd-72w, bd-149, bd-5iv, bd-78w
```

But `bd list --status closed --json` shows they exist.

## Root Cause
Likely the delete command is only looking for open issues, or there's a race condition with auto-import.


Document inconsistent error handling strategy across codebase

**Scope:** Cross-cutting pattern across cmd/bd (create.go, init.go, sync.go, daemon_sync.go)

**Issue:** Three different error handling patterns used inconsistently:

**Pattern A: Exit immediately**
```go
if err := store.CreateIssue(...) {
    fmt.Fprintf(os.Stderr, "Error: %v\n", err)
    os.Exit(1)
}
```

**Pattern B: Warn and continue**
```go
if err := createConfigYaml(...) {
    fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
    // Non-fatal - continue anyway
}
```

**Pattern C: Silent ignore**
```go
_ = store.Close()
```

**Impact:**
- Makes error handling non-deterministic
- Hard to test error paths
- Inconsistent user experience

**Task:**
1. Document when each pattern should be used
2. Add decision tree/flowchart to developer docs
3. Consider creating error handling helpers to enforce consistency
4. Audit codebase and refactor outliers


bd doctor should recommend bd prime migration for existing repos

bd doctor should detect old beads integration patterns and recommend migrating to bd prime approach.

## Current behavior
- bd doctor checks if Claude hooks are installed globally
- Doesn't check project-level integration (AGENTS.md, CLAUDE.md)
- Doesn't recommend migration for repos using old patterns

## Desired behavior
bd doctor should detect and suggest:

1. **Old slash command pattern detected**
   - Check for /beads:* references in AGENTS.md, CLAUDE.md
   - Suggest: These slash commands are deprecated, use bd prime hooks instead

2. **No agent documentation**
   - Check if AGENTS.md or CLAUDE.md exists
   - Suggest: Run 'bd onboard' or 'bd setup claude' to document workflow

3. **Old MCP-only pattern**
   - Check for instructions to use MCP tools but no bd prime hooks
   - Suggest: Add bd prime hooks for better token efficiency

4. **Migration path**
   - Show: 'Run bd setup claude to add SessionStart/PreCompact hooks'
   - Show: 'Update AGENTS.md to reference bd prime instead of slash commands'

## Example output

⚠ Warning: Old beads integration detected in CLAUDE.md
  Found: /beads:* slash command references (deprecated)
  Recommend: Migrate to bd prime hooks for better token efficiency
  Fix: Run 'bd setup claude' and update CLAUDE.md

💡 Tip: bd prime + hooks reduces token usage by 80-99% vs slash commands
  MCP mode: ~50 tokens vs ~10.5k for full MCP scan
  CLI mode: ~1-2k tokens with automatic context recovery

## Benefits
- Helps existing repos adopt new best practices
- Clear migration path for users
- Better token efficiency messaging


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.


Document Claude Code for Web SessionStart hook

Create documentation for using bd in Claude Code for Web:

## Documentation locations
- README.md - Add Claude Code for Web section
- Create docs/CLAUDE_CODE_WEB.md with detailed instructions

## SessionStart hook example
```json
{
  "sessionStart": {
    "script": "npm install -g @beads/bd && bd init --quiet --prefix bd || true"
  }
}
```

## Documentation should cover
- How to configure SessionStart hook in .claude/settings.json
- Verification: Check bd is installed (bd --version)
- Basic workflow in Claude Code for Web
- Troubleshooting common issues
- Note about network restrictions and why npm approach works

## Examples
- Creating issues in web sandbox
- Syncing with git in web environment
- Using MCP server (if applicable)


Clean up merge conflict artifacts in .beads directory

After resolving merge conflicts in .beads/beads.jsonl, leftover artifacts remain as untracked files:
- .beads/beads.base.jsonl
- .beads/beads.left.jsonl

These appear to be temporary files created during merge conflict resolution.

Options to fix:
1. Add these patterns to .beads/.gitignore automatically
2. Clean up these files after successful merge resolution
3. Document that users should delete them manually
4. Add a check in 'bd sync' or 'bd doctor' to detect and remove stale merge artifacts

Preferred solution: Add *.base.jsonl and *.left.jsonl patterns to .beads/.gitignore during 'bd init', and optionally clean them up automatically after successful import.


Need comprehensive daemon health check command (bd daemon doctor?)


Add tests for bd init --contributor wizard

Write integration tests for the contributor wizard:
- Test fork detection logic
- Test planning repo creation
- Test config setup
- Test with/without upstream remote
- Test with SSH vs HTTPS origins


Add tests for getMultiRepoJSONLPaths() edge cases

From bd-xo6b code review: Missing test coverage for getMultiRepoJSONLPaths() edge cases.

Current test gaps:
- No tests for empty paths in config
- No tests for duplicate paths
- No tests for tilde expansion
- No tests for relative paths
- No tests for symlinks
- No tests for paths with spaces
- No tests for invalid/non-existent paths

Test cases needed:

1. Empty path handling:
   Primary = empty, Additional = [empty]
   Expected: Should either use . as default or error gracefully

2. Duplicate detection:
   Primary = ., Additional = [., ./]
   Expected: Should return unique paths only

3. Path normalization:
   Primary = ~/repos/main, Additional = [../other, ./foo/../bar]
   Expected: Should expand to absolute canonical paths

4. Partial failure scenarios:
   What if snapshot capture succeeds for repos 1-2 but fails on repo 3?
   Test that system does not end up in inconsistent state

Files:
- cmd/bd/deletion_tracking_test.go (add new tests)

Dependencies:
Depends on fixing getMultiRepoJSONLPaths() path normalization first.


Issue with desc

This is a description


Add cancellation and timeout tests

Add comprehensive tests for context cancellation and timeout behavior.

## Context
Part of context propagation work. Validates that bd-rtp and bd-yb8 work correctly.

## Test Coverage Needed

### 1. Cancellation Tests
- [ ] Import operation cancelled mid-stream
- [ ] Export operation cancelled mid-stream
- [ ] Database query cancelled during long operation
- [ ] No corruption after cancellation
- [ ] Proper cleanup (defers execute, connections closed)

### 2. Timeout Tests
- [ ] Operations respect context deadlines
- [ ] Appropriate error messages on timeout
- [ ] State remains consistent after timeout

### 3. Signal Handling Tests
- [ ] SIGINT (Ctrl+C) triggers cancellation
- [ ] SIGTERM triggers graceful shutdown
- [ ] Multiple signals handled correctly

## Implementation Approach
```go
func TestImportCancellation(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())

    // Start import in goroutine
    go func() {
        err := runImport(ctx, largeFile)
        assert.Error(err, context.Canceled)
    }()

    // Cancel after short delay
    time.Sleep(100 * time.Millisecond)
    cancel()

    // Verify database integrity
    assertDatabaseConsistent(t, store)
}
```

## Files to Create/Update
- cmd/bd/import_test.go - cancellation tests
- cmd/bd/export_test.go - cancellation tests
- internal/storage/sqlite/*_test.go - context timeout tests

## Acceptance Criteria
- [ ] All critical operations have cancellation tests
- [ ] Tests verify database integrity after cancellation
- [ ] Signal handling tested (if feasible)
- [ ] Test coverage >80% for context paths


Add test coverage for internal/git package


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


Implement content-hash based collision resolution for deterministic convergence

The current collision resolution uses creation timestamps to decide which issue to keep vs. remap. This is non-deterministic when two clones create issues at nearly the same time.

Root cause of bd-71107098:
- Clone A creates test-1="Issue from clone A" at T0
- Clone B creates test-1="Issue from clone B" at T0+30ms
- Clone B syncs first, remaps Clone A's to test-2
- Clone A syncs second, sees collision, remaps Clone B's to test-2
- Result: titles are swapped between clones

Solution:
- Use content-based hashing (title + description + priority + type)
- Deterministic winner: always keep issue with lower hash
- Same collision on different clones produces same result (idempotent)

Implementation:
- Modify ScoreCollisions in internal/storage/sqlite/collision.go
- Replace timestamp-based scoring with content hash comparison
- Ensure hash function is stable across platforms


bd status "Recent Activity" is misleading - should use git history

## Problem

`bd status` shows "Recent Activity (last 7 days)" but the numbers are wrong. It only looks at database timestamps, not git history. Says "141 issues closed in last 7 days" when thousands have actually come and go.

## Issues

1. Only queries database timestamps, not git history
2. 7 days is too long a window
3. Numbers don't reflect actual activity in JSONL git history

## Proposed Fix

Either:
- Query git history of `.beads/beads.jsonl` to get actual activity (last 24-48 hours)
- Remove "Recent Activity" section entirely if not useful
- Make time window configurable and default to 24h

## Example Output (Current)
```
Recent Activity (last 7 days):
Issues Created:    174
Issues Closed:     141
Issues Updated:    37
```
This is misleading when thousands of issues have actually cycled through.


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


Implement postinstall script for binary download

Create npm/scripts/postinstall.js that downloads platform-specific binaries:

## Platform detection
- Detect os.platform() and os.arch()
- Map to GitHub release asset names:
  - linux-amd64 → bd-linux-amd64
  - linux-arm64 → bd-linux-arm64
  - darwin-amd64 → bd-darwin-amd64
  - darwin-arm64 → bd-darwin-arm64
  - win32-x64 → bd-windows-amd64.exe

## Download logic
- Fetch from GitHub releases: https://github.com/steveyegge/beads/releases/latest/download/${asset}
- Save to npm/bin/bd (or bd.exe on Windows)
- Set executable permissions (chmod +x)
- Handle errors gracefully with helpful messages

## Error handling
- Check for unsupported platforms
- Retry on network failures
- Provide manual download instructions if automated fails
- Skip download if binary already exists (for local development)


Fix auto-import creating duplicates instead of updating issues

ROOT CAUSE: server_export_import_auto.go line 221 uses ResolveCollisions: true for ALL auto-imports. This is wrong.

Problem:
- ResolveCollisions is for branch merges (different issues with same ID)
- Auto-import should UPDATE existing issues, not create duplicates
- Every git pull creates NEW duplicate issues with different IDs
- Two agents ping-pong creating endless duplicates

Evidence:
- 31 duplicate groups found (bd duplicates)
- bd-236-246 are duplicates of bd-224-235
- Both agents keep pulling and creating more duplicates
- JSONL file grows endlessly with duplicates

The Fix:
Change checkAndAutoImportIfStale in server_export_import_auto.go:
- Remove ResolveCollisions: true (line 221)
- Use normal import logic that updates existing issues by ID
- Only use ResolveCollisions for explicit bd import --resolve-collisions

Impact: Critical - makes beads unusable for multi-agent workflows


File change test issue


Use in-memory filesystem for test git operations

Use tmpfs/ramdisk for git operations in tests to reduce I/O overhead.

Options:
1. Mount /tmp as tmpfs in CI (GitHub Actions supports this)
2. Use Go's testing.TB.TempDir() which may already use tmpfs on some systems
3. Explicitly create ramdisk for tests on macOS

Expected savings: 20-30% reduction in git operation time


Add tests for daemon sync module


Centralize error handling patterns in storage layer

80+ instances of inconsistent error handling across sqlite.go with mix of %w, %v, and no wrapping.

Location: internal/storage/sqlite/sqlite.go (throughout)

Problem:
- Some use fmt.Errorf("op failed: %w", err) - correct wrapping
- Some use fmt.Errorf("op failed: %v", err) - loses error chain
- Some return err directly - no context
- Hard to debug production issues
- Can't distinguish error types

Solution: Create internal/storage/sqlite/errors.go:
- Define sentinel errors (ErrNotFound, ErrInvalidID, etc.)
- Create wrapDBError(op string, err error) helper
- Convert sql.ErrNoRows to ErrNotFound
- Always wrap with operation context

Impact: Lost error context; inconsistent messages; hard to debug

Effort: 5-7 hours


Add built-in version tracking to bd

Enhance bd to automatically track its own version in metadata.json and detect upgrades.

## Features to Implement
1. Auto-update metadata.json with 'last_bd_version' field on every bd command
2. Add 'bd upgrade status' - check if version changed since last run
3. Add 'bd upgrade review' - show what's new since last-seen version (not just last 3)
4. Add 'bd upgrade ack' - mark current version as acknowledged
5. Smart detection output when version changes (show hint on bd ready, bd list, etc.)

## Implementation Details
- Store last_bd_version in metadata.json
- Store last_version_check timestamp
- Compare on every bd command (cached check, low overhead)
- Only show notification once per session until ack'd

## Example Output
```
$ bd ready
🔄 bd upgraded from v0.23.0 to v0.24.2 since last use
💡 Run 'bd upgrade review' to see what changed

Ready work: 5 issues
...
```

## Acceptance Criteria
- Version automatically tracked in metadata.json
- Upgrade detection works across sessions
- New bd upgrade subcommands functional
- Non-intrusive notifications (show once, then quiet)


Add validation for duplicate external_ref in batch imports

Currently, if a batch import contains multiple issues with the same external_ref, the behavior is undefined. We should detect and handle this case.

Current behavior:
- No validation for duplicate external_ref within a batch
- Last-write-wins or non-deterministic behavior

Proposed solution:
- Detect duplicate external_ref values in incoming batch
- Fail with clear error message OR
- Merge duplicates intelligently (use newest timestamp)
- Add test case for this scenario

Related: bd-1022


Test: N-clone scenario with hash IDs (no collisions)

Comprehensive test to verify hash IDs eliminate collision problems.

## Test: TestHashIDsNClones

### Purpose
Verify that N clones can work offline and sync without ID collisions using hash IDs.

### Test Scenario
```
Setup:
- 1 bare remote repo
- 5 clones (A, B, C, D, E)

Offline Work:
- Each clone creates 10 issues with different titles
- No coordination, no network access
- Total: 50 unique issues

Sync:
- Clones sync in random order
- Each pull/import other clones' issues

Expected Result:
- All 5 clones converge to 50 issues
- Zero ID collisions
- Zero remapping needed
- Alias conflicts resolved deterministically
```

### Implementation
File: cmd/bd/beads_hashid_test.go (new)

```go
func TestHashIDsFiveClones(t *testing.T) {
    tmpDir := t.TempDir()
    remoteDir := setupBareRepo(t, tmpDir)

    // Setup 5 clones
    clones := make(map[string]string)
    for _, name := range []string{"A", "B", "C", "D", "E"} {
        clones[name] = setupClone(t, tmpDir, remoteDir, name)
    }

    // Each clone creates 10 issues offline
    for name, dir := range clones {
        for i := 0; i < 10; i++ {
            createIssue(t, dir, fmt.Sprintf("%s-issue-%d", name, i))
        }
        // No sync yet!
    }

    // Sync in random order
    syncOrder := []string{"C", "A", "E", "B", "D"}
    for _, name := range syncOrder {
        syncClone(t, clones[name], name)
    }

    // Final convergence round
    for _, name := range []string{"A", "B", "C", "D", "E"} {
        finalPull(t, clones[name], name)
    }

    // Verify all clones have all 50 issues
    for name, dir := range clones {
        issues := getIssues(t, dir)
        if len(issues) != 50 {
            t.Errorf("Clone %s: expected 50 issues, got %d", name, len(issues))
        }

        // Verify all issue IDs are hash-based
        for _, issue := range issues {
            if !strings.HasPrefix(issue.ID, "bd-") || len(issue.ID) != 11 {
                t.Errorf("Invalid hash ID: %s", issue.ID)
            }
        }
    }

    // Verify no collision resolution occurred
    // (This would be in logs if it happened)

    t.Log("✓ All 5 clones converged to 50 issues with zero collisions")
}
```

### Edge Case Tests

#### Test: Hash Collision Detection (Artificial)
```go
func TestHashCollisionDetection(t *testing.T) {
    // Artificially inject collision by mocking hash function
    // Verify system detects and handles it
}
```

#### Test: Alias Conflicts Resolved Deterministically
```go
func TestAliasConflictsNClones(t *testing.T) {
    // Two clones assign same alias to different issues
    // Verify deterministic resolution (content-hash ordering)
    // Verify all clones converge to same alias assignments
}
```

#### Test: Mixed Sequential and Hash IDs (Should Fail)
```go
func TestMixedIDsRejected(t *testing.T) {
    // Try to import JSONL with sequential IDs into hash-ID database
    // Verify error or warning
}
```

### Performance Test

#### Benchmark: Hash ID Generation
```go
func BenchmarkHashIDGeneration(b *testing.B) {
    for i := 0; i < b.N; i++ {
        GenerateHashID("title", "description", time.Now(), "workspace-id")
    }
}

// Expected: < 1μs per generation
```

#### Benchmark: N-Clone Convergence Time
```go
func BenchmarkNCloneConvergence(b *testing.B) {
    for _, n := range []int{3, 5, 10, 20} {
        b.Run(fmt.Sprintf("N=%d", n), func(b *testing.B) {
            // Measure total convergence time
        })
    }
}

// Expected: Linear scaling O(N)
```

### Acceptance Criteria
- TestHashIDsFiveClones passes reliably (10/10 runs)
- Zero ID collisions in any scenario
- All clones converge in single round (not multi-round like old system)
- Alias conflicts resolved deterministically
- Performance benchmarks meet targets (<1μs hash gen)

## Files to Create
- cmd/bd/beads_hashid_test.go

## Comparison to Old System
This test replaces:
- TestTwoCloneCollision (bd-71107098) - no longer needed
- TestThreeCloneCollision (bd-cbed9619) - no longer needed
- TestFiveCloneCollision (bd-a40f374f) - no longer needed

Old system required complex collision resolution and multi-round convergence.
New system: single-round convergence with zero collisions.


Create npm package structure with package.json

Set up initial npm package structure for @beads/bd:

## Files to create
- npm/package.json - Package metadata, dependencies, scripts
- npm/bin/bd - CLI wrapper script that invokes native binary
- npm/.gitignore - Ignore downloaded binaries
- npm/README.md - Installation and usage instructions

## package.json structure
- Name: @beads/bd (scoped package)
- Main: index.js (exports binary path)
- Bin: bin/bd (CLI entry point)
- Scripts: postinstall (download binary)
- Keywords: issue-tracker, cli, beads, bd
- License: MIT

## Bin wrapper
Simple Node.js script that:
- Spawns native binary with child_process.spawn
- Passes through all arguments and stdio
- Exits with binary's exit code


Performance Testing Documentation

Create docs/performance-testing.md documenting the performance testing framework.

Sections:
1. Overview - What the framework does, goals
2. Running Benchmarks
   - make bench command
   - Running specific benchmarks
   - Interpreting output (ns/op, allocs/op)
3. Profiling and Analysis
   - Viewing CPU profiles with pprof
   - Reading flamegraphs
   - Memory profiling
   - Finding hotspots
4. User Diagnostics
   - bd doctor --perf usage
   - Sharing profiles with bug reports
   - Understanding the report output
5. Comparing Performance
   - Using benchstat for before/after comparisons
   - Detecting regressions
6. Tips for Optimization
   - Common patterns
   - When to profile vs benchmark

Style:
- Concise, practical examples
- Screenshots/examples of pprof output
- Clear command-line examples
- Focus on workflow, not theory


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.


Consider central serialization package for JSON handling

Multiple parts of the codebase handle JSON serialization of issues with slightly different approaches. Consider creating a centralized serialization package to ensure consistency.

Current serialization locations:
- `cmd/bd/export.go` - JSONL export (issues to file)
- `cmd/bd/import.go` - JSONL import (file to issues)
- `internal/rpc/protocol.go` - RPC JSON marshaling
- `internal/storage/memory/memory.go` - In-memory marshaling

Potential benefits:
- Single source of truth for JSON format
- Consistent field naming
- Easier to add new fields
- Centralized validation

Note: This is marked **optional** because:
- Current serialization mostly works
- May not provide enough benefit to justify refactor
- Risk of breaking compatibility

Decision point: Evaluate if benefits outweigh refactoring cost

Impact: TBD based on investigation - may defer to future work


Optimize CLI test suite - eliminate redundant git init calls

Current: Each of 13 CLI tests calls git init (31s total). Solution: Use single test binary built once in init(), skip git operations where possible, or use mock filesystem.


Separate canonical BD_GUIDE.md from AGENTS.md

Create architectural separation between bd-specific instructions and project-specific instructions.

## Problem
Currently AGENTS.md mixes:
- bd tool documentation (changes with bd upgrades)
- Project-specific workflow (stable, manually maintained)

This makes it hard to update bd instructions without touching project docs.

## Solution
1. Generate .beads/BD_GUIDE.md from 'bd onboard' output
2. Mark it as auto-generated (never manually edit)
3. Version-stamp header with bd version
4. AGENTS.md references it instead of duplicating content
5. Auto-update BD_GUIDE.md when bd version changes

## Implementation
- Add 'bd onboard --output .beads/BD_GUIDE.md' option
- Detect version changes and offer to regenerate
- Add header: '<!-- Auto-generated by bd v0.24.2 - DO NOT EDIT -->'
- Update AGENTS.md to reference BD_GUIDE.md

## Benefits
- Clear separation of concerns
- Deterministic updates (no agent LLM involved)
- Git-trackable diffs show exactly what changed
- Progressive disclosure (agents read when needed)

## Acceptance Criteria
- BD_GUIDE.md auto-generated and version-stamped
- AGENTS.md references it appropriately
- Upgrade workflow auto-updates BD_GUIDE.md
- Git diffs clearly show bd instruction changes


Remove Cache Configuration Docs

Remove documentation of deprecated cache env vars