Export deduplication breaks when JSONL and export_hashes table diverge

## Problem

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

## Symptoms

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

## Root Cause

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

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

## Impact

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

## Reproduction

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

## Current Workaround

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

## Proposed Solutions

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

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

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

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

## Recommended Fix

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

## Files Involved

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


Modify CreateIssue to support parent resurrection

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


SearchIssues N+1 query causes context timeout with GetLabels

scanIssues() calls GetLabels in a loop for every issue, causing N+1 queries and context deadline exceeded errors when used with short timeouts or in-memory databases. This is especially problematic since SearchIssues already supports label filtering via SQL WHERE clauses.


Add migration guide for library consumers

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

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

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

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

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


Imperator clone on beads-sync branch causes bd sync failures


bd-hv01: Missing cleanup of .merged temp file on failure

Problem: deletion_tracking.go:49 creates tmpMerged file but does not clean up on failure, causing disk space leak and potential interference with subsequent syncs.

Fix: Add defer os.Remove(tmpMerged) after creating temp file path.

Files: cmd/bd/deletion_tracking.go:38-89


Fix Homebrew warning about Ruby file location

Homebrew warning: Found Ruby file outside steveyegge/beads tap formula directory.
Warning points to: /opt/homebrew/Library/Taps/steveyegge/homebrew-beads/bd.rb
It should likely be inside a Formula/ directory or similar structure expected by Homebrew taps.


Event-driven daemon architecture

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

## Implementation Progress

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

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

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

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

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

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

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


Jira export script (jsonl2jira.py)

Create a Python script to push beads issues to Jira.

**Requires**: Jira import script to be complete first (need external_ref matching logic working)

**Features needed**:
- Create new Jira issues from beads issues without external_ref
- Update existing Jira issues matched by external_ref
- Map beads fields back to Jira fields
- Handle Jira workflow transitions (status changes may need transitions)
- Support custom field mapping for design/acceptance_criteria/notes

**Challenges**:
- Jira status changes often require workflow transitions, not direct updates
- Need to discover valid transitions via API
- Custom fields vary by Jira instance

**Usage**:
```bash
bd export | python jsonl2jira.py --create-only  # Only create, don't update
bd export | python jsonl2jira.py                 # Create and update
```

**After creation**: Sets external_ref on beads issue to link back


Fix: --parent flag doesn't create parent-child dependency

When using `bd create --parent <id>`, the code generates a hierarchical child ID (e.g., bd-123.1) but never creates a parent-child dependency. This causes `bd epic status` to show zero children even though child issues exist.

Root cause: create.go generates child ID using store.GetNextChildID() but never calls store.AddDependency() with type parent-child.

Fix: After creating the issue when parentID is set, automatically add a parent-child dependency linking child -> parent.


Add test coverage for internal/config package


Audit Current Cache Usage

Understand exactly what code depends on the storage cache

AUDIT COMPLETE

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

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

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

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

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

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

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

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

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


Add bd deleted command for audit trail

Parent: bd-imj

Add command to view deletion history.

Usage:
  bd deleted              # Show recent deletions (last 7 days)
  bd deleted --since=30d  # Show deletions in last 30 days
  bd deleted --all        # Show all tracked deletions
  bd deleted bd-xxx       # Show deletion details for specific issue

Output format:
  bd-xxx  2025-11-25 10:00  stevey  duplicate of bd-yyy
  bd-yyy  2025-11-25 10:05  claude  cleanup

Acceptance criteria:
- List deletions with timestamp, actor, reason
- Filter by time range
- Lookup specific issue ID
- JSON output option for scripting


Add external_ref change tracking and auditing

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

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

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

Related: bd-1022


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


Code Review: PR #481 - Context Engineering Optimizations

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

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

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

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

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


Consider consolidating config.yaml parsing into shared utility

Multiple places parse config.yaml with custom structs:

1. **autoimport.go:148** - `localConfig{SyncBranch}`
2. **main.go:310** - strings.Contains for no-db (fragile, see bd-r6k2)
3. **doctor.go:863** - strings.Contains for no-db (fragile, see bd-r6k2)
4. **internal/config/config.go** - Uses viper (but caches at startup, problematic for tests)

Consider creating a shared utility in `internal/configfile/` or extending the viper config:

```go
// internal/configfile/yaml.go
type YAMLConfig struct {
    SyncBranch string `yaml:"sync-branch"`
    NoDb       bool   `yaml:"no-db"`
    IssuePrefix string `yaml:"issue-prefix"`
    Author     string `yaml:"author"`
}

func LoadYAML(beadsDir string) (*YAMLConfig, error) {
    // Parse config.yaml with proper YAML library
}
```

Benefits:
- Single source of truth for config.yaml structure
- Proper YAML parsing everywhere
- Easier to add new config fields

Trade-off: May add complexity for simple one-off reads.


Investigate bd export/import sync issue - database and JSONL out of sync

Observed in VC repo: database has 1137 issues but beads.jsonl only has 309. Running 'bd export -o .beads/issues.jsonl' doesn't update the file. Running 'bd import' hangs indefinitely.

Reproduction context:
- VC repo: 1137 issues in DB, 309 in JSONL
- Created 4 new issues with bd create
- bd export didn't write them to JSONL
- bd import hung (possibly daemon lock conflict?)

Need to investigate:
1. Why export doesn't update JSONL when DB has more issues
2. Why import hangs
3. Daemon lock interaction with export/import
4. File path handling (issues.jsonl vs beads.jsonl)

## Root Cause Analysis

The issue was NOT about file path handling or export failing to update JSONL. The actual problem was:

**Import/Export hanging when daemon is running** due to SQLite lock contention:

1. When daemon is connected, `PersistentPreRun` in main.go returns early without initializing the `store` variable
2. Import/Export commands then tried to open the database directly with `sqlite.New(dbPath)`
3. This blocked waiting for the database lock that the daemon already holds → **HANGS INDEFINITELY**

## Solution Implemented

Modified both import.go and export.go to:
1. Detect when `daemonClient` is connected
2. Explicitly close the daemon connection before opening direct SQLite access
3. Added debug logging to help diagnose similar issues

This ensures import/export commands always run in direct mode, avoiding lock contention.

## File Path Handling

The file path confusion (issues.jsonl vs beads.jsonl) was a red herring. The code uses `filepath.Glob("*.jsonl")` which correctly finds ANY `.jsonl` file in `.beads/` directory, so both filenames work.


GH#520: Daemon sync-branch commit fails with pre-commit hooks (needs --no-verify)

gitCommitInWorktree in daemon_sync_branch.go missing --no-verify flag, causing commit failures when pre-commit hooks installed. Library function in worktree.go:684 has correct impl. See: https://github.com/steveyegge/beads/issues/520


WASM integration testing

Comprehensive testing of WASM build. Child of epic bd-44d0.

## Tasks
- [ ] Unit tests for WASM module
- [ ] Integration tests with real JSONL files
- [ ] Test all bd commands for parity
- [ ] Performance benchmarks
- [ ] Test in actual Claude Code Web sandbox
- [ ] Document any limitations

