Run all validation checks in one command.

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

Supports --fix-all for auto-repair.

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

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


Test RPC mutation event


Fix or remove TestRemapCollisions test

TestRemapCollisions in internal/storage/sqlite/collision_test.go is failing because we removed the collision remapping implementation (bd-8e05).

Options:
1. Remove the test entirely (collision remapping no longer supported)
2. Update test to verify the new behavior (error when collisions detected)

The test expects RemapCollisions to work, but now it returns an error because hash IDs don't need collision remapping.

File: internal/storage/sqlite/collision_test.go:730


Test hash ID v2


bd find-duplicates - AI-powered duplicate detection

Find semantically duplicate issues.

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

Uses embeddings by default for >100 issues.

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


Hash-based IDs with hierarchical children

Replace sequential auto-increment IDs (bd-1c63eb84, bd-9063acda) with content-hash based IDs (bd-af78e9a2) and hierarchical sequential children (bd-af78e9a2.1, .2, .3).

## Motivation
Current sequential IDs cause collision problems when multiple clones work offline:
- Non-deterministic convergence in N-way scenarios (bd-cbed9619.1, bd-e6d71828)
- Complex collision resolution logic (~2,100 LOC)
- UNIQUE constraint violations during import
- Requires coordination between workers

Hash-based IDs eliminate collisions entirely at the top level, while hierarchical sequential children provide human-friendly IDs within naturally-coordinated contexts (epic ownership).

## Benefits
- ✅ Collision-free distributed ID generation (top-level)
- ✅ Human-friendly IDs for related work (epic children)
- ✅ Eliminates ~2,100 LOC of collision handling code
- ✅ Better git merge behavior (different IDs = different JSONL lines)
- ✅ True offline-first workflows
- ✅ Simpler than dual-system (no alias counter to coordinate)
- ✅ Natural work breakdown structure encoding in IDs
- ✅ Enables parallel CI/CD workers without coordination

## Design

### ID Structure
- **Storage:** bd-af78e9a2 (prefix + 8-char SHA256)
- **CLI input:** Both bd-af78e9a2 AND a3f8e9 accepted (prefix optional)
- **CLI output:** bd-af78e9a2 (always show prefix for copy-paste clarity)
- **External refs:** bd-af78e9a2 (in commits, docs, unambiguous)

**Why keep prefix in storage:**
- Clear in external contexts (git commits, docs, Slack)
- Grep-able across files
- Distinguishable from git commit SHAs
- Supports multiple databases (bd-, ticket-, bug- prefixes)

**Why make optional in CLI:**
- Less typing: bd show a3f8e9 works
- Git-style convenience
- Prefix inferred from context (bd command)

### Hierarchical Children
- **Epic children:** bd-af78e9a2.1, bd-af78e9a2.2, bd-af78e9a2.3 (sequential per parent)
- **Nested epics:** bd-af78e9a2.1.1, bd-af78e9a2.1.2 (up to 3 levels deep)
- **Leaf tasks:** Any issue without children

### Example Hierarchy
```
bd-a3f8e9 [epic] "Auth System"
  ├─ bd-a3f8e9.1 [epic] "Login Flow"
  │   ├─ bd-a3f8e9.1.1 [task] "Design login UI"
  │   ├─ bd-a3f8e9.1.2 [task] "Backend validation"
  │   └─ bd-a3f8e9.1.3 [task] "Integration tests"
  ├─ bd-a3f8e9.2 [epic] "Password Reset"
  │   └─ bd-a3f8e9.2.1 [task] "Email templates"
  └─ bd-a3f8e9.3 [task] "Update documentation"
```

### CLI Usage
```bash
# All of these work (prefix optional in input):
bd show a3f8e9
bd show bd-a3f8e9
bd show a3f8e9.1
bd show bd-a3f8e9.1.2

# Output always shows prefix:
bd-a3f8e9 [epic] Auth System
  Status: open
  ...

# External references use full ID:
git commit -m "Implement login (bd-a3f8e9.1)"
```

### Collision Characteristics
- **Top-level:** NONE (content-based hash)
- **Epic children:** RARE (epics have natural ownership, sequential creation)
- **When they occur:** Easy to resolve (small scope, clear context)

### Storage
- JSONL stores full hierarchical IDs with prefix: bd-a3f8e9.1.2
- Child counters table: child_counters(parent_id, last_child)
- Counter per parent at any depth

### Limits
- Max depth: 3 levels (prevents over-decomposition)
- Max breadth: Unlimited (tested up to 347 children)
- Max ID length: ~20 chars at depth 3 (bd-a3f8e9.12.34.56)

## Breaking Change
This is a v2.0 feature requiring migration. Provide bd migrate --hash-ids tool.

## Timeline
~8 weeks (Phase 1: Hash IDs 3w, Phase 2: Hierarchical children 3w, Phase 3: Testing 2w)
Simplified from original 9-week estimate due to removal of alias system.

## Dependencies
Should complete after bd-7c5915ae (cleanup validation).


Migration tool: sequential → hash IDs

Create migration tool to convert sequential IDs to hierarchical hash-based IDs.

## Command
```bash
bd migrate --to-hash-ids [--dry-run]
```

## Process
1. For each top-level issue (no parent):
   - Generate hash ID from content
   - Create mapping: bd-1c63eb84 → bd-a3f8e9a2

2. For each child issue (has parent):
   - Find parent's new hash ID
   - Assign sequential child number based on creation order
   - bd-b47c034e (parent: bd-1c63eb84) → bd-a3f8e9a2.1

3. Update all references:
   - Dependencies (blocks, parent-child)
   - Comments (issue_id foreign keys)
   - External refs (if containing old IDs)

4. Preserve:
   - Creation timestamps
   - All content
   - All relationships
   - History in comments

## Output
- Mapping file: old_id → new_id (for reference)
- Updated JSONL with new IDs
- Migration log

## Validation
- Verify all relationships intact
- Check no orphaned issues
- Confirm total count unchanged
- Test rollback procedure

## Safety
- Backup database before migration
- Dry-run mode shows what would change
- Rollback script provided


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.


Delete collision resolution code

Remove ~2,100 LOC of ID collision detection and resolution code (no longer needed with hash IDs).

## Files to Delete Entirely
```
internal/storage/sqlite/collision.go              (~800 LOC)
internal/storage/sqlite/collision_test.go         (~300 LOC)
cmd/bd/autoimport_collision_test.go               (~400 LOC)
```

## Code to Remove from Existing Files

### internal/importer/importer.go
Remove:
- `DetectCollisions()` calls
- `ScoreCollisions()` logic
- `RemapCollisions()` calls
- `handleRename()` function
- All collision-related error handling

Keep:
- Basic import logic
- Exact match detection (idempotent import)

### beads_twoclone_test.go
Remove:
- `TestTwoCloneCollision` (bd-71107098)
- `TestThreeCloneCollision` (bd-cbed9619)
- `TestFiveCloneCollision` (bd-a40f374f)
- All N-way collision tests

### cmd/bd/import.go
Remove:
- `--resolve-collisions` flag
- `--dry-run` collision preview
- Collision reporting

## Issues Closed by This Change
- bd-71107098: Add test for symmetric collision
--89: Content-hash collision resolution
- bd-cbed9619: N-way collision resolution epic
- bd-cbed9619.5: Add ScoreCollisions (already done but now unnecessary)
- bd-cbed9619.4: Make DetectCollisions read-only
- bd-cbed9619.3: ResolveNWayCollisions function
- bd-cbed9619.2: Multi-round import convergence
- bd-cbed9619.1: Multi-round convergence for N-way collisions
- bd-e6d71828: Transaction + retry logic for collisions
- bd-70419816: Test case for symmetric collision

## Verification Steps
1. `grep -r "collision" --include="*.go"` → should only find alias conflicts
2. `go test ./...` → all tests pass
3. `go build ./cmd/bd` → clean build
4. Check LOC reduction: `git diff --stat`

## Expected Metrics
- **Files deleted**: 3
- **LOC removed**: ~2,100
- **Test coverage**: Should increase (less untested code)
- **Binary size**: Slightly smaller

## Caution
Do NOT delete:
- Alias conflict resolution (new code in bd-a6abe1c7)
- Duplicate detection (bd-581b80b3, bd-149) - different from ID collisions
- Merge conflict resolution (bd-7e7ddffa.1, bd-5f483051) - git conflicts, not ID collisions

## Files to Modify
- internal/importer/importer.go (remove collision handling)
- cmd/bd/import.go (remove --resolve-collisions flag)
- beads_twoclone_test.go (remove collision tests)
- Delete: internal/storage/sqlite/collision.go
- Delete: internal/storage/sqlite/collision_test.go
- Delete: cmd/bd/autoimport_collision_test.go

## Testing
- Ensure all remaining tests pass
- Manual test: create issue on two clones, sync → no collisions
- Verify error if somehow hash collision occurs (extremely unlikely)


