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


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.


Remove misleading placeholder 'bd merge' command from duplicates output

**Problem:**
The `bd duplicates` command suggests running a command that doesn't exist:
```
bd merge <source-ids> --into <target-id>
```

This is confusing because:
1. `bd merge` is actually a git 3-way JSONL merge driver (takes 4 file paths)
2. The suggested syntax for merging duplicate issues is not implemented
3. Line 75 in duplicates.go even has: `// TODO: performMerge implementation pending`

**Current behavior:**
- Users see suggested command that doesn't work
- No indication that feature is unimplemented
- Related to issue #349 item #2

**Proposed fix:**
Replace line 77 in cmd/bd/duplicates.go with either:

Option A (conservative):
```go
cmd := fmt.Sprintf("# TODO: Merge %s into %s (merge command not yet implemented)",
    strings.Join(sources, " "), target.ID)
```

Option B (actionable):
```go
cmd := fmt.Sprintf("# Duplicate found: %s\n# Manual merge: Close duplicates with 'bd close %s' and link to %s as 'related'",
    strings.Join(sources, " "), strings.Join(sources, " "), target.ID)
```

**Files to modify:**
- cmd/bd/duplicates.go (line ~77)


Set up WASM build pipeline

Configure Go→WASM compilation pipeline. Child of epic bd-44d0.

## Tasks
- [ ] Create build-wasm.sh script (GOOS=js GOARCH=wasm)
- [ ] Test basic WASM module loading in Node.js
- [ ] Set up wasm_exec.js wrapper
- [ ] Add WASM build to CI/CD
- [ ] Document build process

## Validation
- bd.wasm compiles successfully
- Can load in Node.js without errors
- Bundle size <10MB


Extract daemon lifecycle functions to daemon_lifecycle.go


Short tests taking 13+ minutes (performance regression)


Fix autoimport tests for content-hash collision scoring

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

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

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

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

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

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

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

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


Unit tests for Debouncer

Test debouncer batches multiple triggers into single action. Test timer reset on subsequent triggers. Test cancel during wait. Test thread safety.


Add unit tests for hasJSONLChanged() function

The hasJSONLChanged() function has no unit tests. Should test:
- Normal case: hash matches
- Normal case: hash differs
- Edge case: empty file
- Edge case: missing metadata (first run)
- Edge case: corrupted metadata
- Edge case: file read errors
- Edge case: invalid hash format in metadata

Also add performance benchmarks for large JSONL files.


## Problem

bd cleanup deletes closed issues (converting them to tombstones) but does NOT prune expired tombstones. Users expect 'cleanup' to do comprehensive cleanup.

## Current Behavior

1. bd cleanup --force converts closed issues to tombstones
2. Expired tombstones (>30 days) remain in issues.jsonl
3. User must separately run bd compact to prune tombstones
4. bd doctor warns about expired tombstones: Run bd compact to prune

## Expected Behavior

bd cleanup should also prune expired tombstones from issues.jsonl.

## Impact

With v0.30.0 making tombstones the default migration path, this UX gap becomes more visible. Users cleaning up their database should not need to know about a separate bd compact command.

## Proposed Solution

Call pruneExpiredTombstones() at the end of the cleanup command (same function used by compact).

## Files to Modify

- cmd/bd/cleanup.go - Add call to pruneExpiredTombstones after deleteBatch

## Acceptance Criteria

- bd cleanup --force prunes expired tombstones after deleting closed issues
- bd cleanup --dry-run shows what tombstones would be pruned
- JSON output includes tombstone prune results


Optimize --sandbox mode: skip FlushManager

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

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

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

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

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

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

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

Part of bd-olc1


Document -short flag in testing guide

Add documentation about the -short flag and how it's used to skip slow tests. Should explain that developers can run 'go test -short ./...' for fast iteration and 'go test ./...' for full coverage.


Import fails on same-content-different-ID instead of treating as update

## Problem

The importer still has rename detection (importer.go:482-500) that triggers when same content hash has different IDs. With hash IDs, this shouldn't happen, but when it does (test data, bugs, legacy data), the import fails:

```
failed to handle rename bd-ce75 -> bd-5a90: rename collision handling removed - should not occur with hash IDs
```

## Current Behavior

1. Importer finds same content hash with different IDs
2. Calls handleRename() (line 490)
3. handleRename() errors out (line 294): "rename collision handling removed"
4. Import fails

## Expected Behavior

