Daemon gets stuck when auto-import blocked by git conflicts

CRITICAL: The daemon enters a corrupt state that breaks RPC commands when auto-import is triggered but git pull fails due to uncommitted changes.

Impact: This is a data integrity and usability issue that could cause users to lose trust in Beads. The daemon silently fails for certain commands while appearing healthy.

Reproduction:
1. Make local changes to issues (creates uncommitted .beads/beads.jsonl)
2. Remote has updates (JSONL newer, triggers auto-import)
3. Daemon tries to pull but fails: 'cannot pull with rebase: You have unstaged changes'
4. Daemon enters bad state - 'bd show' and other commands return EOF
5. 'bd list' still works, daemon process is running, no errors logged

Technical details:
- Auto-import check runs in handleRequest() before processing RPC commands
- When import is blocked, it appears to corrupt daemon state
- Likely: deadlock, unclosed transaction, or storage handle corruption
- Panic recovery (server_lifecycle_conn.go:183) didn't catch anything - not a panic

Required fix:
- Auto-import must not block RPC command execution
- Handle git pull failures gracefully without corrupting state
- Consider: skip auto-import if git is dirty, queue import for later, or use separate goroutine
- Add timeout/circuit breaker for import operations
- Log clear warnings when auto-import is skipped

Without this fix, users in collaborative environments will frequently encounter mysterious EOF errors that require daemon restarts.


Duplicate tombstone constants in merge and types packages

## Problem

The tombstone constants are duplicated between two packages:

**internal/types/types.go:**
```go
const DefaultTombstoneTTL = 30 * 24 * time.Hour
const ClockSkewGrace = 1 * time.Hour
const StatusTombstone Status = "tombstone"
```

**internal/merge/merge.go:**
```go
const StatusTombstone = "tombstone"
const DefaultTombstoneTTL = 30 * 24 * time.Hour
const ClockSkewGrace = 1 * time.Hour
```

This violates DRY and creates risk of divergence if one is updated but not the other.

## Root Cause

The merge package has its own `Issue` struct (for JSONL parsing) and cannot import types.Issue directly due to the different struct design. However, the constants could be shared.

## Recommendation

Export the constants from types package and import them in merge:

```go
// merge/merge.go
import "github.com/steveyegge/beads/internal/types"

// Use types.StatusTombstone, types.DefaultTombstoneTTL, types.ClockSkewGrace
```

Or create a shared constants package if import cycles are a concern.

## Files
- internal/merge/merge.go:241-248
- internal/types/types.go:77-84, 170


Duplicate DefaultRetentionDays constants

## Problem

There are now two constants for the same value:

1. `deletions.DefaultRetentionDays = 7` in `internal/deletions/deletions.go:184`
2. `configfile.DefaultDeletionsRetentionDays = 7` in `internal/configfile/configfile.go:102`

## Impact
- DRY violation
- Risk of values getting out of sync
- Confusing which one to use

## Fix
Remove the constant from `deletions` package and have it import from `configfile`, or create a shared constants package.


Implement git worktree management

Create git worktree lifecycle management for separate beads branch.

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

Estimated effort: 3-4 days


Consolidate check-health DB opens into single connection

The --check-health flag opens the database 3 separate times (once per quick check). Consolidate into a single DB open for better performance, especially on slower filesystems.


Investigate performance issues in VS Code Copilot (Windows)

Beads unusable in Windows 11 VS Code Copilot chat with Sonnet 4.5.
Summary event happens every 3-4 turns, taking 3 minutes.
Copilot summarizes after ~125k tokens despite model supporting 1M.
Large context size of beads might be triggering aggressive summarization.
Need workaround or optimization for context size.


Extract duplicated metadata update code in daemon_sync.go

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

This violates DRY principle and makes maintenance harder.

## Solution
Extract to helper function:

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

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

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

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

## Files
- cmd/bd/daemon_sync.go

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


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


getLocalSyncBranch YAML parsing is fragile

The getLocalSyncBranch() function in autoimport.go uses a simple line-by-line YAML parser that could fail on:
- Indented sync-branch: (nested in a section)
- Comments containing sync-branch:
- Multi-line values
- Values with special characters

Current implementation at autoimport.go:161-172 does:
```go
for _, line := range strings.Split(string(data), "\n") {
    line = strings.TrimSpace(line)
    if strings.HasPrefix(line, "sync-branch:") {
        value := strings.TrimPrefix(line, "sync-branch:")
        ...
    }
}
```

The reason we can't use viper's cached config is that it's initialized at process startup from the main repo's CWD, not from the current directory at runtime. This causes issues in tests where t.Chdir() changes the directory.

Options:
1. Accept the limitation (sync-branch must be top-level, unquoted)
2. Use a proper YAML library (gopkg.in/yaml.v3) to parse just this value
3. Refactor config package to support re-initialization per directory


Fix CI failures before 0.21.3 release

CI is failing on multiple jobs:
1. Nix flake: Tests fail due to missing git in build environment
2. Windows tests: Need to check what's failing
3. Linux tests: Need to check what's failing
4. Linter errors: Many unchecked errors need fixing

