Testing & Validation

Ensure reliability through comprehensive testing.

Completed comprehensive Agent Mail test coverage analysis and implementation.

**Test Coverage Summary:**
- 66 total tests across 5 files
- 51 unit tests for HTTP adapter (0.02s)
- 15 integration tests for multi-agent scenarios (~55s total)

**New Tests Added:**
Created `test_multi_agent_coordination.py` (4 tests, 11s) covering:
1. Fairness: 10 agents competing for 5 issues → exactly 1 claim per issue
2. Notifications: End-to-end message delivery between agents
3. Handoff: Clean reservation transfer from agent1 to agent2
4. Idempotency: Double reserve/release by same agent

**Coverage Quality:**
✅ Collision prevention (race conditions)
✅ Graceful degradation (7 failure modes)
✅ TTL/expiration behavior
✅ Multi-agent coordination
✅ JSONL consistency
✅ HTTP error handling
✅ Authorization and configuration

**Intentionally Skipped:**
- Path traversal (validated elsewhere)
- Retry policies (nice-to-have)
- HTTPS/TLS (out of scope)
- Slow tests (50+ agents, soak tests)

See `tests/integration/AGENT_MAIL_TEST_COVERAGE.md` for details.

All tests pass. Agent Mail integration is well-tested and reliable for multi-agent scenarios.


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


bd-hv01: Storage backend extensibility broken by type assertion

Problem: deletion_tracking.go:69-82 uses type assertion for DeleteIssue which breaks if someone adds a new storage backend.

Fix: Check capability before starting merge or add DeleteIssue to Storage interface.

Files: cmd/bd/deletion_tracking.go:69-82, internal/storage/storage.go


Documentation

Complete documentation for Agent Mail integration to enable adoption.


bd cleanup fails with CHECK constraint on status/closed_at mismatch

## Problem

Running bd cleanup --force fails with:

Error: failed to create tombstone for bd-okh: sqlite3: constraint failed: CHECK constraint failed: (status = 'closed') = (closed_at IS NOT NULL)

## Root Cause

The database has a CHECK constraint ensuring closed issues have closed_at set and non-closed issues do not. When bd cleanup tries to convert a closed issue to a tombstone, this constraint fails.

## Impact

- bd cleanup --force cannot complete
- 722 closed issues cannot be cleaned up
- Blocks routine maintenance

## Investigation Needed

1. Check the bd-okh record in the database
2. Determine if this is data corruption or a bug in tombstone creation

## Proposed Solutions

1. If data corruption: Add a bd doctor --fix check that repairs status/closed_at mismatches
2. If code bug: Fix the tombstone creation to properly handle the constraint

## Files to Investigate

- internal/storage/sqlite/sqlite.go - DeleteIssue / tombstone creation
- Schema CHECK constraint definition

## Acceptance Criteria

- Identify root cause (data vs code bug)
- bd cleanup --force completes successfully
- bd doctor detects status/closed_at mismatches


Add 'new' as alias for 'create' command


bd export should verify JSONL line count matches database count

After export completes, bd should verify that the JSONL file line count matches the number of issues exported. This would catch silent failures where the export appears to succeed but doesn't actually write all issues.

Real-world scenario from VC project:
- Ran direct SQL DELETE to remove 240 issues
- Ran 'bd export -o .beads/issues.jsonl'
- No error shown, appeared to succeed
- But JSONL file was not updated (still had old line count)
- Later session found all 240 issues still in JSONL
- Had to repeat the cleanup

If export had verified line count, it would have immediately shown:
  Error: Export verification failed
    Expected: 276 issues
    JSONL file: 516 lines
    Mismatch indicates export failed to write all issues

This is especially important because:
1. JSONL is source of truth in git
2. Silent export failures cause data inconsistency
3. Users assume export succeeded if no error shown
4. The verification is cheap (just count lines)

Implementation:
- After writing JSONL, count lines in file
- Compare to len(exportedIDs)
- If mismatch, remove temp file and return error
- Show clear error message with both counts


YABB: Spurious issue updates during normal operations

Issue bd-627d was updated during config refactoring session without any actual changes to it. Only timestamps and content_hash changed.

Observed: Running various bd commands (list, create, etc.) caused bd-627d updated_at to change from 14:14 to 14:31.

Expected: Issues should only be updated when explicitly modified.

This causes:
- Dirty JSONL after every session
- False conflicts in git
- Confusing git history

Likely culprit: Daemon auto-import/export cycle or database migration touching all issues.

Investigated thoroughly - unable to reproduce. The import logic has IssueDataChanged() checks before calling UpdateIssue (importer/importer.go:458). All tests pass. May have been fixed by recent refactorings. Closing as cannot reproduce - please reopen with specific repro steps if it occurs again.


Test dependency parent


Test auto-start in fred


Benchmark git traffic reduction

Automated benchmark comparing git operations with/without Agent Mail.

Acceptance Criteria:
- Script that processes 50 issues
- Counts git operations (pull, commit, push)
- Generates comparison report
- Verifies ≥70% reduction
- Fails if regression detected

File: tests/benchmarks/git_traffic.py

Output: Without Agent Mail: 450 git operations, With Agent Mail: 135 git operations, Reduction: 70%

Implemented automated benchmark script with following features:
- Processes configurable number of issues (default 50)
- Compares git operations in two modes: git-only vs Agent Mail
- Generates detailed comparison report with statistics
- Exit code reflects pass/fail based on 70% reduction target
- Results: 98.5% reduction (200 ops → 3 ops) for 50 issues

Files created:
- tests/benchmarks/git_traffic.py (main benchmark script)
- tests/benchmarks/README.md (documentation)
- tests/benchmarks/git_traffic_50_issues.md (sample results)

The benchmark vastly exceeds the 70% target, showing 98.5% reduction.


Replace BEADS_DB with BEADS_DIR environment variable

Implement BEADS_DIR as a replacement for BEADS_DB to point to the .beads directory instead of the database file directly.

Rationale:
- With --no-db mode, there's no .db file to point to
- The .beads directory is the logical unit (contains config.yaml, db files, jsonl files)
- More intuitive: point to the beads directory not the database file

Implementation:
1. Add BEADS_DIR environment variable support
2. Maintain backward compatibility with BEADS_DB
3. Priority order: BEADS_DIR > BEADS_DB > auto-discovery
4. If BEADS_DIR is set, look for config.yaml in that directory to find actual database path
5. Update documentation and migration guide

Files to modify:
- beads.go (FindDatabasePath function)
- cmd/bd/main.go (initialization)
- Documentation (CLI_REFERENCE.md, TROUBLESHOOTING.md, etc.)
- MCP integration (integrations/beads-mcp/src/beads_mcp/config.py)

Testing:
- Ensure BEADS_DB still works (backward compatibility)
- Test BEADS_DIR with both db and --no-db modes
- Test priority order when both are set
- Update integration tests

Related to GitHub issue #179


Fix bd --version and bd completion to work without database


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 TestNWayCollision for 5+ clones

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

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

## Implementation Tasks

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

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

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

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

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

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

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

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

    tmpDir := t.TempDir()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Testing Strategy

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

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

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

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


Fix TestExportUpdatesMetadata to test actual daemon functions

## Problem
TestExportUpdatesMetadata (daemon_sync_test.go:296) manually replicates the metadata update logic rather than calling the fixed functions (createExportFunc/createSyncFunc).

This means:
- Test verifies the CONCEPT works
- But doesn't verify the IMPLEMENTATION in the actual daemon functions
- If daemon code is wrong, test still passes