Same content hash + different IDs should be treated as an **update**, not a rename:
- Keep existing ID (already in database)
- Update fields if incoming has newer timestamp
- Discard incoming ID (it's wrong - hash should have generated same ID)

## Impact

- Import fails on legitimate edge cases (test data, data corruption)
- Cryptic error message
- Blocks sync operations

## Fix

In handleRename() or import loop, instead of erroring:
```go
// Same content, different ID - treat as update
if incoming.UpdatedAt.After(existing.UpdatedAt) {
    existing.Status = incoming.Status
    // ... copy other fields
    s.UpdateIssue(ctx, existing)
}
result.Updated++
```

## Files
- internal/importer/importer.go:271-294 (handleRename)
- internal/importer/importer.go:482-500 (rename detection)

## Repro
Import JSONL with bd-ce75 and bd-5a90 (both "Test parent issue" but different content hashes).


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


Test coverage improvement initiative (47.8% → 65%)

Umbrella epic for improving overall test coverage from 47.8% to 65%+.

## Summary

| Package | Current | Target | Issue |
|---------|---------|--------|-------|
| internal/compact | 18.2% | 70% | bd-thgk |
| cmd/bd/doctor/fix | 23.9% | 50% | bd-fx7v |
| cmd/bd | 26.2% | 50% | bd-llfl |
| internal/daemon | 27.3% | 60% | bd-n386 |
| cmd/bd/setup | 28.4% | 50% | bd-sh4c |
| internal/syncbranch | 33.0% | 70% | bd-io8c |
| internal/export | 37.1% | 60% | bd-6sm6 |
| internal/lockfile | 42.0% | 60% | bd-9w3s |
| internal/rpc | 47.5% | 60% | bd-m8ro |
| internal/beads | 48.1% | 70% | bd-tvu3 |
| internal/storage | N/A | basic | bd-a15d |

## Packages with good coverage (no action needed)
- internal/types: 91.0%
- internal/utils: 88.9%
- internal/deletions: 84.3%
- internal/config: 84.2%
- internal/autoimport: 83.5%
- internal/validation: 80.8%
- internal/merge: 78.9%
- internal/routing: 74.3%
- internal/storage/sqlite: 73.1%
- internal/importer: 70.2%

Overall target: 65%+ (from current 47.8%)


Add fallback to polling on watcher failure

Detect fsnotify.NewWatcher() errors and log warning. Auto-switch to polling mode with 5s ticker. Add BEADS_WATCHER_FALLBACK env var to control behavior.


Add merge helper commands (bd sync --merge)

Add commands to merge beads branch back to main.

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

Estimated effort: 2-3 days


Add circular dependency detection to bd doctor

Added cycle detection as Check #10 in bd doctor command. Uses same recursive CTE query as DetectCycles() to find circular dependencies. Reports error status with count and fix suggestion if cycles found.


Add guidance for self-hosting projects

The contributor-workflow-analysis.md is optimized for OSS contributors making PRs to upstream projects. However, it doesn't address projects like VC that use beads for their own development (self-hosting).

Self-hosting projects differ from OSS contributors:
- No upstream/downstream distinction (they ARE the project)
- May run automated executors (not just humans)
- In bootstrap/early phase (stability matters)
- Single team/owner (not multiple contributors with permissions)

Guidance needed on:
- When self-hosting projects should stay single-repo (default, recommended)
- When they should adopt multi-repo (team planning, multi-phase dev)
- How automated executors should handle multi-repo (if at all)
- Special considerations for projects in bootstrap phase

Examples of self-hosting projects: VC (building itself with beads), internal tools, pet projects


Fix TestListCommand duplicate dependency constraint violation


Daemon crash recovery: panic handler + socket cleanup

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

This ensures better diagnostics and cleaner state after crashes.


Refactor cmd/bd/daemon.go for testability and maintainability


wish: list & ready show issues as hierarchy tree

`bd ready` and `bd list` just show a flat list, and it's up to the reader to parse which ones are dependent or sub-issues of others. It would be much easier to understand if they were shown in a tree format


Implement git worktree management

Create git worktree lifecycle management for separate beads branch.

Tasks:
- Create internal/git/worktree.go
- Implement CreateBeadsWorktree(branch, path)
- Implement RemoveBeadsWorktree(path)
- Implement CheckWorktreeHealth(path)
- Configure sparse checkout (only .beads/)
- Implement SyncJSONLToWorktree()
- Handle worktree errors gracefully
- Auto-cleanup on config change

Estimated effort: 3-4 days


Expand checkHooksQuick to verify all hook versions

Currently checkHooksQuick only checks post-merge hook version. Should also check pre-commit, pre-push, and post-checkout for completeness. Keep it lightweight but catch more outdated hooks.


Fix variable shadowing in GetNextChildID resurrection code

## Problem
Minor variable shadowing in hash_ids.go:

```go
// Line 33: err declared here
err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM issues WHERE id = ?`, parentID).Scan(&count)

// Line 38: err reused/shadowed here
resurrected, err := s.TryResurrectParent(ctx, parentID)
```

While Go allows this, it can be confusing and some linters flag it.

## Solution
Use different variable name:

```go
resurrected, resurrectErr := s.TryResurrectParent(ctx, parentID)
if resurrectErr != nil {
    return "", fmt.Errorf("failed to resurrect parent %s: %w", parentID, resurrectErr)
}
```

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

## Priority
Low - code works correctly, just a style improvement


Improve test coverage for internal/beads (48.1% → 70%)

The beads package is a core package with 48.1% coverage. As the primary package for issue management, it should have at least 70% coverage.

Current coverage: 48.1%
Target coverage: 70%


Document CLI + hooks as recommended approach over MCP

Update documentation to position CLI + bd prime hooks as the primary recommended approach over MCP server, explaining why minimizing context matters even with large context windows (compute cost, energy, environment, latency).


Vendor and integrate beads-merge tool

Incorporate @neongreen's beads-merge 3-way merge tool into bd to solve:
- Multi-workspace deletion sync (bd-hv01)
- Git merge conflicts in JSONL
- Field-level intelligent merging

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

**Integration approach**: Vendor the Go code with attribution, pending @neongreen's approval (GitHub issue #240)

**Benefits**:
- Prevents deletion resurrection bug
- Smart dependency merging (union + dedup)
- Timestamp handling (max wins)
- Detects deleted-vs-modified conflicts
- Works as git merge driver

**Acceptance criteria**:
- beads-merge code vendored into bd codebase
- Available as `bd merge` command
- Git merge driver setup during `bd init`
- Tests verify 3-way merge logic
- Documentation updated
- @neongreen credited


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


Refactor create_test.go to use shared DB setup

Convert TestCreate_* functions to use test suites with shared database setup.

Example transformation:
- Before: 10 separate tests, each with newTestStore()
- After: 1 TestCreate() with 10 t.Run() subtests sharing one DB

Estimated speedup: 10x faster (1 DB setup instead of 10)


Extract duplicated metadata update code in daemon_sync.go

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

This violates DRY principle and makes maintenance harder.

## Solution
Extract to helper function:

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

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

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

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

## Files
- cmd/bd/daemon_sync.go

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


Backend task


Add ON DELETE CASCADE to child_counters schema

Update schema.go child_counters table foreign key with ON DELETE CASCADE. When parent deleted, child counter should also be deleted. If parent is resurrected, counter gets recreated from scratch. Add migration for existing databases.


Batch test 3


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.


Widespread double JSON encoding bug in daemon mode RPC calls

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

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

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

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

All commands now tested and working correctly with daemon mode.


Consolidate ID generation and validation into ids.go

Extract ID logic into ids.go: ValidateIssueIDPrefix, GenerateIssueID, EnsureIDs. Move GetAdaptiveIDLength here. Unify single and bulk ID generation flows.


Test issue 1


Test Epic 2

## 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


Refactor sqlite.go (2298 lines)

Break down internal/storage/sqlite/sqlite.go into smaller, more focused modules. The file is currently 2298 lines and should be split into logical components.


isLikelyHash rejects 3-char all-letter base36 hashes

ExtractIssuePrefix fails for multi-hyphen prefixes when the 3-char hash suffix has no digits.

Example: `xa-adt-bat` returns prefix `xa` instead of `xa-adt`.

Root cause: `isLikelyHash()` requires at least one digit to distinguish hashes from English words, but base36 hashes can be all-letters by chance.

Affected: 20 issues in xa-adapt with suffixes like bat, dev, fbi, oil, etc.


Add tests for getMultiRepoJSONLPaths() edge cases

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

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

Test cases needed:

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

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

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

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

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

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


Add concurrent import race condition tests

Currently no tests verify behavior when multiple clones import simultaneously with external_ref matching.

Scenarios to test:
1. Two clones import same external_ref update at same time
2. Clone A imports while Clone B updates same issue
3. Verify transaction isolation prevents corruption
4. Document expected behavior (last-write-wins vs timestamp-based)

Related: bd-1022
Files: internal/importer/external_ref_test.go


Baseline quality gate failure: lint

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

This blocks the executor from claiming work until fixed.

Error: golangci-lint failed: exit status 1

Output:
```
cmd/bd/search.go:39:12: Error return value of `cmd.Help` is not checked (errcheck)
			cmd.Help()
			        ^