Update JSONL format to use hash IDs

Update JSONL format to store hierarchical hash-based IDs.

## Changes
- ID field: bd-af78e9a2 (top-level) or bd-af78e9a2.1.2 (hierarchical)
- No alias field needed (removed from original plan)
- All other fields remain the same

## Example
```json
{"id":"bd-af78e9a2","title":"Auth System","type":"epic",...}
{"id":"bd-af78e9a2.1","title":"Login Flow","type":"epic",...}
{"id":"bd-af78e9a2.1.1","title":"Design UI","type":"task",...}
```

## Benefits
- Hierarchical structure visible in JSONL
- Git merge conflicts reduced (different IDs = different lines)
- Easy to grep for epic children: grep "bd-af78e9a2\." issues.jsonl

## Backward Compatibility
Migration tool will convert bd-1c63eb84 → bd-{hash} with mapping preserved.


Add child_counters table to database schema

Add child_counters table to support sequential child ID generation within parent contexts.

## Schema
```sql
CREATE TABLE child_counters (
    parent_id TEXT PRIMARY KEY,
    last_child INTEGER NOT NULL DEFAULT 0,
    FOREIGN KEY (parent_id) REFERENCES issues(id) ON DELETE CASCADE
);
```

## Usage
- Counter per parent (at any depth)
- Atomic increment: INSERT...ON CONFLICT DO UPDATE
- bd-a3f8e9 → .1, .2, .3
- bd-a3f8e9.1 → .1.1, .1.2, .1.3
- Works up to 3 levels deep


Design hash ID generation algorithm

Design and specify the hash-based ID generation algorithm.

## Requirements
- Deterministic: same inputs → same ID
- Collision-resistant: ~2^32 space for 8-char hex
- Fast: <1μs per generation
- Includes timestamp for uniqueness
- Includes creator/workspace for distributed uniqueness

## Proposed Algorithm
```go
func GenerateIssueID(title, desc string, created time.Time, workspaceID string) string {
    h := sha256.New()
    h.Write([]byte(title))
    h.Write([]byte(desc))
    h.Write([]byte(created.Format(time.RFC3339Nano)))
    h.Write([]byte(workspaceID))
    hash := hex.EncodeToString(h.Sum(nil))
    return "bd-" + hash[:8]  // 8-char prefix = 2^32 space
}
```

## Open Questions
1. 8 chars (2^32) or 16 chars (2^64) for collision resistance?
2. Include priority/type in hash? (Pro: more entropy. Con: immutable)
3. How to handle workspace ID generation? (hostname? UUID?)
4. What if title+desc change? (Answer: ID stays same - hash only used at creation)

## Deliverables
- Design doc: docs/HASH_ID_DESIGN.md
- Collision probability analysis
- Performance benchmarks
- Prototype implementation in internal/types/id_generator.go

## Next Session: Continue bd-a5e2bd80.5

We've completed:
- ✅ bd-a5e2bd80.6: Hash ID algorithm (returns full 64-char hash)
- ✅ bd-a5e2bd80.13: child_counters table + getNextChildNumber()
- ✅ Docs updated for 6-char progressive design

**TODO for bd-a5e2bd80.5:**
Implement progressive collision retry in CreateIssue():
1. Try hash[:6] first (bd-a3f2dd)
2. On UNIQUE constraint → try hash[:7] (bd-a3f2dda)
3. On collision again → try hash[:8] (bd-a3f2dda8)
4. Max 3 attempts, then error

Location: internal/storage/sqlite/sqlite.go CreateIssue() around line 748
Pattern: Detect sqlite UNIQUE constraint error, retry with longer hash

See: internal/types/id_generator.go GenerateHashID() - now returns full hash


Update documentation for hash IDs and aliases

Update documentation for hash-based hierarchical ID system.

## Files to Update
- README.md: Quick example of hash IDs and hierarchical children
- QUICKSTART.md: Show bd create with --parent flag
- commands/create.md: Document --parent flag and depth limits
- AGENTS.md: Update examples to use hash ID format
- FAQ.md: Add "Why hash IDs?" section

## Topics to Cover
### Hash IDs
- Why content-based hashing?
- Collision-free guarantees
- Git-style prefix matching
- Example: bd show a3f8e9

### Hierarchical Children
- Epic → child tasks with sequential IDs
- Up to 3 levels deep
- Natural work breakdown structure
- Example: bd-a3f8e9.1.2

### Migration
- How to migrate from sequential IDs
- Backward compatibility (old IDs in comments/docs)
- Timeline and breaking change notice

### Best Practices
- When to use nested epics vs flat tasks
- Epic ownership for collision avoidance
- Using bd tree for visualization
- Querying hierarchies

## Examples
Include real-world examples:
- Small project: 1-level hierarchy (epic → tasks)
- Large project: 2-level (epic → sub-epics → tasks)
- Complex: 3-level (epic → features → stories → tasks)


Add hierarchy visualization commands

Add commands to visualize and navigate hierarchical issue structures.

## Commands

### bd tree <id>
Show hierarchical tree view:
```
bd tree a3f8e9

bd-a3f8e9 [epic] Auth System
├─ bd-a3f8e9.1 [epic] Login Flow
│  ├─ bd-a3f8e9.1.1 [task] Design login UI ✓
│  ├─ bd-a3f8e9.1.2 [task] Backend validation (in progress)
│  └─ bd-a3f8e9.1.3 [task] Integration tests
├─ bd-a3f8e9.2 [epic] Password Reset ✓
└─ bd-a3f8e9.3 [task] Update documentation
```

### bd stats <id> --recursive
Show progress statistics:
```
Auth System (bd-a3f8e9): 7/27 complete (25%)
  Login Flow: 2/3 complete (67%)
  Password Reset: 3/3 complete (100%) ✓
  Documentation: 2/21 complete (10%)
```

### bd list <id> --leaves
Show only leaf nodes (actual work):
```
bd list a3f8e9 --leaves

bd-a3f8e9.1.1 [task] Design login UI
bd-a3f8e9.1.2 [task] Backend validation
...
```

## Sort Order
- Implement numeric comparison of ID components
- Ensure bd-a3f8e9.10 comes after bd-a3f8e9.9 (not lexicographic)


Implement hierarchical child ID generation

Implement sequential child ID generation within parent contexts.

## Function Signature
```go
func (s *SQLiteStorage) getNextChildID(ctx context.Context, parentID string) (string, error)
```

## Logic
1. Insert or update child_counters for parent_id
2. Return incremented counter
3. Format as parentID.{counter}
4. Works at any depth (bd-a3f8e9.1 → bd-a3f8e9.1.5)

## Collision Handling
- In single-player mode: No collisions (sequential)
- In multi-player mode (future): Rare collisions, manual resolution needed
- Epic ownership makes collisions naturally rare

## Integration
- Called from CreateIssue when --parent flag is used
- Validates parent exists and depth <= 3


CLI accepts hash ID prefixes (Git-style)

Implement prefix-optional parsing for hash IDs throughout CLI.

## Parsing Logic
```go
// internal/utils/id_parser.go
func ParseIssueID(input string) (issueID string, err error) {
    // Already has prefix: bd-a3f8e9 or bd-a3f8e9.1.2
    if strings.HasPrefix(input, "bd-") {
        return input, nil
    }

    // Add prefix: a3f8e9 → bd-a3f8e9
    // Works with hierarchical too: a3f8e9.1.2 → bd-a3f8e9.1.2
    return "bd-" + input, nil
}

// Prefix matching for partial IDs
func ResolvePartialID(input string) (fullID string, err error) {
    parsedID := ParseIssueID(input)  // Ensure prefix

    // Query with LIKE for prefix match
    matches := storage.FindByPrefix(parsedID)

    if len(matches) == 0 {
        return "", fmt.Errorf("no issue found matching %q", input)
    }
    if len(matches) > 1 {
        return "", fmt.Errorf("ambiguous ID %q matches: %v", input, matches)
    }
    return matches[0].ID, nil
}
```

## Behavior Examples
```bash
# All these inputs work:
bd show a3f8e9          # Add prefix → bd-a3f8e9
bd show bd-a3f8e9       # Already has prefix
bd show a3f8            # Shorter prefix match
bd show a3f8e9.1        # Hierarchical without prefix
bd show bd-a3f8e9.1.2   # Full hierarchical with prefix

# Output always shows full ID with prefix:
bd-a3f8e9 [epic] Auth System
bd-a3f8e9.1 [epic] Login Flow
bd-a3f8e9.1.2 [task] Backend validation
```

## Error Handling
```bash
# Not found
$ bd show xyz789
Error: no issue found matching "bd-xyz789"

# Ambiguous (multiple matches)
$ bd show a3
Error: ambiguous ID "bd-a3" matches: bd-a3f8e9, bd-a34721, bd-a38f92
Use more characters to disambiguate.
```

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

### Read operations
- bd show <id>
- bd list --id <ids>
- bd dep tree <id>