Need to fix before tagging v0.21.3 release.

Fixed linter errors (errcheck, misspell), Nix flake git dependency, and import database discovery bug. Tests still failing - need to investigate further.


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

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

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

    Fixes GH#346.


Auto-import runs during --no-auto-import operations via stats/ready commands

Even when using --no-auto-import flag, certain commands like 'bd stats' and 'bd ready' still trigger auto-import internally, which can cause the git history backfill bug to corrupt data.

Example:
  bd stats --no-auto-import
  # Still prints 'Purged bd-xxx (recovered from git history...)'

The flag should completely disable auto-import for the command, but it appears some code paths still trigger it.

Workaround: Use --allow-stale instead, or --sandbox mode.


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


Update Metrics and Health Endpoints

Remove cache-related metrics from health/metrics endpoints


Fix FindBeadsDir to prioritize main repo .beads for worktrees

The FindBeadsDir function should prioritize finding .beads in the main repository root when accessed from a worktree, rather than finding worktree-local .beads directories. This ensures proper sharing of the database across all worktrees.


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)


bd sync --import-only doesn't update DB mtime causing bd doctor false warning


Missing test for IsExpiredTombstone edge cases

## Problem

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

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

## Current Test Coverage

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

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

## Recommendation

Add explicit unit tests for `IsExpiredTombstone()`:

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

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


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


bd doctor: detect and fix stale beads-sync branch

## Problem

When beads-sync diverges significantly from main (on source code, not just .beads/), it needs to be reset. After a force-push reset, other clones/contributors will have orphaned local beads-sync branches.

## Symptoms
- Local beads-sync is many commits behind main on source files
- Local beads-sync doesn't share recent history with remote beads-sync (after force-push)
- bd sync may behave unexpectedly

## Proposed bd doctor checks

1. **Check beads-sync alignment with main**
   - If beads-sync exists and is >N commits behind main on non-.beads/ files, warn
   - Suggests: reset beads-sync to main

2. **Check beads-sync alignment with remote**
   - If local beads-sync has diverged from origin/beads-sync (no common recent ancestor)
   - This happens after someone force-pushed a reset
   - Suggests: reset local beads-sync to origin/beads-sync

## Proposed fixes (--fix)

- Reset local beads-sync branch to origin/beads-sync or main
- Handle worktree if present (git worktree remove, then recreate on next sync)

## Context
Discovered when beads-sync drifted 126 commits behind main. Required manual reset and force-push. Contributors will need to reset their local branches too.


Prevent multiple daemons from running on the same repo

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

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

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

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


Add MCP functions for repair commands

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

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


Extract Daemon struct and config into internal/daemonrunner

Create internal/daemonrunner with Config struct and Daemon struct. Move daemon runtime logic from cmd/bd/daemon.go Run function into Daemon.Start/Stop methods.

Refactoring complete! Created internal/daemonrunner package with:
- Config struct (config.go)
- Daemon struct with Start/Stop methods (daemon.go)
- RPC server lifecycle (rpc.go)
- Sync loop implementation (sync.go)
- Git operations (git.go)
- Process management (process.go, flock_*.go)
- Logger setup (logger.go)
- Platform-specific signal handling (signals_*.go)
- Database fingerprint validation (fingerprint.go)

Build succeeds and most daemon tests pass. Import functionality still delegated to cmd/bd (marked with TODO(bd-b5a3)).


Respond to GitHub issue #479 with solution

Once bd reset is implemented and released, respond to GitHub issue #479.

## Response should include
- Announce the new bd reset command
- Show basic usage examples
- Link to any documentation
- Thank the user for the feedback

## Example response
```
Thanks for raising this! We've added a `bd reset` command to handle this case.

Usage:
- `bd reset` - Reset to clean state (prompts for confirmation)
- `bd reset --backup` - Create backup first
- `bd reset --hard` - Also clean up git history

This is available in version X.Y.Z.
```

## Notes
- Wait until feature is merged and released
- Consider if issue should be closed or left for user confirmation


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


Implement bd migrate command

Add bd migrate command to move issues between repos with filtering. Should support: filtering by status/priority/labels, dry-run mode, preserving dependencies, handling source_repo field updates.


Refactor check-health DB access to avoid repeated path resolution

The runCheckHealth lightweight checks (hintsDisabled, checkVersionMismatch, checkSyncBranchQuick) each have duplicated database path resolution logic. Extract a helper function to DRY this up.


Optimize --sandbox mode: skip FlushManager

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

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

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

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

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

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

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

Part of bd-olc1


Batch test 1


Latency test manual


Rapid 4


Add mermaid output format for bd dep tree

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

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

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

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


Add tombstone validation: require deleted_at for tombstone status

ValidateWithCustomStatuses() doesn't enforce that tombstones must have deleted_at set. Per bd-2m7 design: 'If status == tombstone, then deleted_at MUST be set'. Also should validate that non-tombstones cannot have deleted_at set.

Location: internal/types/types.go:109-114

Add validation:
- if i.Status == StatusTombstone && i.DeletedAt == nil: error
- if i.Status != StatusTombstone && i.DeletedAt != nil: error