cmd/bd/clean.go:118:15: G304: Potential file inclusion via variable (gosec)
	file, err := os.Open(gitignorePath)
	             ^
cmd/bd/doctor/gitignore.go:98:12: G306: Expect WriteFile permissions to be 0600 or less (gosec)
	if err := os.WriteFile(gitignorePath, []byte(GitignoreTemplate), 0644); err != nil {
	          ^
cmd/bd/merge.go:121:16: G204: Subprocess launched with variable (gosec)
			gitRmCmd := exec.Command("git", "rm", "-f", "--quiet", fullPath)
			            ^
cmd/bd/doctor.go:167:20: `cancelled` is a misspelling of `canceled` (misspell)
		fmt.Println("Fix cancelled.")
		                 ^
cmd/bd/flush_manager.go:139:42: `cancelling` is a misspelling of `canceling` (misspell)
		// Send shutdown request FIRST (before cancelling context)
		                                       ^
cmd/bd/flush_manager.go:261:15: `cancelled` is a misspelling of `canceled` (misspell)
			// Context cancelled (shouldn't normally happen)
			           ^
cmd/bd/flush_manager.go:269:55: (*FlushManager).performFlush - result 0 (error) is always nil (unparam)
func (fm *FlushManager) performFlush(fullExport bool) error {
                                                      ^
8 issues:
* errcheck: 1
* gosec: 3
* misspell: 3
* unparam: 1

```


Issue to close


Command Set Standardization & Flag Consistency

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

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

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

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


Log errors from timer-triggered flushes instead of discarding


Document which files to commit after bd init --branch

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

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

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

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

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


Add 'bd comment' as alias for 'bd comments add'

The command 'bd comments add' is verbose and unintuitive. Add 'bd comment' as a shorter alias that works the same way.

## Rationale
- More natural: 'bd comment <issue-id> <text>' reads better than 'bd comments add <issue-id> <text>'
- Matches user expectations: users naturally try 'bd comment' first
- Follows convention: other commands like 'bd create', 'bd show', 'bd close' are verbs

## Implementation
Could be implemented as:
1. A new command that wraps bd comments add
2. An alias registered in cobra
3. Keep 'bd comments add' for backwards compatibility

## Examples
```bash
bd comment bd-1234 'This is a comment'
bd comment bd-1234 'Multi-line comment' --body 'Additional details here'
```


Add simple cleanup command to delete closed issues

Users want a simple command to delete all closed issues without requiring Anthropic API key (unlike compact). Requested in GH #243.


Week 1 task


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 status "Recent Activity" is misleading - should use git history

## Problem

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

## Issues

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

## Proposed Fix

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

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


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

Comprehensive test to verify hash IDs eliminate collision problems.

## Test: TestHashIDsNClones

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### Edge Case Tests

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

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

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

### Performance Test

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

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

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

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

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

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

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

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


CLI tests (cli_fast_test.go) are slow and should be integration tests

The TestCLI_* tests in cmd/bd/cli_fast_test.go are taking 4-5 seconds each (40+ seconds total), making them the slowest part of the fast test suite.

Current timings:
- TestCLI_Import: 4.73s
- TestCLI_Blocked: 4.33s
- TestCLI_DepTree: 4.15s
- TestCLI_Close: 3.59s
- TestCLI_DepAdd: 3.50s
- etc.

These tests compile the bd binary once in init(), but then execute it multiple times per test with filesystem operations. Despite being named "fast", they're actually end-to-end CLI integration tests.

Options:
1. Tag with //go:build integration (move to integration suite)
2. Optimize: Use in-memory databases, reduce exec calls, better parallelization
3. Keep as-is but understand they're the baseline for "fast" tests

Total test suite currently: 13.8s (cmd/bd alone is 12.8s, and most of that is these CLI tests)

Fixed by reusing existing bd binary from repo root instead of rebuilding.

Before: 15+ minutes (rebuilding binary for every test package)
After: ~12 seconds (reuses pre-built binary)

The init() function now checks for ../../bd first before falling back to building. This means `go build && go test` is now fast.


Add test coverage for internal/autoimport package


Add mermaid output format for bd dep tree

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

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

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

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


Test simple issue


Refactor: deduplicate FindJSONLInDir and FindJSONLPath

## Background

After fixing bd-tqo, we now have two nearly identical functions for finding the JSONL file:
- `autoimport.FindJSONLInDir(dbDir string)` in internal/autoimport/autoimport.go
- `beads.FindJSONLPath(dbPath string)` in internal/beads/beads.go

Both implement the same logic:
1. Prefer issues.jsonl
2. Fall back to beads.jsonl for legacy support
3. Skip deletions.jsonl and merge artifacts
4. Default to issues.jsonl if nothing found

## Problem

Code duplication means bug fixes need to be applied in multiple places (as we just experienced with bd-tqo).

## Proposed Solution

Extract shared logic to a utility package that both can import. Options:
1. Create `internal/jsonlpath` package with the core logic
2. Have `autoimport` import `beads` and call `FindJSONLPath` (but APIs differ slightly)
3. Move to `internal/utils` if appropriate

Need to verify no import cycles would be created.

## Affected Files
- internal/autoimport/autoimport.go
- internal/beads/beads.go


Centralize BD_DEBUG logging into debug package

The codebase has 43 scattered instances of `if os.Getenv("BD_DEBUG") != ""` debug checks across 6 files. Centralize into a debug logging package.

Current locations:
- `cmd/bd/main.go` - 15 checks
- `cmd/bd/autoflush.go` - 6 checks
- `cmd/bd/nodb.go` - 4 checks
- `internal/rpc/server.go` - 2 checks
- `internal/rpc/client.go` - 5 checks
- `cmd/bd/daemon_autostart.go` - 11 checks

Target structure:
```
internal/debug/
└── debug.go
```

Benefits:
- Centralized debug logging
- Easier to add structured logging later
- Testable (can mock debug output)
- Consistent debug message format

Impact: Removes 43 scattered checks, improves code clarity


GH#518: Document bd setup command

bd setup (e.g. bd setup cursor) is undocumented. Users only find it by grepping source. Need proper docs. See: https://github.com/steveyegge/beads/issues/518


Baseline quality gate failure: test

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

This blocks the executor from claiming work until fixed.

Error: go test failed: exit status 1

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

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


bd doctor: add --dry-run flag to preview fixes without applying

Currently bd doctor --fix applies all fixes. Add a --dry-run flag to preview what would be fixed without making changes. This helps users understand the impact before committing to repairs.


Fix bd init to bootstrap from sync-branch when configured

When sync-branch is configured in config.yaml, bd init should import from that branch instead of HEAD. Currently it reads HEAD:.beads/issues.jsonl which may be empty or stale when issues are on the sync branch. Location: cmd/bd/autoimport.go:97 checkGitForIssues()


Test worktree sync branch commit


Performance Testing Framework

Add comprehensive performance testing for beads focusing on optimization guidance and validating 10K+ database scale. Uses standard Go tooling, follows existing patterns, minimal complexity.

Components:
- Benchmark suite for critical operations at 10K-20K scale
- Fixture generator for realistic test data (epic hierarchies, cross-links)
- User diagnostics via bd doctor --perf
- Always-on profiling integration

Goals:
- Identify bottlenecks for optimization work
- Validate performance at 10K+ issue scale
- Enable users to collect diagnostics for bug reports
- Support both SQLite and JSONL import paths


Fix goroutine leak in daemon restart

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

Location: cmd/bd/daemons.go:251

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

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

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

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

Impact: Goroutine leak on every daemon restart

Effort: 2 hours


bd prime: Detect ephemeral branches and adjust workflow output

When 'bd prime' runs on a branch with no upstream (ephemeral branch), it should output a different SESSION CLOSE PROTOCOL.

**Current output (wrong for ephemeral branches):**
```
[ ] 1. git status
[ ] 2. git add <files>
[ ] 3. bd sync
[ ] 4. git commit -m "..."
[ ] 5. bd sync
[ ] 6. git push
```

**Needed output for ephemeral branches:**
```
[ ] 1. git status
[ ] 2. git add <files>
[ ] 3. bd sync --from-main  (pull updates from main)
[ ] 4. git commit -m "..."
[ ] 5. (no push - branch is ephemeral)
```

**Detection:** `git rev-parse --abbrev-ref --symbolic-full-name @{u}` returns error code 128 if no upstream.

Also update Sync & Collaboration section to mention `bd sync --from-main` for ephemeral branches.

**Use case:** Gastown polecats work on ephemeral local branches that are never pushed. Their code gets merged to main via local merge, and beads changes stay local (communicated via gm mail to Overseer).


Test update


Add 'new' as alias for 'create' command


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


Refactor daemon local-only sync functions to reduce duplication

PR #433 added three new local-only sync functions (createLocalSyncFunc, createLocalExportFunc, createLocalAutoImportFunc) that are largely copy-paste of existing ones with git operations removed. Refactor to use a single implementation with a skipGit bool parameter or extract shared logic into helper functions to reduce ~200 lines of duplication.


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


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


Create backup and restore procedures

Disaster recovery procedures for Agent Mail data.

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

File: deployment/agent-mail/backup.sh


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

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


Test parent issue


Add export test verifying tombstones are included in JSONL output

The export now includes tombstones (bd-dve) but there's no test that:
1. Creates issues in DB including a tombstone
2. Runs export
3. Verifies the JSONL contains the tombstone with all fields

Add a test: TestExportIncludesTombstones


Add Claude Code web installation docs to README

GH #439 reported installation issues in Claude Code web environment. The go install fallback works, but users need guidance. Add a section to README documenting the workaround: go install + PATH export.


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

## Problem

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

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

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

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

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

## Fix
Escape regex special characters:

```go
import "regexp"

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


Update documentation for merge driver auto-config

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

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

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


Missing test for IsExpiredTombstone edge cases

## Problem

The `IsExpiredTombstone()` function has several edge cases that aren't directly tested:

1. **Invalid DeletedAt timestamp** - Returns false (safety), but not tested
2. **ttl=0 fallback to DefaultTombstoneTTL** - Not tested directly
3. **Tombstone with empty DeletedAt** - Returns false, tested implicitly but not explicitly
4. **Clock skew grace period effect** - Not tested (would need time mocking)

## Current Test Coverage

The existing tests use `time.Now()` which makes them non-deterministic:
```go
DeletedAt: time.Now().Add(-24 * time.Hour).Format(time.RFC3339)
```

This works but doesn't test the expiration boundary precisely.

## Recommendation

Add explicit unit tests for `IsExpiredTombstone()`:

```go
func TestIsExpiredTombstone(t *testing.T) {
    tests := []struct {
        name      string
        status    string
        deletedAt string
        ttl       time.Duration
        expected  bool
    }{
        {"non-tombstone returns false", "open", "2024-01-01T00:00:00Z", 0, false},
        {"empty deleted_at returns false", "tombstone", "", 0, false},
        {"invalid timestamp returns false", "tombstone", "not-a-date", 0, false},
        {"within TTL returns false", "tombstone", time.Now().Add(-1*time.Hour).Format(time.RFC3339), 24*time.Hour, false},
        {"beyond TTL returns true", "tombstone", time.Now().Add(-48*time.Hour).Format(time.RFC3339), 24*time.Hour, true},
    }
    // ...
}
```

## Files
- internal/merge/merge.go:255-290
- internal/merge/merge_test.go (add TestIsExpiredTombstone)


Prevent multiple daemons from running on the same repo

Multiple bd daemons running on the same repo clone causes race conditions and data corruption risks.

**Problem:**
- Nothing prevents spawning multiple daemons for the same repository
- Multiple daemons watching the same files can conflict during sync operations
- Observed: 4 daemons running simultaneously caused sync race condition

**Solution:**
Implement daemon singleton enforcement per repo:
1. Use a lock file (e.g., .beads/.daemon.lock) with PID
2. On daemon start, check if lock exists and process is alive
3. If stale lock (dead PID), clean up and acquire lock
4. If active daemon exists, either:
   - Exit with message 'daemon already running (PID xxx)'
   - Or offer --replace flag to kill existing and take over
5. Release lock on graceful shutdown

**Edge cases to handle:**
- Daemon crashes without releasing lock (stale PID detection)
- Multiple repos in same directory tree (each repo gets own lock)
- Race between two daemons starting simultaneously (atomic lock acquisition)


troubleshoot GH#278 daemon exits every few secs


Debug MCP Agent Mail tool execution errors

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

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

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

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

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

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

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


Git merge driver uses invalid placeholders (%L, %R instead of %A, %B)

## Problem

The beads git merge driver is configured with invalid Git placeholders:

```
git config merge.beads.driver "bd merge %A %O %L %R"
```

Git doesn't recognize `%L` or `%R` as valid merge driver placeholders. The valid placeholders are:
- `%O` = base (common ancestor)
- `%A` = current version (ours)
- `%B` = other version (theirs)

## Impact

- Affects ALL users when they have `.beads/beads.jsonl` merge conflicts
- Automatic JSONL merge fails with error: "error reading left file: failed to open file: open 7: no such file or directory"
- Users must manually resolve conflicts instead of getting automatic merge

## Root Cause

The `bd init` command (or wherever the merge driver is configured) is using non-standard placeholders. When Git encounters `%L` and `%R`, it either passes them literally or interprets them incorrectly.

## Fix

Update the merge driver configuration to:
```
git config merge.beads.driver "bd merge %A %O %A %B"
```

Where:
- 1st `%A` = output file (current file, will be overwritten)
- `%O` = base (common ancestor)
- 2nd `%A` = left/current version
- `%B` = right/other version

## Action Items

1. Fix `bd init` (or equivalent setup command) to use correct placeholders
2. Add migration/warning for existing users with misconfigured merge driver
3. Update documentation with correct merge driver setup
4. Consider adding validation when `bd init` is run


Stress test: event storm handling

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


Fresh clone: bd should suggest 'bd init' when no database exists

On a fresh clone of a repo using beads, running `bd stats` or `bd list` gives a cryptic error:

```
Error: failed to open database: post-migration validation failed: migration invariants failed:
  - required_config_present: required config key missing: issue_prefix (database has 2 issues)
```

**Expected**: A helpful message like:
```
No database found. This appears to be a fresh clone.
Run 'bd init --prefix <prefix>' to hydrate from the committed JSONL file.
Found: .beads/beads.jsonl (38 issues)
```

**Why this matters**: The current UX is confusing for new contributors or fresh clones. The happy path should be obvious.


Missing test: base is tombstone, both left and right are live

The tombstone merge tests do not cover the case where the base version is a tombstone but both left and right have resurrected/live versions. This scenario could occur if: 1) Clone A deletes an issue, 2) Clone B and Clone C both sync from A (getting tombstone), 3) Both B and C independently recreate an issue with same ID. The current code would likely fall through to standard mergeIssue() which may not be correct. Need to add test and verify behavior. Files: internal/merge/merge_test.go, internal/merge/merge.go:347-387


