Support --parent flag in daemon mode

Added support for hierarchical child issue creation using --parent flag in daemon mode. Previously only worked in direct mode.


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

## Problem

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

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

## Current Behavior

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

## Expected Behavior

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

## Impact

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

## Fix

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

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

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


Rapid 3


Isolate RPC server startup into rpc_server.go

Create internal/daemonrunner/rpc_server.go with StartRPC function. Move startRPCServer logic here and return typed handle.


Testing event-driven mode!


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


Update AGENTS.md with event-driven mode

Document BEADS_DAEMON_MODE env var. Explain opt-in during Phase 1. Add troubleshooting for watcher failures.


Batch test 1


Remove obsolete renumber.go command (hash IDs eliminated need)


Add multi-repo support to export metadata updates

## Problem
The bd-ymj fix only updates metadata for the main jsonlPath, but the codebase supports multi-repo mode where exports write to multiple JSONL files.

Other daemon_sync operations handle multi-repo correctly:
- Lines 509-518: Snapshots captured for all multi-repo paths
- Lines 578-587: Deletions applied for all multi-repo paths

But metadata updates are missing!

## Investigation Needed
Metadata is stored globally in database (`last_import_hash`, `last_import_mtime`), but multi-repo has per-repo JSONL files. Current schema may not support tracking multiple JSONL files.

Options:
1. Store metadata per-repo (new schema)
2. Store combined hash of all repos
3. Document that multi-repo doesn't need this metadata (why?)

## Solution (if needed)
```go
// After export
if multiRepoPaths := getMultiRepoJSONLPaths(); multiRepoPaths != nil {
    // Multi-repo mode: update metadata for each JSONL
    for _, path := range multiRepoPaths {
        updateExportMetadata(exportCtx, store, path, log)
    }
} else {
    // Single-repo mode: update metadata for main JSONL
    updateExportMetadata(exportCtx, store, jsonlPath, log)
}
```

## Files
- cmd/bd/daemon_sync.go (createExportFunc, createSyncFunc)
- Possibly: internal/storage/sqlite/metadata.go (schema changes)

## Related
Depends on bd-ar2.1 (extract helper function first)


bd import auto-detects wrong prefix from directory name instead of issue IDs

When importing issues.jsonl into a fresh database, 'bd import' prints:

  ✓ Initialized database with prefix 'beads' (detected from issues)

But the issues all have prefix 'bd-' (e.g., bd-03r). It appears to be detecting the prefix from the directory name (.beads/) rather than from the actual issue IDs in the JSONL.

This causes import to fail with:
  validate ID prefix for bd-03r: issue ID 'bd-03r' does not match configured prefix 'beads'

Workaround: Run 'bd config set issue_prefix bd' before import, or use 'bd init --prefix bd'.


Fix unparam unused parameter in cmd/bd/doctor.go:1879