### Write operations
- bd update <id>
- bd close <id>
- bd reopen <id>
- bd label add <id>
- bd dep add <id1> <id2>
- bd comment add <id>

### Multiple IDs
```bash
# All work with optional prefix:
bd show a3f8e9 b7c2d1          # → bd-a3f8e9 bd-b7c2d1
bd dep add a3f8e9.1 b7c2d1     # → bd-a3f8e9.1 blocks bd-b7c2d1
```

## Display Format
**Always show full ID with prefix** in output for:
- Copy-paste clarity
- External reference (git commits, docs)
- Unambiguous identification

```bash
$ bd list
bd-a3f8e9    [P1] Auth System           [epic] open
bd-b7c2d1    [P2] Fix daemon crash      [bug]  open
bd-1a2b3c4d  [P3] Add logging           [task] open
```

## Files to Create/Modify
- internal/utils/id_parser.go (new - parsing logic)
- cmd/bd/show.go
- cmd/bd/update.go
- cmd/bd/close.go
- cmd/bd/reopen.go
- cmd/bd/dep.go
- cmd/bd/label.go
- cmd/bd/comment.go
- cmd/bd/list.go

## Testing
- Test prefix omitted: a3f8e9 → bd-a3f8e9
- Test prefix included: bd-a3f8e9 → bd-a3f8e9
- Test hierarchical: a3f8e9.1.2 → bd-a3f8e9.1.2
- Test partial match: a3f8 → bd-a3f8e9
- Test ambiguous ID error
- Test not found error
- Test mixed input: bd show a3f8e9 bd-b7c2d1

Implementation complete for CLI direct mode. All read and write commands now accept issue IDs with or without prefix (e.g., '170' or 'bd-a5e2bd80.3').

Completed:
- Created internal/utils/id_parser.go with ParseIssueID and ResolvePartialID
- Updated all CLI commands (show, update, close, reopen, dep, label, comments)
- Added comprehensive tests for ID parsing
- Works with both sequential IDs (current) and hierarchical IDs (for future hash implementation)

Note: RPC/daemon mode still needs prefix in current implementation. RPC handlers need update (bd-a5e2bd80.9 will handle this as part of hash ID rollout).


Update JSONL format to use hash IDs


Implement hash ID generation in CreateIssue function.

## For Top-Level Issues
```go
func generateHashID(prefix, title, description, creator string, timestamp time.Time) string {
    content := fmt.Sprintf("%s|%s|%s|%d", title, description, creator, timestamp.UnixNano())
    hash := sha256.Sum256([]byte(content))
    shortHash := hex.EncodeToString(hash[:4]) // 8 hex chars
    return fmt.Sprintf("%s-%s", prefix, shortHash)
}
```

## For Child Issues
```go
func (s *SQLiteStorage) createChildIssue(parentID string, issue *types.Issue) error {
    // Validate parent exists and depth <= 3
    childNum := s.getNextChildID(parentID)
    issue.ID = fmt.Sprintf("%s.%d", parentID, childNum)
    // ... create issue
}
```

## CLI Integration
```bash
bd create "Auth System"              # → bd-a3f8e9a2
bd create "Login Flow" --parent a3f8e9  # → bd-a3f8e9a2.1
bd create "Design UI" --parent a3f8e9.1 # → bd-a3f8e9a2.1.1
```

## Validation
- Reject depth > 3
- Ensure parent exists
- Check parent is epic type (optional, for UX)

Work completed on feature/hash-ids branch. Reverted from main to avoid breaking changes. Will merge after migration strategy (bd-a5e2bd80.1) is ready.


Design hash ID generation algorithm

## Next Session: Continue bd-0953ea41

We've completed:
- ✅ bd-a5e2bd80.14: Hash ID algorithm (returns full 64-char hash)
- ✅ bd-a5e2bd80.13: child_counters table + getNextChildNumber()
- ✅ Docs updated for 6-char progressive design

**TODO for bd-0953ea41:**
Implement progressive collision retry in CreateIssue():
1. Try hash[:6] first (bd-a3f2dd)
2. On UNIQUE constraint → try hash[:7] (bd-a3f2dda)
3. On collision again → try hash[:8] (bd-a3f2dda8)
4. Max 3 attempts, then error

Location: internal/storage/sqlite/sqlite.go CreateIssue() around line 748
Pattern: Detect sqlite UNIQUE constraint error, retry with longer hash

See: internal/types/id_generator.go GenerateHashID() - now returns full hash


Dogfood: Migrate beads repo to hash IDs

Final validation: migrate the beads project itself to hash-based IDs.

## Purpose
Dogfooding the migration on beads' own issue database to:
1. Validate migration tool works on real data
2. Discover edge cases
3. Verify all workflows still work
4. Build confidence for users

## Pre-Migration Checklist
- [ ] All bd-a5e2bd80 child tasks completed
- [ ] All tests pass: `go test ./...`
- [ ] Migration tool tested on test databases
- [ ] Documentation updated
- [ ] MCP server updated and published
- [ ] Clean git status

## Migration Steps

### 1. Create Backup
```bash
# Backup database
cp -r .beads .beads.backup-1761798568

# Backup JSONL
cp .beads/beads.jsonl .beads/beads.jsonl.backup

# Create git branch for migration
git checkout -b hash-id-migration
git add .beads.backup-*
git commit -m "Pre-migration backup"
```

### 2. Run Migration (Dry Run)
```bash
bd migrate --hash-ids --dry-run > migration-plan.txt
cat migration-plan.txt

# Review:
# - Number of issues to migrate
# - Hash collision check (should be zero)
# - Text reference updates
# - Dependency updates
```

### 3. Run Migration (Real)
```bash
bd migrate --hash-ids 2>&1 | tee migration-log.txt

# Expected output:
# ✓ Backup created: .beads/beads.db.backup-1234567890
# ✓ 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/beads.jsonl
# ✓ Migration complete!
```

### 4. Validation

#### Database Integrity
```bash
# Check all issues have hash IDs
bd list | grep -v "bd-[a-f0-9]\{8\}" && echo "FAIL: Non-hash IDs found"

# Check all issues have aliases
sqlite3 .beads/beads.db "SELECT COUNT(*) FROM issues WHERE alias IS NULL"
# Should be 0

# Check no alias duplicates
sqlite3 .beads/beads.db "SELECT alias, COUNT(*) FROM issues GROUP BY alias HAVING COUNT(*) > 1"
# Should be empty
```

#### Functionality Tests
```bash
# Test show by hash ID
bd show bd-

# Test show by alias
bd show #1

# Test create new issue
bd create "Test issue after migration" -p 2
# Should get hash ID + alias

# Test update
bd update #1 --priority 1

# Test dependencies
bd dep tree #1

# Test export
bd export
git diff .beads/beads.jsonl
# Should show hash IDs
```

#### Text Reference Validation
```bash
# Check that old IDs were updated in descriptions
grep -r "bd-[0-9]\{1,3\}[^a-f0-9]" .beads/beads.jsonl && echo "FAIL: Old ID format found"

# Verify hash ID references exist
grep -o "bd-[a-f0-9]\{8\}" .beads/beads.jsonl | sort -u | wc -l
# Should match number of hash IDs
```

### 5. Commit Migration
```bash
git add .beads/beads.jsonl .beads/beads.db
git commit -m "Migrate to hash-based IDs (v2.0)

- Migrated 150 issues to hash IDs
- Preserved aliases (#1-#150)
- Updated 87 dependencies
- Updated 234 text references
- Zero hash collisions

Migration log: migration-log.txt"

git push origin hash-id-migration
```

### 6. Create PR
```bash
gh pr create --title "Migrate to hash-based IDs (v2.0)" --body "## Summary
Migrates beads project to hash-based IDs as part of v2.0 release.

## Migration Stats
- Issues migrated: 150
- Dependencies updated: 87
- Text references updated: 234
- Hash collisions: 0
- Aliases assigned: 150

## Validation
- ✅ All tests pass
- ✅ Database integrity verified
- ✅ All workflows tested (show, update, create, deps)
- ✅ Text references updated correctly
- ✅ Export produces valid JSONL

## Files Changed
- `.beads/beads.jsonl` - Hash IDs in all entries
- `.beads/beads.db` - Schema updated with aliases

## Rollback
If issues arise:
\`\`\`bash
mv .beads.backup-1234567890 .beads
bd export
\`\`\`

See migration-log.txt for full details."
```

### 7. Merge and Cleanup
```bash
# After PR approval
git checkout main
git merge hash-id-migration
git push origin main

# Tag release
git tag v2.0.0
git push origin v2.0.0

# Cleanup
rm migration-log.txt migration-plan.txt
git checkout .beads.backup-*  # Keep in git history
```

## Rollback Procedure
If migration fails or has issues:

```bash
# Restore backup
mv .beads .beads.failed-migration
mv .beads.backup-1234567890 .beads

# Regenerate JSONL
bd export

# Verify restoration
bd list
git diff .beads/beads.jsonl

# Cleanup
git checkout hash-id-migration
git reset --hard main
```