Fix SyncJSONLToWorktree to merge instead of overwrite

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

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

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

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

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


Remove unreachable utility functions

Several small utility functions are unreachable:

Files to clean:
1. `internal/storage/sqlite/hash.go` - `computeIssueContentHash` (line 17)
   - Check if entire file can be deleted if only contains this function

2. `internal/config/config.go` - `FileUsed` (line 151)
   - Delete unused config helper

3. `cmd/bd/git_sync_test.go` - `verifyIssueOpen` (line 300)
   - Delete dead test helper

4. `internal/compact/haiku.go` - `HaikuClient.SummarizeTier2` (line 81)
   - Tier 2 summarization not implemented
   - Options: implement feature OR delete method

Impact: Removes 50-100 LOC depending on decisions


Rapid 5


Audit Current Cache Usage

Understand exactly what code depends on the storage cache


Add label-based filtering to bd ready command

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

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

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

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


Implement filesystem shims for WASM

WASM needs JS shims for filesystem access. Child of epic bd-44d0.

## Tasks
- [ ] Implement file read/write shims
- [ ] Map WASM syscalls to Node.js fs API
- [ ] Handle .beads/ directory discovery
- [ ] Test with real JSONL files
- [ ] Support both absolute and relative paths

## Technical Notes
- Use Node.js fs module via syscall/js
- Consider MEMFS for in-memory option