Document which files to commit after bd init --branch

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

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

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

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

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


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


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

## Feature Request

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

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

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

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


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

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

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

**Proposed behavior:**
- `bd sync --squash` accumulates pending exports without committing
- Commits accumulated changes on session end or explicit `bd sync` (without --squash)
- Default behavior unchanged (immediate commits for orchestration)

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

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


bd doctor doesn't detect version mismatches or stale daemons


Use -short flag in CI for PR checks

Update CI configuration to use -short flag for PR checks, run full tests nightly.

The slow tests already support testing.Short() and will be skipped.

Expected savings: ~20 seconds for PR checks (fast tests only)

Implementation:
- Update .github/workflows/ci.yml to add -short flag for PR tests
- Create/update nightly workflow for full test runs
- Update README/docs about test strategy

File: .github/workflows/ci.yml:30


Auto-resolve all field conflicts in merge.go

Eliminate manual conflict resolution by adding deterministic auto-resolve rules for all fields:

- Priority: higher priority wins (lower number = more urgent)
- IssueType: local wins
- Notes: concatenate both sides with separator
- Title: side with latest updated_at on the issue wins
- Description: side with latest updated_at on the issue wins

Currently true conflicts (both sides changed same field to different values) fail sync. With this change, NO conflicts ever require manual resolution.

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


Remove unreachable RPC methods


Auto-configure git merge driver during `bd init`

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

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

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


bd import fails on duplicate external_ref with no resolution options

When JSONL contains duplicate external_ref values (e.g., two issues both have external_ref='BS-170'), bd import fails entirely with no resolution options.

User must manually edit JSONL to remove duplicates, which is error-prone.

Example error:
```
batch import contains duplicate external_ref values:
external_ref 'BS-170' appears in issues: [opal-39 opal-43]
```

Should handle this similar to duplicate issue detection - offer to merge, pick one, or clear duplicates.


Test issue 2


Fix auto-import creating duplicates instead of updating issues

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

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

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

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

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