## Current Test Flow
```go
// Test does:
exportToJSONLWithStore()  // ← Direct call
// ... manual metadata update ...
exportToJSONLWithStore()  // ← Direct call again
```

## Should Be
```go
// Test should:
createExportFunc(...)()  // ← Call actual daemon function
// ... verify metadata was updated by the function ...
createExportFunc(...)()  // ← Call again, should not fail
```

## Challenge
createExportFunc returns a closure that's run by the daemon. Testing this requires:
- Mock logger
- Proper context setup
- Git operations might need mocking

## Files
- cmd/bd/daemon_sync_test.go

## Acceptance Criteria
- Test calls actual createExportFunc and createSyncFunc
- Test verifies metadata updated by daemon code, not test code
- Both functions tested (export and sync)


Add optional post-merge git hook example for bd sync

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

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

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

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


User Diagnostics (bd doctor --perf)

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

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

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

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

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

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


Export deduplication breaks when JSONL and export_hashes table diverge

## Problem

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

## Symptoms

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

## Root Cause

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

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

## Impact

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

## Reproduction

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

## Current Workaround

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

## Proposed Solutions

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

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

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

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

## Recommended Fix

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

## Files Involved

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

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

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

### Bug Manifestation During Recovery

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

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

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

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

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

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

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

### Key Learnings

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

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

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

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

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

### Immediate Action Required

**DISABLE EXPORT DEDUPLICATION NOW**

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

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

### Long-term Fix

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


Code Health & Technical Debt Cleanup

Comprehensive codebase cleanup to remove dead code, refactor monolithic files, deduplicate utilities, and improve maintainability. Based on ultrathink code health analysis conducted 2025-10-27.

Goals:
- Remove ~1,500 LOC of dead/unreachable code
- Split 2 monolithic files (server.go 2,273 LOC, sqlite.go 2,136 LOC) into focused modules
- Deduplicate scattered utility functions (normalizeLabels, BD_DEBUG checks)
- Consolidate test coverage (2,019 LOC of collision tests)
- Improve code navigation and reduce merge conflicts

Impact: Reduces codebase by ~6-8%, improves maintainability, faster CI/CD

Estimated Effort: 11 days across 4 phases


Week 1 task


Add tests for bd init --team wizard

Write integration tests for the team wizard:
- Test branch detection
- Test sync branch creation
- Test protected branch workflow
- Test auto-sync configuration


Issue to close


Migration tool: sequential → hash IDs

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

## Command: bd migrate --hash-ids

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

## Migration Process

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

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

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

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

    return nil
}
```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Batch test 2


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)


Add tests for Integration Layer Implementation

While implementing bd-wfmw, noticed missing tests

Reviewed existing coverage:
- Basic test coverage exists in lib/test_beads_mail_adapter.py
- Integration tests cover failure scenarios in tests/integration/test_mail_failures.py
- Good coverage of: enabled/disabled modes, graceful degradation, 409 conflicts, HTTP errors, config
- Missing: authorization headers detail, request body structure validation, concurrent reservation timing, TTL edge cases


Fix failing integration tests in beads-mcp

The `beads-mcp` test suite has failures in `tests/test_bd_client_integration.py` (assertion error in `test_init_creates_beads_directory`) and errors in `tests/test_worktree_separate_dbs.py` (setup failures finding database). These need to be investigated and fixed to ensure a reliable CI baseline.


Add tests for tombstone edge cases

Missing test coverage for tombstone implementation:

1. Test tombstone validation (deleted_at requirement)
2. Test GetReadyWork excludes tombstones
3. Test SearchIssues behavior with tombstones
4. Integration test for creating/retrieving a tombstone via the delete command
5. Test that status=tombstone update is rejected


Add --body-file flag to bd create for reading descriptions from files

## Problem

Creating issues with long/complex descriptions via CLI requires shell escaping gymnastics:

```bash
# Current workaround - awkward heredoc quoting
bd create --title="..." --description="$(cat <<'EOF'
...markdown...
EOF
)"

# Often fails with quote escaping errors in eval context
# Agents resort to writing temp files then reading them
```

## Proposed Solution

Add `--body-file` and `--description-file` flags to read description from a file, matching `gh` CLI pattern.

```bash
# Natural pattern that aligns with training data
cat > /tmp/desc.md << 'EOF'
...markdown content...
EOF

bd create --title="..." --body-file=/tmp/desc.md
```

## Implementation

### 1. Add new flags to `bd create`

```go
createCmd.Flags().String("body-file", "", "Read description from file (use - for stdin)")
createCmd.Flags().String("description-file", "", "Alias for --body-file")
```

### 2. Flag precedence

- If `--body-file` or `--description-file` is provided, read from file
- If value is `-`, read from stdin
- Otherwise fall back to `--body` or `--description` flag
- If neither provided, description is empty (current behavior)

### 3. Error handling

- File doesn't exist → clear error message
- File not readable → clear error message
- stdin specified but not available → clear error message

## Benefits

✅ **Matches training data**: `gh issue create --body-file file.txt` is a common pattern
✅ **No shell escaping issues**: File content is read directly
✅ **Works with any content**: Markdown, special characters, quotes, etc.
✅ **Agent-friendly**: Agents already write complex content to temp files
✅ **User-friendly**: Easier for humans too when pasting long descriptions

## Related Commands

Consider adding similar support to:
- `bd update --body-file` (for updating descriptions)
- `bd comment --body-file` (if/when we add comments)

## Examples

```bash
# From file
bd create --title="Add new feature" --body-file=feature.md

# From stdin
echo "Quick description" | bd create --title="Bug fix" --body-file=-

# With other flags
bd create \
  --title="Security issue" \
  --type=bug \
  --priority=0 \
  --body-file=security-report.md \
  --label=security