Linting issue: checkGitHooks - path is unused (unparam) at cmd/bd/doctor.go:1879:20. Error: func checkGitHooks(path string) doctorCheck {


Add sync resilience guardrails for forced pushes and prefix mismatches

Beads can get into unrecoverable sync states when remote forces pushes occur (e.g., rebases) combined with prefix mismatches from multi-worker scenarios. Add detection, prevention, and auto-recovery features to handle this gracefully.


Optimize GetReadyWork performance - 752ms on 10K database (target: <50ms)

# Performance Analysis (10K Issue Database)

Analyzed using CPU profiles from benchmark suite on Apple M2 Pro.

## Operation Performance

| Operation                        | Time    | Allocations | Memory |
|----------------------------------|---------|-------------|--------|
| bd ready (GetReadyWork)          | ~752ms  | 167,466     | 16MB   |
| bd list (SearchIssues no filter) | ~11.6ms | 89,214      | 5.8MB  |
| bd list (SearchIssues filtered)  | ~9.2ms  | 62,365      | 3.5MB  |
| bd create (CreateIssue)          | ~2.6ms  | 146         | 8.6KB  |
| bd update (UpdateIssue)          | ~0.32ms | 364         | 15KB   |
| bd close (UpdateIssue)           | ~0.32ms | 364         | 15KB   |

**Target: <50ms for all operations on 10K database**

**Current issue: GetReadyWork is 15x over target (752ms vs 50ms)**

## Root Cause

GetReadyWork (internal/storage/sqlite/ready.go:90-128) uses recursive CTE to propagate blocking:
- 65x slower than SearchIssues
- Recalculates entire blocked issue tree on every call
- Algorithm:
  1. Find directly blocked issues via 'blocks' dependencies
  2. Recursively propagate blockage to descendants (max depth: 50)
  3. Exclude all blocked issues from results

## CPU Profile Analysis

- Database syscalls (pthread_cond_signal, syscall6): ~75%
- SQLite engine overhead: inherent to recursive CTE
- Application code (query construction): <1%

**Bottleneck is the recursive CTE query execution, not application code.**

## Optimization Recommendations

### High Impact (Likely to achieve <50ms target)

1. **Cache blocked issue calculation**
   - Add `blocked_issues` table updated on dependency changes
   - Trade write complexity for read speed (ready called >> dependency changes)
   - Eliminates recursive CTE on every read

2. **Add/verify database indexes**
   ```sql
   CREATE INDEX IF NOT EXISTS idx_dependencies_blocked
     ON dependencies(issue_id, type, depends_on_id);
   CREATE INDEX IF NOT EXISTS idx_issues_status
     ON issues(status);
   ```

### Medium Impact

3. **Reduce allocations** (167K allocations for GetReadyWork)
   - Profile `scanIssues()` for object pooling opportunities
   - Reuse slice capacity for repeated calls

### Low Impact (Not recommended)
- Query optimization for CRUD operations (already <3ms)
- Connection pooling tuning (not showing in profiles)

## Verification

Run benchmarks to validate optimization:
```bash
make bench-quick
go tool pprof -http=:8080 internal/storage/sqlite/bench-cpu-*.prof
```

Profile files automatically generated in `internal/storage/sqlite/`.


Graph Link: relates_to for knowledge graph

Implement relates_to link type for loose associations.

New command:
- bd relate <id1> <id2> - creates bidirectional relates_to link

Query support:
- bd show <id> --related shows related issues
- bd list --related-to <id>

Storage:
- relates_to stored as JSON array of issue IDs
- Consider: separate junction table for efficiency at scale?

This enables 'see also' connections without blocking or hierarchy.


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

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

Current coverage: 48.1%
Target coverage: 70%


GH#497: bd onboard copilot-instructions.md is beads-specific, not generic

bd onboard outputs beads repo specific content (Go/SQLite/Cobra) for copilot-instructions.md instead of a generic template. AGENTS.md output is correct. See: https://github.com/steveyegge/beads/issues/497


Improve compact/daemon/merge documentation and UX

Multiple documentation and UX issues encountered:
1. "bd compact --analyze" fails with misleading "requires SQLite storage" error when daemon is running. Needs --no-daemon or better error.
2. "bd merge" help text is outdated (refers to 3-way merge instead of issue merging).
3. Daemon mode purpose isn't clear to local-only users.
4. Compact/cleanup commands are hard to discover.

Proposed fixes:
- Fix compact+daemon interaction or error message.
- Update "bd merge" help text.
- Add "when to use daemon" section to docs.
- Add maintenance section to quickstart.


Support separate branch for beads commits

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

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

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

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


CI failing on all 3/4 test jobs despite individual tests passing

CI has been broken for a day+ with mysterious test failures. Issue #173 on GitHub tracks this.

## Current Status
- **Lint job**: ✅ PASSING
- **Test (Linux)**: ❌ FAILING (exit code 1)
- **Test (Windows)**: ❌ FAILING (exit code 1)
- **Test Nix Flake**: ❌ FAILING (exit code 1)

## Key Observations
All three failing jobs show identical pattern:
- Individual test output shows PASS for every test
- Final result: `FAIL github.com/steveyegge/beads/cmd/bd`
- Exit code 1 despite no visible test failures
- Last visible test output before failure: "No Reason Issue" test (TestCloseCommand/close_without_reason)

## Investigation So Far
1. All tests appear to pass when examined individually
2. Likely causes:
   - Race detector finding data races during test cleanup (`-race` flag)
   - Panic/error occurring after main tests complete
   - Test harness issue not reporting actual failure
   - Possible regression from PR #203 (dependency_type changes)

## Recent CI Runs
- Run 19015040655 (latest): 3/4 failing
- Multiple recent commits tried to fix Windows/lint issues
- Agent on rrnewton/beads fork attempting fixes (2/4 passing there)

## Next Steps
1. Run tests locally with `-race -v` to see full output
2. Check for unreported test failures or panics
3. Examine test cleanup/teardown code
4. Review recent changes around close command tests
5. Consider if race detector is too sensitive or catching real issues

## Progress Update

### ✅ Fixed (commits 09bd4d3, 21a29bc)
1. **Daemon auto-import** - Always recompute content_hash in importer to avoid stale hashes
2. **TestScripts failures** - Added bd binary to PATH for shell subprocess tests
3. **Test infrastructure** - Added .gitignore to test repos, fixed last_import_time metadata

### ✅ CI Status (Run 19015638968)
- **Test (Linux)**: ✅ SUCCESS - All tests passing
- **Test (Windows)**: ❌ FAILURE - Pre-existing Windows test failures
- **Test Nix Flake**: ❌ FAILURE - Build fails with same test errors
- **Lint**: ❌ FAILURE - Pre-existing issue in migrate.go:647

### ❌ Remaining Issues (not related to original bd-1231)

**Windows failures:**
- TestFindDatabasePathEnvVar
- TestHashIDs_MultiCloneConverge
- TestHashIDs_IdenticalContentDedup
- TestDatabaseReinitialization (5 subtests)
- TestFindBeadsDir_NotFound
- TestMetricsSnapshot/uptime

**Lint failure:**
- cmd/bd/migrate.go:647:37: cleanupWALFiles - result 0 (error) is always nil (unparam)

**Nix failure:**
- Build fails during test phase with same test errors

### Next Steps
1. Investigate Windows-specific test failures
2. Fix linting issue in migrate.go
3. Debug Nix build test failures


Create npm package structure for bd-wasm

Set up npm package for distribution:
- Create package.json with bd-wasm name
- Bundle bd.wasm + wasm_exec.js
- Create CLI wrapper (bin/bd) that invokes WASM
- Add installation scripts if needed
- Configure package for Claude Code Web sandbox compatibility


Testing & Validation

Ensure reliability through comprehensive testing.

Completed comprehensive Agent Mail test coverage analysis and implementation.

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

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

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

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

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

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


Make MCP compaction settings configurable (COMPACTION_THRESHOLD, PREVIEW_COUNT)

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

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

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

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

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


## Problem

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

## Current Behavior

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

## Expected Behavior

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

## Impact

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

## Proposed Solution

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

## Files to Modify

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

## Acceptance Criteria

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


Log errors from timer-triggered flushes instead of discarding


RPC diagnostics: BD_RPC_DEBUG timing logs

Add lightweight diagnostic logging for RPC connection attempts:
- BD_RPC_DEBUG=1 prints to stderr:
  - Socket path being dialed
  - Socket exists check result
  - Dial start/stop time
  - Connection outcome
- Improve bd daemon --status messaging when lock not held

This helps field triage of connection issues without verbose daemon logs.


Sync validation false positive: legitimate deletions trigger 'data loss detected'

## Problem
`bd sync` fails with false positive data loss detection when legitimate deletions occur:
```
Post-import validation failed: import reduced issue count: 26 → 25 (data loss detected!)
```

## Root Cause
The validation in `sync.go:329-340` counts DB issues BEFORE import, but `purgeDeletedIssues()` in `importer.go:159` legitimately removes issues DURING import. The validation doesn't account for expected deletions.

**The Flow:**
```
sync.go:293     → beforeCount = countDBIssues() = 26
sync.go:310-319 → sanitizeJSONL removes deleted issues from JSONL (RemovedCount=N)
sync.go:323     → importFromJSONL() runs subprocess
                  └→ importer.go:159 purgeDeletedIssues() removes issues from DB
sync.go:331     → afterCount = countDBIssues() = 25
sync.go:335     → validatePostImport(26, 25) → ERROR
```

## Fix
Pass `sanitizeResult.RemovedCount` to validation and account for expected deletions:

```go
// sync.go around line 335
expectedDecrease := 0
if sanitizeResult != nil {
    expectedDecrease = sanitizeResult.RemovedCount
}
if err := validatePostImportWithDeletions(beforeCount, afterCount, expectedDecrease); err != nil {
    // ...
}
```

```go
// integrity.go - new or modified function
func validatePostImportWithDeletions(before, after, expectedDeletions int) error {
    if after < before - expectedDeletions {
        return fmt.Errorf("unexpected data loss: %d → %d (expected max decrease: %d)",
            before, after, expectedDeletions)
    }
    // ...
}
```

## Files
- cmd/bd/sync.go:329-340
- cmd/bd/integrity.go:289-301


Design contributor namespace isolation for beads pollution prevention

## Problem

When contributors work on beads-the-project using beads-the-tool, their personal work-tracking issues leak into PRs. The .beads/issues.jsonl is intentionally tracked (it's the project's issue database), but contributors' local issues pollute the diff.

This is a recursion problem unique to self-hosting projects.

## Possible Solutions to Explore

1. **Contributor namespaces** - Each contributor gets a private prefix (e.g., `bd-steve-xxxx`) that's gitignored or filtered
2. **Separate database** - Contributors use BEADS_DIR pointing elsewhere for personal tracking
3. **Issue ownership/visibility flags** - Mark issues as "local-only" vs "project"
4. **Prefix-based filtering** - Configure which prefixes are committed vs ignored

## Design Considerations

- Should be zero-friction for contributors (no manual setup)
- Must not break existing workflows
- Needs to work with sync/collaboration features
- Consider: what if a "personal" issue graduates to "project" issue?

## Expansion Needed

This is a placeholder. Needs detailed design exploration before implementation.


Auto-compact deletions during bd sync

Parent: bd-imj

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

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

## Implementation

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

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

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

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

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

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


Wire config to import pipeline

Connect import.orphan_handling config to importer.go and sqlite validation functions. Pass mode flag through call chain. Implement all four modes (strict/resurrect/skip/allow) with proper error messages and logging.


Add git operations for --hard reset

Implement git integration for hard reset mode.

## Operations Needed
1. `git rm -rf .beads/*.jsonl` - remove data files from index
2. `git commit -m 'beads: reset to clean state'` - commit removal
3. After re-init: `git add .beads/` and commit fresh state

## Edge Cases to Handle
- Uncommitted changes in .beads/ - warn or error
- Detached HEAD state - warn, maybe block
- Git not initialized - skip git ops, warn
- Git operations fail mid-way - clear error messaging

## Interface
```go
type GitState struct {
    IsRepo       bool
    IsDirty      bool   // uncommitted changes in .beads/
    IsDetached   bool   // detached HEAD
    Branch       string // current branch name
}

func CheckGitState(beadsDir string) (*GitState, error)
func GitRemoveBeads(beadsDir string) error
func GitCommitReset(message string) error
func GitAddAndCommit(beadsDir, message string) error
```

## Location
`internal/reset/git.go` - keep with reset package for now

Note: Codebase has no central git package. internal/compact/git.go is compact-specific.
Future refactoring could extract shared git utilities, but YAGNI for now.


Implement hash ID generation in CreateIssue

Replace sequential ID generation with hash-based IDs in CreateIssue function.

## Current Behavior (internal/storage/sqlite/sqlite.go)
```go
func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue) error {
    // ID comes from auto-increment counter
    // Collisions possible across clones
}
```

## New Behavior
```go
func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue) error {
    // Generate hash ID if not provided
    if issue.ID == "" {
        issue.ID = idgen.GenerateHashID(
            issue.Title,
            issue.Description,
            time.Now(),
            s.workspaceID,
        )
    }

    // Assign next alias
    issue.Alias = s.getNextAlias()

    // Insert with hash ID + alias
    // ...
}
```

## Workspace ID Generation
Add to database initialization:
```go
// Generate stable workspace ID (persisted in .beads/workspace_id)
workspaceID := getOrCreateWorkspaceID()
```

Options for workspace ID:
1. Hostname + random suffix
2. UUID (random)
3. Git remote URL hash (deterministic per repo)

Recommended: Option 3 (git remote hash) for reproducibility

## Hash Collision Detection
```go
// On insert, check for collision (unlikely but possible)
existing, err := s.GetIssue(ctx, issue.ID)
if err == nil {
    // Hash collision! Add random suffix and retry
    issue.ID = issue.ID + "-" + randomSuffix(4)
}
```

## Files to Create/Modify
- internal/types/id_generator.go (new)
- internal/storage/sqlite/sqlite.go (CreateIssue)
- internal/storage/sqlite/workspace.go (new - workspace ID management)
- .beads/workspace_id (new file, git-ignored)

## Testing
- Test hash ID generation is deterministic
- Test collision detection and retry
- Test workspace ID persistence
- Benchmark: hash generation performance (<1μs)


Fix failing integration tests in beads-mcp

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


Test exclusive file reservations with two agents

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

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

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


Add migration registry and simplify New()

Create migrations.go with Migration type and registry. Change New() to: openDB -> initSchema -> RunMigrations(db). This removes 8+ separate migrate functions from New().


Auto-routing and maintainer detection

Implement intelligent routing to automatically send new issues to correct repo based on user's maintainer vs contributor status, with discovered issues inheriting parent's source_repo.


bd hooks install fails in git worktrees

When bd is used in a git worktree, bd hooks install fails with 'mkdir .git: not a directory' because .git is a file (gitdir pointer) not a directory. Beads should detect and follow the .git gitdir pointer to install hooks in the correct location. This blocks normal worktree workflows.

## Symptoms of this bug:
- Git hooks don't install automatically
- Auto-sync doesn't run (5-second debounce)
- Hash mismatch warnings in bd output
- Daemon fails to start with 'auto_start_failed'

## Workaround:
Use `git rev-parse --git-dir` to find the actual hooks directory and copy hooks manually:
```bash
mkdir -p $(git rev-parse --git-dir)/hooks
cp -r .beads-hooks/* $(git rev-parse --git-dir)/hooks/
```


Blocked issue


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)