Fix default .beads/.gitignore to ignore merge artifacts (GH #274)

Updated the default .gitignore template created by `bd init` to properly ignore merge artifacts and fix overly broad patterns.

Changes:
- Added `*.db?*` pattern for database files with query strings
- Added explicit patterns for merge artifacts: beads.{base,left,right}.{jsonl,meta.json}
- Changed `!*.jsonl` to `!issues.jsonl` to avoid including merge artifact JSONL files

This fixes GitHub issue #274 reported by rscorer.


bd delete fails to find closed issues

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

## Expected Behavior
Should delete the closed issues

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

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

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


Create npm package structure with package.json

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

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

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

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


Investigate jujutsu integration for beads

Research and document how beads could integrate with jujutsu (jj), the next-generation VCS. Key areas to explore:
- How jj's operation model differs from git (immutable operations, working-copy-as-commit)
- JSONL sync strategy with jj's conflict resolution model
- Daemon compatibility with jj's more frequent rewrites
- Whether auto-import/export needs changes for jj workflows
- Example configurations and documentation updates needed


Benchmark Helper Functions

Extend existing benchmark helpers in internal/storage/sqlite/bench_helpers_test.go (or create if organizing separately).

Existing helper (in compact_bench_test.go):
- setupBenchDB(tb) - Creates temp SQLite database with basic config
  * Used by compact and cycle benchmarks
  * Returns (*SQLiteStorage, cleanup func())

New helpers to add:
- setupLargeBenchDB(b *testing.B) storage.Storage
  * Creates 10K issue database using LargeSQLite fixture
  * Returns configured storage instance

- setupXLargeBenchDB(b *testing.B) storage.Storage
  * Creates 20K issue database using XLargeSQLite fixture
  * Returns configured storage instance

Implementation options:
1. Add to existing compact_bench_test.go (co-located with setupBenchDB)
2. Create new bench_helpers_test.go for organization

Both approaches:
- Build tag: //go:build bench
- Uses fixture generator from internal/testutil/fixtures
- Follows existing setupBenchDB() pattern
- Handles database cleanup

These helpers reduce duplication across new benchmark functions and provide consistent large-scale database setup.


bd validate - Comprehensive health check

Run all validation checks in one command.

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

Supports --fix-all for auto-repair.

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

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


Ready issue


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

## Problem

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

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

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

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

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

## Fix
Escape regex special characters:

```go
import "regexp"

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


bd prime: AI context loading and Claude Code integration

Implement `bd prime` command and Claude Code hooks for context recovery. Hooks work with BOTH MCP server and CLI approaches - they solve the context memory problem (keeping bd workflow fresh after compaction) not the tool access problem (MCP vs CLI).


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

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

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

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

Impact: False positive warnings in bd doctor output


Rapid 5


Auto-detect scenarios and prompt users

Detect when user is in fork/contributor scenario and prompt with helpful suggestions. Check: git remote relationships, existing .beads config, repo ownership. Suggest appropriate wizard.


Priority 0 in merge may incorrectly win over set priorities

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


Document 3-clone ID non-determinism in collision resolution

Document the known behavior of 3+ way collision resolution where ID assignments may vary based on sync order, even though content always converges correctly.

Updates needed:
- Update bd-71107098 notes to mark 2-clone case as solved
- Document 3-clone ID non-determinism as known limitation
- Add explanation to ADVANCED.md or collision resolution docs
- Explain why this happens (pairwise hash comparison is deterministic, but multi-way ID allocation uses sync-order dependent counters)
- Clarify trade-offs: content convergence ✅ vs ID stability ❌

Key points to document:
- Hash-based resolution is pairwise deterministic
- Content always converges correctly (all issues present with correct data)
- Numeric ID assignments in 3+ way collisions depend on sync order
- This is acceptable for most use cases (content convergence is primary goal)
- Full determinism would require complex multi-way comparison


Sync should cleanup snapshot files after completion

After sync completion, orphan .base.jsonl and .left.jsonl snapshot files remain in .beads/ directory. These should be cleaned up on successful sync.


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

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

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

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

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

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

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


Daemon crashes silently on RPC query after startup

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

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

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

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

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

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


> bd sync --no-daemon
→ Exporting pending changes to JSONL...
→ Committing changes to sync branch 'beads-sync'...
Error committing to sync branch: failed to create worktree: failed to create worktree parent directory: mkdir /var/home/matt/dev/beads/fix-ci/.git: not a directory

**Problem Diagnosed**: The `bd sync` command was failing with "mkdir /var/home/matt/dev/beads/fix-ci/.git: not a directory" because it was being executed from the wrong directory.

**Root Cause**: The command was run from `/var/home/matt/dev/beads` (where the `fix-ci` worktree exists) instead of the main repository directory `/var/home/matt/dev/beads/main`. Since `fix-ci` is a git worktree with a `.git` file (not directory), the worktree creation logic failed when trying to create `<current_dir>/.git/beads-worktrees/<branch>`.

**Solution Verified**: Execute `bd sync` from the main repository directory:
```bash
cd main && bd sync --dry-run
```


Fix N-way collision convergence

Epic to fix the N-way collision convergence problem documented in n-way-collision-convergence.md.

## Problem Summary
The current collision resolution implementation works correctly for 2-way collisions but does not converge for 3-way (and by extension N-way) collisions. TestThreeCloneCollision demonstrates this with reproducible failures.

## Root Causes Identified
1. Pairwise resolution doesn't scale - each clone makes local decisions without global context
2. DetectCollisions modifies state during detection (line 83-86 in collision.go)
3. No remapping history - can't track transitive remap chains (test-1 → test-2 → test-3)
4. Import-time resolution is too late - happens after git merge

## Solution Architecture
Replace pairwise resolution with deterministic global N-way resolution using:
- Content-addressable identity (content hashing)
- Global collision resolution (sort all versions by hash)
- Read-only detection phase (separate from modification)
- Idempotent imports (content-first matching)

## Success Criteria
- TestThreeCloneCollision passes without skipping
- All clones converge to identical content after final pull
- No data loss (all issues present in all clones)
- Works for N workers (test with 5+ clones)
- Idempotent imports (importing same JSONL multiple times is safe)

## Implementation Phases
See child issues for detailed breakdown of each phase.


Implement content-first idempotent import

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

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

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

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

## Implementation Tasks

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

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

    result := &Result{...}

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

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

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

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

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

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

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

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

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

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

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

    return result, nil
}
```

### 2. Implement helper functions

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

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

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

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

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

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

### 3. Add idempotency tests

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

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

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

## Testing Strategy

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

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

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

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

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

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

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


bd sync corrupts issues.jsonl in multi-clone environments

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

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

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

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

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


Standardize daemon detection: use tryDaemonLock probe before RPC

Before attempting RPC connection, call tryDaemonLock() to check if lock is held:
- If lock NOT held: skip RPC attempt (no daemon running)
- If lock IS held: proceed with RPC + short timeout

This is extremely cheap and eliminates unnecessary connection attempts.

Apply across all client entry points that probe for daemon.


Git history fallback missing timeout - could hang on large repos

## Problem

The git commands in `checkGitHistoryForDeletions` have no timeout. On large repos with extensive history, `git log --all -S` or `git log --all -G` can take a very long time (minutes).

## Location
`internal/importer/importer.go:899` and `:930`

## Impact
- Import could hang indefinitely
- User has no feedback that git search is running
- No way to cancel except killing the process

## Fix
Add context with timeout to git commands:

```go
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "git", ...)
```

Also consider adding a `--since` flag to bound the git history search.


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

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

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

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


Auto-invoke 3-way merge for JSONL conflicts

Currently when git pull encounters merge conflicts in .beads/issues.jsonl, the post-merge hook fails with an error message pointing users to manual resolution or the beads-merge tool.

This is a poor user experience - the conflict detection is working, but we should automatically invoke the advanced 3-way merging instead of just telling users about it.

**Current behavior:**
- Detect conflict markers in JSONL
- Display error with manual resolution options
- Exit with failure

**Desired behavior:**
- Detect conflict markers in JSONL
- Automatically invoke beads-merge 3-way merge
- Only fail if automatic merge cannot resolve the conflicts

**Reference:**
- beads-merge tool: https://github.com/neongreen/mono/tree/main/beads-merge
- Error occurs in post-merge hook during bd sync after git pull


Registry has cross-process race condition

The global daemon registry (~/.beads/registry.json) can be corrupted when multiple daemons from different workspaces write simultaneously.

**Root cause:**
- Registry uses an in-process mutex but no file-level locking
- Register() and Unregister() release the mutex between read and write
- Multiple daemon processes can interleave their read-modify-write cycles

**Evidence:**
Found registry.json with double closing bracket: `]]` instead of `]`

**Fix options:**
1. Use file locking (flock/fcntl) around the entire read-modify-write cycle
2. Use atomic write pattern (write to temp file, rename)
3. Both (belt and suspenders)

**Files:**
- internal/daemon/registry.go:46-64 (readEntries)
- internal/daemon/registry.go:67-87 (writeEntries)
- internal/daemon/registry.go:90-108 (Register - the race window)


Beads commands operate on stale database without warning

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

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

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

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

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

## Implementation Complete

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

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

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

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

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


Rapid 1


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

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


Update python-agent example with Agent Mail integration

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

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

File: examples/python-agent/agent.py


Add depth-based batch creation in upsertIssues

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


Add integration tests for bd reset command

End-to-end integration tests for the reset command.

## Test Scenarios

### Basic reset
1. Init beads, create some issues
2. Run bd reset --force
3. Verify .beads/ is fresh, issues gone

### Hard reset
1. Init beads, create issues, commit
2. Run bd reset --hard --force
3. Verify git history has reset commits

### Backup functionality
1. Init beads, create issues
2. Run bd reset --backup --force
3. Verify backup exists with correct contents
4. Verify main .beads/ is reset

### Dry run
1. Init beads, create issues
2. Run bd reset --dry-run
3. Verify nothing changed

### Confirmation prompt
1. Init beads
2. Run bd reset (no --force)
3. Verify prompts for confirmation
4. Test both y and n responses

## Location
tests/integration/reset_test.go or similar


Publish official devcontainer configuration

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


Override test

## Overview

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

## Success Criteria

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

## Background

[Provide context and motivation]

## Scope

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

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


File change test issue


Add tests for helper functions (GetDirtyIssueHash, GetAllDependencyRecords, export hashes)

Several utility functions have 0% coverage:
- GetDirtyIssueHash (dirty.go)
- GetAllDependencyRecords (dependencies.go)
- GetExportHash, SetExportHash, ClearAllExportHashes (hash.go)

These are lower priority but should have basic coverage.


Update LINTING.md with current baseline

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


Add 'bd message send' command for Agent Mail messaging

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

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

## Proposed Commands

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

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

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

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

## Example Usage

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

# Check inbox
bd message inbox --unread-only

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

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

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

## Implementation Summary

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

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

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

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

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

## Implementation Details

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

## Documentation

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

## Testing

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


Auth tokens expire too quickly

## Summary

[Brief description of the bug]

## Steps to Reproduce

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

## Expected Behavior

[What should happen]

## Actual Behavior

[What actually happens]

## Environment

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

## Additional Context

[Screenshots, logs, or other relevant information]


Add test for legacy deletions.jsonl to tombstone conversion

The importer now converts legacy deletions.jsonl entries to tombstones (bd-dve), but there's no dedicated test that:
1. Creates a deletions.jsonl with entries
2. Imports issues (some in deletions, some not)
3. Verifies the converted tombstones have correct fields from the deletion record

The existing TestImportIssues_TombstoneNotFilteredByDeletionsManifest tests that tombstones bypass the filter, but doesn't verify the conversion of legacy deletions to tombstones.

Add a test: TestImportIssues_LegacyDeletionsConvertedToTombstones


Implement transaction retry logic for SQLITE_BUSY

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

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

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

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

Impact: Spurious failures under concurrent write load

Effort: 3 hours


Make DetectCollisions read-only (separate detection from modification)

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

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

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

## Implementation Tasks

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Baseline quality gate failure: test

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

This blocks the executor from claiming work until fixed.

Error: go test failed: exit status 1

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

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


cmd/bd tests fail without -short flag (parallel test deadlock)


Channel isolation test - beads

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


Support --reason flag in daemon mode for reopen command

The reopen.go command has a TODO at line 61 to add reason as a comment once RPC supports AddComment. Currently --reason flag is ignored in daemon mode with a warning.


bd doctor: add SQLite integrity check (PRAGMA integrity_check)

Add a database integrity check using SQLite's PRAGMA integrity_check to detect corruption. Currently bd doctor checks schema compatibility but not data integrity. This would help diagnose corrupted databases that pass schema checks but have internal inconsistencies.


Fix silent error handling in RPC response writing

Marshal and write errors silently ignored in writeResponse, can send partial JSON and hang clients.

Location: internal/rpc/server_lifecycle_conn.go:228-232

Problem:
- json.Marshal error ignored - cyclic reference sends corrupt JSON
- Write error ignored - connection closed, no indication to caller
- WriteByte error ignored - client hangs waiting for newline
- Flush error ignored - partial data buffered

Current code:
func (s *Server) writeResponse(writer *bufio.Writer, resp Response) {
    data, _ := json.Marshal(resp)  // Ignored!
    _, _ = writer.Write(data)      // Ignored!
    _ = writer.WriteByte('\n')     // Ignored!
    _ = writer.Flush()             // Ignored!
}

Solution: Return errors, handle in caller, close connection on error

Impact: Client hangs waiting for response; corrupt JSON sent

Effort: 1 hour


Add docs/architecture/event_driven.md

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


Install and test MCP Agent Mail locally

Install MCP Agent Mail on a single development machine and verify basic functionality.

Acceptance Criteria:
- Server installed via one-line installer
- Server running on port 8765
- Can register a project via HTTP
- Can register an agent identity
- Web UI accessible at /mail

Tested local installation. Server runs on port 8765, web UI works. MCP API tool execution has errors - needs debugging. See /tmp/bd-muls-report.md for details.


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

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

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

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

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

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


Design migration path from deletions.jsonl to tombstones

Plan the migration from current deletions manifest to inline tombstones. Steps: 1) Read existing deletions.jsonl and convert to tombstones during import, 2) Continue reading deletions.jsonl for backward compat, 3) Stop writing to deletions.jsonl, 4) Eventually remove deletions.jsonl support. Consider version flags and deprecation warnings.