## Post-Migration Communication

### GitHub Issue/Discussion
```markdown
## Beads v2.0 Released: Hash-Based IDs

We've migrated beads to hash-based IDs! 🎉

**What changed:**
- Issues now use hash IDs (bd-af78e9a2) instead of sequential (bd-cb64c226.3)
- Human-friendly aliases (#42) for easy reference
- Zero collision risk in distributed workflows

**Action required:**
If you have a local clone, you need to migrate:

\`\`\`bash
git pull origin main
bd migrate --hash-ids
git push origin main
\`\`\`

See MIGRATION.md for details.

**Benefits:**
- ✅ No more ID collisions
- ✅ Work offline without coordination
- ✅ Simpler codebase (-2,100 LOC)

Questions? Reply here or see docs/HASH_IDS.md
```

## Success Criteria
- [ ] Migration completes without errors
- [ ] All validation checks pass
- [ ] PR merged to main
- [ ] v2.0.0 tagged and released
- [ ] Documentation updated
- [ ] Community notified
- [ ] No rollback needed within 1 week

## Files to Create
- migration-log.txt (transient)
- migration-plan.txt (transient)

## Timeline
Execute after all other bd-a5e2bd80 tasks complete (estimated: ~8 weeks from start)


Update MCP server for hash IDs

Update beads-mcp server to support hash IDs and aliases.

## Changes Needed

### 1. MCP Function Signatures (No Change)
Functions already use issue IDs as strings, so they work with hash IDs:

```python
# These already work!
beads_show(issue_id: str)           # Accepts bd-af78e9a2 or #42
beads_update(issue_id: str, ...)    # Accepts both formats
beads_close(issue_ids: List[str])   # Accepts both formats
```

### 2. Add Alias Resolution Helper
File: integrations/beads-mcp/src/beads_mcp/server.py

```python
def resolve_issue_id(issue_id: str) -> str:
    """Resolve alias to hash ID if needed."""
    # Hash ID: pass through
    if issue_id.startswith('bd-') and len(issue_id) == 11:
        return issue_id

    # Alias: #42 or 42
    alias_str = issue_id.lstrip('#')
    try:
        alias = int(alias_str)
        # Call bd to resolve
        result = subprocess.run(
            ['bd', 'alias', 'find', f'bd-{alias}'],
            capture_output=True, text=True
        )
        if result.returncode == 0:
            return result.stdout.strip()
    except ValueError:
        pass

    # Invalid format
    raise ValueError(f"Invalid issue ID: {issue_id}")
```

### 3. Update Response Formatting
Show aliases in responses:

```python
@server.call_tool()
async def beads_show(issue_id: str) -> List[TextContent]:
    resolved_id = resolve_issue_id(issue_id)

    result = subprocess.run(['bd', 'show', resolved_id], ...)

    # Parse response and add alias info
    # Format: "bd-af78e9a2 (alias: #42)"
    ...
```

### 4. Add beads_alias_* Functions

```python
@server.call_tool()
async def beads_alias_list() -> List[TextContent]:
    """List all alias mappings."""
    result = subprocess.run(['bd', 'alias', 'list'], ...)
    return [TextContent(type="text", text=result.stdout)]

@server.call_tool()
async def beads_alias_set(alias: int, issue_id: str) -> List[TextContent]:
    """Manually assign alias to issue."""
    result = subprocess.run(['bd', 'alias', 'set', str(alias), issue_id], ...)
    return [TextContent(type="text", text=result.stdout)]

@server.call_tool()
async def beads_alias_compact() -> List[TextContent]:
    """Compact aliases to fill gaps."""
    result = subprocess.run(['bd', 'alias', 'compact'], ...)
    return [TextContent(type="text", text=result.stdout)]
```

### 5. Update Documentation
File: integrations/beads-mcp/README.md

```markdown
## Issue IDs (v2.0+)

The MCP server accepts both hash IDs and aliases:

```python
# Using hash IDs
await beads_show(issue_id="bd-af78e9a2")

# Using aliases
await beads_show(issue_id="#42")
await beads_show(issue_id="42")  # Shorthand
```

## Alias Management

New functions for alias control:

- `beads_alias_list()` - List all alias mappings
- `beads_alias_set(alias, issue_id)` - Manually assign alias
- `beads_alias_compact()` - Compact aliases to fill gaps

## Migration

After migrating to hash IDs:
1. Update beads-mcp: `pip install --upgrade beads-mcp`
2. Restart MCP server
3. All existing workflows continue to work
```

### 6. Version Compatibility
Detect and handle both v1.x and v2.0 formats:

```python
def detect_beads_version() -> str:
    """Detect if beads is using sequential or hash IDs."""
    result = subprocess.run(['bd', 'list', '-n', '1'], ...)
    first_id = parse_first_issue_id(result.stdout)

    if first_id.startswith('bd-') and len(first_id) > 11:
        return '2.0'  # Hash ID
    else:
        return '1.x'  # Sequential ID

# On startup
beads_version = detect_beads_version()
logger.info(f"Detected beads version: {beads_version}")
```

## Testing

### Unit Tests
File: integrations/beads-mcp/tests/test_hash_ids.py

```python
def test_resolve_hash_id():
    """Hash IDs pass through unchanged."""
    assert resolve_issue_id("bd-af78e9a2") == "bd-af78e9a2"

def test_resolve_alias():
    """Aliases resolve to hash IDs."""
    # Mock bd alias find command
    assert resolve_issue_id("#42") == "bd-af78e9a2"
    assert resolve_issue_id("42") == "bd-af78e9a2"

def test_invalid_id():
    """Invalid IDs raise ValueError."""
    with pytest.raises(ValueError):
        resolve_issue_id("invalid")
```

### Integration Tests
```python
async def test_show_with_hash_id(mcp_server):
    result = await mcp_server.beads_show(issue_id="bd-af78e9a2")
    assert "bd-af78e9a2" in result[0].text

async def test_show_with_alias(mcp_server):
    result = await mcp_server.beads_show(issue_id="#42")
    assert "bd-af78e9a2" in result[0].text  # Resolved
```

## Backward Compatibility
The MCP server should work with both:
- Beads v1.x (sequential IDs)
- Beads v2.0+ (hash IDs)

Detection happens at runtime based on issue ID format.

## Files to Modify
- integrations/beads-mcp/src/beads_mcp/server.py
- integrations/beads-mcp/README.md
- integrations/beads-mcp/tests/test_hash_ids.py (new)
- integrations/beads-mcp/pyproject.toml (bump version)

## Deployment
```bash
cd integrations/beads-mcp
# Bump version to 2.0.0
poetry version 2.0.0
# Publish to PyPI
poetry publish --build
```


Remove collision resolution system (~724 LOC in collision.go)

The entire collision resolution system can be removed now that hash IDs eliminate real collisions:
- internal/storage/sqlite/collision.go (724 lines)
- Functions: DetectCollisions, ScoreCollisions, RemapCollisions, remapCollisionsOnce
- Types: CollisionResult, CollisionDetail, RenameDetail
- Hash-based IDs only have accidental collisions that are resolved by adding another character from hash
- This removes ~2,100 LOC of complex collision resolution logic mentioned in HASH_ID_DESIGN.md

Completed: Removed collision.go code from 704 → 138 lines. Deleted RemapCollisions, ScoreCollisions, and all remapping logic. Only DetectCollisions remains for idempotent import checking.


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

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

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


Remove sequential ID code path and simplify to hash-only

Now that we have adaptive hash IDs with 4-char starting length, sequential IDs are obsolete.

**Remove:**
- Sequential ID generation logic (nextSequentialID, issue_counters table)
- id_mode config (always use hash)
- Migration complexity for sequential→hash
- Tests for sequential IDs

**Simplify:**
- ID generation code (just adaptive hash)
- Database schema (no counters table)
- Documentation (remove sequential references)
- Init command (no --id-mode flag needed)

**Benefits:**
- Less code to maintain
- Simpler mental model
- No migration path needed
- Hash IDs are superior in every way with adaptive length

**Discovered from:** bd-ea2a13 (adaptive ID length implementation showed sequential IDs are no longer needed)


bd list should output exactly one line per issue (no header/footer)

Another agent ran into confusion: `bd list --status all | wc -l` output 509 lines, but there are only 165 issues. The extra lines are headers/footers.

Agents expect `bd list` to output exactly one line per issue for easy piping/counting. Current output includes decorative headers and footers that break this expectation.

Solution: Either remove headers/footers entirely, or add a `--no-header` flag that suppresses them for agent-friendly output.


bd daemons list hangs indefinitely

The 'bd daemons list' command appears to hang and never complete. Needs investigation into why it's blocking and add timeout or fix the underlying issue.


Update LINTING.md with current baseline

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


Unit tests for FileWatcher

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


Add test case for symmetric collision (both clones create same ID simultaneously)