Stealth mode global gitignore should use absolute project path

**GitHub Issue:** #538

**Problem:**
`bd init --stealth` adds `.beads/` to the global gitignore, which ignores ALL `.beads/` folders across all repositories. Users who want stealth mode in one project but open beads usage in others are blocked.

**Solution:**
Change stealth mode to use absolute paths instead of generic patterns:

**Before (current):**
```
# Beads stealth mode configuration (added by bd init --stealth)
.beads/
.claude/settings.local.json
```

**After (proposed):**
```
# Beads stealth mode: /Users/foo/work-project (added by bd init --stealth)
/Users/foo/work-project/.beads/
/Users/foo/work-project/.claude/settings.local.json
```

**Implementation:**
1. Modify `setupGlobalGitIgnore()` in `cmd/bd/init.go`
2. Get current working directory (absolute path)
3. Use absolute path patterns instead of generic ones
4. Update comment to show which project the entry is for

**Tradeoffs:**
- If project directory moves, gitignore entry becomes stale (acceptable - user can re-run `bd init --stealth`)
- Multiple stealth projects = multiple entries (works correctly)

**Testing:**
- Verify absolute path is added to global gitignore
- Verify other projects' .beads/ folders are NOT ignored
- Test with existing global gitignore file
- Test creating new global gitignore file


Run final validation and cleanup checks

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

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

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

Impact: Confirms all cleanup objectives achieved successfully

## Validation Results (Oct 31, 2025)

**Dead Code:** ✅ Removed 5 unreachable functions (~200 LOC)
- computeIssueContentHash, shouldSkipExport (autoflush.go)
- addDependencyUnchecked, removeDependencyIfExists (dependencies.go)
- isUniqueConstraintError (util.go)

**Tests:** ✅ All pass
**Coverage:**
- Main package: 39.6%
- cmd/bd: 19.5%
- internal/daemon: 37.8%
- internal/storage/sqlite: 58.1%
- internal/rpc: 58.6%

**Build:** ✅ Clean (24.5 MB binary)
**Linting:** 247 issues (mostly errcheck on defer/Close statements)
**Integration Tests:** ✅ All pass
**Metrics:** 55,622 LOC across 200 Go files
**Git:** 3 files modified (dead code removal)