Implement bd stale command to find abandoned/forgotten issues

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

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

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

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

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


Add test coverage for internal/autoimport package


Remove Cache-Related Tests

Delete or update tests that assume multi-repo caching


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.


Sync should cleanup snapshot files after completion

## Problem
`bd sync` leaves orphaned merge artifact files (beads.base.jsonl, beads.left.jsonl) after completion, causing:
1. Doctor warnings about 'Multiple JSONL files found'
2. Confusion during debugging
3. Potential stale data issues on next sync

## Root Cause
`SnapshotManager` creates these files for 3-way merge deletion tracking but `Cleanup()` is never called after sync completes (success or failure).

## Fix
Call `SnapshotManager.Cleanup()` at end of successful sync:

```go
// sync.go after successful validation
sm := NewSnapshotManager(jsonlPath)
sm.Cleanup()
```

## Files
- cmd/bd/sync.go (add cleanup call)
- cmd/bd/snapshot_manager.go (Cleanup method exists at line 188)


bd init UX Improvements

bd init leaves users with incomplete setup, requiring manual bd doctor --fix. Issues found: (1) git hooks not installed if user declines prompt, (2) no auto-migration when CLI is upgraded, (3) stale merge driver configs from old versions. Fix by making bd init more robust with better defaults and auto-migration.