Investigate database pollution - issue count anomalies

Multiple repos showing inflated issue counts suggesting cross-repo pollution:
- ~/src/dave/beads: 895 issues (675 open) - clearly polluted
- ~/src/stevey/src/beads: 280 issues (expected ~209-220) - possibly polluted

Need to investigate:
1. Source of pollution (multi-repo sync issues?)
2. How many duplicate/foreign issues exist
3. Whether recent sync operations caused cross-contamination
4. How to clean up and prevent future pollution

Investigation findings:

**Root cause identified:**
- NOT cross-repo contamination
- NOT automated test leakage (tests properly use t.TempDir())
- Manual testing during template feature development (Nov 2-4)
- Commit ba325a2: "test issues were accidentally committed during template feature development"

**Database growth timeline:**
- Nov 3: 19 issues (baseline)
- Nov 2-5: +244 issues (massive development spike)
- Nov 6-7: +40 issues (continued growth)
- Current: 291 issues → 270 after cleanup

**Test pollution breakdown:**
- 21 issues matching "Test " prefix pattern
- Most created Nov 2-5 during feature development
- Pollution from manual `./bd create "Test issue"` commands in production workspace
- All automated tests properly isolated with t.TempDir()

**Cleanup completed:**
- Ran scripts/cleanup-test-pollution.sh successfully
- Removed 21 test issues
- Database reduced from 291 → 270 issues (7.2% cleanup)
- JSONL synced to git

**Prevention strategy:**
- Filed follow-up issue for prevention mechanisms
- Script can be deleted once prevention is in place
- Tests are already properly isolated - no code changes needed there


Export deduplication breaks when JSONL and export_hashes table diverge

## Problem

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

## Symptoms

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

## Root Cause

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

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

## Impact

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

## Reproduction

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

## Current Workaround

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

## Proposed Solutions

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

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

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

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

## Recommended Fix

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

## Files Involved

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

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

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

### Bug Manifestation During Recovery

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

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

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

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

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

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

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

### Key Learnings

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

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

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

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

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

### Immediate Action Required

**DISABLE EXPORT DEDUPLICATION NOW**

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

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

### Long-term Fix

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


Test child issue


Code Health Review Dec 2025: Technical Debt Cleanup

Epic grouping technical debt identified in the Dec 16, 2025 code health review.

## Overall Health Grade: B (Solid foundation, needs cleanup)

### P1 (High Priority):
- bd-74w1: Consolidate duplicate path-finding utilities
- bd-b6xo: Remove/fix ClearDirtyIssues() race condition
- bd-b3og: Fix TestImportBugIntegration deadlock

### P2 (Medium Priority):
- bd-05a8: Split large files (doctor.go, sync.go)
- bd-qioh: Standardize error handling patterns
- bd-rgyd: Split queries.go (1586 lines)
- bd-9g1z: Fix/remove TestFindJSONLPathDefault

### P3 (Low Priority):
- bd-ork0: Add comments to 30+ ignored errors
- bd-4nqq: Remove dead test code in info_test.go
- bd-dhza: Reduce global state in main.go

## Key Areas:
1. Code duplication in path utilities
2. Large monolithic files (5 files >1000 lines)
3. Global state (25+ variables, 3 deprecated)
4. Silent error suppression (30+ instances)
5. Test gaps and dead test code
6. Atomicity risks in batch operations


Test numeric 1


Fix variable shadowing in GetNextChildID resurrection code

## Problem
Minor variable shadowing in hash_ids.go:

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

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

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

## Solution
Use different variable name:

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

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

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


Use stable repo identifiers instead of full paths in metadata keys

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

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

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

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

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


Separate process/lock/PID concerns into process.go

Create internal/daemonrunner/process.go with: acquireDaemonLock, PID file read/write, stopDaemon, isDaemonRunning, getPIDFilePath, socket path helpers, version check.


Code Review Sweep: thorough

Perform thorough code review sweep based on accumulated activity.

**AI Reasoning:**
Significant code volume added (150,273 lines) across multiple critical areas, including cmd/bd, internal/storage/sqlite, and internal/rpc. High file change count (616) indicates substantial refactoring or new functionality. The metrics suggest potential for subtle architectural or implementation issues that warrant review.

**Scope:** thorough
**Target Areas:** cmd/bd, internal/storage/sqlite, internal/rpc
**Estimated Files:** 12
**Estimated Cost:** $5

**Task:**
Review files for non-obvious issues that agents miss during focused work:
- Inefficiencies (algorithmic, resource usage)
- Subtle bugs (race conditions, off-by-one, copy-paste)
- Poor patterns (coupling, complexity, duplication)
- Missing best practices (error handling, docs, tests)
- Unnamed anti-patterns

File discovered issues with detailed reasoning and suggestions.


Rapid 2


Add bd init --contributor wizard

Interactive wizard for OSS contributor setup. Guides user through: fork workflow setup, separate planning repo configuration, auto-detection of fork relationships, examples of common OSS workflows.