```

## Testing

- Test with normal files
- Test with stdin (`-`)
- Test with non-existent files (error handling)
- Test with binary files (should handle gracefully)
- Test with empty files (valid - empty description)
- Test that `--description-file` and `--body-file` are equivalent aliases


bd doctor should check for multiple DBs, multiple JSONLs, daemon health


Latency test


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.


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


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)


Code Review: PR #481 - Context Engineering Optimizations

Comprehensive code review of the merged context engineering PR (PR #481) that reduces MCP context usage by 80-90%.

## Summary
The PR successfully implements lazy tool schema loading and minimal issue models to reduce context window usage. Overall implementation is solid and well-tested.

## Positive Findings
✅ Well-designed models (IssueMinimal, CompactedResult)
✅ Comprehensive test coverage (28 tests, all passing)
✅ Clear documentation and comments
✅ Backward compatibility preserved (show() still returns full Issue)
✅ Sensible defaults (COMPACTION_THRESHOLD=20, PREVIEW_COUNT=5)
✅ Tool catalog complete with all 15 tools documented

## Issues Identified
See linked issues for specific followup tasks.

## Context Engineering Architecture
- discover_tools(): List tool names only (~500 bytes vs ~15KB)
- get_tool_info(name): Get specific tool details on-demand
- IssueMinimal: Lightweight model for list views (~80 bytes vs ~400 bytes)
- CompactedResult: Auto-compacts results with >20 issues
- _to_minimal(): Conversion function (efficient, no N+1 issues)


Add security tests for file path validation in clean command

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

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

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

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

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

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


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


Update AGENTS.md with Agent Mail workflow

Update agent workflow section to include Agent Mail coordination as optional step.

Acceptance Criteria:
- Add Agent Mail to recommended workflow
- Show both with/without examples
- Update "Multi-Agent Patterns" section
- Cross-reference to AGENT_MAIL.md

File: AGENTS.md (lines 468-475)


Manual task


Support separate branch for beads commits

Allow beads to commit to a separate branch (e.g., beads-metadata) using git worktrees to support protected main branch workflows.

Solves GitHub Issue #205 - Users need to protect main branch while maintaining beads workflow.

Key advantages:
- Works on any git platform
- Main branch stays protected
- No disruption to user's working directory
- Backward compatible (opt-in via config)
- Minimal disk overhead (sparse checkout)

Total estimate: 17-24 days (4-6 weeks with parallel work)


Add unit tests for parent resurrection

Test resurrection with deleted parent (should succeed), resurrection with never-existed parent (should fail gracefully), multi-level resurrection (bd-abc.1.2 with both parents missing). Verify tombstone creation and is_tombstone flag.


convertDeletionToTombstone uses hardcoded default values

The convertDeletionToTombstone helper (importer.go:1114-1130) uses hardcoded defaults:
- Priority: 2
- IssueType: types.TypeTask
- Title: "(deleted)"

These defaults work but could be improved:
1. Consider using Priority: 0 (unset) to distinguish from user-set priority
2. OriginalType is empty string - consider setting IssueType to empty to be consistent
3. Title could include the original ID for debugging: "(deleted: bd-xyz)"

Low priority cleanup for consistency.


Fixture Generator for Realistic Test Data

Create internal/testutil/fixtures/fixtures.go with functions to generate realistic test data at scale.

Functions:
- LargeSQLite(storage) - 10K issues, native SQLite
- XLargeSQLite(storage) - 20K issues, native SQLite
- LargeFromJSONL(storage) - 10K issues imported from JSONL
- XLargeFromJSONL(storage) - 20K issues imported from JSONL

Data characteristics:
- Epic hierarchies (depth 4): Epic → Feature → Task → Subtask
- Cross-linked dependencies (tasks blocking across epics)
- Realistic status/priority/label distribution
- Representative assignees and temporal data

Implementation:
- Single file: internal/testutil/fixtures/fixtures.go
- No config structs, simple direct functions
- Seeded RNG for reproducibility
- Reusable by both benchmarks and tests


Fix TestTwoCloneCollision to compare content not timestamps

The test at beads_twoclone_test.go:204-207 currently compares full JSON output including timestamps, causing false negative failures.

Current behavior:
- Both clones converge to identical semantic content
- Clone A: test-2="Issue from clone A", test-1="Issue from clone B"
- Clone B: test-1="Issue from clone B", test-2="Issue from clone A"
- Titles match IDs correctly, no data corruption
- Only timestamps differ (expected and acceptable)

Fix needed:
- Replace exact JSON comparison with content-aware comparison
- Normalize or ignore timestamp fields when asserting convergence
- Test should PASS after this fix

This blocks completion of bd-71107098.


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


Latency test manual


Create Go adapter library (optional)

For agents written in Go, provide native adapter library instead of shelling out to curl.

Acceptance Criteria:
- agentmail.Client struct
- HTTP client with timeout/retry logic
- Same API as Python adapter
- Example usage in examples/go-agent/
- Unit tests

File: pkg/agentmail/client.go

Note: Lower priority - can shell out to curl initially


Clarify JSONL size bounds with multi-repo

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

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

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

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


Add test coverage for internal/git package


Use stable repo identifiers instead of full paths in metadata keys

## Problem
bd-ar2.2 uses full absolute paths as metadata key suffixes:
```go
updateExportMetadata(exportCtx, store, path, log, path)
// Creates: last_import_hash:/Users/stevey/src/cino/beads/repo1/.beads/issues.jsonl
```

## Issues
1. Absolute paths differ across machines/clones
2. Keys are very long
3. If user moves repo or clones to different path, metadata becomes orphaned
4. Metadata won't be portable across team members

## Better Approach
Use source_repo identifiers from multi-repo config (e.g., ".", "../frontend"):
```go
// Get mapping of jsonl_path -> source_repo identifier
for _, path := range multiRepoPaths {
    repoKey := getRepoKeyForPath(path)  // e.g., ".", "../frontend"
    updateExportMetadata(exportCtx, store, path, log, repoKey)
}
```

This creates stable keys like:
- `last_import_hash:.`
- `last_import_hash:../frontend`

## Related
Depends on bd-ar2.10 (hasJSONLChanged update)


Makefile Integration for Benchmarks

Add a single 'bench' target to the Makefile for running performance benchmarks.

Target:
.PHONY: bench

bench:
	go test -bench=. -benchtime=3s -tags=bench \
		-cpuprofile=cpu.prof -memprofile=mem.prof \
		./internal/storage/sqlite/
	@echo ""
	@echo "Profiles generated. View flamegraph:"
	@echo "  go tool pprof -http=:8080 cpu.prof"

Features:
- Single simple target, no complexity
- Always generates CPU and memory profiles
- Clear output on how to view results
- 3 second benchmark time for reliable results
- Uses bench build tag for heavy benchmarks

Usage:
  make bench                              # Run all benchmarks
  go test -bench=BenchmarkGetReadyWork... # Run specific benchmark


Investigate and upgrade to modernc.org/sqlite 1.39.1+

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

**Issue:** [deleted:bd-47], GH #144

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

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

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

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


Mass deletion logging for forensics

Log detailed information when the safety check triggers during auto-push.

When >50% issues vanish AND >5 existed:
- Log which issues vanished
- Log the before/after counts
- Log timestamp for forensic analysis

This gives users visibility into what happened if something goes wrong, without blocking the sync.

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


Override test

## Overview

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

## Success Criteria

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

## Background

[Provide context and motivation]

## Scope

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

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


Inconsistent stderr printing: bd-feh warning still prints directly


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


Audit and standardize JSON output across all commands

Ensure consistent JSON format and error handling when --json flag is used.

**Scope:**
1. Verify all commands respect --json flag
2. Standardize success response format
3. Standardize error response format
4. Document JSON schemas

**Commands to audit:**
- Core CRUD: create, update, delete, show, list, search ✓
- Queries: ready, blocked, stale, count, stats, status
- Deps: dep add/remove/tree/cycles
- Labels: label commands
- Comments: comments add/list/delete
- Epics: epic status/close-eligible
- Export/import: already support --json ✓

**Testing:**
- Success cases return valid JSON
- Error cases return valid JSON (not plain text)
- Consistent field naming (snake_case vs camelCase)
- Array vs object wrapping consistency


Code Review: PR #551 - Persist close_reason to issues table

Code review of PR #551 which fixes close_reason persistence bug.

## Summary
The PR correctly fixes a bug where close_reason was only stored in the events table, not in the issues.close_reason column. This caused `bd show --json` to return empty close_reason.

## What Was Fixed
- ✅ CloseIssue now updates both close_reason and closed_at
- ✅ ReOpenIssue clears both close_reason and closed_at
- ✅ Comprehensive tests added for both storage and CLI layers
- ✅ Clear documentation in queries.go about dual storage strategy

## Quality Assessment
✅ Tests cover both storage layer and CLI JSON output
✅ Handles reopen case (clearing close_reason)
✅ Good comments explaining dual-storage design
✅ No known issues

## Potential Followups
See linked issues for suggestions.


Batch test 5


Export doesn't update last_import_hash metadata causing perpetual 'JSONL content has changed' errors

After a successful export, the daemon doesn't update last_import_hash or last_import_mtime metadata. This causes hasJSONLChanged to return true on the next export attempt, blocking with 'refusing to export: JSONL content has changed since last import'.

Related to GH #334.

Scenario:
1. Daemon exports successfully → JSONL updated, hash changes
2. Metadata NOT updated (last_import_hash still points to old hash)
3. Next mutation triggers export
4. validatePreExport calls hasJSONLChanged
5. hasJSONLChanged sees current JSONL hash != last_import_hash
6. Export blocked with 'JSONL content has changed since last import'
7. Issue stuck: can't export, can't make progress

This creates a catch-22 where the system thinks JSONL changed externally when it was the daemon itself that changed it.

Current behavior:
- Import updates metadata (import.go:310-336)
- Export does NOT update metadata (daemon_sync.go:307)
- Result: metadata becomes stale after every export


Optimize Memory backend GetIssueByExternalRef with index

Currently GetIssueByExternalRef in Memory storage uses O(n) linear search through all issues.

Current code (memory.go:282-308):
for _, issue := range m.issues {
    if issue.ExternalRef != nil && *issue.ExternalRef == externalRef {
        return &issueCopy, nil
    }
}

Proposed optimization:
- Add externalRefToID map[string]string to MemoryStorage
- Maintain it in CreateIssue, UpdateIssue, DeleteIssue
- Achieve O(1) lookup like SQLite's index

Impact: Low (--no-db mode typically has smaller datasets)
Related: bd-1022


Use RAM disk for Windows CI tests

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

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

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


Fix daemon exiting after 5s on macOS due to PID 1 parent monitoring

GitHub issue #278 reports that the daemon exits after <=5 seconds on macOS because it incorrectly treats PID 1 (launchd) as a dead parent.

When the daemon detaches on macOS, it gets reparented to PID 1 (launchd), which is the init process. The checkParentProcessAlive function was incorrectly treating PID 1 as a sign that the parent died.

Fixed by changing the logic to treat PID 1 as a valid parent for detached daemons.


mergePriority: negative priorities not handled consistently


Add `bd merge` command wrapping 3-way merge logic

Implement CLI command to invoke beads-merge functionality.

**Interface**:
```bash
bd merge <output> <base> <left> <right>
bd merge --debug <output> <base> <left> <right>
```

**Behavior**:
- Exit code 0 on clean merge
- Exit code 1 if conflicts (write conflict markers)
- Support --debug flag for verbose output
- Match beads-merge's existing behavior

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


Make MCP compaction settings configurable (COMPACTION_THRESHOLD, PREVIEW_COUNT)

Currently COMPACTION_THRESHOLD (20) and PREVIEW_COUNT (5) are hardcoded constants at the module level. This prevents users from tuning context engineering behavior.

## Current State
```python
COMPACTION_THRESHOLD = 20  # Compact results with more than 20 issues
PREVIEW_COUNT = 5          # Show first 5 issues in preview
```

## Problems
1. No way for MCP clients to request different preview sizes
2. No way to disable compaction for debugging
3. No way to adapt to different context window sizes
4. Values are not documented in tool schema

## Recommended Solution
Add environment variables or MCP tool parameters:
- `BEADS_MCP_COMPACTION_THRESHOLD` env var (default: 20)
- `BEADS_MCP_PREVIEW_COUNT` env var (default: 5)
- Optional `compact_threshold` and `preview_size` parameters to list/ready tools

## Implementation Notes
- Keep current defaults for backward compatibility
- Read from environment at server initialization
- Document in CONTEXT_ENGINEERING.md
- Add tests verifying env var behavior


bd init --contributor should configure sync.remote=upstream for fork workflows

When running `bd init --contributor` in a fork workflow (where `upstream` remote points to the original repo), the wizard should configure beads to sync from `upstream/main` rather than `origin/main`.

**Current behavior:**
- Contributor mode detects the fork setup (upstream remote exists)
- Sets up planning repo and auto-routing
- Does NOT configure sync remote
- `bd sync` on feature branches shows "No upstream configured, using --from-main mode" and syncs from `origin/main`

**Expected behavior:**
- Contributor mode should also set `sync.remote = upstream` (or similar config)
- `bd sync` should pull beads from `upstream/main` (source of truth)

**Why this matters:**
- The fork's `origin/main` may be behind `upstream/main`
- Contributors want the latest issues from the source repo
- Code PRs go: local -> origin -> upstream, but beads should come FROM upstream

**Suggested fix:**
Add to `runContributorWizard()` after detecting fork:
```go
if isFork {
    store.SetConfig(ctx, "sync.remote", "upstream")
}
```


Document sync.require_confirmation_on_mass_delete config option


Improve test coverage for internal/lockfile (42.0% → 60%)

The lockfile package has only 42.0% test coverage. Lock file handling is critical for preventing data corruption in concurrent scenarios.

Current coverage: 42.0%
Target coverage: 60%


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

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

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


Production Readiness (Optional)

Enable multi-machine deployments with containerization and monitoring.


Implement bd stale command to find abandoned/forgotten issues

Add bd stale command to surface issues that haven't been updated recently and may need attention.

Use cases:
- In-progress issues with no recent activity (may be abandoned)
- Open issues that have been forgotten
- Issues that might be outdated or no longer relevant

Query logic should find non-closed issues where updated_at exceeds a time threshold.

Should support:
- --days N flag (default 30-90 days)
- --status filter (e.g., only in_progress)
- --json output for automation

References GitHub issue #184 where user expected this command to exist.


Sync sanitize incorrectly removes newly created issues

## Problem

The sync sanitize process incorrectly identifies newly created issues as 'deleted issues resurrected by git merge' and removes them from the local JSONL file.

## Reproduction

1. Create a new issue: bd create --title='Test issue'
2. Run bd sync
3. Observe: New issue appears in sanitize removal list
4. Issue is removed from local JSONL but preserved on beads-sync branch

## Observed Behavior

During sync, the sanitize step outputs:
```
→ Sanitized JSONL: removed 738 deleted issue(s) that were resurrected by git merge
  - bd-08ea  (newly created issue!)
  - bd-tnsq  (newly created issue!)
  ...