Pre-push hook and daemon export produce different JSONL

After committing and pushing, git status shows .beads/beads.jsonl as dirty. Investigation shows:

1. Pre-push hook ran successfully and exported DB → JSONL
2. Push completed
3. Shortly after, daemon exported DB → JSONL again with different content
4. Diff shows comments added to old issues (bd-23a8, bd-6049, bd-87a0)

Timeline:
- Commit c731c45 "Update beads JSONL"
- Pre-push hook exported JSONL
- Push succeeded
- Daemon PID 33314 exported again with different content

Questions:
1. Did someone run a command between commit and daemon export?
2. Is there a timing issue where pre-push hook doesn't capture all DB changes?
3. Should pre-commit hook flush daemon changes before committing?

The comments appear to be from Nov 5 (created_at: 2025-11-05T08:38:46Z) but are only appearing in JSONL now. This suggests the DB had these comments but they weren't exported during pre-push.

Possible causes:
- Pre-push hook uses BEADS_NO_DAEMON=1 which might skip pending writes
- Daemon has unflushed changes in memory
- Race condition between pre-push export and daemon's periodic export

Improved fix based on oracle code review:
1. Pre-push now flushes pending changes first (prevents debounce race)
2. Uses git status --porcelain to catch all change types
3. Handles both beads.jsonl and issues.jsonl
4. Works even if bd not installed (git-only check)


Cache getMultiRepoJSONLPaths() result during sync to avoid redundant calls

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

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

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

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

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

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

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

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

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

        // ... later ...

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

        // ... later ...

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

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

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


Deletions pruning doesn't include results in JSON output

## Problem

When `bd compact --json` runs with deletions pruning, the prune results are silently discarded:

```go
// Only report if there were deletions to prune
if result.PrunedCount > 0 {
    if jsonOutput {
        // JSON output will be included in the main response
        return  // <-- BUG: results are NOT included anywhere
    }
    ...
}
```

## Location
`cmd/bd/compact.go:925-929`

## Impact
- JSON consumers don't know deletions were pruned
- No way to audit pruning via automation

## Fix
Return prune results and include in JSON output structure:

```json
{
  "success": true,
  "compacted": {...},
  "deletions_pruned": {
    "count": 5,
    "retention_days": 7
  }
}
```


CONFIG.md docs for sync.require_confirmation_on_mass_delete missing threshold detail

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


Audit Current Cache Usage