TestTwoCloneCollision demonstrates the problem, but we need a simpler unit test for the collision resolver itself.

Test should verify:
- Two issues with same ID, different content
- Content hash determines winner deterministically
- Result is same regardless of which clone imports first
- No title swapping occurs

This can be a simpler test than the full integration test.


test without prefix


Address gosec security warnings (102 issues)

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

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


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.


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`


Add --parent flag to bd list command

Add --parent flag to bd list to filter issues by parent ID.

## Usage
```bash
bd list --parent 165        # List all children of bd-a5e2bd80
bd list --parent a3f8e9     # List all children (with prefix match)
bd list --parent a3f8e9 -s open  # Combine with status filter
```

## Behavior
- Show only direct children (not recursive by default)
- Add --recursive flag for full subtree
- Works with current parent-child dependency type
- With hierarchical IDs (bd-a5e2bd80), this will be natural: filter by ID prefix

## Implementation
Query dependencies table:
```sql
SELECT i.* FROM issues i
JOIN dependencies d ON i.id = d.issue_id
WHERE d.depends_on_id = ? AND d.type = 'parent-child'
```

With hierarchical IDs (post bd-a5e2bd80):
```sql
SELECT * FROM issues
WHERE id LIKE 'bd-a3f8e9.%'  -- Much simpler!
```

## Related
- Complements bd tree command (bd-a5e2bd80.16)
- Will be cleaner with hierarchical IDs from bd-a5e2bd80
- Could add --leaves flag (show only leaf nodes, no sub-epics)

## Files
- cmd/bd/list.go (add --parent flag)
- May want to add --recursive flag too


Audit Current Cache Usage

Understand exactly what code depends on the storage cache

- Document showing all cache dependencies
- Confirmation that removing cache won't break MCP
- List of tests that need updating

Files to examine:
- internal/rpc/server_cache_storage.go (cache implementation)
- internal/rpc/client.go (how req.Cwd is set)
- internal/rpc/server_*.go (all getStorageForRequest calls)
- integrations/beads-mcp/ (MCP multi-repo logic)

Tasks:
- Document all callers of getStorageForRequest()
- Verify req.Cwd is only set by RPC client for database discovery
- Confirm MCP server doesn't rely on multi-repo cache behavior
- Check if any tests assume multi-repo routing
- Review environment variables: BEADS_DAEMON_MAX_CACHE_SIZE, BEADS_DAEMON_CACHE_TTL, BEADS_DAEMON_MEMORY_THRESHOLD_MB


Final validation test


Remove SyncAllCounters() and all call sites

SyncAllCounters() is already a no-op (bd-aa744b) since hash IDs don't use counters. Clean up all the dead code:

Remove:
- internal/storage/sqlite/sqlite.go:636 - SyncAllCounters() implementation
- internal/storage/memory/memory.go:947 - SyncAllCounters() implementation
- All call sites throughout codebase (20+ locations found)
- Remove from Storage interface if it exists

This is safe cleanup of code that already does nothing.


Add docs/architecture/event_driven.md

Copy event_driven_daemon.md into docs/ folder. Add to documentation index.


Remove Daemon Storage Cache

The daemon's multi-repo storage cache is the root cause of stale data bugs. Since global daemon is deprecated, we only ever serve one repository, making the cache unnecessary complexity. This epic removes the cache entirely for simpler, more reliable direct storage access.

For local daemon (single repository), eliminate the cache entirely:
- Use s.storage field directly (opened at daemon startup)
- Remove getStorageForRequest() routing logic
- Remove server_cache_storage.go entirely (~300 lines)
- Remove cache-related tests
- Simplify Server struct

Benefits:
✅ No staleness bugs: Always using live SQLite connection
✅ Simpler code: Remove ~300 lines of cache management
✅ Easier debugging: Direct storage access, no cache indirection
✅ Same performance: Cache was always 1 entry for local daemon anyway

- Daemon has no storage cache code
- All tests pass
- MCP integration works
- No stale data bugs
- Documentation updated
- Performance validated


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.


Performance Validation

Confirm no performance regression from cache removal

- Benchmarks show no significant regression
- Document performance characteristics
- Confirm single SQLite connection is reused

Benchmarks: go test -bench=. -benchmem ./internal/rpc/...

Metrics to track:
- Request latency (p50, p99)
- Throughput (requests/sec)
- Memory usage
- SQLite connection overhead

Expected results:
- Latency: Same or better (no cache overhead)
- Throughput: Same (cache was always 1 entry)
- Memory: Lower (no cache structs)
- Connection overhead: Zero (single connection reused)


Delete server_cache_storage.go

Remove the entire cache implementation file (~286 lines)

- File deleted from repository
- No compilation errors
- No references to deleted functions

Functions being removed:
- StorageCacheEntry struct
- evictStaleStorage() - LRU eviction
- evictCacheBasedOnMemory() - memory pressure eviction
- getStorageForRequest() - cache lookup and routing
- findDatabaseForCwd() - database discovery
- evictStorageForRequest() - manual eviction


Remove Storage Cache from Server Struct

Eliminate cache fields and use s.storage directly

- Server struct has no cache fields
- NewServer() doesn't initialize cache
- Start() doesn't run cache cleanup goroutines
- Stop() only closes single s.storage

Changes needed:
- Remove cache-related fields from Server struct in server_core.go
- Remove cache size/TTL parsing from env vars in NewServer()
- Remove cleanup ticker goroutine from Start()
- Remove cache cleanup logic from Stop()


Audit Current Cache Usage

AUDIT COMPLETE

getStorageForRequest() callers: 17 production + 11 test
- server_issues_epics.go: 8 calls
- server_labels_deps_comments.go: 4 calls
- server_export_import_auto.go: 2 calls
- server_compact.go: 2 calls
- server_routing_validation_diagnostics.go: 1 call
- server_eviction_test.go: 11 calls (DELETE entire file)

Pattern everywhere: store, err := s.getStorageForRequest(req) → store := s.storage

req.Cwd usage: Only for multi-repo routing. Local daemon always serves 1 repo, so routing is unused.

MCP server: Uses separate daemons per repo (no req.Cwd usage found). NOT affected by cache removal.

Cache env vars to deprecate:
- BEADS_DAEMON_MAX_CACHE_SIZE (used in server_core.go:63)
- BEADS_DAEMON_CACHE_TTL (used in server_core.go:72)
- BEADS_DAEMON_MEMORY_THRESHOLD_MB (used in server_cache_storage.go:47)

Server struct fields to remove:
- storageCache, cacheMu, maxCacheSize, cacheTTL, cleanupTicker, cacheHits, cacheMisses

Tests to delete:
- server_eviction_test.go (entire file - 9 tests)
- limits_test.go cache assertions

Special consideration: ValidateDatabase endpoint uses findDatabaseForCwd() outside cache. Verify if used, then remove or inline.

Safe to proceed with removal - cache always had 1 entry in local daemon model.


Remove Storage Cache from Server Struct


Audit Current Cache Usage


Verify MCP Server Compatibility

Ensure MCP server works with cache-free daemon

- MCP integration tests pass
- Documented confirmation of MCP multi-repo strategy
- No regressions in MCP functionality

Test scenarios:
1. Single repo workflow: MCP with one project directory
2. Multi-repo workflow: MCP switching between projects (uses separate daemons)
3. Daemon restart: Verify no stale data after daemon restart

Questions to answer:
- Does MCP rely on req.Cwd routing to single daemon for multiple repos?
- Or does MCP start separate daemons per repo (recommended)?
- Do existing MCP tests pass?

Files to review:
- integrations/beads-mcp/src/beads_mcp/server.py
- integrations/beads-mcp/tests/test_multi_project_switching.py


Update Metrics and Health Endpoints

Remove cache-related metrics from health/metrics endpoints

- bd daemon --health output has no cache fields
- bd daemon --metrics output has no cache fields
- No compilation errors

Changes needed:
- Remove cache_size from health endpoint in server_routing_validation_diagnostics.go
- Remove cache_size, cache_hits, cache_misses from metrics endpoint
- Remove CacheHits and CacheMisses fields from internal/rpc/metrics.go


Remove Cache-Related Tests

Delete or update tests that assume multi-repo caching

- server_eviction_test.go deleted
- limits_test.go updated (no cache assertions)
- All tests pass: go test ./internal/rpc/...

Tests to delete:
- TestCacheEviction
- TestMemoryPressureEviction
- TestMtimeInvalidation
- TestConcurrentCacheAccess
- TestSubdirectoryCanonicalization
- TestManualEviction
- TestLRUEviction

Files to update:
- internal/rpc/server_eviction_test.go (DELETE entire file)
- internal/rpc/limits_test.go (remove cache assertions)


Fix multi-round convergence for N-way collisions

## Problem

N-way collision resolution is working (IDs get remapped correctly), but clones don't fully converge after a single final pull. Each clone is missing some issues that other clones have.

From TestFiveCloneCollision results:
- Clone A has: A, B
- Clone B has: A, B
- Clone C has: A, B, C
- Clone D has: A, B, C, D
- Clone E has: A, B, C, E

**Expected**: All clones should have A, B, C, D, E after final pull.

## Root Cause

The current sync workflow does:
1. Each clone syncs in order (resolving collisions locally)
2. Final pull to get all changes

But the final pull itself may need import with collision resolution, which creates new commits. These new commits aren't propagated to other clones, so they remain incomplete.

## Proposed Solution

**Option 1: Multi-round final sync**
- After final pull, do additional sync rounds until all clones converge
- Check convergence by comparing issue counts or content hashes
- Maximum N rounds for N clones

**Option 2: Iterative pull-import-push**
- Each clone: pull → import with --resolve-collisions → push
- Repeat until no new changes
- Guaranteed convergence but may create commit spam

**Option 3: Fix auto-import to be truly idempotent**
- Ensure importing same JSONL multiple times produces no new commits
- May require smarter content-based deduplication

## Acceptance Criteria

- TestFiveCloneCollision passes without t.Skip
- All N clones have all N issues after convergence
- Convergence happens in bounded rounds (≤ N)
- No data loss or duplication
- Works for arbitrary N (tested with 5, 10 clones)

## Impact

This is the final blocker for bd-cbed9619 epic completion.


Implement content-first idempotent import

## Overview
Phase 4: Refactor import to be content-first and idempotent, ensuring importing same JSONL multiple times always converges correctly.

## Current Problem
Current import is ID-first:
1. Look up by ID
2. If exists, update
3. If not exists, create

This causes issues when:
- Same content arrives with different IDs (renames not detected)
- Multiple rounds of import needed for convergence
- Import order affects final state

## Solution
Make import content-first and idempotent:
1. Hash all incoming and existing issues
2. Match by content hash first (detect renames)
3. Handle ID conflicts second (using global resolution)
4. Ensure importing same data multiple times = no-op

## Implementation Tasks

### 1. Refactor ImportIssues to be content-first
File: internal/importer/importer.go

```go
func ImportIssues(ctx context.Context, dbPath string, store storage.Storage,
    issues []*types.Issue, opts Options) (*Result, error) {

    result := &Result{...}

    sqliteStore, needCloseStore, err := getOrCreateStore(ctx, dbPath, store)
    if err != nil {
        return nil, err
    }
    if needCloseStore {
        defer func() { _ = sqliteStore.Close() }()
    }

    // Phase 1: Compute content hashes for all incoming issues
    for _, issue := range issues {
        issue.ContentHash = issue.ComputeContentHash()
    }

    // Phase 2: Build content hash maps
    incomingByHash := buildHashMap(issues)
    dbIssues, _ := sqliteStore.SearchIssues(ctx, "", types.IssueFilter{})
    dbByHash := buildHashMap(dbIssues)
    dbByID := buildIDMap(dbIssues)

    // Phase 3: Content-first matching
    var newIssues []*types.Issue
    var idConflicts []*CollisionDetail

    for hash, incoming := range incomingByHash {
        if existing, found := dbByHash[hash]; found {
            // Same content exists
            if existing.ID == incoming.ID {
                // Exact match - idempotent case
                result.Unchanged++
            } else {
                // Same content, different ID - rename detected
                // Delete old ID, keep new ID (incoming is canonical)
                if err := handleRename(ctx, sqliteStore, existing, incoming); err != nil {
                    return nil, err
                }
                result.Updated++
            }
        } else {
            // New content - check for ID collision
            if existingWithID, found := dbByID[incoming.ID]; found {
                // ID exists but different content - collision
                idConflicts = append(idConflicts, &CollisionDetail{
                    ID:            incoming.ID,
                    IncomingIssue: incoming,
                    ExistingIssue: existingWithID,
                })
            } else {
                // Truly new issue
                newIssues = append(newIssues, incoming)
            }
        }
    }

    // Phase 4: Resolve ID conflicts using global algorithm
    if len(idConflicts) > 0 {
        if !opts.ResolveCollisions {
            return nil, fmt.Errorf("collision detected")
        }

        idMapping, err := sqlite.ResolveNWayCollisions(ctx, sqliteStore,
            idConflicts, issues)
        if err != nil {
            return nil, err
        }

        if err := applyIDMapping(ctx, sqliteStore, idMapping); err != nil {
            return nil, err
        }

        result.IDMapping = idMapping
        result.Collisions = len(idConflicts)
    }

    // Phase 5: Create new issues
    if len(newIssues) > 0 {
        if err := sqliteStore.CreateIssues(ctx, newIssues, "import"); err != nil {
            return nil, err
        }
        result.Created = len(newIssues)
    }

    // Phase 6: Import dependencies, labels, comments (existing logic)
    // ...

    return result, nil
}
```

### 2. Implement helper functions

```go
// buildHashMap creates a map of content hash → issue
func buildHashMap(issues []*types.Issue) map[string]*types.Issue {
    result := make(map[string]*types.Issue)
    for _, issue := range issues {
        result[issue.ContentHash] = issue
    }
    return result
}