```

The newly created issues get removed locally but remain on beads-sync branch.

## Expected Behavior

Newly created issues should NOT be removed by sanitize. The sanitize should only remove issues that:
1. Were previously deleted (have tombstones or are in deletions manifest)
2. Are being resurrected from old git history

## Root Cause Investigation

The sanitize logic likely compares the local DB snapshot against some reference and incorrectly classifies new issues as 'resurrected deleted issues'. Possible causes:
- Snapshot protection logic not accounting for new issues
- Deletion manifest containing stale entries
- Race condition between export and sanitize

## Impact

- New issues disappear from local JSONL after sync
- Issues remain on beads-sync but cause confusion
- Multi-agent workflows affected when agents can't see new issues locally
- Requires manual intervention to recover

## Files to Investigate

- cmd/bd/sync.go - sanitize logic
- cmd/bd/snapshot_manager.go - snapshot comparison
- Deletion manifest handling

## Acceptance Criteria

- [ ] Newly created issues survive sync without being sanitized
- [ ] Only truly deleted/resurrected issues are removed
- [ ] Add test case for this scenario


Complete remaining sync branch daemon tests

4 remaining test scenarios in daemon_sync_branch_test.go need completion:

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

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

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

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

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

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


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


Add depth-based batch creation in upsertIssues

Replace single batch creation with depth-level batching (max depth 3). Create issues at depth 0, then 1, then 2, then 3. Prevents parent validation errors when importing hierarchical issues in same batch. File: internal/importer/importer.go:534-546


Add edge case tests for export metadata updates

## Current Coverage
TestExportUpdatesMetadata tests basic happy path only.

## Missing Test Cases

### 1. Multi-Repo Mode
- Export with multi-repo configuration
- Verify metadata updated for ALL JSONL files
- Critical gap (related to bd-ar2.2)

### 2. computeJSONLHash Failure
- JSONL file deleted/inaccessible after export
- Should log warning, not crash
- Verify behavior on next export

### 3. SetMetadata Failures
- Database write-protected
- Disk full
- Should log warnings, continue

### 4. os.Stat Failure
- JSONL file deleted after export, before stat
- Should handle gracefully (mtime just not updated)

### 5. Concurrent Exports
- Two daemon instances export simultaneously
- Verify metadata doesn't get corrupted
- Race condition test

### 6. Clock Skew
- System time goes backward between exports
- Verify mtime handling still works

### 7. Partial Export Failure
- Multi-repo mode: some exports succeed, some fail
- What metadata should be updated?

### 8. Large JSONL Files
- Performance test with large files (10k+ issues)
- Verify hash computation doesn't timeout

## Files
- cmd/bd/daemon_sync_test.go

## Implementation Priority
1. Multi-repo test (P1 - related to critical issue)
2. Failure handling tests (P2)
3. Performance/edge cases (P3)


Replace Windows CI full test suite with smoke tests

## Problem

Windows CI tests consistently timeout at 30 minutes despite multiple optimization attempts:
- Split into parallel jobs (cmd vs internal) - still times out
- RAM disk for temp/cache - marginal improvement (~50%)
- RAM disk for workspace - breaks due to t.Chdir() incompatibility with ImDisk

Root cause: Windows I/O is fundamentally 20x slower than Linux:
- NTFS slow for small file operations
- Windows Defender scans every file access
- Git on Windows is 5-10x slower
- Process spawning is expensive

Linux tests complete in ~1.5 min with race detector. Windows takes 30+ min without.

## Current State

- Windows jobs have continue-on-error: true (don't block PRs)
- Tests timeout before completing, providing zero signal
- RAM disk experiments added complexity without solving the problem

## Solution: Smoke Tests Only

Replace full test suite with minimal smoke tests that verify Windows compatibility in < 2 minutes.

### What to Test
1. Binary builds - go build succeeds (already separate step)
2. Binary executes - bd version works
3. Init works - bd init creates valid database
4. Basic CRUD - create, list, show, update, close
5. Path handling - Windows backslash paths work

### Proposed CI Change

Replace test-windows-cmd and test-windows-internal with single smoke test job:
- Build bd.exe
- Run: bd version
- Run: bd init --quiet --prefix smoke
- Run: bd create --title "Windows smoke test" --type task
- Run: bd list
- Run: bd show smoke-001
- Run: bd update smoke-001 --status in_progress
- Run: bd close smoke-001

### Benefits
- Completes in < 2 minutes vs 30+ minute timeouts
- Provides actual pass/fail signal
- Verifies Windows binary works
- Removes RAM disk complexity
- Single job instead of two parallel jobs

### Files to Change
- .github/workflows/ci.yml - Replace test-windows-cmd and test-windows-internal jobs

### Migration Steps
1. Remove test-windows-cmd job
2. Remove test-windows-internal job
3. Add single test-windows job with smoke tests
4. Remove continue-on-error (tests should now pass reliably)
5. Clean up RAM disk remnants from previous attempts

### Context
- Parent issue: bd-5we (Use RAM disk for Windows CI tests)
- RAM disk approach proved insufficient
- This is the pragmatic solution after extensive experimentation


Add --yes flag to bd doctor --fix for non-interactive mode

## Feature Request

Add a `--yes` or `-y` flag to `bd doctor --fix` that automatically confirms all prompts, enabling non-interactive usage in scripts and CI/CD pipelines.

## Current Behavior
`bd doctor --fix` prompts for confirmation before applying fixes, which blocks automated workflows.

## Desired Behavior
`bd doctor --fix --yes` should apply all fixes without prompting.

## Use Cases
- CI/CD pipelines that need to auto-fix issues
- Scripts that automate repository setup
- Pre-commit hooks that want to silently fix issues


bd resolve-conflicts - Git merge conflict resolver

Automatically resolve JSONL merge conflicts.

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

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

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


Test exclusive file reservations with two agents

Simulate two agents racing to claim the same issue and verify that exclusive reservations prevent collision.

Acceptance Criteria:
- Agent A reserves bd-123 → succeeds
- Agent B tries to reserve bd-123 → fails with clear error message
- Agent B can see who has the reservation
- Reservation expires after TTL
- Agent B can claim after expiration

Successfully tested file reservations:
- Agent BrownBear reserved bd-123 → granted
- Agent ChartreuseHill tried same → conflicts returned
- System correctly prevents collision


Test P1 priority


Clarify exclusive lock protocol compatibility with multi-repo

The contributor-workflow-analysis.md proposes per-repo file locking (Decision #7) using flock on JSONL files. However, VC (a downstream library consumer) uses an exclusive lock protocol (vc-195, requires Beads v0.17.3+) that allows bd daemon and VC executor to coexist.

Need to clarify:
- Does the proposed per-repo file locking work with VC's existing exclusive lock protocol?
- Do library consumers like VC need to adapt their locking logic?
- Can multiple repos be locked atomically for cross-repo operations?

Context: contributor-workflow-analysis.md lines 662-681


Daemon crashes silently on RPC query after startup

The daemon fails to handle 'show' RPC commands when:
1) JSONL is newer than database (needs import)
2) git pull fails due to uncommitted changes

Symptoms:
- Daemon appears to run (ps shows process)
- 'bd list' and other commands work fine
- 'bd show <id>' returns "failed to read response: EOF"
- No panic or error logged in daemon.log

Root cause likely: auto-import deadlock or state corruption when import is blocked by git conflicts.

Workaround:
- Restart daemon after syncing git state (commit/push changes)
- OR use --no-daemon flag for all commands

The panic recovery added in server_lifecycle_conn.go:183 didn't catch any panics, confirming this isn't a panic-based crash.

Root cause found and fixed: Two bugs - (1) nil pointer check missing in handleShow causing panic, (2) double JSON encoding in show.go ID resolution. Both fixed. bd show now works with daemon.


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


cmd/bd test suite is absurdly slow - 279 tests taking 8+ minutes

# Problem

The cmd/bd test suite is painfully slow:
- **279 tests** in cmd/bd alone
- Full suite takes **8+ minutes** to run
- Even with the 16 slowest integration tests now tagged with `integration` build tag, the remaining tests still take forever

This makes the development loop unusable. We can't wait 8+ minutes every time we want to run tests.

# Root Cause Analysis

## 1. Sheer Volume
279 tests is too many for a single package. Even at 0.1s per test, that's 28 seconds minimum just for cmd/bd.

## 2. Each Test Creates Full Database + Temp Directories
Every test does heavy setup:
- Creates temp directory (`t.TempDir()` or `os.MkdirTemp`)
- Initializes SQLite database
- Sets up git repo in many cases
- Creates full storage layer

Example from the tests:
```go
func setupCLITestDB(t *testing.T) string {
    tmpDir := createTempDirWithCleanup(t)
    runBDInProcess(t, tmpDir, "init", "--prefix", "test", "--quiet")
    return tmpDir
}
```

This happens 279 times!

## 3. Tests Are Not Properly Categorized
We have three types of tests mixed together:
- **Unit tests** - should be fast, test single functions
- **Integration tests** - test full workflows, need DB/git
- **End-to-end tests** - test entire CLI commands

They're all lumped together in cmd/bd, all running every time.

# What We've Already Fixed

Added `integration` build tags to 16 obviously-slow test files:
- import_profile_test.go (performance benchmarking tests)
- export_mtime_test.go (tests with time.Sleep calls)
- cli_fast_test.go (full CLI integration tests)
- delete_test.go, import_uncommitted_test.go, sync_local_only_test.go (git integration)
- And 10 more in internal/ packages

These are now excluded from the default `go test ./...` run.

# Proposed Solutions

## Option 1: Shared Test Fixtures (Quick Win)
Create a shared test database that multiple tests can use:
```go
var testDB *sqlite.SQLiteStorage
var testDBOnce sync.Once