Understand exactly what code depends on the storage cache

Audit complete. See CACHE_AUDIT.md for full findings.

Summary:
- Cache was already removed in commit 322ab63 (2025-10-28)
- server_cache_storage.go deleted (~286 lines)
- All getStorageForRequest calls replaced with s.storage
- All environment variables removed
- MCP multi-repo works via per-project daemon architecture
- All tests updated and passing
- Only stale comment in server.go needed fixing


Improve timestamp comparison readability in import

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

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

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

This is a minor refactor for code clarity.

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


Add global verbosity flags (--verbose, --quiet)

Add consistent verbosity controls across all commands.

**Current state:**
- bd init has --quiet flag
- No other commands have verbosity controls
- Debug output controlled by BD_VERBOSE env var

**Proposal:**
Add persistent flags:
- --verbose / -v: Enable debug output
- --quiet / -q: Suppress non-essential output

**Implementation:**
- Add to rootCmd.PersistentFlags()
- Replace BD_VERBOSE checks with flag checks
- Standardize output levels:
  * Quiet: Errors only
  * Normal: Errors + success messages
  * Verbose: Errors + success + debug info

**Files to modify:**
- cmd/bd/main.go (add flags)
- internal/debug/debug.go (respect flags)
- Update all commands to respect quiet mode

**Testing:**
- Verify --verbose shows debug output
- Verify --quiet suppresses normal output
- Ensure errors always show regardless of mode


Add cancellation and timeout tests

Add comprehensive tests for context cancellation and timeout behavior.

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

## Test Coverage Needed

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

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

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

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

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

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

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

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

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


Investigate TestRoutingIntegration failure

TestRoutingIntegration/maintainer_with_SSH_remote failed during pre-commit check with "expected role maintainer, got contributor".
This occurred while running `go test -short ./...` on darwin/arm64.
The failure appears unrelated to storage/sqlite changes.
Need to investigate if this is a flaky test or environmental issue.


Add import metrics for external_ref matching statistics

Add observability for external_ref matching behavior during imports to help debug and optimize import operations.

Metrics to track:
- Number of issues matched by external_ref
- Number of issues matched by ID
- Number of issues matched by content hash
- Number of external_ref updates vs creates
- Average import time with vs without external_ref

Output format:
- Add to ImportResult struct
- Include in import command output
- Consider structured logging

Use cases:
- Debugging slow imports
- Understanding match distribution
- Optimizing import performance

Related: bd-1022


Add performance benchmarks for multi-repo hydration