Add migration guide for library consumers

The contributor-workflow-analysis.md has excellent migration examples for CLI users (lines 508-549) but lacks examples for library consumers like VC that use beadsLib in Go/TypeScript code.

Library consumers need to know:
- Whether their existing code continues to work unchanged (backward compatibility)
- How config.toml is automatically read (transparent hydration)
- When and how to use explicit multi-repo configuration
- What happens if config.toml doesn't exist (defaults)

Example needed:
```go
// Before (v0.17.3)
store, err := beadsLib.NewSQLiteStorage(".beads/vc.db")

// After (v0.18.0 with multi-repo) - still works!
store, err := beadsLib.NewSQLiteStorage(".beads/vc.db")
// Automatically reads .beads/config.toml if present

// Explicit multi-repo (if needed)
cfg := beadsLib.Config{
    Primary: ".beads/vc.db",
    Additional: []string{"~/.beads-planning"},
}
store, err := beadsLib.NewStorageWithConfig(cfg)
```


Add error handling guidance for export metadata update failures

## Problem
The bd-ymj fix treats all metadata update failures as warnings:

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

But if metadata updates fail, the NEXT export will fail with "JSONL content has changed since last import".

## Questions
1. Should metadata failures be critical (return error)?
2. Should we implement retry logic?
3. Is warning acceptable (safe, just requires import before next export)?

## Current Behavior
- Metadata failure is silent (only logged)
- User won't know until next export fails
- Next export requires import first (safe but annoying)

## Recommendation
Add clear comment explaining the trade-off:

```go
// Note: Metadata update failures are treated as warnings, not errors.
// This is acceptable because the worst case is the next export will
// require an import first, which is safe and prevents data loss.
// Alternative: Make this critical and fail the export if metadata
// updates fail (but this makes exports more fragile).
if err := store.SetMetadata(ctx, "last_import_hash", currentHash); err != nil {
    log.log("Warning: failed to update last_import_hash: %v", err)
    log.log("Next export may require running 'bd import' first")
}
```

## Files
- cmd/bd/daemon_sync.go

## Decision Needed
Choose approach based on failure mode preferences


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

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

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

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


GH#517: Fix Claude setting wrong priority syntax on new install

Claude uses 'medium' instead of P2/2 for priority, causing infinite error loops. bd prime hook or docs not clear enough. See: https://github.com/steveyegge/beads/issues/517


Split large cmd/bd files: doctor.go (2948 lines), sync.go (2121 lines)

Code health review found several oversized files:

1. doctor.go - 2948 lines, 48 functions mixed together
   - Should split into doctor/checks/*.go for individual diagnostics
   - applyFixes() and previewFixes() are nearly identical

2. sync.go - 2121 lines
   - ZFC (Zero Flush Check) logic embedded inline (lines 213-247)
   - Multiple mode handlers should be extracted

3. init.go - 1732 lines
4. compact.go - 1097 lines
5. show.go - 1069 lines

Recommendation: Extract into focused sub-packages or split into logical files.


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


GetNextChildID doesn't attempt parent resurrection from JSONL history

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

Related to GH #334 and #278.

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

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

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


YABB: Spurious issue updates during normal operations

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

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

Expected: Issues should only be updated when explicitly modified.

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

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

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


Add tests for extracted modules

Create tests for migrations.go, hash_ids.go, batch_ops.go, and validators.go


CRDT-based architecture for guaranteed convergence (v2.0)

## Vision
Redesign beads around Conflict-Free Replicated Data Types (CRDTs) to provide mathematical guarantees for N-way collision resolution at arbitrary scale.

## Current Limitations
- Content-hash based collision resolution fails at 5+ clones
- Non-deterministic convergence in multi-round scenarios
- UNIQUE constraint violations during rename operations
- No formal proof of convergence properties

## CRDT Benefits
- Provably convergent (Strong Eventual Consistency)
- Commutative/Associative/Idempotent operations
- No coordination required between clones
- Scales to 100+ concurrent workers
- Well-understood mathematical foundations

## Proposed Architecture

### 1. UUID-Based IDs
Replace sequential IDs with UUIDs:
- Current: bd-1c63eb84, bd-9063acda, bd-4d80b7b1
- CRDT: bd-a1b2c3d4-e5f6-7890-abcd-ef1234567890
- Human aliases maintained separately: #42 maps to UUID

### 2. Last-Write-Wins (LWW) Elements
Each field becomes an LWW register:
- title: (timestamp, clone_id, value)
- status: (timestamp, clone_id, value)
- Deterministic conflict resolution via Lamport timestamp + clone_id tiebreaker

### 3. Operation Log
Track all operations as CRDT ops:
- CREATE(uuid, timestamp, clone_id, fields)
- UPDATE(uuid, field, timestamp, clone_id, value)
- DELETE(uuid, timestamp, clone_id) - tombstone, not hard delete

### 4. Sync as Merge
Syncing becomes merging two CRDT states:
- No merge conflicts possible
- Deterministic merge function
- Guaranteed convergence

## Implementation Phases

### Phase 1: Research & Design (4 weeks)
- Study existing CRDT implementations (Automerge, Yjs, Loro)
- Design schema for CRDT-based issue tracking
- Prototype LWW-based Issue CRDT
- Benchmark performance vs current system

### Phase 2: Parallel Implementation (6 weeks)
- Implement CRDT storage layer alongside SQLite
- Build conversion tools: SQLite ↔ CRDT
- Maintain backward compatibility with v1.x format
- Migration path for existing databases

### Phase 3: Testing & Validation (4 weeks)
- Formal verification of convergence properties
- Stress testing with 100+ clone scenario
- Performance profiling and optimization
- Documentation and examples

### Phase 4: Migration & Rollout (4 weeks)
- Release v2.0-beta with CRDT backend
- Gradual migration from v1.x
- Monitoring and bug fixes
- Final v2.0 release

## Risks & Mitigations

**Risk 1: Performance overhead**
- Mitigation: Benchmark early, optimize hot paths
- CRDTs can be slower than append-only logs
- May need compaction strategy

**Risk 2: Storage bloat**
- Mitigation: Implement operation log compaction
- Tombstone garbage collection for deleted issues
- Periodic snapshots to reduce log size

**Risk 3: Breaking changes**
- Mitigation: Maintain v1.x compatibility layer
- Gradual migration tools
- Dual-mode operation during transition

**Risk 4: Complexity**
- Mitigation: Use battle-tested CRDT libraries
- Comprehensive documentation
- Clear migration guide

## Success Criteria
- 100-clone collision test passes without failures
- Formal proof of convergence properties
- Performance within 2x of current system
- Zero manual conflict resolution required
- Backward compatible with v1.x databases

## Timeline
18-20 weeks total (4-5 months)

## References
- Automerge: https://automerge.org
- Yjs: https://docs.yjs.dev
- Loro: https://loro.dev
- CRDT theory: Shapiro et al, A comprehensive study of CRDTs
- Related issues: bd-0dcea000, bd-4d7fca8a, bd-6221bdcd


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

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

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

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

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

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

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


Fix mypy errors in beads-mcp

Running `mypy .` in `integrations/beads-mcp` reports 287 errors. These should be addressed to improve type safety and code quality.


bd-hv01: No snapshot versioning or timestamps causes stale data usage

Problem: If sync is interrupted (crash, kill -9, power loss), stale snapshots persist indefinitely. Next sync uses stale data leading to incorrect deletions.

Fix: Add metadata to snapshots with timestamp, version, and commit SHA. Validate snapshots are recent (< 1 hour old), from compatible version, and from expected git commit.

Files: cmd/bd/deletion_tracking.go (all snapshot functions)


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.


bd ready taking 5 seconds with 132 issues (89 closed)

User reports bd ready is annoyingly slow on M2 Mac - 5 seconds for 132 issues (89 closed). Started noticing after hash-based IDs update. Need to investigate performance regression. Reported in GH #243.

Root cause identified: Not a query performance issue, but stale daemon locks causing 5s timeout delays.

Fixed in bd-ndyz (closed) via 5 sub-issues:
- bd-expt: Fast-fail socket checks (200ms timeout)
- bd-wgu4: Lock probe before RPC attempts
- bd-1mzt: Self-heal stale artifacts
- bd-vcg5: Panic recovery + socket cleanup
- bd-j7e2: RPC diagnostics (BD_RPC_DEBUG)

All fixes merged. Ready for v0.22.2 release.


Split monolithic sqlite.go into focused files

internal/storage/sqlite/sqlite.go is 1050 lines containing initialization, 20+ CRUD methods, query building, and schema management.

Split into:
- store.go: Store struct & initialization (150 lines)
- bead_queries.go: Bead CRUD (300 lines)
- work_queries.go: Work queries (200 lines)
- stats_queries.go: Statistics (150 lines)
- schema.go: Schema & migrations (150 lines)
- helpers.go: Common utilities (100 lines)

Impact: Impossible to understand at a glance; hard to find specific functionality; high cognitive load

Effort: 6-8 hours


Document protected branch workflow

Create comprehensive documentation for protected branch workflow.

Tasks:
- Add "Protected Branch Workflow" section to AGENTS.md
- Create docs/PROTECTED_BRANCHES.md guide
- Update README.md quick start
- Add examples to examples/protected-branch/
- Update bd init --help documentation
- Add troubleshooting guide
- Add migration guide for existing users
- Record demo video (optional)

Estimated effort: 2-3 days

Completed protected branch workflow documentation. Created comprehensive guide (docs/PROTECTED_BRANCHES.md), updated AGENTS.md with workflow section, added feature to README.md, and created working example (examples/protected-branch/). All commands verified working (bd init --branch, bd sync --status, bd sync --merge, bd config get/set sync.branch).


Add fsnotify dependency to go.mod


Fix Windows-specific test failures in CI

Several tests are failing on Windows but passing on Linux:

**Failing tests:**
- TestFindDatabasePathEnvVar
- TestHashIDs_MultiCloneConverge
- TestHashIDs_IdenticalContentDedup
- TestDatabaseReinitialization (all 5 subtests):
  - fresh_clone_auto_import
  - database_removal_scenario
  - legacy_filename_support
  - precedence_test
  - init_safety_check
- TestFindBeadsDir_NotFound
- TestMetricsSnapshot/uptime (in internal/rpc)

**CI Run:** https://github.com/steveyegge/beads/actions/runs/19015638968

These are likely path separator or filesystem behavior differences between Windows and Linux.

Fixed all Windows path issues:
1. TestFindDatabasePathEnvVar - expects canonicalized paths ✅
2. TestHashIDs tests - use platform-specific bd.exe command ✅
3. TestMetricsSnapshot/uptime - enforce minimum 1 second uptime ✅
4. TestFindBeadsDir_NotFound - allow finding .beads in parent dirs ✅
5. TestDatabaseReinitialization - fix git path conversion on Windows (git returns /c/Users/... but filepath needs C:\Users\...) ✅

CI run in progress to verify all fixes.


GH#519: bd sync fails when sync.branch equals current branch

bd sync tries to create worktree when sync.branch matches current checkout, fails with 'already used by worktree'. Should detect and commit directly. See: https://github.com/steveyegge/beads/issues/519


bd sync outputs excessive 'Skipping' messages for tombstoned issues

During sync, every tombstoned issue outputs a 'Skipping bd-xxx (in deletions manifest)' message. With 96+ tombstones, this creates ~200 lines of noise that obscures the actual sync status.

**Current behavior:**
```
Skipping bd-0ih (in deletions manifest: deleted 0001-01-01 by )
Skipping bd-0is (in deletions manifest: deleted 0001-01-01 by )
... (96 more lines)
✓ Sync complete
```

**Expected behavior:**
- Suppress individual skip messages by default
- Show summary: 'Skipped 96 tombstoned issues'
- Or add --verbose flag to show individual skips

**Impact:** Makes sync output unreadable, hard to spot actual errors or warnings.


Daemon race condition: stale export overwrites recent DB changes

**Symptom:**
Merged bd-fc2d into bd-fb05 in ~/src/beads (commit ce4d756), pushed to remote. The ~/src/fred/beads daemon then exported its stale DB state and committed (8cc1bb4), reverting bd-fc2d back to "open" status.

**Timeline:**
1. 21:45:12 - Merge committed from ~/src/beads (ce4d756): bd-fc2d closed
2. 21:49:42 - Daemon in ~/src/fred/beads exported stale state (8cc1bb4): bd-fc2d open again

**Root cause:**
The fred/beads daemon had a stale database (bd-fc2d still open) and didn't auto-import the newer JSONL before exporting. When it exported, it overwrote the merge with its stale state.

**Expected behavior:**
Daemon should detect that JSONL is newer than its last export and import before exporting.

**Actual behavior:**
Daemon exported stale DB state, creating a conflicting commit that reverted upstream changes.

**Impact:**
Multi-workspace setups with daemons can silently lose changes if one daemon has stale state and exports.


Extract hash ID generation functions to hash_ids.go

Move generateHashID, getNextChildNumber, GetNextChildID to hash_ids.go


Git history fallback assumes .beads is direct child of repo root

## Problem

`checkGitHistoryForDeletions` assumes the repo structure:

```go
repoRoot := filepath.Dir(beadsDir)  // Assumes .beads is in repo root
jsonlPath := filepath.Join(".beads", "beads.jsonl")
```

But `.beads` could be in a subdirectory (monorepo, nested project), and the actual JSONL filename could be different (configured via `metadata.json`).

## Location
`internal/importer/importer.go:865-869`

## Impact
- Git search will fail silently for repos with non-standard structure
- Monorepo users won't get deletion propagation

## Fix
1. Use `git rev-parse --show-toplevel` to find actual repo root
2. Compute relative path from repo root to JSONL
3. Or use `git -C <dir>` to run from beadsDir directly


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


Integration test: mutation to export latency

Measure time from bd create to JSONL update. Verify <500ms latency. Test with multiple rapid mutations to verify batching.

Test added to daemon_test.go as TestMutationToExportLatency().

Currently skipped with note that it should be enabled once bd-146 (event-driven daemon) is fully implemented and enabled by default.

The test structure is complete:
1. Sets up test environment with fast debounce (500ms)
2. SingleMutationLatency: measures latency from mutation to JSONL update
3. RapidMutationBatching: verifies multiple mutations batch into single export

Once event-driven mode is default, remove the t.Skip() line and the test will validate <500ms latency.


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


Benchmark git traffic reduction

Automated benchmark comparing git operations with/without Agent Mail.

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

File: tests/benchmarks/git_traffic.py

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

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

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

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


Stress test: event storm handling

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


Test graceful degradation when server unavailable

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

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

Success Metric: Zero functional difference when Agent Mail unavailable


Update docs for import bug fix

Update AGENTS.md, README.md, TROUBLESHOOTING.md with import.orphan_handling config documentation. Document resurrection behavior, tombstones, config modes. Add troubleshooting section for import failures with deleted parents.


Add internal/ai package for LLM integration

Shared AI client for repair commands.

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

Env vars:
- BEADS_AI_PROVIDER
- BEADS_AI_API_KEY
- BEADS_AI_MODEL

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


Auto-detect issue prefix from existing JSONL in 'bd init'

When running `bd init` in a fresh clone with existing JSONL, it should auto-detect the issue prefix from the JSONL file instead of requiring `--prefix`.

Currently you must specify `--prefix ef` manually. But the JSONL file already contains issues like `ef-1it`, `ef-1jp` etc., so the prefix is known.

**Ideal UX**:
```
$ bd init
Detected issue prefix 'ef' from existing JSONL (38 issues).
✓ Database initialized...
```

This would make fresh clone hydration a single command: `bd init` with no flags.


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.


Parameterize SQLite busy_timeout in store.go

Modify sqlite.New() to accept configurable busy_timeout.

## Current State
`internal/storage/sqlite/store.go` hardcodes:
```go
connStr = "file:" + path + "?_pragma=foreign_keys(ON)&_pragma=busy_timeout(30000)&..."
```

## Implementation Options

### Option A: Add timeout parameter to New()
```go
func New(ctx context.Context, path string, busyTimeout time.Duration) (*SQLiteStorage, error)
```
Pro: Simple, explicit
Con: Changes function signature (breaking change for callers)

### Option B: Options struct pattern
```go
type Options struct {
    BusyTimeout time.Duration // default 30s
}
func New(ctx context.Context, path string, opts *Options) (*SQLiteStorage, error)
```
Pro: Extensible, nil means defaults
Con: More boilerplate

### Recommendation: Option A with default
```go
func New(ctx context.Context, path string) (*SQLiteStorage, error) {
    return NewWithOptions(ctx, path, 30*time.Second)
}

func NewWithOptions(ctx context.Context, path string, busyTimeout time.Duration) (*SQLiteStorage, error)
```

## Acceptance Criteria
- busy_timeout is parameterized in connection string
- 0 timeout means no waiting (immediate SQLITE_BUSY)
- All existing callers continue to work (via default wrapper)

Part of bd-olc1


Extract daemon config functions to daemon_config.go


sync branch not match config

./bd sync
→ Exporting pending changes to JSONL...
→ No changes to commit
→ Pulling from sync branch 'gh-386'...
Error pulling from sync branch: failed to create worktree: failed to create worktree parent directory: mkdir /var/home/matt/dev/beads/worktree-db-fail/.git: not a directory
matt@blufin-framation ~/d/b/worktree-db-fail (worktree-db-fail) [1]> bd config list

Configuration:
  auto_compact_enabled = false
  compact_batch_size = 50
  compact_model = claude-3-5-haiku-20241022
  compact_parallel_workers = 5
  compact_tier1_days = 30
  compact_tier1_dep_levels = 2
  compact_tier2_commits = 100
  compact_tier2_days = 90
  compact_tier2_dep_levels = 5
  compaction_enabled = false
  issue_prefix = worktree-db-fail
  sync.branch = worktree-db-fail


Remove unused global daemon infrastructure (internal/daemonrunner/)

The internal/daemonrunner/ package (1,468 LOC) contains the old global daemon implementation that is no longer used. We now use per-workspace daemons.

Deadcode analysis shows all these functions are unreachable:
- Daemon.Start, runGlobalDaemon, setupLock
- validateSingleDatabase, validateSchemaVersion
- registerDaemon, unregisterDaemon
- validateDatabaseFingerprint
- Full git client implementation (NewGitClient, HasUpstream, HasChanges, Commit, Push, Pull)
- Helper functions: isGitRepo, gitHasUpstream, gitHasChanges, gitCommit

The entire package appears unused since switching to per-workspace daemon architecture.

Files to remove:
- daemon.go (9,436 bytes)
- git.go (3,510 bytes)
- sync.go (6,401 bytes)
- fingerprint.go (2,076 bytes)
- process.go (3,332 bytes)
- rpc.go (994 bytes)
- config.go (486 bytes)
- logger.go (1,579 bytes)
- flock_*.go (platform-specific file locking)
- signals_*.go (platform-specific signal handling)
- All test files

Total cleanup: ~1,500 LOC


Use external_ref as primary matching key for import updates

Enable re-syncing from external systems (Jira, GitHub, Linear) by using external_ref as the primary matching key during imports. Currently imports treat any content change as a collision, making it impossible to sync updates from external systems without creating duplicates.

See GH #142 for detailed proposal and implementation plan.

Key changes needed:
1. Add findByExternalRef() query function
2. Update DetectCollisions() to match by external_ref first
3. Update import_shared.go to update existing issues when external_ref matches
4. Add index on external_ref for performance
5. Preserve local issues (no external_ref) from being overwritten

This enables hybrid workflows: import external backlog, break down with local tasks, re-sync anytime.

## Code Review Complete ✅

**Overall Assessment**: EXCELLENT - Production ready

### Implementation Quality
- ✓ Clean architecture with proper interface extension
- ✓ Dual backend support (SQLite + Memory)
- ✓ Smart matching priority: external_ref → ID → content hash
- ✓ O(1) lookups with database index
- ✓ Timestamp-based conflict resolution
- ✓ Comprehensive test coverage (11 test cases)

### Follow-up Issues Filed
High Priority (P2):
- bd-897a: Add UNIQUE constraint on external_ref column
- bd-7315: Add validation for duplicate external_ref in batch imports

Medium Priority (P3):
- bd-f9a1: Add index usage verification test
- bd-3f6a: Add concurrent import race condition tests

Low Priority (P4):
- bd-e166: Improve timestamp comparison readability
- bd-9e23: Optimize Memory backend with index
- bd-537e: Add external_ref change tracking
- bd-df11: Add import metrics
- bd-9f4a: Document external_ref in content hash

### Key Features
✅ External systems (Jira, GitHub, Linear) can re-sync without duplicates
✅ Hybrid workflows: import external backlog, add local tasks, re-sync anytime
✅ Local issues protected from being overwritten
✅ Timestamp checking ensures only newer updates applied
✅ Performance optimized with database index

**Confidence Level**: 95% - Ship it! 🚀


bd sync false positive: conflict detection triggers on JSON-encoded angle brackets in issue content

The bd sync --import-only command incorrectly detects conflict markers when issue descriptions contain the text '<<<<<<<' or '>>>>>>>' as legitimate content (e.g., documentation about git conflict markers).

**Reproduction:**
1. Create issue with design field containing: 'Read file, extract <<<<<<< / ======= / >>>>>>> markers'
2. Export to JSONL (gets JSON-encoded as \u003c\u003c\u003c...)
3. Commit and push
4. Pull from remote
5. bd sync --import-only fails with: 'Git conflict markers detected in JSONL file'

**Root cause:**
The conflict detection appears to decode JSON before checking for conflict markers, causing false positives when issue content legitimately contains these strings.

**Expected behavior:**
Conflict detection should only trigger on actual git conflict markers (literal '<<<<<<<' bytes in the raw file), not on JSON-encoded content within issue fields.

**Test case:**
VC project at ~/src/dave/vc has vc-85 'JSONL Conflict Parser' which documents conflict parsing and triggers this bug.

**Suggested fixes:**
1. Only scan for literal '<<<<<<<' bytes (not decoded JSON content)
2. Parse JSONL first and only flag unparseable lines
3. Check git merge state (git status) to confirm actual conflict
4. Add --skip-conflict-check flag for override


Create examples for common workflows

Add examples/ subdirectories: OSS contributor workflow, team branch workflow, multi-phase development, multiple personas (architect/implementer). Each with README and sample configs.


Remove duplicate countIssuesInJSONLFile function

init.go and doctor.go both defined countIssuesInJSONLFile. Removed the init.go version which is now unused. The doctor.go version (which calls countJSONLIssues) is the canonical implementation.


Extract daemon server functions to daemon_server.go


Fix TestImportBugIntegration deadlock in importer_test.go

Code health review found internal/importer/importer_test.go has TestImportBugIntegration skipped with:

TODO: Test hangs due to database deadlock - needs investigation

This indicates a potential unresolved concurrency issue in the importer. The test has been skipped for an unknown duration.

Fix: Investigate the deadlock, fix the underlying issue, and re-enable the test.


bd message: Refactor duplicated error messages


Test P1 priority


Extract database search logic into helper function

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

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

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

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

Related to [deleted:bd-e16b] implementation.


Remove dead test code in info_test.go

Code health review found cmd/bd/info_test.go has two tests permanently skipped:

- TestInfoCommand
- TestInfoCommandNoDaemon

Both skip with: 'Manual test - bd info command is working, see manual testing'

These are essentially dead code. Either automate them or remove them entirely.


Clean up snapshot files after successful merge

After a successful 3-way merge and import during 'bd sync', the snapshot files (beads.base.jsonl, beads.left.jsonl, and their .meta.json files) are left in the .beads/ directory indefinitely.

These files are only needed temporarily during the merge process:
- beads.base.jsonl: snapshot from last successful import
- beads.left.jsonl: snapshot before git pull

Once the merge succeeds and the new JSONL is imported, these files serve no purpose and should be cleaned up.

Current behavior:
- sync.go:269 calls updateBaseSnapshot() after successful import
- UpdateBase() updates beads.base.jsonl to the new state
- beads.left.jsonl is never removed
- Both files accumulate in .beads/ directory

Expected behavior:
- After successful merge and import, clean up both snapshot files
- Only retain snapshots between sync operations (create on export, use during merge, clean up after import)

The cleanup logic exists (SnapshotManager.Cleanup()) but is only called on validation failures (deletion_tracking.go:48), not on success.

Discovered in vc project where stale snapshot files from Nov 8 merge were still present.


Enforce daemon singleton per workspace with file locking

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

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

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

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

## Fix Summary

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

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

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

## Root Cause

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

## Testing

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

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


bd info whats-new missing releases

Current release is v0.25.1 but `bd info --whats-new` stops at v0.23.0, indicating something is missing in the create new release workflow.


bd delete bulk operations fight with auto-import/daemon causing data resurrection

When bulk deleting issues (e.g., 244 closed issues older than 24h), the process fights with auto-import and daemon infrastructure:

**Expected behavior:**
- Delete 244 issues from 468-issue database
- Export to JSONL (224 lines)
- Commit and push
- Result: 224 issues

**Actual behavior:**
- Delete 244 issues
- Import runs (from stale git JSONL with 468 issues)
- Resurrects deleted issues back into database
- Export writes 356 lines (not 224)
- Math: 468 - 244 = 224, but got 356 (132 issues resurrected)

**Root cause:**
Auto-import keeps re-importing from git during the delete operation, before the new JSONL is committed. The workflow is:
1. Delete from DB
2. Auto-import runs (reads old JSONL from git with deleted issues still present)
3. Issues come back
4. Export writes partially-deleted state

**Solution options:**
1. Add `--no-auto-import` flag to bulk delete operations
2. Atomic delete-export-commit operation that suppresses imports
3. Dedicated `bd prune` command that handles this correctly
4. Lock file to prevent auto-import during bulk mutations

**Impact:**
- Bulk cleanup operations don't work reliably
- Makes it nearly impossible to prune old closed issues
- Confusing UX (delete 244, but only 112 actually removed)

**FIXED**: Auto-import now skips during delete operations to prevent resurrection.

**Root cause confirmed**: Auto-import was running in PersistentPreRun before delete executed, causing it to re-import stale JSONL from git and resurrect deleted issues.

**Solution implemented**:
1. Added delete to skip list in main.go PersistentPreRun (alongside import and sync --dry-run)
2. Delete operations now complete atomically without auto-import interference
3. Added comprehensive test (TestBulkDeleteNoResurrection) to prevent regression

**Test verification**:
- Creates 20 issues, deletes 10
- Verifies no resurrection after delete
- Confirms JSONL has correct count (10 remaining)
- All existing tests still pass


Add validation for duplicate external_ref in batch imports

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

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

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

Related: bd-1022


GH#522: Add --type flag to bd update command

Allow changing issue type (task/epic/bug/feature) via bd update --type. Storage layer already supports it. Needed for TUI tools like Abacus. See: https://github.com/steveyegge/beads/issues/522


Configure database connection pool limits for daemon mode

Database connection pool not configured for file-based databases when running in daemon mode.

Location: internal/storage/sqlite/sqlite.go:108-116

Problem:
- Daemon is a long-running server handling concurrent RPC requests
- Multiple CLI commands hit same daemon simultaneously
- Go default: unlimited connections (MaxOpenConns=0)
- SQLite IMMEDIATE transactions serialize on write lock
- Can have 100+ goroutines blocked waiting, each holding connection
- Results in connection exhaustion and 'database is locked' errors

Current code only limits in-memory DBs:
if isInMemory {
    db.SetMaxOpenConns(1)
    db.SetMaxIdleConns(1)
}
// File DBs: UNLIMITED connections!

Fix:
if !isInMemory {
    maxConns := runtime.NumCPU() + 1  // 1 writer + N readers
    db.SetMaxOpenConns(maxConns)
    db.SetMaxIdleConns(2)
    db.SetConnMaxLifetime(0)
}

Impact: 'database is locked' errors under concurrent load in daemon mode

Note: NOT an issue for direct CLI usage (each process isolated). Only affects daemon mode where multiple CLI commands share one database pool.

Effort: 1 hour


Run final validation and cleanup checks

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

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

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

Impact: Confirms all cleanup objectives achieved successfully

## Validation Results

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

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

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

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


Document -short flag in testing guide

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


Event-driven daemon architecture

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

## Implementation Progress

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

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

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

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

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

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

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


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

## Feature Request

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

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

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

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


GH#505: Add bd reset/wipe command

Add command to cleanly reset/wipe beads database. User reports painful manual process to start fresh. See GitHub issue #505.


Auto-recovery mode (bd sync --auto-recover)

Add bd sync --auto-recover flag that: detects problematic sync state, backs up .beads/issues.db with timestamp, rebuilds DB from JSONL atomically, verifies consistency, reports what was fixed. Provides safety valve when sync integrity fails.


Week 1 task


Issue 2


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.


Implement `bd setup cursor` for Cursor IDE integration

Create a `bd setup cursor` command that integrates Beads workflow into Cursor IDE via .cursorrules file. Unlike Claude Code (which has hooks), Cursor uses a static rules file to provide context to its AI.


bd cleanup should also prune expired tombstones

## Problem

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

## Current Behavior

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

## Expected Behavior

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

## Impact

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

## Proposed Solution

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

```
✓ Deleted 15 closed issue(s)
✓ Pruned 3 expired tombstone(s) (older than 30 days)
```

## Files to Modify

- `cmd/bd/cleanup.go` - Add call to pruneExpiredTombstones after deleteBatch
- May need to move pruneExpiredTombstones to shared location if not already accessible

## Acceptance Criteria

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


Deletion propagation via deletions manifest

## Problem

When `bd cleanup -f` or `bd delete` removes issues in one clone, those deletions don't propagate to other clones. The import logic only creates/updates, never deletes. This causes "resurrection" where deleted issues reappear.

## Root Cause

Import sees DB issues not in JSONL and assumes they're "local unpushed work" rather than "intentionally deleted upstream."

## Solution: Deletions Manifest

Add `.beads/deletions.jsonl` - an append-only log of deleted issue IDs with metadata.

### Format
```jsonl
{"id":"bd-xxx","ts":"2025-11-25T10:00:00Z","by":"stevey"}
{"id":"bd-yyy","ts":"2025-11-25T10:05:00Z","by":"claude","reason":"duplicate of bd-zzz"}
```

### Fields
- `id`: Issue ID (required)
- `ts`: ISO 8601 UTC timestamp (required)
- `by`: Actor who deleted (required)
- `reason`: Optional context ("cleanup", "duplicate of X", etc.)

### Import Logic
```
For each DB issue not in JSONL:
  1. Check deletions manifest → if found, delete from DB
  2. Fallback: check git history → if found, delete + backfill manifest
  3. Neither → keep (local unpushed work)
```

### Conflict Resolution
Simultaneous deletions from multiple clones are handled naturally:
- Append-only design means both clones append their deletion records
- On merge, file contains duplicate entries (same ID, different timestamps)
- `LoadDeletions` deduplicates by ID (keeps any/first entry)
- Result: deletion propagates correctly, duplicates are harmless

### Pruning Policy
- Default retention: 7 days (configurable via `deletions.retention_days`)
- Auto-compact during `bd sync` is **opt-in** (disabled by default)
- Hard cap: `deletions.max_entries` (default 50000)
- Git fallback handles pruned entries (self-healing)

### Self-Healing
When git fallback catches a resurrection (pruned entry), it backfills the manifest. One-time git scan cost, then fast again.

### Size Estimates
- ~80 bytes/entry
- 7-day retention with 100 deletions/day = ~56KB
- Git compressed: ~10KB

## Benefits
- ✅ Deletions propagate across clones
- ✅ O(1) lookup (no git scan in normal case)
- ✅ Works in shallow clones
- ✅ Survives history rewrite
- ✅ Audit trail (who deleted what when)
- ✅ Self-healing via git fallback
- ✅ Bounded size via time-based pruning

## References
- Investigation session: 2025-11-25
- Related: bd-2q6d (stale database warnings)


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-f8b764c9 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-f8b764c9 tasks complete (estimated: ~8 weeks from start)


Workaround for FastMCP outputSchema bug in Claude Code

The beads MCP server (v0.23.1) successfully connects to Claude Code, but all tools fail to load with a schema validation error due to a bug in FastMCP 2.13.1.

Error: "Invalid literal value, expected \"object\"" in outputSchema.

Root Cause: FastMCP generates outputSchema with $ref at root level without "type": "object" for self-referential models (Issue).

Workaround: Use slash commands (/beads:ready) or wait for FastMCP fix.


Add --type flag to bd update for changing issue type (task/epic/bug/feature). Storage layer already supports it. See GitHub issue #522.


bd export should detect and prevent stale database exports

## Problem

When `bd export` is run with a stale database (older than issues.jsonl), it silently overwrites the JSONL file with stale data, causing data loss.

## What Happened (vc project)

1. Agent A created 4 new issues and exported to issues.jsonl (commit 99a9d58)
2. Agent A closed an issue and exported again (commit 58b4613) - JSONL now has 4 epics
3. Agent B had stale database (from before step 1)
4. Agent B worked on unrelated issue and exported (commit 0609233)
5. Agent B's export **overwrote issues.jsonl**, removing the 4 epics created by Agent A
6. Required manual recovery by re-exporting from Agent A's correct database

## Expected Behavior

`bd export` should detect that the database is stale and either:
- **Refuse to export** with error message explaining the issue
- **Warn prominently** and require explicit --force flag to override
- **Auto-import first** to sync database before exporting

## How to Detect Staleness

Compare modification times (similar to VC's ValidateDatabaseFreshness):
1. Check .db, .db-wal, .db-shm timestamps (use newest for WAL mode)
2. Check issues.jsonl timestamp
3. If JSONL is newer by >1 second: database is stale

## Suggested Fix

Add staleness check in `bd export`:

```go
func Export(dbPath, jsonlPath string, force bool) error {
    // Check if database is stale
    if !force {
        if err := checkDatabaseFreshness(dbPath, jsonlPath); err != nil {
            return fmt.Errorf("database is stale: %w\n" +
                "Run 'bd import %s' first to sync, or use --force to override",
                err, jsonlPath)
        }
    }

    // Proceed with export...
}
```

## Impact

- **Severity**: High (silent data loss)
- **Frequency**: Happens in multi-agent workflows when agents don't sync
- **Workaround**: Manual recovery (re-export from correct database)

## References

- VC issue tracker: commits 58b4613 -> 0609233 -> c41c638
- VC has similar check: `storage.ValidateDatabaseFreshness()`
- Tolerance: 1 second (handles filesystem timestamp precision)

Fixed with ID-based comparison instead of just count. Now detects:
1. DB has fewer issues than JSONL (count check)
2. DB has different issues than JSONL (ID comparison)

Both scenarios now properly refuse export unless --force is used.

Implementation uses getIssueIDsFromJSONL() to build a set of IDs from JSONL, then checks if any JSONL IDs are missing from DB. Shows specific missing issue IDs in error message.


bd migrate --update-repo-id triggers auto-import that resurrects deleted issues

**Bug:** Running `bd migrate --update-repo-id` can resurrect previously deleted issues from git history.

## What Happened

User deleted 490 closed issues:
- Deletion committed successfully (06d655a) with JSONL at 48 lines
- Database had 48 issues after deletion
- User ran `bd migrate --update-repo-id` to fix legacy database
- Migration triggered daemon auto-import
- JSONL had been restored to 538 issues (from commit 6cd3a32 - before deletion)
- Auto-import loaded the old JSONL over the cleaned database
- Result: 490 deleted issues resurrected

## Root Cause

The auto-import logic in `cmd/bd/sync.go:130-136`:
```go
if isJSONLNewer(jsonlPath) {
    fmt.Println("→ JSONL is newer than database, importing first...")
    if err := importFromJSONL(ctx, jsonlPath, renameOnImport); err != nil {
```

This checks if JSONL mtime is newer than database and auto-imports. The problem:
1. Git operations (pull, merge, checkout) can restore old JSONL files
2. Restored file has recent mtime (time of git operation)
3. Auto-import sees "newer" JSONL and imports it
4. Old data overwrites current database state

## Timeline

- 19:59: Commit 6cd3a32 restored JSONL to 538 issues from d99222d
- 20:22: Commit 3520321 (bd sync)
- 20:23: Commit 06d655a deleted 490 issues → JSONL now 48 lines
- 20:23: User ran `bd migrate --update-repo-id`
- Migration completed, daemon started
- Daemon saw JSONL (restored earlier to 538) was "newer" than database
- Auto-import resurrected 490 deleted issues

## Impact

- **Critical data loss bug** - user deletions can be undone silently
- Affects any workflow that uses git branches, merges, or checkouts
- Auto-import has no safety checks against importing older data
- Users have no warning that old data will overwrite current state

## Fix Options

1. **Content-based staleness** (not mtime-based)
   - Compare JSONL content hash vs database content hash
   - Only import if content actually changed
   - Prevents re-importing old data with new mtime

2. **Database timestamp check**
   - Store "last export timestamp" in database metadata
   - Only import JSONL if it's newer than last export
   - Prevents importing old JSONL after git operations

3. **User confirmation**
   - Before auto-import, show diff of what will change
   - Require confirmation for large changes (>10% issues affected)
   - Safety valve for destructive imports

4. **Explicit sync mode**
   - Disable auto-import entirely
   - Require explicit `bd sync` or `bd import` commands
   - Trade convenience for safety

## Recommended Solution

Combination of #1 and #2:
- Add `last_export_timestamp` to database metadata
- Check JSONL mtime > last_export_timestamp before importing
- Add content hash check as additional safety
- Show warning if importing would delete >10 issues

This preserves auto-import convenience while preventing data loss.

## Files Involved

- `cmd/bd/sync.go:130-136` - Auto-import logic
- `cmd/bd/daemon_sync.go` - Daemon export/import cycle
- `internal/autoimport/autoimport.go` - Staleness detection

## Reproduction Steps

1. Create and delete some issues, commit to git
2. Checkout an earlier commit (before deletion)
3. Checkout back to current commit
4. JSONL file now has recent mtime but old content
5. Run any bd command that triggers auto-import
6. Deleted issues are resurrected


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

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

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

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

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