func getSharedTestDB(t *testing.T) storage.Storage {
    testDBOnce.Do(func() {
        // Create one DB for all tests
    })
    return testDB
}
```

**Pros**: Easy to implement, immediate speedup
**Cons**: Tests become less isolated, harder to debug failures

## Option 2: Table-Driven Tests (Medium Win)
Collapse similar tests into table-driven tests:
```go
func TestCreate(t *testing.T) {
    tests := []struct{
        name string
        args []string
        want string
    }{
        {"basic issue", []string{"create", "Test"}, "created"},
        {"with description", []string{"create", "Test", "-d", "desc"}, "created"},
        // ... 50 more cases
    }

    db := setupOnce(t) // Setup once, not 50 times
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // test using shared db
        })
    }
}
```

**Pros**: Dramatically reduces setup overhead, tests run in parallel
**Cons**: Requires refactoring, tests share more state

## Option 3: Split cmd/bd Tests Into Packages (Big Win)
Move tests into focused packages:
- `cmd/bd/internal/clitests` - CLI integration tests (mark with integration tag)
- `cmd/bd/internal/unittests` - Fast unit tests
- Keep only essential tests in cmd/bd

**Pros**: Clean separation, easy to run just fast tests
**Cons**: Requires significant refactoring

## Option 4: Parallel Execution (Quick Win)
Add `t.Parallel()` to independent tests:
```go
func TestSomething(t *testing.T) {
    t.Parallel() // Run this test concurrently with others
    // ...
}
```

**Pros**: Easy to add, can cut time in half on multi-core machines
**Cons**: Doesn't reduce actual test work, just parallelizes it

## Option 5: In-Memory Databases (Medium Win)
Use `:memory:` SQLite databases instead of file-based:
```go
store, err := sqlite.New(ctx, ":memory:")
```

**Pros**: Faster than disk I/O, easier cleanup
**Cons**: Some tests need actual file-based DBs (export/import tests)

# Recommended Approach

**Short-term (this week)**:
1. Add `t.Parallel()` to all independent tests in cmd/bd
2. Use `:memory:` databases where possible
3. Create table-driven tests for similar test cases

**Medium-term (next sprint)**:
4. Split cmd/bd tests into focused packages
5. Mark more integration tests appropriately

**Long-term (backlog)**:
6. Consider shared test fixtures with proper isolation

# Current Status

We've tagged 16 files with `integration` build tag, but the remaining 279 tests in cmd/bd still take 8+ minutes. This issue tracks fixing the cmd/bd test performance specifically.

# Target

Get `go test ./...` (without `-short` or `-tags=integration`) down to **under 30 seconds**.

# THE REAL ROOT CAUSE (Updated Analysis)

After examining the actual test code, the problem is clear:

## Every Test Creates Its Own Database From Scratch

Look at `create_test.go`:
```go
func TestCreate_BasicIssue(t *testing.T) {
    tmpDir := t.TempDir()                          // ← Creates temp dir
    testDB := filepath.Join(tmpDir, ".beads", "beads.db")
    s := newTestStore(t, testDB)                   // ← Opens NEW SQLite connection
                                                    // ← Runs migrations
                                                    // ← Sets config
    // ... actual test (3 lines)
}