The contributor-workflow-analysis.md asserts sub-second queries (line 702) and describes smart caching via file mtime tracking (Decision #4, lines 584-618), but doesn't provide concrete performance benchmarks.

VC's requirement (from VC feedback section):
- Executor polls GetReadyWork() every 5-10 seconds
- Queries must be sub-second (ideally <100ms)
- Smart caching must avoid re-parsing JSONLs on every query

Suggested performance targets to validate:
- File stat overhead: <1ms per repo
- Hydration (when needed): <500ms for typical JSONL (<25k)
- Query (from cache): <10ms
- Total GetReadyWork(): <100ms (VC's requirement)

Also test at scale:
- N=1 repo (baseline)
- N=3 repos (typical)
- N=10 repos (edge case)

These benchmarks are critical for library consumers like VC that run automated polling loops.


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.


Q4 Platform Improvements

## Overview

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

## Success Criteria

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

## Background

[Provide context and motivation]

## Scope

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

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


Clarify .beads/.gitattributes handling in Protected Branches docs

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


Fix SyncJSONLToWorktree to merge instead of overwrite

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

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

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

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

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


Investigate why test issues persist in database

Test issues (bd-0do3, bd-cjxp, bd-phr2, etc.) keep appearing in ready/list output, cluttering real work. These appear to be leftover test data from test runs.

Need to investigate:
1. Why are test issues not being cleaned up after tests?
2. Are tests creating issues in the main database instead of test databases?
3. Should we add better test isolation or cleanup hooks?
4. Can we add a label/prefix to distinguish test issues from real issues?

These test issues have characteristics:
- Empty descriptions
- Generic titles like "Test issue 0", "Bug P0", "Issue to reopen with reason"
- Created around 2025-11-07 19:00-19:07
- Some assigned to test users like "alice", "bob", "testuser"

## Root Cause Analysis

**Problem**: Python MCP integration tests created test issues in production `.beads/beads.db` instead of isolated test databases.

**Evidence**:
- 29 test issues created on Nov 7, 2025 at 19:00-19:07
- Patterns: "Bug P0", "Test issue X", assignees "alice"/"bob"/"testuser"
- Git commit 0e8936b shows test issues committed to .beads/beads.jsonl
- Tests were being fixed for workspace isolation around the same time

**Why It Happened**:
1. Before commit 0e8936b, `test_client_lazy_initialization()` didn't set `BEADS_WORKING_DIR`
2. Tests fell back to discovering `.beads/` in the project root directory
3. Auto-sync committed test issues to production database

**Resolution**:
1. ✅ Closed 29 test pollution issues (bd-0do3, bd-cjxp, etc.)
2. ✅ Added `failIfProductionDatabase()` guard in Go test helpers
3. ✅ Added production pollution checks in RPC test setup
4. ✅ Created `conftest.py` with pytest safety checks for Python tests
5. ✅ Added `BEADS_TEST_MODE` env var to mark test execution
6. ✅ Tests now fail fast if they detect production database usage

**Prevention**:
- All test helper functions now verify database paths are in temp directories
- Python tests fail immediately if BEADS_DB points to production
- BEADS_TEST_MODE flag helps identify test vs production execution
- Clear error messages guide developers to use proper test isolation

**Files Modified**:
- cmd/bd/test_helpers_test.go - Added failIfProductionDatabase()
- internal/rpc/rpc_test.go - Added temp directory verification
- integrations/beads-mcp/tests/conftest.py - New file with pytest safeguards


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

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

**Proposed solutions:**

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

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

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

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

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


Remove Cache Configuration Docs

Remove documentation of deprecated cache env vars


bd detect-pollution - Test pollution detector

Detect test issues that leaked into production DB.

Pattern matching for:
- Titles starting with 'test', 'benchmark', 'sample'
- Sequential numbering (test-1, test-2)
- Generic descriptions
- Created in rapid succession

Optional AI scoring for confidence.

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


Add bd hooks install command to embed git hooks in binary

Currently git hooks are installed via `examples/git-hooks/install.sh`, which only exists in the beads source repo. Users who install bd via installer/homebrew/npm can't easily install hooks.

**Proposal:**
Add `bd hooks install` command that:
- Embeds hook scripts in the bd binary (using go:embed)
- Installs them to .git/hooks/ in current repo
- Backs up existing hooks
- Makes them executable

**Commands:**
- `bd hooks install` - Install all hooks
- `bd hooks uninstall` - Remove hooks
- `bd hooks list` - Show installed hooks status

**Benefits:**
- Works for all bd users, not just source repo users
- More discoverable (shows in bd --help)
- Consistent with bd workflow
- Can version hooks with bd releases


Fix checkExistingBeadsData to be worktree-aware

The checkExistingBeadsData function in cmd/bd/init.go checks for .beads in the current working directory, but for worktrees it should check the main repository root instead. This prevents proper worktree compatibility.


Tombstone export includes tombstones even with --status filter

In export.go, we set IncludeTombstones: true unconditionally. However, if a user runs:

  bd export --status=open

They might not expect tombstones in the output. The IncludeTombstones flag should probably only be set when no status filter is specified, or when --status=tombstone is explicitly requested.

Current behavior: exports tombstones regardless of status filter
Expected behavior: respect status filter, only include tombstones when explicitly requested or no filter


Use in-memory filesystem for test git operations

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

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

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


Update bash-agent example with Agent Mail integration

Add Agent Mail integration to examples/bash-agent/agent.sh using curl for HTTP calls.

Acceptance Criteria:
- Health check function using curl
- Reserve issue before claiming
- Send notifications on status change
- Release on completion
- Graceful degradation if curl fails
- No bash errors when Agent Mail unavailable

File: examples/bash-agent/agent.sh


Test dependency parent


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

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

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

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


GH#243: Recurring stale daemon.lock causes 5s delays

User reports daemon.lock keeps becoming stale after running Claude with beads.

Symptom:
- bd ready takes 5 seconds (exact)
- daemon.lock exists but socket is missing
- bd daemons killall temporarily fixes it
- Problem recurs after using beads with AI agents

User on v0.22.0, Macbook M2, 132 issues (89 closed)

Hypothesis: Daemon is crashing or exiting uncleanly during agent sessions, leaving stale lock file.

Need to:
1. Add crash logging to daemon to understand why it's exiting
2. Improve cleanup on daemon exit (ensure lock is always removed)
3. Add automatic stale lock detection/cleanup
4. Consider making daemon more resilient to crashes

Oracle analysis complete. Converting to epic with 5 focused sub-issues:
1. RPC fast-fail with socket stat + short timeouts (P0)
2. Standardize daemon detection with lock probe (P1)
3. Crash recovery improvements (P2)
4. Self-heal stale artifacts (P2)
5. Diagnostics and debugging (P3)


Fix FOREIGN KEY constraint failures in AddComment and ApplyCompaction

The 'CloseIssue', 'UpdateIssueID', and 'RemoveLabel' methods were fixed in PR #348 to prevent foreign key constraint failures when operating on non-existent issues.

However, the Oracle identified two other methods that follow the same problematic pattern:
1. `AddComment` (in `internal/storage/sqlite/events.go`)
2. `ApplyCompaction` (in `internal/storage/sqlite/compact.go`)

These methods attempt to insert an event record after updating the issue, without verifying that the issue update actually affected any rows. This leads to a foreign key constraint failure if the issue does not exist.

We need to:
1. Create reproduction tests for these failure cases
2. Apply the same fix pattern: check `RowsAffected()` after the update, and return a proper "issue not found" error if it is 0, before attempting to insert the event.
3. Standardize the error message format to "issue %s not found" or "issue not found: %s" for consistency.