Windows binaries lack SQLite support (GH #253)

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

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

Needs new release to fix for users.


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

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

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

    Fixes GH#346.


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

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


Add bd migrate-tombstones command

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


Test graceful degradation when server unavailable

Verify that agents continue working normally when Agent Mail server is stopped or unreachable.

Acceptance Criteria:
- Agent detects server unavailable on startup
- Logs "falling back to Beads-only mode"
- All bd commands work normally
- Agent can claim issues (no reservations, like today)
- Git sync operates as normal
- No errors or crashes

Success Metric: Zero functional difference when Agent Mail unavailable


Update JSONL format to use hash IDs

Update JSONL import/export to use hash IDs, store aliases separately.

## Current JSONL Format
```jsonl
{"id":"bd-1c63eb84","title":"Fix bug","status":"open",...}
{"id":"bd-9063acda","title":"Add test","status":"open",...}
```

## New JSONL Format (Option A: Include Alias)
```jsonl
{"id":"bd-af78e9a2","alias":1,"title":"Fix bug","status":"open",...}
{"id":"bd-e5f6a7b8","alias":2,"title":"Add test","status":"open",...}
```

## New JSONL Format (Option B: Hash ID Only)
```jsonl
{"id":"bd-af78e9a2","title":"Fix bug","status":"open",...}
{"id":"bd-e5f6a7b8","title":"Add test","status":"open",...}
```

Store aliases in separate .beads/aliases.jsonl (local only, git-ignored):
```jsonl
{"hash":"bd-af78e9a2","alias":1}
{"hash":"bd-e5f6a7b8","alias":2}
```

**Recommendation**: Option B (hash only in main JSONL)
- Cleaner git diffs (no alias conflicts)
- Aliases are workspace-local preference
- Main JSONL is canonical, portable

## Export Changes
File: cmd/bd/export.go
```go
// Export issues with hash IDs
for _, issue := range issues {
    json := marshalIssue(issue)  // Uses issue.ID (hash)
    // Don't include alias in JSONL
}

// Separately export aliases to .beads/aliases.jsonl
exportAliases(issues)
```

## Import Changes
File: cmd/bd/import.go, internal/importer/importer.go
```go
// Import issues by hash ID
issue := unmarshalIssue(line)
// Assign new alias on import (don't use incoming alias)
issue.Alias = getNextAlias()

// No collision detection needed! Hash IDs are globally unique
```

## Dependency Reference Format
No change needed - already uses issue IDs:
```json
{"depends_on_id":"bd-af78e9a2","type":"blocks"}
```

## Files to Modify
- cmd/bd/export.go (use hash IDs)
- cmd/bd/import.go (import hash IDs, assign aliases)
- internal/importer/importer.go (remove collision detection!)
- .gitignore (add .beads/aliases.jsonl)

## Testing
- Test export produces hash IDs
- Test import assigns new aliases
- Test dependencies preserved with hash IDs
- Test no collision detection triggered


Latency test 1


Publish official devcontainer configuration

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


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

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

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

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

Commit: d46177d


Imperator does not know its own mail identity


Add TTL to deletions manifest entries

Deletions manifest entries should have a TTL (time-to-live) to automatically expire after a certain period. Stale deletion entries can poison beads installations when an agent gets out of sync, causing issues to remain deleted even when they shouldn't be. Adding expiration would limit the blast radius of stale deletions.


Refactor: extract common helpers in sync-branch hook checks

Code review of #532 fix identified duplication between checkSyncBranchHookCompatibility and checkSyncBranchHookQuick.

Suggested refactoring:
1. Extract getPrePushHookPath(path string) helper for git dir + hook path resolution
2. Extract extractHookVersion(hookContent string) helper for version parsing
3. Consider whether Warning for custom hooks is appropriate (vs OK with info message)
4. Document the intentional behavior difference between full check (Warning for custom) and quick check (OK for custom)


Add database maintenance commands section to QUICKSTART.md

**Problem:**
Users don't discover `bd compact` or `bd cleanup` commands until their database grows large. These maintenance commands aren't mentioned in quickstart documentation.

Related to issue #349 item #4.

**Current state:**
docs/QUICKSTART.md ends at line 217 with "See README.md for full documentation" but has no mention of maintenance operations.

**Proposed addition:**
Add a "Database Maintenance" section after line 140 (before "Advanced: Agent Mail" section) covering:
- When database grows (many closed issues)
- How to view compaction statistics
- How to compact old issues
- How to delete closed issues
- Warning about permanence

**Example content:**
```markdown
## Database Maintenance

As your project accumulates closed issues, the database grows. Use these commands to manage size:

```bash
# View compaction statistics
bd compact --stats

# Preview compaction candidates (30+ days closed)
bd compact --analyze --json

# Apply agent-generated summary
bd compact --apply --id bd-42 --summary summary.txt

# Immediately delete closed issues (use with caution!)
bd cleanup --force
```

**When to compact:**
- Database file > 10MB with many old closed issues
- After major project milestones
- Before archiving a project phase
```

**Files to modify:**
- docs/QUICKSTART.md (add section after line 140)


Add security tests for WriteFile permissions in doctor command

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

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

In cmd/bd/doctor/gitignore.go:98, os.WriteFile uses 0644 permissions, flagged by gosec G306 as potentially too permissive.

Add tests to verify:
- File is created with appropriate permissions (0600 or less)
- Existing file permissions are not loosened
- File ownership is correct
- Sensitive data handling if .gitignore contains secrets

This ensures .gitignore files are created with secure permissions to prevent unauthorized access.

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


Fix PR #319: Performance Improvements - CI failures and lint errors

PR #319 (Performance Improvements) has excellent performance optimizations but is blocked by CI failures.

## The PR
- URL: https://github.com/steveyegge/beads/pull/319
- Author: @rsnodgrass (Ryan)
- Claimed improvements: bd ready 20.5x faster (752ms → 36.6ms), startup 10.5x faster

## CI Failures

### Lint Errors (8 total)
1. cmd/bd/deletion_tracking.go:57 - unchecked os.Remove
2. cmd/bd/import.go:548 - unchecked os.RemoveAll
3. cmd/bd/message.go:205 - unchecked resp.Body.Close
4. cmd/bd/migrate_issues.go:633 - unchecked fmt.Scanln
5. cmd/bd/migrate_issues.go:701 - unchecked MarkFlagRequired
6. cmd/bd/migrate_issues.go:702 - unchecked MarkFlagRequired
7. cmd/bd/show.go:610 - gosec G104 unhandled error
8. cmd/bd/show.go:614 - gosec G104 unhandled error

### Test Failures
All syncbranch_test.go tests failing with:
"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues"

This suggests the PR branch needs rebasing on current main.

## Required Work

### 1. Fix Lint Errors
Add proper error handling for all 8 flagged locations. Most can use _ = or log warnings.

### 2. Rebase on Current Main
The migration test failures indicate the branch is out of sync. Need to:
- git fetch upstream
- git rebase upstream/main
- Resolve any conflicts
- Verify tests pass locally

### 3. Verify CI Passes
- All lint checks green
- All tests pass (Linux, Windows, Nix)

## Optional Improvements
- Consider splitting into smaller PRs (core index, WASM cache, testing infra)
- Add documentation for benchmark usage
- Extract helper functions in doctor/perf.go for better testability

## Value
This PR delivers real performance improvements. The index optimization alone is worth merging quickly once CI is fixed.


Issue 1 to reopen


Update documentation for hash IDs and aliases

Update all documentation to explain hash-based IDs and aliasing system.

## Files to Update

### 1. README.md
Add section explaining hash IDs:
```markdown
## Issue IDs

Beads uses **hash-based IDs** for collision-free distributed issue tracking:

- **Hash ID**: `bd-af78e9a2` (8-char SHA256 prefix, immutable, globally unique)
- **Alias**: `#42` (sequential number, mutable, human-friendly)

### Using IDs
```bash
bd show bd-af78e9a2   # Use hash ID
bd show #42           # Use alias
bd show 42            # Use alias (shorthand)
```

### Why Hash IDs?
- **Collision-free**: Work offline without ID conflicts
- **Distributed**: No coordination needed between clones
- **Git-friendly**: Different IDs = different JSONL lines, fewer merge conflicts

### Aliases
Aliases are workspace-local shortcuts for hash IDs. They're:
- Automatically assigned on issue creation
- Reassigned deterministically on sync (if conflicts)
- Can be manually controlled with `bd alias` commands
```

### 2. AGENTS.md
Update agent workflow:
```markdown
## Hash-Based IDs (v2.0+)

Beads v2.0 uses hash-based IDs to eliminate collision problems:

**When creating issues**:
```bash
bd create "Fix bug" -p 1
# → Creates bd-af78e9a2 with alias #1
```

**When referencing issues**:
- In text: Use hash IDs (stable): "See bd-af78e9a2 for details"
- In CLI: Use aliases (readable): `bd update #42 --status done`

**After sync**:
- Alias conflicts resolved automatically (content-hash ordering)
- No ID collisions possible
- No remapping needed

**Migration from v1.x**:
```bash
bd migrate --hash-ids  # One-time migration
```
```

### 3. QUICKSTART.md (if exists)
Show alias usage in examples:
```bash
# Create issue (gets hash ID + alias)
bd create "Fix authentication bug" -p 1
# → Created bd-af78e9a2 (alias: #1)

# Reference by alias
bd show #1
bd update #1 --status in_progress
bd close #1 --reason "Fixed"
```

### 4. ADVANCED.md
Add section on hash ID internals:
```markdown
## Hash ID Generation

Hash IDs are generated deterministically:

```go
SHA256(title || description || timestamp || workspace_id)[:8]
```

**Collision probability**:
- 8 hex chars = 2^32 space = ~4 billion IDs
- Birthday paradox: 50% collision probability at ~65,000 issues
- For typical projects (<10,000 issues), collision risk is negligible

**Collision detection**:
If a hash collision occurs (extremely rare), beads:
1. Detects on insert (UNIQUE constraint)
2. Appends random suffix: `bd-af78e9a2-a1b2`
3. Retries insert

## Alias Conflict Resolution

When multiple clones assign same alias to different issues:

**Strategy**: Content-hash ordering (deterministic)
- Sort conflicting issue IDs lexicographically
- Lowest hash ID keeps the alias
- Others reassigned to next available aliases

**Example**:
```
Clone A: Assigns #42 to bd-a1b2c3d4
Clone B: Assigns #42 to bd-e5f6a7b8
After sync: bd-a1b2c3d4 keeps #42 (lower hash)
            bd-e5f6a7b8 gets #100 (next available)
```
```

### 5. MIGRATION.md (new file)
```markdown
# Migrating to Hash-Based IDs (v2.0)

## Overview
Beads v2.0 introduces hash-based IDs to eliminate collision problems. This is a **breaking change** requiring migration.

## Migration Steps

### 1. Backup
```bash
cp -r .beads .beads.backup
git commit -am "Pre-migration backup"
```

### 2. Run Migration
```bash
# Dry run first
bd migrate --hash-ids --dry-run

# Apply migration
bd migrate --hash-ids
```

### 3. Commit Changes
```bash
git add .beads/issues.jsonl
git commit -m "Migrate to hash-based IDs (v2.0)"
git push origin main
```

### 4. Coordinate with Collaborators
All clones must migrate before syncing:
1. Notify team: "Migrating to v2.0 on [date]"
2. All collaborators pull latest
3. All run `bd migrate --hash-ids`
4. All push changes
5. Resume normal work

## Rollback
```bash
# Restore backup
mv .beads.backup .beads
bd export  # Regenerate JSONL
git checkout .beads/issues.jsonl
```

## FAQ

**Q: Can I mix v1.x and v2.0 clones?**
A: No. All clones must be on same version.

**Q: Will my old issue IDs work?**
A: No, but aliases preserve the numbers: bd-1c63eb84 → #1

**Q: What happens to links like "see bd-cb64c226.3"?**
A: Migration updates all text references automatically.
```

### 6. CHANGELOG.md
```markdown
## v2.0.0 (YYYY-MM-DD)

### Breaking Changes
- **Hash-based IDs**: Issues now use collision-free hash IDs (bd-af78e9a2)
  instead of sequential IDs (bd-1c63eb84, bd-9063acda)
- **Aliasing system**: Human-friendly aliases (#42) for hash IDs
- **Migration required**: Run `bd migrate --hash-ids` to convert v1.x databases

### Added
- `bd alias` command for manual alias control
- `bd migrate --hash-ids` migration tool
- Alias conflict resolution (deterministic, content-hash ordering)

### Removed
- ID collision detection and resolution (~2,100 LOC)
- `bd import --resolve-collisions` flag (no longer needed)

### Benefits
- ✅ Zero ID collisions in distributed workflows
- ✅ Simpler codebase (-1,350 net LOC)
- ✅ Better git merge behavior
- ✅ True offline-first operation
```

## Testing
- Build docs locally (if using doc generator)
- Check all links work
- Verify examples are correct
- Spellcheck

## Files to Create/Modify
- README.md (hash ID section)
- AGENTS.md (workflow updates)
- ADVANCED.md (internals)
- MIGRATION.md (new)
- CHANGELOG.md (v2.0 entry)
- docs/ (any other docs)


Fix remaining FK constraint failures in AddComment and ApplyCompaction

Follow-up to PR #348 (Fix FOREIGN KEY constraint failed).

The initial fix addressed CloseIssue, UpdateIssueID, and RemoveLabel.
However, `AddComment` (in internal/storage/sqlite/events.go) and `ApplyCompaction` (in internal/storage/sqlite/compact.go) still suffer from the same pattern: inserting an event after an UPDATE without verifying the UPDATE affected any rows.

This causes "FOREIGN KEY constraint failed" errors when operating on non-existent issues, instead of clean "issue not found" errors.

Task:
1. Apply the same fix pattern to `AddComment` and `ApplyCompaction`: check RowsAffected() after UPDATE and before event INSERT.
2. Ensure error messages are consistent ("issue %s not found").
3. Verify with reproduction tests (create a test that calls these methods with a non-existent ID).


Test child issue


Optimize cmd/bd long-mode tests by switching to in-process testing

The long-mode CLI tests in cmd/bd are slow (1.4-4.4 seconds each) because they spawn full bd processes via exec.Command() for every operation.

Current approach:
- Each runBD() call spawns new bd process via exec.Command(testBD, args...)
- Each process initializes Go runtime, loads SQLite, parses CLI flags
- Tests run serially (create → update → show → close)
- Even with --no-daemon flag, there's significant process spawn overhead

Example timing from test run:
- TestCLI_PriorityFormats: 2.21s
- TestCLI_Show: 2.26s
- TestCLI_Ready: 2.29s
- TestCLI_Import: 4.42s

Optimization strategy:
1. Convert most tests to in-process testing (call bd functions directly)
2. Reuse test databases across related operations instead of fresh init each time
3. Keep a few exec.Command() tests that batch multiple operations to verify the CLI path works end-to-end

This should reduce test time from ~40s to ~5s for the affected tests.

Converted all CLI tests in cli_fast_test.go to use in-process testing via rootCmd.Execute(). Created runBDInProcess() helper that:
- Calls rootCmd.Execute() directly instead of spawning processes
- Uses mutex to serialize execution (rootCmd/viper not thread-safe)
- Properly cleans up global state (store, daemonClient)
- Returns only stdout to avoid JSON parsing issues with stderr warnings

Performance results:
- In-process tests: ~0.6s each (cached even faster)
- exec.Command tests: ~3.7s each
- Speedup: ~10x faster

Kept TestCLI_EndToEnd() that uses exec.Command for end-to-end validation of the actual binary.


Add warning when staleness check errors

## Problem

When ensureDatabaseFresh() calls CheckStaleness() and it errors (corrupted metadata, permission issues, etc.), we silently proceed with potentially stale data.

**Location:** cmd/bd/staleness.go:27-32

**Scenarios:**
- Corrupted metadata table
- Database locked by another process
- Permission issues reading JSONL file
- Invalid last_import_time format in DB

## Current Code

```go
isStale, err := autoimport.CheckStaleness(ctx, store, dbPath)
if err \!= nil {
    // If we can't determine staleness, allow operation to proceed
    // (better to show potentially stale data than block user)
    return nil
}
```

## Fix

```go
isStale, err := autoimport.CheckStaleness(ctx, store, dbPath)
if err \!= nil {
    fmt.Fprintf(os.Stderr, "Warning: Could not verify database freshness: %v\n", err)
    fmt.Fprintf(os.Stderr, "Proceeding anyway. Data may be stale.\n\n")
    return nil
}
```

## Impact
Medium - users should know when staleness check fails

## Effort
Easy - 5 minutes


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.


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


Blocking issue


Test FK constraint via close


Auto-compact deletions during bd sync

Parent: bd-imj

## Task
Optionally prune deletions manifest during sync when threshold exceeded.

**Note: Opt-in feature** - disabled by default to avoid sync latency.

## Implementation

In `bd sync`:
```go
func (s *Syncer) Sync() error {
    // ... existing sync logic ...

    // Auto-compact only if enabled
    if s.config.GetBool("deletions.auto_compact", false) {
        deletionCount := deletions.Count(".beads/deletions.jsonl")
        threshold := s.config.GetInt("deletions.auto_compact_threshold", 1000)

        if deletionCount > threshold {
            retentionDays := s.config.GetInt("deletions.retention_days", 7)
            if err := s.compactor.PruneDeletions(retentionDays); err != nil {
                log.Warnf("Failed to auto-compact deletions: %v", err)
                // Non-fatal, continue sync
            }
        }
    }

    // ... rest of sync ...
}
```

## Configuration
```yaml
deletions:
  retention_days: 7
  auto_compact: false              # Opt-in, disabled by default
  auto_compact_threshold: 1000     # Trigger when > N entries (if enabled)
```

## Acceptance Criteria
- [ ] Auto-compact disabled by default
- [ ] Enabled via config `deletions.auto_compact: true`
- [ ] Sync checks deletion count only when enabled
- [ ] Auto-prunes when threshold exceeded
- [ ] Failure is non-fatal (logged warning)
- [ ] Test: no compaction when disabled
- [ ] Test: compaction triggers when enabled and threshold exceeded


Cache getMultiRepoJSONLPaths() result during sync to avoid redundant calls

From bd-xo6b code review: getMultiRepoJSONLPaths() is called 3x per sync cycle.

**Current behavior:**
daemon_sync.go calls getMultiRepoJSONLPaths() three times per sync:
- Line 505: Snapshot capture before pull
- Line 575: Merge/prune after pull
- Line 613: Base snapshot update after import

**Cost per call:**
- Config lookup (likely cached, but still overhead)
- Path construction: O(N) where N = number of repos
- String allocations: (N + 1) × filepath.Join() calls

**Total per sync:** 3N path constructions + 3 config lookups + 3 slice allocations

**Impact:**
- For N=3 repos: Negligible (< 1ms)
- For N=10 repos: Still minimal
- For N=100+ repos: Wasteful

**Solution:**
Call once at sync start, reuse result:

```go
func createSyncFunc(...) func() {
    return func() {
        // ... existing setup ...

        // Call once at start
        multiRepoPaths := getMultiRepoJSONLPaths()

        // Snapshot capture
        if multiRepoPaths != nil {
            for _, path := range multiRepoPaths {
                if err := captureLeftSnapshot(path); err != nil { ... }
            }
        }

        // ... later ...

        // Merge/prune - reuse same paths
        if multiRepoPaths != nil {
            for _, path := range multiRepoPaths { ... }
        }

        // ... later ...

        // Base snapshot update - reuse same paths
        if multiRepoPaths != nil {
            for _, path := range multiRepoPaths { ... }
        }
    }
}
```

**Files:**
- cmd/bd/daemon_sync.go:449-636 (createSyncFunc)

**Note:** This is a performance optimization, not a correctness fix. Low priority unless multi-repo usage scales significantly.


Add unit tests for config modes

Test all four orphan_handling modes: strict (fails), resurrect (creates tombstone), skip (logs warning), allow (imports orphan). Verify error messages and logging output for each mode.


Extract hash ID generation functions to hash_ids.go

Move generateHashID, getNextChildNumber, GetNextChildID to hash_ids.go


Add tests for internal/importer package

Currently 0.0% coverage. Need tests for JSONL import logic including collision detection and resolution.


Document shared memory test isolation pattern in test_helpers.go

Tests were failing because :memory: creates a shared database across all tests. The fix is to use "file::memory:?mode=memory&cache=private" for test isolation.

Should document this pattern in test_helpers.go and potentially update newTestStore to use private memory by default.


Rapid 3


Extract batch operations to batch_ops.go

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


Verify MCP Server Compatibility

Ensure MCP server works with cache-free daemon


Pre-sync integrity check (bd sync --check)

Add bd sync --check flag that detects problematic states before attempting sync: forced pushes on sync branch (via git reflog), prefix mismatches in JSONL, orphaned children with missing parents. Output combined diagnostic without modifying state.


Implement `bd prime` command for AI context loading

Create a `bd prime` command that outputs AI-optimized markdown containing essential Beads workflow context. This provides an alternative to the MCP server for token-conscious users and enables context recovery after compaction/clearing.


Add orphan detection migration

Create migration to detect orphaned children in existing databases. Query: SELECT id FROM issues WHERE id LIKE '%.%' AND substr(id, 1, instr(id || '.', '.') - 1) NOT IN (SELECT id FROM issues). Log results, let user decide action (delete orphans or convert to top-level).


Add tests for internal/autoimport package

Currently 0.0% coverage. Need tests for auto-import functionality that detects and imports updated JSONL files.


~/src/beads daemon has 'sql: database is closed' errors - zombie daemon