## Test Coverage Target
- >90% of bd CLI commands work identically


Review multi-repo deletion tracking implementation

Thoroughly review the multi-repo deletion tracking fix (bd-4oob):

Files changed:
- cmd/bd/deletion_tracking.go: Added getMultiRepoJSONLPaths() helper
- cmd/bd/daemon_sync.go: Updated snapshot capture/update logic for multi-repo
- cmd/bd/deletion_tracking_test.go: Added 2 new tests (287 lines)

Review focus areas:
1. Correctness: Does getMultiRepoJSONLPaths() handle all edge cases?
2. Performance: Calling getMultiRepoJSONLPaths() 3x per sync (snapshot capture, merge, base update) - should we cache?
3. Error handling: What if some repos fail snapshot operations but others succeed?
4. Race conditions: Multiple daemons in different repos?
5. Test coverage: Are TestMultiRepoDeletionTracking and TestMultiRepoSnapshotIsolation sufficient?
6. Path handling: Absolute vs relative paths, tilde expansion

This is fresh code - needs careful review before considering deletion tracking production-ready.

Code review completed. Overall assessment: Core deletion tracking logic is sound, but error handling and path handling issues make this not yet production-ready for multi-repo scenarios.

Key findings:

CRITICAL ISSUES (Priority 1):
1. Inconsistent error handling in daemon_sync.go - snapshot/merge fail hard but base update warns. Can leave DB in inconsistent state with no rollback. See bd-sjmr.
2. No path normalization in getMultiRepoJSONLPaths() - tilde expansion, relative paths, duplicates not handled. See bd-iye7.

SHOULD FIX (Priority 2):
3. Missing test coverage for edge cases - empty paths, duplicates, partial failures. See bd-kdoh.
4. Performance - getMultiRepoJSONLPaths() called 3x per sync (minor issue). See bd-we4p.

WHAT WORKS WELL:
- Atomic file operations with PID-based temp files
- Good snapshot isolation between repos
- Race condition protection via exclusive locks
- Solid test coverage for happy path scenarios

VERDICT: Address bd-iye7 and bd-sjmr before considering deletion tracking production-ready for multi-repo mode.

Detailed review notes available in conversation history.


Fix Windows-specific test failures in CI

Several tests are failing on Windows but passing on Linux:

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

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

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

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

CI run in progress to verify all fixes.


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

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

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

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

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

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

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


Fix autoimport tests for content-hash collision scoring

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

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

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

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

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

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

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

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


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

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

Current coverage: 47.5%
Target coverage: 60%


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

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

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

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

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

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

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

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

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


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


Add sync.branch configuration

Add configuration layer to support sync.branch setting via config file, environment variable, or CLI flag.

Tasks:
- Add sync.branch field to config schema
- Add BEADS_SYNC_BRANCH environment variable
- Add --branch flag to bd init
- Add bd config get/set sync.branch commands
- Validation (branch name format, conflicts)
- Config migration for existing users

Estimated effort: 1-2 days


Update AGENTS.md with Agent Mail workflow

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

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

File: AGENTS.md (lines 468-475)


bd config set succeeds but doesn't persist to config.toml

Commands like `bd config set daemon.auto_push true` return "Set daemon.auto_push = true" but the config file is never created and `bd info --json | jq '.config'` returns null.

**Steps to reproduce:**
1. Run `bd config set daemon.auto_push true`
2. See success message: "Set daemon.auto_push = true"
3. Check `cat .beads/config.toml` → file doesn't exist
4. Check `bd info --json | jq '.config'` → returns null

**Expected:**
- .beads/config.toml should be created with the setting
- bd info should show the config value

**Impact:**
Users can't enable auto-push/auto-commit via CLI as documented in AGENTS.md


Daemon crash recovery: panic handler + socket cleanup

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

This ensures better diagnostics and cleaner state after crashes.


Create deployment scripts for GCP

Automated provisioning scripts for GCP Compute Engine deployment.