// buildIDMap creates a map of ID → issue
func buildIDMap(issues []*types.Issue) map[string]*types.Issue {
    result := make(map[string]*types.Issue)
    for _, issue := range issues {
        result[issue.ID] = issue
    }
    return result
}

// handleRename handles content match with different IDs
func handleRename(ctx context.Context, s *SQLiteStorage,
    existing *types.Issue, incoming *types.Issue) error {

    // Delete old ID
    if err := s.DeleteIssue(ctx, existing.ID); err != nil {
        return fmt.Errorf("failed to delete old ID %s: %w", existing.ID, err)
    }

    // Create with new ID
    if err := s.CreateIssue(ctx, incoming, "import-rename"); err != nil {
        return fmt.Errorf("failed to create renamed issue %s: %w",
            incoming.ID, err)
    }

    // Update references from old ID to new ID
    idMapping := map[string]string{existing.ID: incoming.ID}
    return updateReferences(ctx, s, idMapping)
}
```

### 3. Add idempotency tests

Test cases:
1. Import same JSONL twice → second import reports all Unchanged
2. Import, modify DB, import again → reports Updated
3. Import with rename, import again → idempotent
4. Import with collision resolution, import again → idempotent

### 4. Update handleCollisions to use new flow
Current handleCollisions in importer.go needs to be updated to:
- Use content-first matching
- Call new ResolveNWayCollisions
- Apply results using ApplyCollisionResolution

## Acceptance Criteria
- Import matches by content hash before checking IDs
- Importing same JSONL multiple times is idempotent (reports Unchanged)
- Rename detection works (same content, different ID)
- ID conflicts resolved using global algorithm
- Result.Unchanged correctly tracks idempotent imports
- TestThreeCloneCollision passes
- All existing import tests still pass

## Testing Strategy

### Unit Tests
- buildHashMap correctly indexes by content hash
- buildIDMap correctly indexes by ID
- handleRename deletes old, creates new, updates references

### Integration Tests
- Import same data twice → idempotent
- Import renamed issue → handled correctly
- Import with collision → resolved globally
- Final pull after 3-way collision → all clones converge

### Property Tests
- Idempotency: Import(x); Import(x) ≡ Import(x)
- Commutativity: Import(a); Import(b) ≡ Import(b); Import(a) (for non-colliding issues)
- Convergence: After N rounds of sync, all clones identical

## Files to Modify
- internal/importer/importer.go (major refactor of ImportIssues)
- internal/importer/importer_test.go (new tests)
- cmd/bd/import_bug_test.go (update for new behavior)

## Dependencies
- Requires bd-cbed9619.5 (ContentHash field)
- Requires bd-cbed9619.4 (read-only detection)
- Requires bd-cbed9619.3 (global resolution)

## Risk Mitigation
Major refactor of import logic. Recommend:
1. Comprehensive tests before modifying
2. Feature flag to enable/disable
3. Keep old import code path for rollback
4. Test with all existing import tests
5. Manual testing with real repositories

## Success Metrics
After this phase:
- TestThreeCloneCollision should PASS
- All clones converge after final pull
- Import is demonstrably idempotent
- No data loss in N-way scenarios


Implement global N-way collision resolution algorithm

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

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

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

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

## Implementation Tasks

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

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

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

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

    idMapping := make(map[string]string)

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

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

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

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

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

    return idMapping, nil
}
```

### 2. Implement helper functions

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

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

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

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

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

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

### 5. Add comprehensive tests

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

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

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

## Testing Strategy

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

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

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

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

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


Make DetectCollisions read-only (separate detection from modification)

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

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

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

## Implementation Tasks

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Add content-addressable identity to Issue type

## Overview
Phase 1: Add content hashing to enable global identification of issues regardless of their assigned IDs.

## Current Problem
The system identifies issues only by ID (e.g., test-1, test-2). When multiple clones create the same ID with different content, there's no way to identify that these are semantically different issues without comparing all fields.