func TestCreate_WithDescription(t *testing.T) {
    tmpDir := t.TempDir()                          // ← Creates ANOTHER temp dir
    testDB := filepath.Join(tmpDir, ".beads", "beads.db")
    s := newTestStore(t, testDB)                   // ← Opens ANOTHER SQLite connection
    // ... actual test (3 lines)
}
```

**This happens 279 times!**

## These Tests Don't Need Isolation!

Most tests are just checking:
- "Can I create an issue with a title?"
- "Can I create an issue with a description?"
- "Can I add labels?"

They don't conflict with each other. They could all share ONE database!

## The Fix: Test Suites with Shared Setup

Instead of:
```go
func TestCreate_BasicIssue(t *testing.T) {
    s := newTestStore(t, t.TempDir()+"/db")  // ← Expensive!
    // test
}

func TestCreate_WithDesc(t *testing.T) {
    s := newTestStore(t, t.TempDir()+"/db")  // ← Expensive!
    // test
}
```

Do this:
```go
func TestCreate(t *testing.T) {
    // ONE setup for all subtests
    s := newTestStore(t, t.TempDir()+"/db")

    t.Run("basic_issue", func(t *testing.T) {
        t.Parallel()  // Can run concurrently - tests don't conflict
        // test using shared `s`
    })

    t.Run("with_description", func(t *testing.T) {
        t.Parallel()
        // test using shared `s`
    })

    // ... 50 more subtests, all using same DB
}
```

**Result**: 50 tests → 1 database setup instead of 50!

## Why This Works

SQLite is fine with concurrent reads and isolated transactions. These tests:
- ✅ Create different issues (no ID conflicts)
- ✅ Just read back what they created
- ✅ Don't depend on database state from other tests

They SHOULD share a database!

## Real Numbers

Current:
- 279 tests × (create dir + init SQLite + migrations) = **8 minutes**

After fix:
- 10 test suites × (create dir + init SQLite + migrations) = **30 seconds**
- 279 subtests running in parallel using those 10 DBs = **5 seconds**

**Total: ~35 seconds instead of 8 minutes!**

## Implementation Plan

1. **Group related tests** into suites (Create, List, Update, Delete, etc.)
2. **One setup per suite** instead of per test
3. **Use t.Run() for subtests** with t.Parallel()
4. **Keep tests that actually need isolation** separate (export/import tests, git operations)

This is way better than shuffling tests into folders!

## Progress Update (2025-11-21)

✅ **Completed**:
- Audited all 280 tests, created TEST_SUITE_AUDIT.md ([deleted:bd-c49])
- Refactored create_test.go to shared DB pattern ([deleted:bd-y6d])
- Proven the pattern works: 11 tests now run in 0.04s with 1 DB instead of 11

❌ **Current Reality**:
- Overall test suite: Still 8+ minutes (no meaningful change)
- Only 1 of 76 test files refactored
- Saved ~10 DB initializations out of 280

## Acceptance Criteria (REALISTIC)

This task is NOT complete until:
- [ ] All P1 files refactored (create ✅, dep, stale, comments, list, ready)
- [ ] Test suite runs in < 2 minutes
- [ ] Measured and verified actual speedup

## Next Steps

1. Refactor remaining 5 P1 files: dep_test.go, stale_test.go, comments_test.go, list_test.go, ready_test.go
2. Measure actual time improvement after each file
3. Continue with P2 files if needed to hit <2min target


Fix hasJSONLChanged to support per-repo metadata keys

## Problem
After bd-ar2.2, we store metadata with per-repo keys like `last_import_hash:<path>`, but `hasJSONLChanged` and `validatePreExport` still only check global keys.

## Impact
- Multi-repo mode won't benefit from bd-ymj fix
- validatePreExport will fail incorrectly or pass incorrectly
- Metadata updates are written but never read

## Location
- cmd/bd/integrity.go:97 (hasJSONLChanged)
- cmd/bd/integrity.go:138 (validatePreExport)

## Solution
Update hasJSONLChanged to accept optional keySuffix parameter:
```go
func hasJSONLChanged(ctx context.Context, store storage.Storage, jsonlPath string, keySuffix string) bool {
    // Build metadata keys with optional suffix
    hashKey := "last_import_hash"
    mtimeKey := "last_import_mtime"
    if keySuffix != "" {
        hashKey += ":" + keySuffix
        mtimeKey += ":" + keySuffix
    }
    // ... rest of function
}
```

Update all callers to pass correct keySuffix.


Channel isolation test - beads

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


Priority 0 in merge may incorrectly win over set priorities

In mergePriority(), priority 0 (which may mean 'unset' due to Go's zero value) beats any explicitly set priority like P1, P2, etc. Should probably treat 0 as 'no priority set' and not let it win conflicts.


Fix Nix flake build test failures

Nix build is failing during test phase with same test errors as Windows.

**Error:**
```
error: Cannot build '/nix/store/rgyi1j44dm6ylrzlg2h3z97axmfq9hzr-beads-0.9.9.drv'.
Reason: builder failed with exit code 1.
FAIL github.com/steveyegge/beads/cmd/bd 16.141s
```

This may be related to test environment setup or the same issues affecting Windows tests.


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

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

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

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

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

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

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

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

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

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


Auth tokens expire too quickly

## Summary

[Brief description of the bug]

## Steps to Reproduce

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

## Expected Behavior

[What should happen]

## Actual Behavior

[What actually happens]

## Environment

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

## Additional Context

[Screenshots, logs, or other relevant information]


CONFIG.md docs for sync.require_confirmation_on_mass_delete missing threshold detail

The docs say 'if >50% of issues vanish' but the actual code (worktree.go:380) also requires '>5 issues existed before'. The docs should mention both conditions for completeness.


PushSyncBranch may fail if worktree was cleaned up


Extract computeJSONLHash helper to eliminate code duplication

SHA256 hash computation is duplicated in 3 places:
- cmd/bd/integrity.go:50-52
- cmd/bd/sync.go:611-613
- cmd/bd/import.go:318-319

Extract to shared helper function computeJSONLHash(jsonlPath string) (string, error) that includes proper #nosec comment and error handling.


Add richer query capabilities to bd list

Current bd list filters are limited to basic field matching (status, priority, type, assignee, label). This forces users to resort to piping through jq for common queries.

Missing query capabilities:
- Pattern matching: --title-contains, --desc-contains
- Date ranges: --created-after, --updated-before, --closed-after
- Empty/null checks: --empty-description, --no-assignee, --no-labels
- Numeric ranges: --priority-min, --priority-max
- Complex boolean logic: --and, --or operators
- Full-text search: --search across all text fields
- Negation: --not-status, --exclude-label

Example use cases:
- Find issues with empty descriptions
- Find stale issues not updated in 30 days
- Find high-priority bugs with no assignee
- Search for keyword across title/description/notes

Implementation approach:
- Add query builder pattern to storage layer
- Support --query DSL for complex queries
- Keep simple flags for common cases
- Add --json output for programmatic use

## Progress Update

**Completed:**
- ✅ Extended IssueFilter struct with new fields (pattern matching, date ranges, empty/null checks, priority ranges)
- ✅ Updated SQLite SearchIssues implementation
- ✅ Added CLI flags to list.go
- ✅ Added parseTimeFlag helper
- ✅ Comprehensive tests added - all passing

**Remaining:**
- ⚠ RPC layer needs updating (internal/rpc/protocol.go ListArgs)
- ⚠ Daemon handler needs to forward new filters
- ⚠ End-to-end testing with daemon mode
- 📝 Documentation updates

**Files Modified:**
- internal/types/types.go
- internal/storage/sqlite/sqlite.go
- cmd/bd/list.go
- cmd/bd/list_test.go

**Next Steps:**
1. Update RPC protocol
2. Update daemon handler
3. Test with daemon mode
4. Update docs


Add index usage verification test for external_ref lookups

Currently we test that idx_issues_external_ref index exists, but we don't verify that it's actually being used by the query planner.

Proposed solution:
- Add test using EXPLAIN QUERY PLAN
- Verify that 'SEARCH TABLE issues USING INDEX idx_issues_external_ref' appears in plan
- Ensures O(1) lookup performance is maintained

Related: bd-1022
Files: internal/storage/sqlite/external_ref_test.go:260


bd-hv01: Brittle string comparison breaks with JSON field reordering

## Problem
deletion_tracking.go:125 uses string comparison to detect unchanged issues:

```go
if leftLine, existsInLeft := leftIndex[id]; existsInLeft && leftLine == baseLine {
    deletions = append(deletions, id)
}
```

This breaks if:
- JSON field order changes (legal in JSON)
- Timestamps updated by import/export
- Whitespace/formatting changes
- Floating point precision varies

## Example Failure
```json
// baseLine
{"id":"bd-1","priority":1,"status":"open"}
// leftLine (same data, different order)
{"id":"bd-1","status":"open","priority":1}
```
These are semantically identical but string comparison fails.

## Fix
Parse and compare JSON semantically:
```go
func jsonEquals(a, b string) bool {
    var objA, objB map[string]interface{}
    json.Unmarshal([]byte(a), &objA)
    json.Unmarshal([]byte(b), &objB)
    return reflect.DeepEqual(objA, objB)
}
```

## Files Affected
- cmd/bd/deletion_tracking.go:125
- cmd/bd/deletion_tracking.go:134-170 (buildIDToLineMap)


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


bd sync: use worktree for sync.branch commits (non-daemon mode)

## Summary

When `sync.branch` is configured (e.g., `beads-sync`), `bd sync` should commit beads changes to that branch via git worktree, even when the user's working directory is on a different branch (e.g., `main`).

Currently:
- `bd sync` always commits to the current branch
- Daemon with `--auto-commit` uses worktrees to commit to sync branch
- Gap: `bd sync` ignores sync.branch config

## Goal

Workers stay on `main` (or feature branches) while beads metadata automatically flows to `beads-sync` branch. No daemon required.

## Design Considerations

1. **Worktree lifecycle**: Create on first use, reuse thereafter
2. **Daemon interaction**: Ensure daemon and bd sync don't conflict
3. **MCP server**: Uses daemon or direct mode - needs same behavior
4. **Pull semantics**: Pull from sync branch, not current branch
5. **Push semantics**: Push to sync branch remote
6. **Error handling**: Worktree corruption, missing branch, etc.

## Affected Components

- `cmd/bd/sync.go` - Main changes
- `cmd/bd/daemon_sync_branch.go` - Reuse existing functions
- `internal/git/worktree.go` - Already implemented
- MCP server (if it bypasses daemon)

## Edge Cases

- Fresh clone (no sync branch exists yet)
- Worktree exists but is corrupted
- Concurrent bd sync from multiple processes
- sync.branch configured but remote doesn't have that branch
- User switches sync.branch config mid-session


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


Modify CreateIssue to support parent resurrection

Update internal/storage/sqlite/sqlite.go:182-196 to call TryResurrectParent before failing on missing parent. Coordinate with EnsureIDs changes for consistent behavior. Handle edge case where parent never existed in JSONL (fail gracefully).


Split internal/rpc/server.go into focused modules

The file `internal/rpc/server.go` is 2,273 lines with 50+ methods, making it difficult to navigate and prone to merge conflicts. Split into 8 focused files with clear responsibilities.

Current structure: Single 2,273-line file with:
- Connection handling
- Request routing
- All 40+ RPC method implementations
- Storage caching
- Health checks & metrics
- Cleanup loops

Target structure:
```
internal/rpc/
├── server.go          # Core server, connection handling (~300 lines)
├── methods_issue.go   # Issue operations (~400 lines)
├── methods_deps.go    # Dependency operations (~200 lines)
├── methods_labels.go  # Label operations (~150 lines)
├── methods_ready.go   # Ready work queries (~150 lines)
├── methods_compact.go # Compaction operations (~200 lines)
├── methods_comments.go # Comment operations (~150 lines)
├── storage_cache.go   # Storage caching logic (~300 lines)
└── health.go          # Health & metrics (~200 lines)
```

Migration strategy:
1. Create new files with appropriate methods
2. Keep `server.go` as main file with core server logic
3. Test incrementally after each file split
4. Final verification with full test suite


string(rune()) pattern in other test files still only works for single digits

The bd-c5m fix only addressed worktree_divergence_test.go, but several other test files have the same issue:

- cmd/bd/stale_test.go:218, 234
- cmd/bd/export_integrity_integration_test.go:45-47, 279-280
- internal/storage/sqlite/comments_test.go:146, 165
- internal/storage/sqlite/blocked_cache_test.go:292
- internal/storage/sqlite/compact_bench_test.go:117
- internal/storage/sqlite/cycle_detection_test.go:418

These use string(rune('0'+i)) which produces wrong characters when i >= 10.
Should use strconv.Itoa(i) instead.


bd sync corrupts issues.jsonl in multi-clone environments

During parallel swarm operations on 2025-11-29, bd sync corrupted the issues database. Commit 93718056 purged all 80 issues from issues.jsonl.

Reproduction:
1. Multiple clones of same repo (polecat swarm)
2. Each clone runs bd sync in parallel
3. One sync overwrites others, resulting in data loss

Even in single-clone scenarios (~/src/beads), running bd sync after git pull seems to nuke the JSONL.

Workaround: Use bd import -i .beads/issues.jsonl --no-git-history instead of bd sync.

Root cause investigation needed. Options:
1. Pessimistic locking (flock)
2. Single point of merge (polecats on ephemeral branches)
3. Beads-level locking (bd sync --lock)


Rapid 4


Vendor beads-merge Go code into internal/merge/

Copy beads-merge source code from @neongreen's repo into bd codebase.

**Tasks**:
- Create `internal/merge/` package
- Copy merge algorithm code
- Add attribution header to all files
- Update imports to use bd's internal types
- Add LICENSE/ATTRIBUTION file crediting @neongreen
- Keep original algorithm intact

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


Extract batch operations to batch_ops.go

Move validateBatchIssues, generateBatchIDs, bulkInsertIssues, bulkRecordEvents, bulkMarkDirty, CreateIssues to batch_ops.go


bd sync --squash: batch multiple syncs into single commit

For solo developers who don't need real-time multi-agent coordination, add a --squash option to bd sync that accumulates changes and commits them in a single commit rather than one commit per sync.

This addresses the git history pollution concern (many 'bd sync: timestamp' commits) while preserving the default behavior needed for orchestration.

**Proposed behavior:**
- `bd sync --squash` accumulates pending exports
- Only commits when explicitly requested or on session end
- Default behavior unchanged (immediate commits for orchestration)

**Use case:** Solo developers who want cleaner git history but don't need real-time coordination between agents.

**Related:** PR #411 (docs: reduce bd sync commit pollution)
**See also:** Multi-repo support as alternative solution (docs/MULTI_REPO_AGENTS.md)


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.


Multi-repo contributor workflow support

Implement separate repository support for OSS contributors to prevent PR pollution while maintaining git ledger and multi-clone sync. Based on contributor-workflow-analysis.md Solution #4.


Reduce sync rounds in multiclone tests

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

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

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

File: beads_hash_multiclone_test.go:70, :132


Fix TestListCommand duplicate dependency constraint violation


Add external_ref change tracking and auditing

Currently we don't track when external_ref is added, removed, or changed. This would be useful for debugging and auditing.

Proposed features:
- Log event when external_ref changes
- Track in events table with old/new values
- Add query to find issues where external_ref changed
- Add metrics: issues with external_ref vs without

Use cases:
- Debugging import issues
- Understanding which issues are externally managed
- Auditing external system linkage

Related: bd-1022


bd find-duplicates - AI-powered duplicate detection

Find semantically duplicate issues.

Approaches:
1. Mechanical: Exact title/description matching
2. Embeddings: Cosine similarity (cheap, scalable)
3. AI: LLM-based semantic comparison (expensive, accurate)

Uses embeddings by default for >100 issues.

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


Final validation test


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


Implement `bd setup claude` command for Claude Code integration

Create a `bd setup claude` command that installs Claude Code integration files (slash commands and hooks). This is idempotent and safe to run multiple times.


Clarify .beads/.gitattributes handling in Protected Branches docs

Protected Branches docs quick start leaves untracked `.beads` directory and `.gitattributes`.
Question: Are these changes meant to be checked into the protected branch?
Need to clarify if these should be ignored or committed, or if the instructions are missing a step.


Improve test coverage for internal/rpc (47.5% → 60%)

The RPC package has only 47.5% test coverage. RPC is the communication layer for daemon operations.

Current coverage: 47.5%
Target coverage: 60%


getLocalSyncBranch silently ignores YAML parse errors

In autoimport.go:170-172, YAML parsing errors are silently ignored. If a user has malformed YAML in config.yaml, sync-branch will just silently be empty with no feedback.

Recommendation: Add debug logging since this function is only called during auto-import, and debugging silent failures is painful.

Add: debug.Logf("Warning: failed to parse config.yaml: %v", err)


Add internal/ai package for AI-assisted repairs

Add AI integration package to support AI-powered repair commands.

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

Features:
- Conflict resolution analysis
- Duplicate detection via embeddings
- Configuration via env vars (BEADS_AI_PROVIDER, BEADS_AI_API_KEY, etc.)

See repair_commands.md lines 357-425 for design.


Integration Layer Implementation

Build the adapter layer that makes Agent Mail optional and non-intrusive.

Progress: bd-m9th (Python adapter library) completed with full test coverage. Next: bd-fzbg (update python-agent example with Agent Mail integration).


Add transaction helper to replace manual COMMIT/ROLLBACK

Create tx.go with withTx helper that handles transaction lifecycle. Replace manual transaction blocks in create/insert/update paths.

Refactoring complete:
- Created withTx() helper in util.go
- Added ExecInTransaction() as deprecated wrapper for backward compatibility
- Refactored all manual transaction blocks to use withTx():
  - events.go: AddComment
  - dirty.go: MarkIssuesDirty, ClearDirtyIssuesByID
  - labels.go: executeLabelOperation
  - dependencies.go: AddDependency, RemoveDependency
  - compact.go: ApplyCompaction
- All tests pass successfully


Implement "bd daemons logs" subcommand

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


Exclude tombstones from SearchIssues by default

SearchIssues() returns tombstones if they match search criteria. Per design, users generally shouldn't see deleted issues in search results.

Location: internal/storage/sqlite/queries.go:1155-1311 and transaction.go:887-1044

Options:
1. Add default tombstone exclusion unless explicitly filtering for them
2. Add IncludeTombstones field to IssueFilter

Recommendation: Add 'status != tombstone' to WHERE clause unless filter.Status == StatusTombstone