Acceptance Criteria:
- Terraform/gcloud scripts
- Static IP allocation
- Firewall rules
- NGINX reverse proxy config
- TLS setup (Let's Encrypt)
- Systemd service file

File: deployment/agent-mail/gcp/


Documentation

Complete documentation for Agent Mail integration to enable adoption.


Replace getStorageForRequest with Direct Access

Replace all getStorageForRequest(req) calls with s.storage


fix: bd onboard hangs on Windows


Consider using types.Status in merge package for type safety

The merge package uses string for status comparison (e.g., result.Status == closed, issue.Status == StatusTombstone). The types package defines Status as a type alias with validation. While the merge package needs its own Issue struct for JSONL flexibility, it could import and use types.Status for constants to get compile-time type checking. Current code: if left == closed || right == closed. Could be: if left == string(types.StatusClosed). This is low priority since string comparison works correctly. Files: internal/merge/merge.go:44, 488, 501-521


Add WaitGroup tracking to FileWatcher goroutines

FileWatcher spawns goroutines without WaitGroup tracking, causing race condition on shutdown.

Location: cmd/bd/daemon_watcher.go:123-182, 215-291

Problem:
- Goroutines spawned without sync.WaitGroup
- Close() cancels context but doesn't wait for goroutines to exit
- Race condition: goroutine may access fw.debouncer during Close() cleanup
- No guarantee goroutine stopped before fw.watcher.Close() is called

Solution:
- Add sync.WaitGroup field to FileWatcher
- Track goroutines with wg.Add(1) and defer wg.Done()
- Call wg.Wait() in Close() before cleanup

Impact: Race condition on daemon shutdown; potential panic

Effort: 2 hours


Issue 2


Fix bd doctor to recommend issues.jsonl as canonical (not beads.jsonl)


Split test suites: fast vs. integration

Reorganize tests into separate packages/files for fast unit tests vs slow integration tests.

Benefits:
- Clear separation of concerns
- Easier to run just fast tests during development
- Can parallelize CI jobs better

Files to organize:
- beads_hash_multiclone_test.go (slow integration tests)
- beads_integration_test.go (medium-speed integration tests)
- Other test files (fast unit tests)


mergePriority: negative priorities not handled consistently


Implicit deletion cases do not check for tombstones in remaining side

In merge3WayWithTTL(), the implicit deletion cases (lines 426-435) handle when an issue exists in base and one side but not the other. The current logic does not check if the remaining side is a tombstone:

Case: inBase && inLeft && !inRight (deleted implicitly in right)
- If leftIssue is a tombstone, we should keep it (tombstone propagation)
- Currently: continue (drops the tombstone)

Case: inBase && !inLeft && inRight (deleted implicitly in left)
- If rightIssue is a tombstone, we should keep it
- Currently: continue (drops the tombstone)

This could cause tombstones to be lost during merge if one side has a tombstone and the other side implicitly deleted. The tombstone should win since its an explicit deletion record.

Files: internal/merge/merge.go:426-435


Add Claude setup tip with dynamic priority

Add a predefined tip that suggests running `bd setup claude` when Claude Code is detected but not configured. This tip should have higher priority (shown more frequently) until the setup is complete.


Implement topological sort for import ordering

Refactor upsertIssues() to sort issues by hierarchy depth before batch creation. Ensures parents are created before children, fixing latent bug where parent-child pairs in same batch can fail if ordered wrong. Sort by dot count, create in depth-order batches (0→1→2→3).


Daemons don't exit when parent process dies, causing accumulation and race conditions

Multiple daemon processes accumulate over time because daemons don't automatically stop when their parent process (e.g., coding agent) is killed. This causes:

1. Race conditions: 8+ daemons watching same .beads/beads.db, each with own 30s debounce timer
2. Git conflicts: Multiple daemons racing to commit/push .beads/issues.jsonl
3. Resource waste: Orphaned daemons from sessions days/hours old still running

Example: User had 8 daemons from multiple sessions (12:37AM, 7:20PM, 7:22PM, 7:47PM, 9:19PM yesterday + 9:54AM, 10:55AM today).

Solutions to consider:
1. Track parent PID and exit when parent dies
2. Use single global daemon instead of per-session
3. Document manual cleanup: pkill -f "bd daemon"
4. Add daemon lifecycle management (auto-cleanup of stale daemons)

Implementation complete:

1. Added ParentPID field to DaemonLockInfo struct (stored in daemon.lock JSON)
2. Daemon now tracks parent PID via os.Getppid() at startup
3. Both event loops (polling and event-driven) check parent process every 10 seconds
4. Daemon gracefully exits if parent process dies (detected via isProcessRunning check)
5. Handles edge cases:
   - ParentPID=0: Older daemons without tracking (ignored)
   - ParentPID=1: Adopted by init means parent died (exits)
   - Otherwise checks if parent process is still running

The fix prevents daemon accumulation by ensuring orphaned daemons automatically exit within 10 seconds of parent death.


Add close_reason field to CLI schema and documentation

PR #551 persists close_reason, but the CLI documentation may not mention this field as part of the issue schema.

## Current State
- close_reason is now persisted in database
- `bd show --json` will return close_reason in JSON output
- Documentation may not reflect this new field

## What's Missing
- CLI reference documentation for close_reason field
- Schema documentation showing close_reason is a top-level issue field
- Example output showing close_reason in bd show --json
- bd close command documentation should mention close_reason parameter is optional

## Suggested Action
1. Update README.md or CLI reference docs to list close_reason as an issue field
2. Add example to bd close documentation
3. Update any type definitions or schema specs
4. Consider adding close_reason to verbose list output (bd list --verbose)


Mass deletion logging for forensics

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

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

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

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


Deletions not propagated across multi-workspace sync

## Problem

When working with multiple beads workspaces (clones) sharing the same git remote, deleted issues keep coming back.

## Reproduction

1. Clone A deletes issue `bd-xyz` via `bd delete bd-xyz --force`
2. Clone A daemon syncs and pushes to GitHub
3. Clone B still has `bd-xyz` in its database
4. Clone B daemon exports and pushes its JSONL
5. Clone A pulls and imports → `bd-xyz` comes back!

## Root Cause

**No deletion tracking mechanism.** The system has no way to distinguish between:
- "Issue doesn't exist in JSONL because it was deleted"
- "Issue doesn't exist in JSONL because the export is stale"

Import treats missing issues as "not in this export" rather than "explicitly deleted."

## Solution Options

1. **Tombstone records** - Keep deleted issues in JSONL with `"status":"deleted"` or `"deleted_at"` field
2. **Deletion log** - Separate `.beads/deletions.jsonl` file tracking all deleted IDs
3. **Three-way merge** - Import compares: DB state, old JSONL, new JSONL
4. **Manual conflict resolution** - Detect resurrection and prompt user

## Related

- Similar to resurrection logic for orphaned children (bd-cc4f)
- beads-merge tool handles this better with 3-way merge


Audit and standardize JSON output across all commands

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

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

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

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


Database import silently fails when daemon version != CLI version


Add uninstall documentation (GH #445)


Unit tests for FileWatcher

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


Issue with desc

This is a description


Improve CheckStaleness error handling

## Problem

CheckStaleness returns 'false' (not stale) for multiple error conditions instead of returning errors. This masks problems.

**Location:** internal/autoimport/autoimport.go:253-285

## Edge Cases That Return False

1. **Invalid last_import_time format** (line 259-262)
2. **No JSONL file found** (line 267-277)
3. **JSONL stat fails** (line 279-282)

## Fix

Return errors for abnormal conditions:

```go
lastImportTime, err := time.Parse(time.RFC3339, lastImportStr)
if err != nil {
    return false, fmt.Errorf("corrupted last_import_time: %w", err)
}

if jsonlPath == "" {
    return false, fmt.Errorf("no JSONL file found")
}

stat, err := os.Stat(jsonlPath)
if err != nil {
    return false, fmt.Errorf("cannot stat JSONL: %w", err)
}
```

## Impact
Medium - edge cases are rare but should be handled

## Effort
30 minutes - requires updating callers in RPC server


MCP server: "No workspace set" and "chunk longer than limit" errors

Two related errors reported in beads-mcp v0.21:

**Error 1: "No workspace set" after successful set_context**
```
✓ Set beads context
✗ list
  Error calling tool 'list': No workspace set. Either provide workspace_root
  parameter or call set_context() first.
```

Hypothesis: Environment variable persistence issue between MCP tool calls, or ContextVar not being set correctly by @with_workspace decorator.

**Error 2: "Separator is found, but chunk is longer than limit"**
```
✗ list
  Error calling tool 'list': Separator is found, but chunk is longer than limit
```

Hypothesis: MCP protocol output size limit exceeded. Large issue databases may produce JSON output that exceeds MCP stdio buffer limits.

Platform: Fedora 43, using copilot-cli with Sonnet 4.5

Workaround: CLI works fine (`bd list --status open --json`)

## Fixes Implemented

**Issue 1: "No workspace set" after successful set_context** ✅ FIXED

Root cause: os.environ doesn't persist across MCP tool calls. When set_context() set BEADS_WORKING_DIR in os.environ, that change was lost on the next tool call.

Solution:
- Added module-level _workspace_context dict for persistent storage (server.py:51)
- Modified set_context() to store in both persistent dict and os.environ (server.py:265-287)
- Modified with_workspace() decorator to check persistent context first (server.py:129-133)
- Updated where_am_i() to check persistent context (server.py:302-330)

**Issue 2: "chunk longer than limit"** ✅ FIXED

Root cause: MCP stdio protocol has buffer limits. Large issue lists with full dependencies/dependents exceed this.

Solution:
- Reduced default list limit from 50 to 20 (server.py:356, models.py:122)
- Reduced max list limit from 1000 to 100 (models.py:122)
- Strip dependencies/dependents from list() and ready() responses (server.py:343-350, 368-373)
- Full dependency details still available via show() command

## Testing

✅ Python syntax validated with py_compile
✅ Changes are backward compatible
✅ Persistent context falls back to os.environ for compatibility

Users should now be able to call set_context() once and have it persist across all subsequent tool calls. Large databases will no longer cause buffer overflow errors.


Updated title


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

## Problem

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

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

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

## Impact

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

## Root Cause

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

## Fix

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

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

## Action Items

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


Fix dependency naming inconsistencies (GH #440)

Parent-child dependency documentation is backwards and UI labels are confusing.\n\nProblems:\n1. DEPENDENCIES.md says 'bd dep add PARENT CHILD' but this is rejected\n2. bd show displays epic children under 'Blocks' instead of 'Children'\n3. bd dep tree EPIC shows nothing (need --direction=up)\n4. Inconsistent with 'bd epic status' which uses 'children'\n\nSee: https://github.com/steveyegge/beads/issues/440


bd-hv01: Silent partial deletion failures cause DB inconsistency

Problem: deletion_tracking.go:76-77 logs deletion errors as warnings but continues. If deletion fails midway (database locked, disk full), some issues delete but others don't. System thinks all deletions succeeded.

Impact: Database diverges from JSONL, silent corruption, issues may resurrect on next sync.

Fix: Collect errors and fail the operation:
var deletionErrors []error
for _, id := range acceptedDeletions {
    if err := d.DeleteIssue(ctx, id); err != nil {
        deletionErrors = append(deletionErrors, fmt.Errorf("issue %s: %w", id, err))
    }
}
if len(deletionErrors) > 0 {
    return false, fmt.Errorf("deletion failures: %v", deletionErrors)
}

Files: cmd/bd/deletion_tracking.go:73-82


Document error handling strategy for metadata update failures

Multiple places silently ignore metadata update failures (sync.go:614-617, import.go:320-322) with non-fatal warnings. This is intentional (degrades gracefully to mtime-based approach) but not well documented.

Add comments explaining:
- Why these failures are non-fatal
- How system degrades gracefully
- What the fallback behavior is (mtime-based detection)


Document CompactedResult response format in CONTEXT_ENGINEERING.md

The CompactedResult is a new response format that MCP clients need to understand, but it's not documented in CONTEXT_ENGINEERING.md.

## What's Missing
- Example CompactedResult JSON response
- How to detect if a result is compacted
- How to request full results (disable compaction or increase limit)
- Guidance on handling both list[IssueMinimal] and CompactedResult return types
- Migration guide for clients expecting list[Issue] from ready() and list()

## Documentation Gaps
Currently CONTEXT_ENGINEERING.md explains the optimization strategy but doesn't show:
1. Sample CompactedResult response
2. Client-side code examples for handling compaction
3. When compaction is triggered (>20 results)
4. How to get all results if needed

## Suggested Additions
Add new section \"Handling Large Result Sets\" with:
- CompactedResult schema documentation
- Python client example for handling both response types
- Guidance on re-issuing queries with better filters


Test issue 2


Speed up main_test.go - tests hang indefinitely due to nil rootCtx

## Problem

main_test.go tests are hanging indefinitely, making them unusable and blocking development.

## Root Cause

Tests that call flushToJSONL() or autoImportIfNewer() hang forever because:
- rootCtx is nil in test environment (defined in cmd/bd/main.go:67)
- flushToJSONL() uses rootCtx for DB operations (autoflush.go:503)
- When rootCtx is nil, DB calls timeout/hang indefinitely

## Affected Tests (10+)

- TestAutoFlushOnExit
- TestAutoFlushJSONLContent
- TestAutoFlushErrorHandling
- TestAutoImportIfNewer
- TestAutoImportDisabled
- TestAutoImportWithUpdate
- TestAutoImportNoUpdate
- TestAutoImportMergeConflict
- TestAutoImportConflictMarkerFalsePositive
- TestAutoImportClosedAtInvariant

## Proof

```bash
# Before fix: TestAutoFlushJSONLContent HANGS (killed after 2+ min)
# After adding rootCtx init: Completes in 0.04s
# Speedup: ∞ → 0.04s
```

## Solution

See MAIN_TEST_OPTIMIZATION_PLAN.md for complete fix plan.

**Quick Fix (5 min)**: Add to each hanging test:
```go
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
oldRootCtx := rootCtx
rootCtx = ctx
defer func() { rootCtx = oldRootCtx }()
```

**Full Optimization (40 min total)**:
1. Fix rootCtx (5 min) - unblocks tests
2. Reduce sleep durations 10x (2 min) - saves ~280ms
3. Use in-memory DBs (10 min) - saves ~1s
4. Share test fixtures (15 min) - saves ~1.2s
5. Fix skipped debounce test (5 min)

## Expected Results

- Tests go from hanging → <5s total runtime
- Keep critical integration test coverage
- Tests caught real bugs: bd-270, bd-206, bd-160

## Files

- Analysis: MAIN_TEST_REFACTOR_NOTES.md
- Solution: MAIN_TEST_OPTIMIZATION_PLAN.md
- Tests: cmd/bd/main_test.go (18 tests, 1322 LOC)

## Priority

P1 - Blocking development, tests unusable in current state


Add "bd daemons" command for multi-daemon management

Add a new "bd daemons" command with subcommands to manage daemon processes across all beads repositories/worktrees. Should show all running daemons with metadata (version, workspace, uptime, last sync), allow stopping/restarting individual daemons, auto-clean stale processes, view logs, and show exclusive lock status.


Add integration tests for CompactedResult in MCP server

The CompactedResult model is used for list operations when results exceed COMPACTION_THRESHOLD (20 items), but there are no integration tests verifying the actual MCP server behavior with large result sets.

## What's Missing
- Integration test that mocks a large result set (>20 issues) and verifies CompactedResult is returned
- Verification that preview_count matches PREVIEW_COUNT (5)
- Verification that total_count reflects actual result size
- Test that compaction threshold is configurable (currently hardcoded COMPACTION_THRESHOLD=20)

## Why This Matters
The compaction logic is critical for preventing context overflow with large lists. Without integration tests, we can't verify it works correctly with the actual MCP protocol.

## Suggested Implementation
Create test_mcp_compaction.py with:
1. test_list_with_large_result_set_returns_compacted_result()
2. test_ready_work_respects_compaction_threshold()
3. test_compacted_result_structure_is_valid()


bd import silently succeeds when database matches working tree but not git HEAD

**Critical**: bd import reports '0 created, 0 updated' when database matches working tree JSONL, even when working tree is ahead of git HEAD. This gives false confidence that everything is synced with the source of truth.

## Reproduction

1. Start with database synced to working tree .beads/issues.jsonl (376 issues)
2. Git HEAD has older version of .beads/issues.jsonl (354 issues)
3. Run: bd import .beads/issues.jsonl
4. Output: 'Import complete: 0 created, 0 updated'

## Problem

User expects 'bd import' after 'git pull' to sync database with committed state, but:
- Command silently succeeds because DB already matches working tree
- No warning that working tree has uncommitted changes
- User falsely believes everything is synced with git
- Violates 'JSONL in git is source of truth' principle

## Expected Behavior

When .beads/issues.jsonl differs from git HEAD, bd import should:
1. Detect uncommitted changes: git diff --quiet HEAD .beads/issues.jsonl
2. Warn user: 'Warning: .beads/issues.jsonl has uncommitted changes (376 lines vs 354 in HEAD)'
3. Clarify status: 'Import complete: 0 created, 0 updated (already synced with working tree)'
4. Recommend: 'Run git diff .beads/issues.jsonl to review uncommitted work'

## Impact

- Users can't trust 'bd import' status messages
- Silent data loss risk if user assumes synced and runs git checkout
- Breaks mental model of 'JSONL in git = source of truth'
- Critical for VC's landing-the-plane workflow


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


Add schema compatibility probe to prevent silent migration failures

Issue #262 revealed a serious bug: migrations may fail silently, causing UNIQUE constraint errors later.

Root cause:
- sqlite.New() runs migrations once on open
- checkVersionMismatch() prints 'database will be upgraded automatically' but only updates metadata
- If migrations fail or daemon runs older version, queries expecting new columns fail with 'no such column'
- Import logic misinterprets this as 'not found' and tries INSERT on existing ID
- Result: UNIQUE constraint failed: issues.id

Fix strategy (minimal):
1. Add schema probe in sqlite.New() after RunMigrations
   - SELECT all expected columns from all tables with LIMIT 0
   - If fails, retry RunMigrations and probe again
   - If still fails, return fatal error with clear message
2. Fix checkVersionMismatch to not claim 'will upgrade' unless probe passes
3. Only update bd_version after successful migration probe
4. Add schema verification before import operations
5. Map 'no such column' errors to clear actionable message

Related: #262


Week 1 task


Wrap git operations in GitClient interface

Create internal/daemonrunner/git.go with GitClient interface (HasUpstream, HasChanges, Commit, Push, Pull). Default implementation using os/exec. Use in Syncer and Run loop for testability.


Test dependency child


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


Add command injection prevention tests for git rm in merge command

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

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

In cmd/bd/merge.go:121, exec.Command is called with variable fullPath, flagged by gosec G204 for potential command injection.

Add tests covering:
- File paths with shell metacharacters (;, |, &, $, etc.)
- Paths with spaces and special characters
- Paths attempting command injection (e.g., 'file; rm -rf /')
- Validation that fullPath is properly sanitized
- Only valid git-tracked files can be removed

This is a critical security path preventing arbitrary command execution.

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


TestImportIssues_LegacyDeletionsConvertedToTombstones is failing

The test TestImportIssues_LegacyDeletionsConvertedToTombstones in internal/importer/importer_test.go is failing:

Expected 3 created (1 regular + 2 tombstones from deletions.jsonl), got 2
Expected tombstone for test-deleted2 not found

The test expects legacy deletions.jsonl entries to be converted to tombstones during import, but test-deleted2 is not being converted.

Location: internal/importer/importer_test.go:1344


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

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

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


Code Review Followup Summary: PR #481 + PR #551

## Merged PRs Summary

### PR #551: Persist close_reason to issues table
- ✅ Merged successfully
- ✅ Bug fix: close_reason now persisted in database column (not just events table)
- ✅ Comprehensive test coverage added
- ✅ Handles reopen case (clearing close_reason)

**Followup Issues Filed:**
- bd-lxzx: Document close_reason in JSONL export format
- bd-077e: Update CLI documentation for close_reason field

---

### PR #481: Context Engineering Optimizations (80-90% context reduction)
- ✅ Merged successfully
- ✅ Lazy tool discovery: discover_tools() + get_tool_info()
- ✅ Minimal issue models: IssueMinimal (~80% smaller than full Issue)
- ✅ Result compaction: Auto-compacts results >20 items
- ✅ All 28 tests passing
- ⚠ Breaking change: ready() and list() return type changed

**Followup Issues Filed:**
- bd-b318: Add integration tests for CompactedResult
- bd-4u2b: Make compaction settings configurable (THRESHOLD, PREVIEW_COUNT)
- bd-2kf8: Document CompactedResult response format in CONTEXT_ENGINEERING.md
- bd-pdr2: Document backwards compatibility considerations

---

## Overall Assessment

Both PRs are production-ready with solid implementations. All critical functionality works and tests pass. Followup issues focus on:
1. Documentation improvements (5 issues)
2. Integration test coverage (1 issue)
3. Configuration flexibility (1 issue)
4. Backwards compatibility guidance (1 issue)

No critical bugs or design issues found.

## Review Completed By
Code review process completed. Issues auto-created for tracking improvements.


troubleshoot GH#278 daemon exits every few secs


Create multi-agent race condition test

Automated test that runs 2+ agents simultaneously to verify collision prevention.

Acceptance Criteria:
- Script spawns 2 agents in parallel
- Both try to claim same issue
- Only one succeeds (via reservation)
- Other agent skips to different work
- Verify in JSONL that no duplicate claims
- Test with Agent Mail enabled/disabled

File: tests/integration/test_agent_race.py

Success Metric: Zero duplicate claims with Agent Mail, collisions without it


Code review follow-up for bd-dvd and bd-ymj fixes

Track improvements and issues identified during code review of parent resurrection (bd-dvd) and export metadata (bd-ymj) bug fixes.

## Context
Code review identified several areas for improvement:
- Code duplication in metadata updates
- Missing multi-repo support
- Test coverage gaps
- Potential race conditions

## Related Issues
Original bugs fixed: bd-dvd, bd-ymj

## Goals
- Eliminate code duplication
- Add multi-repo support where needed
- Improve test coverage
- Address edge cases


Batch test 3


Sync protection mechanism resurrects deleted issues

During bd sync, the 'Protecting N issue(s) from left snapshot' logic resurrects issues that are in the deletions manifest, causing UNIQUE constraint failures on subsequent syncs.

## Reproduction
1. Delete an issue with bd delete (creates tombstone + deletions.jsonl entry)
2. Run bd sync - succeeds
3. Run bd sync again - fails with 'UNIQUE constraint failed: issues.id'

## Root Cause
The protection mechanism (designed to prevent data loss during 3-way merge) keeps a snapshot of issues before sync. When importing after pull, it restores issues from this snapshot even if they're in the deletions manifest.

## Observed Behavior
- bd-3pd was deleted and added to deletions.jsonl
- First sync exports tombstone, commits, pulls, imports - succeeds
- Second sync: protection restores bd-3pd from left snapshot
- Import tries to create bd-3pd which already exists → UNIQUE constraint error

## Expected Behavior
Issues in deletions manifest should NOT be restored by protection mechanism.

## Workaround
Manually delete from DB: sqlite3 .beads/beads.db 'DELETE FROM issues WHERE id = "bd-xxx"'

## Files to Investigate
- cmd/bd/sync.go - protection logic
- cmd/bd/snapshot_manager.go - left snapshot handling
- internal/importer/importer.go - import with protection


convertDeletionToTombstone uses hardcoded default values

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

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

Low priority cleanup for consistency.


Document bd-wasm installation and usage

Create documentation for bd-wasm:
- Update README with npm installation instructions
- Add troubleshooting section for WASM-specific issues
- Document known limitations vs native bd
- Add examples for Claude Code Web sandbox usage
- Update INSTALLING.md with bd-wasm option


Implement filesystem shims for WASM

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

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

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


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


Add tests for daemon config module


Add t.Parallel() to slow hash multiclone tests

Add t.Parallel() to TestHashIDs_MultiCloneConverge and TestHashIDs_IdenticalContentDedup so they run concurrently.

Expected savings: ~10 seconds (from 20s to ~11s)

Implementation:
- Add t.Parallel() call at start of each test function
- Verify tests don't share resources that would cause conflicts
- Run tests to confirm they work in parallel

File: beads_hash_multiclone_test.go:34, :101


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.


Client self-heal: remove stale pid when lock free + socket missing

When client detects:
- Socket is missing AND
- tryDaemonLock shows lock NOT held

Then automatically:
1. Remove stale daemon.pid file
2. Optionally auto-start daemon (behind BEADS_AUTO_START_DAEMON=1 env var)

This prevents stale artifacts from accumulating after daemon crashes.


Performance Testing Documentation

Create docs/performance-testing.md documenting the performance testing framework.

Sections:
1. Overview - What the framework does, goals
2. Running Benchmarks
   - make bench command
   - Running specific benchmarks
   - Interpreting output (ns/op, allocs/op)
3. Profiling and Analysis
   - Viewing CPU profiles with pprof
   - Reading flamegraphs
   - Memory profiling
   - Finding hotspots
4. User Diagnostics
   - bd doctor --perf usage
   - Sharing profiles with bug reports
   - Understanding the report output
5. Comparing Performance
   - Using benchstat for before/after comparisons
   - Detecting regressions
6. Tips for Optimization
   - Common patterns
   - When to profile vs benchmark

Style:
- Concise, practical examples
- Screenshots/examples of pprof output
- Clear command-line examples
- Focus on workflow, not theory


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


Add unit tests for reset package

Comprehensive unit tests for internal/reset package.

## Test Cases

### ValidateState tests
- .beads/ exists → success
- .beads/ missing → appropriate error
- git dirty state detection

### CountImpact tests
- Empty .beads/ → zero counts
- With issues → correct count (open vs closed)
- With tombstones → correct count
- Returns HasUncommitted correctly

### Backup tests
- Creates backup with correct timestamp format
- Preserves all files and permissions
- Returns correct path
- Handles missing .beads/ gracefully
- Errors on pre-existing backup dir

### Git operation tests
- CheckGitState detects dirty, detached, not-a-repo
- GitRemoveBeads removes correct files
- GitCommitReset creates commit with message
- Operations skip gracefully when not in git repo

### Reset tests (with mocks/temp dirs)
- Soft reset removes files, calls init
- Hard reset includes git operations
- Dry run doesn't modify anything
- SkipInit flag prevents re-initialization
- Daemon killall is called
- Backup is created when requested

## Approach
- Can start with interface definitions (TDD style)
- Use testify for assertions
- Create temp directories for isolation
- Mock git operations where needed
- Test completion depends on implementation tasks

## File Location
`internal/reset/reset_test.go`
`internal/reset/backup_test.go`
`internal/reset/git_test.go`


Create Python adapter library

Create beads_mail_adapter.py library that wraps Agent Mail HTTP calls with health checks and graceful degradation.

Acceptance Criteria:
- AgentMailAdapter class with health check on init
- enabled flag auto-disables if server unreachable
- All methods wrapped in try/catch (non-blocking failures)
- Methods: reserve_issue(), release_issue(), notify(), check_inbox()
- Environment-based configuration (AGENT_MAIL_URL, AGENT_MAIL_TOKEN)
- Unit tests for enabled/disabled modes

File: lib/beads_mail_adapter.py


Add programmatic tip injection API

Allow tips to be programmatically injected at runtime based on detected conditions. This enables dynamic tips (not just pre-defined ones) to be shown with custom priority and frequency.


Database pollution in ~/src/dave/vc: 895 issues vs canonical 310

~/src/dave/vc/.beads/beads.db has 895 total issues (675 open, 149 closed), but canonical ~/src/vc/.beads/vc.db has only 310 issues (230 open). This is 585 extra issues - likely pollution from other repositories.

Need to:
1. Identify which issues are polluted (use detect-pollution)
2. Compare issue IDs between dave/vc and canonical vc databases
3. Determine pollution source (beads repo? other repos?)
4. Clean up polluted database
5. Root cause: why did pollution occur?

Investigation findings so far:
- Polluted DB (~/src/dave/vc/.beads/beads.db): 241 issues (180 open, 43 closed)
- Canonical DB (~/src/vc/.beads/vc.db): 310 issues (230 open, 62 closed)
- Contradiction: Polluted has FEWER issues, not more (241 < 310, diff of 69)
- Only 1 unique ID in polluted: vc-55fi
- All source_repo fields are set to "." in both databases
- Issue description claims 895 issues in polluted vs 310 canonical - numbers don't match current state
- Possible: Pollution was already partially cleaned, or issue description refers to different database?


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

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


Improve test coverage for cmd/bd/doctor/fix (23.9% → 50%)

The doctor/fix package has only 23.9% test coverage. The doctor fix functionality is important for troubleshooting.

Current coverage: 23.9%
Target coverage: 50%


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


Test issue 2


Benchmark Suite for Critical Operations

Extend existing benchmark suite with comprehensive benchmarks for critical operations at 10K-20K scale.

Existing benchmarks (keep these):
- cycle_bench_test.go - Cycle detection up to 5K issues (linear, tree, dense graphs)
- compact_bench_test.go - Compaction candidate queries (100 issues)
- internal/rpc/bench_test.go - Daemon vs direct mode comparison

New benchmarks to add in sqlite_bench_test.go (~10-12 total):
1. GetReadyWork - Simple, deep hierarchies, cross-linked (CRITICAL - not currently benchmarked)
2. SearchIssues - No filters, complex filters (CRITICAL - not currently benchmarked)
3. CreateIssue - Single issue creation
4. UpdateIssue - Status/priority/assignee changes
5. AddDependency - Extend to 10K/20K scale (currently only up to 5K)
6. JSONL Export - Full export performance
7. JSONL Import - Import performance

Scale levels:
- Large: 10K issues (5K open, 5K closed)
- XLarge: 20K issues (10K open, 10K closed)

Implementation:
- NEW FILE: internal/storage/sqlite/sqlite_bench_test.go
- Keep existing cycle_bench_test.go and compact_bench_test.go unchanged
- Build tag: //go:build bench
- Standard testing.B benchmarks
- b.ReportAllocs() for memory tracking
- Test both SQLite and JSONL-imported databases

Always generates CPU and memory profiles for analysis.


Add bd daemon --stop-all command to kill all daemon processes

Currently there's no easy way to stop all running bd daemon processes. Users must resort to pkill -f 'bd daemon' or similar shell commands.

Add a --stop-all flag to bd daemon that:
1. Finds all running bd daemon processes (not just the current repo's daemon)
2. Gracefully stops them all
3. Reports how many were stopped

This is useful when:
- Multiple daemons are running and causing race conditions
- User wants a clean slate before running bd sync
- Debugging daemon-related issues


Run final validation and cleanup checks

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

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

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

Impact: Confirms all cleanup objectives achieved successfully

## Validation Results

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

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

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

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


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.


Run final validation and cleanup checks

## Validation Results (Oct 31, 2025)

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

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

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


Complete and commit beads-mcp type safety improvements

Uncommitted type safety work in beads-mcp needs review and completion


Fix TestTwoCloneCollision timeout


Add Agent Mail to QUICKSTART.md

Mention Agent Mail as optional advanced feature in quickstart guide.

File: docs/QUICKSTART.md


Add --parent flag to bd onboard output

bd onboard didn't document --parent flag for epic subtasks, causing AI agents to guess wrong syntax. Added --parent example and CLI help section pointing to bd <cmd> --help.

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


CLI accepts both hash IDs and aliases

Update all CLI commands to accept both hash IDs (bd-af78e9a2) and aliases (#42, or just 42).

## Parsing Logic
```go
// internal/utils/id_parser.go
func ParseIssueID(input string) (issueID string, err error) {
    // Hash ID: bd-af78e9a2
    if strings.HasPrefix(input, "bd-") {
        return input, nil
    }

    // Alias: #42 or 42
    aliasStr := strings.TrimPrefix(input, "#")
    alias, err := strconv.Atoi(aliasStr)
    if err != nil {
        return "", fmt.Errorf("invalid issue ID: %s", input)
    }

    // Resolve alias to hash ID
    return storage.GetIssueIDByAlias(alias)
}
```

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

### 1. bd show
```bash
bd show bd-af78e9a2        # Hash ID
bd show #42                # Alias
bd show 42                 # Alias (shorthand)
bd show bd-af78e9a2 #42    # Mixed (multiple IDs)
```

### 2. bd update
```bash
bd update #42 --status in_progress
bd update bd-af78e9a2 --priority 1
```

### 3. bd close
```bash
bd close #42 --reason "Done"
```

### 4. bd dep add/tree
```bash
bd dep add #42 #1 --type blocks
bd dep tree bd-af78e9a2
```

### 5. bd label add/remove
```bash
bd label add #42 critical
```

### 6. bd merge
```bash
bd merge #42 #43 --into #41
```

## Display Format
Default to showing aliases in output:
```bash
$ bd list
#1   Fix authentication bug          P1  open
#2   Add logging to daemon           P2  open
#42  Investigate jujutsu integration P3  open
```

With `--format=hash` flag:
```bash
$ bd list --format=hash
bd-af78e9a2  Fix authentication bug          P1  open
bd-e5f6a7b8  Add logging to daemon           P2  open
bd-1a2b3c4d  Investigate jujutsu integration P3  open
```

## Files to Modify
- internal/utils/id_parser.go (new)
- cmd/bd/show.go
- cmd/bd/update.go
- cmd/bd/close.go
- cmd/bd/reopen.go
- cmd/bd/dep.go
- cmd/bd/label.go
- cmd/bd/merge.go
- cmd/bd/list.go (add --format flag)

## Testing
- Test hash ID parsing
- Test alias parsing (#42, 42)
- Test mixed IDs in single command
- Test error on invalid ID
- Test alias resolution failure


Add export test verifying tombstones are included in JSONL output

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

Add a test: TestExportIncludesTombstones


Delete skipped tests for "old buggy behavior"

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

Test functions to remove:

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

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

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

Impact: Removes ~150 LOC of permanently skipped tests


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

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

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

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

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

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

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

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

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


Implement signal-aware context in CLI commands

Replace context.Background() with signal.NotifyContext() to enable graceful cancellation.

## Context
Part of context propagation work (bd-350). Phase 1 infrastructure is complete - sqlite.New() now accepts context parameter.

## Implementation
Set up signal-aware context at the CLI entry points:

```go
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
```

## Key Locations
- cmd/bd/main.go:438 (marked with TODO(bd-350))
- cmd/bd/daemon.go:317 (marked with TODO(bd-350))
- Other command entry points

## Benefits
- Ctrl+C cancels ongoing database operations
- Graceful shutdown on SIGTERM
- Better user experience during long operations

## Acceptance Criteria
- [ ] Ctrl+C during import cancels operation cleanly
- [ ] Ctrl+C during export cancels operation cleanly
- [ ] No database corruption on cancellation
- [ ] Proper cleanup on signal (defers execute)


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.


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


Implement tombstone types and schema migration

Add tombstone fields to Issue struct (deleted_at, deleted_by, delete_reason, original_type), add StatusTombstone constant, create SQLite migration for new columns. Per design bd-2m7.


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)


GH#510: Document sync-branch worktree behavior

User confused about beads creating worktree on main branch. Need docs explaining sync-branch worktree mechanism. See: https://github.com/steveyegge/beads/issues/510


bd-hv01: Multi-repo mode not tested with deletion tracking

Problem: Test suite has no coverage for multi-repo mode. ExportToMultiRepo creates multiple JSONL files but snapshot files are hardcoded to single JSONL location.

Impact: Deletion tracking likely silently broken for multi-repo users, could cause data loss.

Fix: Add test and update snapshot logic to handle multiple JSONL files.

Files: cmd/bd/deletion_tracking_test.go, cmd/bd/deletion_tracking.go, cmd/bd/daemon_sync.go:24-34


Add internal/ai package for LLM integration

Shared AI client for repair commands.

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

Env vars:
- BEADS_AI_PROVIDER
- BEADS_AI_API_KEY
- BEADS_AI_MODEL

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


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


Complete remaining sync branch daemon tests

4 remaining test scenarios in daemon_sync_branch_test.go need completion:

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

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

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

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

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

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


Baseline quality gate failure: lint

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

This blocks the executor from claiming work until fixed.

Error: golangci-lint failed: exit status 1

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

```


bd doctor: export diagnostics history for debugging

Add ability to save doctor diagnostics to a file (e.g., diagnostics.json) for historical analysis and bug reporting. This would help track intermittent issues and provide context when filing bug reports. Could integrate with --perf output for comprehensive system snapshots.


Use TryResurrectParentChain instead of TryResurrectParent in GetNextChildID

## Context
Current bd-dvd fix uses TryResurrectParent, which only resurrects the immediate parent. For deeply nested hierarchies, this might not be sufficient.

## Example Scenario
Creating child with parent "bd-abc.1.2" where:
- bd-abc exists in DB
- bd-abc.1 was deleted (missing from DB, exists in JSONL)
- bd-abc.1.2 was deleted (missing from DB, exists in JSONL)

Current fix: Only tries to resurrect "bd-abc.1.2", fails because its parent "bd-abc.1" is missing.

## Solution
Replace TryResurrectParent with TryResurrectParentChain:

```go
if count == 0 {
    // Try to resurrect entire parent chain from JSONL history (bd-dvd fix)
    // This handles deeply nested hierarchies where intermediate parents are also missing
    resurrected, err := s.TryResurrectParentChain(ctx, parentID)
    if err != nil {
        return "", fmt.Errorf("failed to resurrect parent chain for %s: %w", parentID, err)
    }
    if !resurrected {
        return "", fmt.Errorf("parent issue %s does not exist and could not be resurrected from JSONL history", parentID)
    }
}
```

## Investigation
- Check if this scenario actually happens in practice
- TryResurrectParentChain already exists (resurrection.go:174-209)
- May be overkill if users always create parents before children

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

## Testing
Add test case for deeply nested resurrection


bd doctor --json flag not working

The --json flag on bd doctor command doesn't produce JSON output. It continues to show human-readable output instead. The flag is registered locally on doctorCmd but the code uses the global jsonOutput variable set by PersistentPreRun. Need to investigate why the flag isn't being honored.


CRITICAL: bd sync exports before pull, allowing stale DB to corrupt JSONL statuses

## Root Cause

The fix in bd-53c (reverse ZFC check) only checks COUNTS, not content. The real corruption happens when:

1. Polecat A has stale DB with old status values (e.g., status=closed for issues that are now open on remote)
2. Polecat A runs bd sync:
   - **Export FIRST**: DB (status=closed) → JSONL (overwrites correct status=open)
   - Commit: Stale JSONL committed
   - Pull: 3-way merge with remote
   - Merge uses 'closed wins' rule → status stays closed
3. Polecat A pushes → Remote now corrupted with status=closed

## Why bd-53c didn't fix it

The reverse ZFC check compares COUNTS:
```go
if jsonlCount > dbCount  // Only catches count mismatch
```

But in the corruption scenario:
- JSONL count = 5, DB count = 5 (same count!)
- Only the STATUS field differs

## The Real Fix

**PULL BEFORE EXPORT**. The sync order must be:
1. Pull from remote (get latest state)
2. Import merged JSONL to DB
3. THEN export DB changes (if any)

Current order is: Export → Commit → Pull → Import → Push

This is a fundamental architecture change to the sync flow.

## Workaround

Use --no-auto-flush and manually control the sync order, or disable daemon auto-export.

## Evidence

From user investigation:
- At 595b7943 (13:20:30): 5 open issues
- At 10239812 (13:28:39): 0 open issues
- All 5 issues had their status changed from open to closed
- Count stayed at 5 (not a deletion issue)


Isolate RPC server startup into rpc_server.go

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


Implement fs bridge layer for WASM (Go syscall/js to Node.js fs)

Go's os package in WASM returns 'not implemented on js' for mkdir and other file operations. Need to create a bridge layer that:

1. Detects WASM environment (GOOS=js)
2. Uses syscall/js to call Node.js fs module functions
3. Implements wrappers for:
   - os.MkdirAll
   - os.ReadFile / os.WriteFile
   - os.Open / os.Create
   - os.Stat / os.Lstat
   - filepath operations

Approach:
- Create internal/wasm/fs_bridge.go with //go:build js && wasm
- Export Node.js fs functions to Go using global.readFileSync, global.writeFileSync, etc.
- Wrap in Go API that matches os package signatures
- Update beads.go and storage layer to use bridge when in WASM

This unblocks bd-4462 (basic WASM testing) and [deleted:bd-5bbf] (feature parity testing).


Use external_ref as primary matching key for import updates

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

See GH #142 for detailed proposal and implementation plan.

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

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

## Code Review Complete ✅

**Overall Assessment**: EXCELLENT - Production ready

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

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

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

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

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

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


TestHooksCheckGitHooks failing - version mismatch (0.23.0 vs 0.23.1)

The test is checking embedded hook versions and expecting 0.23.1, but got 0.23.0. This appears to be a version consistency issue that needs investigation.

Test output:
```
Hook pre-commit version mismatch: got 0.23.0, want 0.23.1
Hook post-merge version mismatch: got 0.23.0, want 0.23.1
Hook pre-push version mismatch: got 0.23.0, want 0.23.1
```

This is blocking the landing of GH #274 fix.


Batch test 1


Rapid 4


Add label-based filtering to bd ready command

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

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

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

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


Add internal/ai package for AI-assisted repairs

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

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

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

See repair_commands.md lines 357-425 for design.


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.


Fix compact --analyze/--apply error messages to clarify direct mode requirement

**Problem:**
When users run `bd compact --analyze` with daemon running, they get:
```
Error: compact requires SQLite storage
```

This is misleading because they ARE using SQLite (via daemon), but the command needs DIRECT SQLite access.

**Current behavior:**
- Error message suggests they don't have SQLite
- No hint about using --no-daemon flag
- Related to issue #349 item #1

**Proposed fix:**
1. Update error messages in cmd/bd/compact.go lines 106-114 (analyze) and 121-137 (apply)
2. Add explicit hint: "Use --no-daemon flag to bypass daemon"
3. Change SQLite check error from "requires SQLite storage" to "failed to open database in direct mode"

**Files to modify:**
- cmd/bd/compact.go (lines ~106-137)

**Testing:**
- Run with daemon: `bd compact --analyze` should show clear error + hint
- Run with --no-daemon: `bd compact --analyze --no-daemon` should work
- Verify error message is actionable


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.


Enhance bd doctor to suggest reset for broken states

Update bd doctor to detect severely broken states and suggest reset.

## Detection Criteria
Suggest reset when:
- Multiple unfixable errors detected
- Corrupted JSONL that can't be repaired
- Schema version mismatch that can't be migrated
- Daemon state inconsistent and unkillable

## Implementation
Add to doctor's check/fix flow:
```go
if unfixableErrors > threshold {
    suggest('State may be too broken to fix. Consider: bd reset')
}
```

## Output Example
```
✗ Found 5 unfixable errors

  Your beads state may be too corrupted to repair.
  Consider running 'bd reset' to start fresh.
  (Use 'bd reset --backup' to save current state first)
```

## Notes
- Don't auto-run reset, just suggest
- This is lower priority, can be done in parallel with main work


Use RAM disk for Windows CI tests

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

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

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


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


Document external_ref in content hash behavior

The content hash includes external_ref, which has implications that should be documented.

Current behavior:
- external_ref is included in content hash calculation (collision.go:158-160)
- Changing external_ref changes content hash
- This means: local issue → add external_ref → different hash

Implications:
- Local issue + external_ref addition = looks like 'new content'
- May not match by content hash in some scenarios
- Generally correct behavior, but subtle

Action items:
- Document in code comments
- Add to ARCHITECTURE.md or similar
- Add test demonstrating this behavior
- Consider if this is desired long-term

Related: bd-1022
Files: internal/storage/sqlite/collision.go:158-160


Issue with labels


Extract computeJSONLHash helper to eliminate code duplication

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

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


Add tests for daemon lifecycle module


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.