## Solution
Add a ContentHash field to the Issue type that represents the canonical content fingerprint.

## Implementation Tasks

### 1. Add ContentHash field to Issue type
File: internal/types/types.go
```go
type Issue struct {
    ID          string
    ContentHash string  // SHA256 of canonical content
    // ... existing fields
}
```

### 2. Add content hash computation method
Use existing hashIssueContent from collision.go:186 as foundation:
```go
func (i *Issue) ComputeContentHash() string {
    return hashIssueContent(i)
}
```

### 3. Compute hash at creation time
- Modify CreateIssue to compute and store ContentHash
- Modify CreateIssues (batch) to compute hashes

### 4. Compute hash at import time
- Modify ImportIssues to compute ContentHash for all incoming issues
- Store hash in database

### 5. Add database column
- Add migration to add content_hash column to issues table
- Update SELECT/INSERT statements to include content_hash
- Index on content_hash for fast lookups

### 6. Populate existing issues
- Add migration step to compute ContentHash for all existing issues
- Use hashIssueContent function

## Acceptance Criteria
- Issue type has ContentHash field
- Hash is computed automatically at creation time
- Hash is computed for imported issues
- Database stores content_hash column
- All existing issues have non-empty ContentHash
- Hash is deterministic (same content → same hash)
- Hash excludes ID, timestamps (only semantic content)

## Files to Modify
- internal/types/types.go
- internal/storage/sqlite/sqlite.go (schema, CreateIssue, CreateIssues)
- internal/storage/sqlite/migrations.go (new migration)
- internal/importer/importer.go (compute hash during import)
- cmd/bd/create.go (compute hash at creation)

## Testing
- Unit test: same content produces same hash
- Unit test: different content produces different hash
- Unit test: hash excludes ID and timestamps
- Integration test: hash persists in database
- Migration test: existing issues get hashes populated


Remove or deprecate 'bd renumber' command

The renumber command was designed for sequential IDs to compact the ID space. With hash IDs, this command is obsolete. Either remove it entirely or make it error with a helpful message that it's no longer needed.


Test mixed ID system


Add embedding generation for duplicate detection

Use embeddings for scalable duplicate detection.

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

Much cheaper than LLM comparisons for large databases.

Files: internal/embeddings/ (new package)


Update LINTING.md with current baseline

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


Import validation falsely reports data loss on collision resolution

## Problem

Post-import validation reports 'data loss detected!' when import count reduces due to legitimate collision resolution.

## Example

```
Import complete: 1 created, 8 updated, 142 unchanged, 19 skipped, 1 issues remapped
Post-import validation failed: import reduced issue count: 165 → 164 (data loss detected!)
```

This was actually successful collision resolution (bd-70419816 duplicated → remapped to bd-5dae5504), not data loss.

## Impact

- False alarms waste investigation time
- Undermines confidence in import validation
- Confuses users/agents about sync health

## Solution

Improve validation to distinguish:
- Collision-resolution merges (expected count reduction)
- Actual data loss (unexpected disappearance)

Track remapped issue count and adjust expected post-import count accordingly.


Fix dep_add.txt scripttest failure

The dep_add.txt scripttest is failing because it references test-2 which doesn't exist:

Error: no issue found matching "test-2"

This appears to be a test data issue where the test expects sequential IDs (test-1, test-2) but we're now using hash IDs (test-d8a8, etc.).

File: cmd/bd testdata/dep_add.txt:5

Need to either:
1. Update test to use actual created issue IDs
2. Update test to create issues with explicit IDs
3. Fix test to capture the generated IDs and use them


Add MCP functions for repair commands

Add repair commands to beads-mcp for agent access:
- beads_resolve_conflicts()
- beads_find_duplicates()
- beads_detect_pollution()
- beads_validate()

Files: integrations/beads-mcp/src/beads_mcp/server.py


Port N-way test to verify hash IDs prevent collisions

Create TestHashIDsNClones that:
- Sets up 5 clones like TestFiveCloneCollision
- Each clone creates 10 issues offline (different titles)
- Sync in random order
- Verify all clones converge to 50 issues with ZERO collisions
- Verify all IDs are hash-based (bd-a3f8e9a2 format)
- Prove we need zero collision resolution rounds

This replaces bd-a5e2bd80.10 which was too abstract. We can port the existing beads_nway_test.go structure but expect zero collisions.


Rapid 3


Replace getStorageForRequest with Direct Access

Replace all getStorageForRequest(req) calls with s.storage

- No references to getStorageForRequest() in codebase (except in deleted file)
- All handlers use s.storage directly
- Code compiles without errors

Files to update:
- internal/rpc/server_issues_epics.go (~8 calls)
- internal/rpc/server_labels_deps_comments.go (~4 calls)
- internal/rpc/server_compact.go (~2 calls)
- internal/rpc/server_export_import_auto.go (~2 calls)
- internal/rpc/server_routing_validation_diagnostics.go (~1 call)

Pattern: store, err := s.getStorageForRequest(req) → store := s.storage


Batch test 4


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


bd repair-deps - Orphaned dependency cleaner

Find and fix orphaned dependency references.

Implementation:
- Scan all issues for dependencies pointing to non-existent issues
- Report orphaned refs
- Auto-fix with --fix flag
- Interactive mode with --interactive

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


bd validate - Comprehensive health check

Run all validation checks in one command.

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

Supports --fix-all for auto-repair.

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

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


Rapid 4


Hash IDs as default: testing, release, and migration

Drive hash IDs to be the default for Beads. Currently on feature/hash-ids branch, opt-in via --to-hash-ids flag, requires explicit migration. Target repos: ~/src/fred/beads, ~/wyvern, ~/src/wyvern. Deployment: homebrew/bin, MCP servers, all daemons at 0.19.1

1. Checkout feature/hash-ids 2. Bump to 0.19.1 on branch 3. Build/install to homebrew 4. Update MCP servers 5. Restart daemons 6. Migrate all repos 7. Test workers 8. Make hash IDs default 9. Verify

All 3 repos migrated, all daemons at 0.19.1, workers functional, hash IDs default


Add transaction + retry logic for N-way collision resolution

## Problem
Current N-way collision resolution fails on UNIQUE constraint violations during convergence rounds when 5+ clones sync. The RemapCollisions function is non-atomic and performs operations sequentially:
1. Delete old issues (CASCADE deletes dependencies)
2. Create remapped issues (can fail with UNIQUE constraint)
3. Recreate dependencies
4. Update text references

Failure at step 2 leaves database in inconsistent state.

## Solution
Wrap collision resolution in database transaction with retry logic:
- Make entire RemapCollisions operation atomic
- Retry up to 3 times on UNIQUE constraint failures
- Re-sync counters between retries
- Add better error messages for debugging

## Implementation
Location: internal/storage/sqlite/collision.go:342 (RemapCollisions function)

```go
// Retry up to 3 times on UNIQUE constraint failures
for attempt := 0; attempt < 3; attempt++ {
    err := s.db.ExecInTransaction(func(tx *sql.Tx) error {
        // All collision resolution operations
    })
    if !isUniqueConstraintError(err) {
        return err
    }
    s.SyncAllCounters(ctx)
}
```

## Success Criteria
- 5-clone collision test passes reliably
- No partial state on UNIQUE constraint errors
- Automatic recovery from transient ID conflicts

## References
- See beads_nway_test.go:124 for the KNOWN LIMITATION comment
- Related to bd-fb95094c.1 (transaction support)

## Progress Made

1. Added `ExecInTransaction` helper to SQLiteStorage for atomic database operations
2. Added `IsUniqueConstraintError` function to detect UNIQUE constraint violations
3. Wrapped `RemapCollisions` with retry logic (up to 3 attempts) with counter sync between retries
4. Enhanced `handleRename` to detect and handle race conditions where target ID already exists
5. Added defensive checks for when old ID has been deleted by another clone

## Test Results

The changes improve N-way collision handling but don't fully solve the problem:
- Original error: `UNIQUE constraint failed: issues.id` during first convergence round
- With changes: Test proceeds further but encounters different collision scenarios
- New error: `target ID already exists with different content` in later convergence rounds

## Root Cause Analysis

The issue is more complex than initially thought. In N-way scenarios:
1. Clone A remaps bd-1c63eb84 → test-2 → test-4
2. Clone B remaps bd-1c63eb84 → test-3 → test-4
3. Both try to create test-4, but with different intermediate states
4. This creates legitimate content collisions that require additional resolution

## Next Steps

The full solution requires:
1. Making remapping fully deterministic across clones (same input → same remapped ID)
2. OR making `handleRename` more tolerant of mid-flight collisions
3. OR implementing full transaction support for multi-step collision resolution (bd-fb95094c.1)

The retry logic added here provides a foundation but isn't sufficient for complex N-way scenarios.


Batch test 3


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.


Adaptive ID length scaling based on database size

Scale base hash ID length dynamically as database grows to optimize readability vs collision rate.

**Concept**: Start with very short IDs (4 chars) for small databases, automatically increase base length as database grows to keep collision probability under configured threshold (e.g., 25%).

**Benefits**:
- Super readable IDs for new users: bd-a3f2 instead of bd-7f3a86
- Scales gracefully: 4→5→6 chars as database grows
- Users who actively archive old issues keep shorter IDs
- Collision resolution (+1/+2 chars) handles edge cases

**Thresholds (25% max collision)**:
- 0-500 issues: 4 chars (7% collision at 500)
- 501-1500: 5 chars (1.8% collision at 1500)
- 1501+: 6 chars (continues scaling)

**Implementation**:
- Track issue count in database
- Check threshold on ID generation
- Configurable max collision tolerance
- Migration path for existing databases

**Math**: Birthday paradox analysis shows 4-char IDs work great up to ~500 issues. See scripts/collision-calculator.go for full analysis.


Consider renaming or reorganizing hash ID files for clarity

With hash IDs now being the primary/only ID system, consider file naming improvements:

Current structure:
- internal/storage/sqlite/hash.go - hash ID generation
- internal/storage/sqlite/adaptive_length.go - adaptive length logic
- cmd/bd/migrate_hash_ids.go - migration to hash IDs
- docs/HASH_ID_DESIGN.md - design doc
- docs/ADAPTIVE_IDS.md - adaptive length doc

Potential improvements:
1. Rename hash.go → id_generation.go (since hash IDs are THE IDs now)
2. Merge adaptive_length.go into hash.go (they're tightly coupled)
3. Consolidate hash ID docs into one comprehensive guide
4. Consider moving hash ID tests into a more obvious location

This is low priority - just organizational polish for future maintainability.


Stress test: event storm handling

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


Verify MCP Server Compatibility

Ensure MCP server works with cache-free daemon


Add MCP server functions for repair commands

Expose new repair commands via MCP server for agent access:

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

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

See repair_commands.md lines 803-884 for design.


Batch test 1


Remove --resolve-collisions flag and related code

The --resolve-collisions flag is no longer needed with hash IDs:
- Remove flag from cmd/bd/import.go
- Remove ResolveCollisions field from ImportOptions
- Remove collision handling in internal/importer/importer.go
- Update documentation (AGENTS.md, FAQ.md, TROUBLESHOOTING.md, etc.) to remove references
- Simplify import workflow without collision resolution path

Flag still exists in code but no longer does collision remapping (returns error instead). Should remove flag entirely and update docs.


Test new 6-char generation


Code Health & Technical Debt Cleanup

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

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

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

Estimated Effort: 11 days across 4 phases

- All unreachable code identified by `deadcode` analyzer is removed
- RPC server split into <500 LOC files with clear responsibilities
- Duplicate utility functions centralized
- Test coverage maintained or improved
- All tests passing
- Documentation updated


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

- Zero unreachable functions per deadcode analyzer
- All tests pass: `go test ./...`
- Test coverage maintained or improved
- Builds cleanly: `go build ./...`
- Linting shows improvements
- Integration tests all pass
- LOC reduction target achieved (~2,500 LOC)
- No unintended behavior changes
- Git commit messages document all changes

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

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


Consider central serialization package for JSON handling

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

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

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

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

Decision point: Evaluate if benefits outweigh refactoring cost

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

- Create serialization package with documented JSON format
- Migrate export/import to use centralized serialization
- All existing JSONL files can be read with new code
- All tests pass: `go test ./...`
- Export/import round-trip works perfectly
- RPC protocol unchanged (or backwards compatible)

deduplication optional phase-3 refactor serialization


Delete skipped tests for "old buggy behavior"

Three test functions are permanently skipped with comments indicating they test behavior that was fixed in GH#120. These tests will never run again and should be deleted.

Test functions to remove:

1. `cmd/bd/import_collision_test.go:228`
   ```go
   t.Skip("Test expects old buggy behavior - needs rewrite for GH#120 fix")
   ```

2. `cmd/bd/import_collision_test.go:505`
   ```go
   t.Skip("Test expects old buggy behavior - needs rewrite for GH#120 fix")
   ```

3. `internal/storage/sqlite/collision_test.go:919`
   ```go
   t.Skip("Test expects old buggy behavior - needs rewrite for GH#120 fix")
   ```

Impact: Removes ~150 LOC of permanently skipped tests

- Delete the 3 test functions entirely (~150 LOC total)
- Update test file comments to reference GH#120 fix if needed
- All remaining tests pass: `go test ./...`
- No reduction in meaningful test coverage (these test fixed bugs)


Update documentation after code health cleanup

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

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

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

Impact: Keeps documentation in sync with code

- All documentation references to deleted files removed
- New package READMEs written
- Code comments updated for reorganized code
- Migration guide for developers (if needed)
- Architecture diagrams updated (if they exist)


Audit and consolidate collision test coverage

The codebase has 2,019 LOC of collision detection tests across 3 files. Run coverage analysis to identify redundant test cases and consolidate.

Test files:
- `cmd/bd/import_collision_test.go` - 974 LOC
- `cmd/bd/autoimport_collision_test.go` - 750 LOC
- `cmd/bd/import_collision_regression_test.go` - 295 LOC

Total: 2,019 LOC of collision tests

Analysis steps:
1. Run coverage analysis
2. Identify redundant tests
3. Document findings

Consolidation strategy:
- Keep regression tests for critical bugs
- Merge overlapping table-driven tests
- Remove redundant edge case tests covered elsewhere
- Ensure all collision scenarios still tested

Expected outcome: Reduce to ~1,200 LOC (save ~800 lines) while maintaining coverage

Impact: Faster test runs, easier maintenance, clearer test intent

- Coverage analysis completed and documented
- Redundant tests identified (~800 LOC estimated)
- Consolidated test suite maintains or improves coverage
- All remaining tests pass: `go test ./cmd/bd/...`
- Test run time unchanged or faster
- Document which tests were removed and why
- Coverage percentage maintained: `go test -cover ./cmd/bd/` shows same %


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

- Create `internal/debug/debug.go` with `Enabled`, `Logf`, `Printf`
- Add unit tests in `internal/debug/debug_test.go` (test with/without BD_DEBUG)
- Replace all 43 instances of `os.Getenv("BD_DEBUG")` checks with `debug.Logf()`
- Verify debug output works: run with `BD_DEBUG=1 bd status`
- All tests pass: `go test ./...`
- No behavior change (output identical to before)


Extract normalizeLabels to shared utility package

The `normalizeLabels` function appears in multiple locations with identical implementation. Extract to a shared utility package.

Current locations:
- `internal/rpc/server.go:37` (53 lines) - full implementation
- `cmd/bd/list.go:50-52` - uses the server version (needs to use new shared version)

Function purpose:
- Trims whitespace from labels
- Removes empty strings
- Deduplicates labels
- Preserves order

Target structure:
```
internal/util/
├── strings.go         # String utilities
    └── NormalizeLabels([]string) []string
```

Impact: DRY principle, single source of truth, easier to test

- Create `internal/util/strings.go` with `NormalizeLabels`
- Add comprehensive unit tests in `internal/util/strings_test.go`
- Update `internal/rpc/server.go` to import and use `util.NormalizeLabels`
- Update `cmd/bd/list.go` to import and use `util.NormalizeLabels`
- Remove duplicate implementations
- All tests pass: `go test ./...`
- Verify label normalization works: test `bd list --label` commands


Extract SQLite migrations into separate files

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

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

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

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

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

- All 11 migrations extracted to separate files
- Migration version tracking in database
- Migrations run in order on fresh database
- Existing databases upgrade correctly
- All tests pass: `go test ./internal/storage/sqlite/...`
- Database initialization time unchanged or improved
- Add migration rollback capability (optional, nice-to-have)


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

- Remove unreachable functions
- If entire files can be deleted (like hash.go), delete them
- For SummarizeTier2: decide to implement or delete, document decision
- All tests pass: `go test ./...`
- Verify no callers exist for each function


Remove unreachable RPC methods

Several RPC server and client methods are unreachable and should be removed:

Server methods (internal/rpc/server.go):
- `Server.GetLastImportTime` (line 2116)
- `Server.SetLastImportTime` (line 2123)
- `Server.findJSONLPath` (line 2255)

Client methods (internal/rpc/client.go):
- `Client.Import` (line 311) - RPC import not used (daemon uses autoimport)

Evidence:
```bash
go run golang.org/x/tools/cmd/deadcode@latest -test ./...
```

Impact: Removes ~80 LOC of unused RPC code

- Remove the 4 unreachable methods (~80 LOC total)
- Verify no callers: `grep -r "GetLastImportTime\|SetLastImportTime\|findJSONLPath" .`
- All tests pass: `go test ./internal/rpc/...`
- Daemon functionality works: test daemon start/stop/operations


Document bd edit command and verify MCP exclusion

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