Extract validation functions to validators.go

Move validatePriority, validateStatus, validateIssueType, validateTitle, validateEstimatedMinutes, validateFieldUpdate to validators.go


Enforce daemon singleton per workspace with file locking

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

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

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

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

## Fix Summary

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

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

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

## Root Cause

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

## Testing

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

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


Extract duplicated metadata update code in daemon_sync.go

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

This violates DRY principle and makes maintenance harder.

## Solution
Extract to helper function:

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

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

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

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

## Files
- cmd/bd/daemon_sync.go

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


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


Optimize CLI test suite - eliminate redundant git init calls

Current: Each of 13 CLI tests calls git init (31s total). Solution: Use single test binary built once in init(), skip git operations where possible, or use mock filesystem.


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.


Refactor sqlite.go into focused modules

Split sqlite.go (2,298 lines) into focused modules: migrations.go, ids.go, issues.go, events.go, dirty.go, db.go. This will improve maintainability and reduce cognitive load.


Implement clone-scoped ID allocation to prevent N-way collisions

## Problem
Current ID allocation uses per-clone atomic counters (issue_counters table) that sync based on local database state. In N-way collision scenarios:
- Clone B sees {test-1} locally, allocates test-2
- Clone D sees {test-1, test-2, test-3} locally, allocates test-4
- When same content gets assigned test-2 and test-4, convergence fails

Root cause: Each clone independently allocates IDs without global coordination, leading to overlapping assignments for the same content.

## Solution
Add clone UUID to ID allocation to make every ID globally unique:

**Current format:** `test-1`, `test-2`, `test-3`
**New format:** `test-1-a7b3`, `test-2-a7b3`, `test-3-c4d9`

Where suffix is first 4 chars of clone UUID.

## Implementation

### 1. Add clone_identity table
```sql
CREATE TABLE clone_identity (
    clone_uuid TEXT PRIMARY KEY,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```

### 2. Modify getNextIDForPrefix()
```go
func (s *SQLiteStorage) getNextIDForPrefix(ctx context.Context, prefix string) (string, error) {
    cloneUUID := s.getOrCreateCloneUUID(ctx)
    shortUUID := cloneUUID[:4]

    nextNum := s.getNextCounterForPrefix(ctx, prefix)
    return fmt.Sprintf("%s-%d-%s", prefix, nextNum, shortUUID), nil
}
```

### 3. Update ID parsing logic
All places that parse IDs (utils.ExtractIssueNumber, etc.) need to handle new format.

### 4. Migration strategy
- Existing IDs remain unchanged (no suffix)
- New IDs get clone suffix automatically
- Display layer can hide suffix in UI: `bd-cb64c226.3-a7b3` → `#42`

## Benefits
- **Zero collision risk**: Same content in different clones gets different IDs
- **Maintains readability**: Still sequential numbering within clone
- **No coordination needed**: Works offline, no central authority
- **Scales to 100+ clones**: 4-char hex = 65,536 unique clones

## Concerns
- ID format change may break existing integrations
- Need migration path for existing databases
- Display logic needs update to hide/show suffixes appropriately

## Success Criteria
- 10+ clone collision test passes without failures
- Existing issues continue to work (backward compatibility)
- Documentation updated with new ID format
- Migration guide for v1.x → v2.x

## Timeline
Medium-term (v1.1-v1.2), 2-3 weeks implementation

## References
- Related to bd-e6d71828 (immediate fix)
- See beads_nway_test.go for failing N-way tests


Test issue 0


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.


Consolidate ID generation and validation into ids.go

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


Add WebSocket support

## Feature Request

[Describe the desired feature]

## Motivation

[Why is this feature needed? What problem does it solve?]

## Use Cases

1. **Use Case 1**: [description]
2. **Use Case 2**: [description]

## Proposed Solution

[High-level approach to implementing this feature]

## Alternatives Considered

- **Alternative 1**: [description and why not chosen]
- **Alternative 2**: [description and why not chosen]


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


Add ON DELETE CASCADE to child_counters schema

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


PruneDeletions iterates map non-deterministically

## Problem

`PruneDeletions` iterates over `loadResult.Records` which is a map. Go maps iterate in random order, so:

1. `result.PrunedIDs` order is non-deterministic
2. `kept` slice order is non-deterministic → `WriteDeletions` output order varies

## Location
`internal/deletions/deletions.go:213`

## Impact
- Git diffs are noisy (file changes order on each prune)
- Tests could be flaky if they depend on order
- Harder to debug/audit

## Fix
Sort by ID or timestamp before iterating:

```go
// Convert map to slice and sort
var records []DeletionRecord
for _, r := range loadResult.Records {
    records = append(records, r)
}
sort.Slice(records, func(i, j int) bool {
    return records[i].ID < records[j].ID
})
```


Add topological sort utility functions

Create internal/importer/sort.go with utilities for depth-based sorting of issues. Functions: GetHierarchyDepth(id), SortByDepth(issues), GroupByDepth(issues). Include stable sorting for same-depth issues.


Update python-agent example with Agent Mail integration

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

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

File: examples/python-agent/agent.py


bd validate - Comprehensive health check

Run all validation checks in one command.

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

Supports --fix-all for auto-repair.

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

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


Implement content-hash based collision resolution for deterministic convergence

The current collision resolution uses creation timestamps to decide which issue to keep vs. remap. This is non-deterministic when two clones create issues at nearly the same time.

Root cause of bd-71107098:
- Clone A creates test-1="Issue from clone A" at T0
- Clone B creates test-1="Issue from clone B" at T0+30ms
- Clone B syncs first, remaps Clone A's to test-2
- Clone A syncs second, sees collision, remaps Clone B's to test-2
- Result: titles are swapped between clones

Solution:
- Use content-based hashing (title + description + priority + type)
- Deterministic winner: always keep issue with lower hash
- Same collision on different clones produces same result (idempotent)

Implementation:
- Modify ScoreCollisions in internal/storage/sqlite/collision.go
- Replace timestamp-based scoring with content hash comparison
- Ensure hash function is stable across platforms


Issue with labels

This is a description


Test RPC mutation event


Test issue 2


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

```


Create cmd/bd/daemon_debouncer.go (~60 LOC)

Implement Debouncer to batch rapid events into single action. Default 500ms, configurable via BEADS_DEBOUNCE_MS. Thread-safe with mutex.


Improve test coverage for cmd/bd CLI (26.2% → 50%)

The main CLI package (cmd/bd) has only 26.2% test coverage. CLI commands should have at least 50% coverage to ensure reliability.

Key areas with low/no coverage:
- daemon_autostart.go (multiple 0% functions)
- compact.go (several 0% functions)
- Various command handlers

Current coverage: 26.2%
Target coverage: 50%


User Diagnostics (bd doctor --perf)

Extend cmd/bd/doctor.go to add --perf flag for user performance diagnostics.

Functionality:
- Add --perf flag to existing bd doctor command
- Collect system info (OS, arch, Go version, SQLite version)
- Collect database stats (size, issue counts, dependency counts)
- Time key operations on user's actual database:
  * bd ready
  * bd list --status=open
  * bd show <random-issue>
  * bd create (with rollback)
  * Search with filters
- Generate CPU profile automatically (timestamped filename)
- Output simple report with platform info, timings, profile location

Output example:
  Beads Performance Diagnostics
  Platform: darwin/arm64
  Database: 8,234 issues (4,123 open)

  Operation Performance:
    bd ready                  42ms
    bd list --status=open     15ms

  Profile saved: beads-perf-2025-11-13.prof
  View: go tool pprof -http=:8080 beads-perf-2025-11-13.prof

Implementation:
- Extend cmd/bd/doctor.go (~100 lines)
- Use runtime/pprof for CPU profiling
- Use time.Now()/time.Since() for timing
- Rollback test operations (don't modify user's database)


Workaround for FastMCP outputSchema bug in Claude Code

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

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

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

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


Updated title


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)


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


Refactor AGENTS.md to use external references

Suggestion to use external references (e.g., "ALWAYS REFER TO ./beads/prompt.md") instead of including all instructions directly within AGENTS.md.
Reasons:
1. Agents can follow external references.
2. Prevents context pollution/stuffing in AGENTS.md as more tools append instructions.


Complete and commit beads-mcp type safety improvements

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


Comprehensive testing for separate branch workflow including unit tests, integration tests, and performance testing.

Tasks:
- Unit tests for worktree management
- Unit tests for config parsing
- Integration tests: create/update/close → beads branch
- Integration test: merge beads → main
- Integration test: protected branch scenario
- Integration test: network failure recovery
- Integration test: config change handling
- Manual testing guide
- Performance testing (worktree overhead)

Test scenarios: fresh setup, issue operations, merge workflow, protected branch, error handling, migration, multiple workspaces, sparse checkout

Estimated effort: 4-5 days

Completed comprehensive test coverage. Added 4 new integration tests: config change handling, multiple concurrent clones (3-way), performance testing (avg 77ms < 150ms target), and network failure recovery. All tests pass. Coverage includes fresh setup, issue ops, error handling, multiple workspaces, sparse checkout, config changes, network failures, and performance.


Issue 2


Design tombstone storage format

Define the JSON schema for tombstone records. Options: minimal (id, deleted_at, deleted_by, expires_at) vs full preservation (keep all original fields). Trade-offs: storage size vs audit trail. Consider: should tombstones be distinguishable from regular issues by status field or separate record type?


Install and test MCP Agent Mail locally

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

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

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


Multi-clone collision resolution testing and documentation

Epic to track improvements to multi-clone collision resolution based on ultrathinking analysis of-3d844c58 and [deleted:bd-71107098].

Current state:
- 2-clone collision resolution is SOUND and working correctly
- Hash-based deterministic collision resolution works
- Test fails due to timestamp comparison, not actual logic issues

Work needed:
1. Fix TestTwoCloneCollision to compare content not timestamps
2. Add TestThreeCloneCollision for regression protection
3. Document 3-clone ID non-determinism as known behavior


Clarify .beads/.gitattributes handling in Protected Branches docs

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


Implement bd migrate command

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


Use in-memory filesystem for test git operations

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

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

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


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)


Deletions pruning doesn't include results in JSON output

## Problem

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

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

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

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

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

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


Add 'bd message send' command for Agent Mail messaging

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

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

## Proposed Commands

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

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

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

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

## Example Usage

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

# Check inbox
bd message inbox --unread-only

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

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

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

## Implementation Summary

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

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

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

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

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

## Implementation Details

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

## Documentation

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

## Testing

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


Add bd doctor --fix flag to automatically repair issues

Implement a --fix flag for bd doctor that can automatically repair detected issues.

Requirements:
- Add --fix flag to bd doctor command
- Show all fixable issues and prompt for confirmation before applying fixes
- Organize fix implementations under doctor/fix/<type_of_fix>.go
- Each fix type should have its own file (e.g., doctor/fix/hooks.go, doctor/fix/sync.go)
- Display what will be fixed and ask user to confirm (Y/n) before proceeding
- Support fixing issues like:
  - Missing or broken git hooks
  - Sync problems with remote
  - File permission issues
  - Any other auto-repairable issues doctor detects

Implementation notes:
- Maintain separation between detection (existing doctor code) and repair (new fix code)
- Each fix should be idempotent and safe to run multiple times
- Provide clear output about what was fixed
- Log any fixes that fail with actionable error messages


Test issue


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.


Issue 2 to reopen


Add template support for issue creation

Support creating issues from predefined templates to streamline common workflows like epics, bug reports, or feature proposals.

Example usage:
  bd create --from-template epic "Phase 3 Features"
  bd create --from-template bug "Login failure"
  bd template list
  bd template create epic

Templates should include:
- Pre-filled description structure
- Suggested priority and type
- Common labels
- Design/acceptance criteria sections

Implementation notes:
- Store templates in .beads/templates/ directory
- Support YAML or JSON format
- Ship with built-in templates (epic, bug, feature)
- Allow custom project-specific templates


Fix import failure on missing parent issues

Import process fails atomically when JSONL references deleted parent issues. Implement hybrid solution: topological sorting + parent resurrection to handle deleted parents gracefully while maintaining referential integrity. See docs/import-bug-analysis-bd-3xq.md for full analysis.


Add TestNWayCollision for 5+ clones

## Overview
Add comprehensive tests for N-way (5+) collision resolution to verify the solution scales beyond 3 clones.

## Purpose
While TestThreeCloneCollision validates the basic N-way case, we need to verify:
1. Solution scales to arbitrary N
2. Performance is acceptable with more clones
3. Convergence time is bounded
4. No edge cases in larger collision groups

## Implementation Tasks

### 1. Create TestFiveCloneCollision
File: beads_twoclone_test.go (or new beads_nway_test.go)

```go
func TestFiveCloneCollision(t *testing.T) {
    // Test with 5 clones creating same ID with different content
    // Verify all 5 clones converge after sync rounds

    t.Run("SequentialSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "A", "B", "C", "D", "E")
    })

    t.Run("ReverseSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "E", "D", "C", "B", "A")
    })

    t.Run("RandomSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "C", "A", "E", "B", "D")
    })
}
```

### 2. Implement generalized testNCloneCollision
Generalize the 3-clone test to handle arbitrary N:

```go
func testNCloneCollision(t *testing.T, numClones int, syncOrder ...string) {
    t.Helper()

    if len(syncOrder) != numClones {
        t.Fatalf("syncOrder length (%d) must match numClones (%d)",
            len(syncOrder), numClones)
    }

    tmpDir := t.TempDir()

    // Setup remote and N clones
    remoteDir := setupBareRepo(t, tmpDir)
    cloneDirs := make(map[string]string)

    for i := 0; i < numClones; i++ {
        name := string(rune('A' + i))
        cloneDirs[name] = setupClone(t, tmpDir, remoteDir, name)
    }

    // Each clone creates issue with same ID but different content
    for name, dir := range cloneDirs {
        createIssue(t, dir, fmt.Sprintf("Issue from clone %s", name))
    }

    // Sync in specified order
    for _, name := range syncOrder {
        syncClone(t, cloneDirs[name], name)
    }

    // Final pull for convergence
    for name, dir := range cloneDirs {
        finalPull(t, dir, name)
    }

    // Verify all clones have all N issues
    expectedTitles := make(map[string]bool)
    for i := 0; i < numClones; i++ {
        name := string(rune('A' + i))
        expectedTitles[fmt.Sprintf("Issue from clone %s", name)] = true
    }

    for name, dir := range cloneDirs {
        titles := getTitles(t, dir)
        if !compareTitleSets(titles, expectedTitles) {
            t.Errorf("Clone %s missing issues: expected %v, got %v",
                name, expectedTitles, titles)
        }
    }

    t.Log("✓ All", numClones, "clones converged successfully")
}
```

### 3. Add performance benchmarks
Test convergence time and memory usage:

```go
func BenchmarkNWayCollision(b *testing.B) {
    for _, n := range []int{3, 5, 10, 20} {
        b.Run(fmt.Sprintf("N=%d", n), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                // Run N-way collision and measure time
                testNCloneCollisionBench(b, n)
            }
        })
    }
}
```

### 4. Add convergence time tests
Verify bounded convergence:

```go
func TestConvergenceTime(t *testing.T) {
    // Test that convergence happens within expected rounds
    // For N clones, should converge in at most N-1 sync rounds

    for n := 3; n <= 10; n++ {
        t.Run(fmt.Sprintf("N=%d", n), func(t *testing.T) {
            rounds := measureConvergenceRounds(t, n)
            maxExpected := n - 1
            if rounds > maxExpected {
                t.Errorf("Convergence took %d rounds, expected ≤ %d",
                    rounds, maxExpected)
            }
        })
    }
}
```

### 5. Add edge case tests
Test boundary conditions:
- All N clones have identical content (dedup works)
- N-1 clones have same content, 1 differs
- All N clones have unique content
- Mix of collisions and non-collisions

## Acceptance Criteria
- TestFiveCloneCollision passes with all sync orders
- All 5 clones converge to identical content
- Performance is acceptable (< 5 seconds for 5 clones)
- Convergence time is bounded (≤ N-1 rounds)
- Edge cases handled correctly
- Benchmarks show scalability to 10+ clones

## Files to Create/Modify
- beads_twoclone_test.go or beads_nway_test.go
- Add helper functions for N-clone setup

## Testing Strategy

### Test Matrix
| N Clones | Sync Orders | Expected Result |
|----------|-------------|-----------------|
| 3        | A→B→C       | Pass            |
| 3        | C→B→A       | Pass            |
| 5        | A→B→C→D→E   | Pass            |
| 5        | E→D→C→B→A   | Pass            |
| 5        | Random      | Pass            |
| 10       | Sequential  | Pass            |

### Performance Targets
- 3 clones: < 2 seconds
- 5 clones: < 5 seconds
- 10 clones: < 15 seconds

## Dependencies
- Requires bd-cbed9619.5, bd-cbed9619.4, bd-cbed9619.3, bd-dcd6f14b to be completed
- TestThreeCloneCollision must pass first

## Success Metrics
- All tests pass for N ∈ {3, 5, 10}
- Convergence time scales linearly (O(N))
- Memory usage reasonable (< 100MB for 10 clones)
- No data corruption or loss in any scenario


Add test coverage for internal/git package


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

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

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

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

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

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

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


Fix silent error handling in RPC response writing

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

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

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

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

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

Impact: Client hangs waiting for response; corrupt JSON sent

Effort: 1 hour


Add simple cleanup command to delete closed issues

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


Add performance optimization to hasJSONLChanged with mtime fast-path

hasJSONLChanged() reads entire JSONL file to compute SHA256 hash on every check. For large databases (50MB+), this is expensive and called frequently by daemon.

Optimization: Check mtime first as fast-path. Only compute hash if mtime changed. This catches git operations (mtime changes but content doesn't) while avoiding expensive hash computation in common case (mtime unchanged = content unchanged).

Performance impact: 99% of checks will use fast path, only git operations need slow path.


Add orphan detection migration

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


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


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.


Investigate and optimize test suite performance

Test suite is taking very long to run (>45s for cmd/bd tests, full suite timing unknown but was cancelled).

This impacts development velocity and CI/CD performance.

Investigation needed:
- Profile which tests are slowest
- Identify bottlenecks (disk I/O, network, excessive setup/teardown?)
- Consider parallelization opportunities
- Look for redundant test cases
- Check if integration tests can be optimized


Benchmark git traffic reduction

Automated benchmark comparing git operations with/without Agent Mail.

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

File: tests/benchmarks/git_traffic.py

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

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

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

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


Wire config to import pipeline

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


Add security tests for file path validation in clean command

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

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

In cmd/bd/clean.go:118, os.Open(gitignorePath) is flagged by gosec G304 for potential file inclusion via variable without validation.

Add tests covering:
- Path traversal attempts (../../etc/passwd)
- Absolute paths outside project directory
- Symlink following behavior
- Non-existent file handling
- Validation that only .gitignore files in valid locations are opened

This is a security-sensitive code path that needs validation to prevent unauthorized file access.

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


bd init should refuse when JSONL already has issues (safety guard)

When running `bd init` in a directory with an existing JSONL containing issues, bd should refuse and suggest the correct action instead of proceeding.

## The Problem

Current behavior when database is missing but JSONL exists:
```
$ bd create "test"
Error: no beads database found
Hint: run 'bd init' to create a database...
```

This leads users (and AI agents) to reflexively run `bd init`, which can cause:
- Prefix mismatch if wrong prefix specified
- Data corruption if JSONL is damaged
- Confusion about what actually happened

## Proposed Behavior

```
$ bd init --prefix bd

⚠ Found existing .beads/issues.jsonl with 76 issues.

This appears to be a fresh clone, not a new project.

To hydrate the database from existing JSONL:
  bd doctor --fix

To force re-initialization (may cause data loss):
  bd init --prefix bd --force

Aborting.
```

## Trigger Conditions

- `.beads/issues.jsonl` or `.beads/beads.jsonl` exists
- File contains > 0 valid issue lines
- No `--force` flag provided

## Edge Cases

- Empty JSONL (0 issues) → allow init (new project)
- Corrupted JSONL → warn but allow with confirmation
- Existing `.db` file → definitely refuse (weird state)

## Related

- bd-dmb: Fresh clone should suggest hydration (better error messages)
- bd-4ew: bd doctor should detect fresh clone

This issue is about the safety guard on `bd init` itself, not the error messages from other commands.


Add TestNWayCollision for 5+ clones

## Overview
Add comprehensive tests for N-way (5+) collision resolution to verify the solution scales beyond 3 clones.

## Purpose
While TestThreeCloneCollision validates the basic N-way case, we need to verify:
1. Solution scales to arbitrary N
2. Performance is acceptable with more clones
3. Convergence time is bounded
4. No edge cases in larger collision groups

## Implementation Tasks

### 1. Create TestFiveCloneCollision
File: beads_twoclone_test.go (or new beads_nway_test.go)

```go
func TestFiveCloneCollision(t *testing.T) {
    // Test with 5 clones creating same ID with different content
    // Verify all 5 clones converge after sync rounds

    t.Run("SequentialSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "A", "B", "C", "D", "E")
    })

    t.Run("ReverseSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "E", "D", "C", "B", "A")
    })

    t.Run("RandomSync", func(t *testing.T) {
        testNCloneCollision(t, 5, "C", "A", "E", "B", "D")
    })
}
```

### 2. Implement generalized testNCloneCollision
Generalize the 3-clone test to handle arbitrary N:

```go
func testNCloneCollision(t *testing.T, numClones int, syncOrder ...string) {
    t.Helper()

    if len(syncOrder) != numClones {
        t.Fatalf("syncOrder length (%d) must match numClones (%d)",
            len(syncOrder), numClones)
    }

    tmpDir := t.TempDir()

    // Setup remote and N clones
    remoteDir := setupBareRepo(t, tmpDir)
    cloneDirs := make(map[string]string)

    for i := 0; i < numClones; i++ {
        name := string(rune('A' + i))
        cloneDirs[name] = setupClone(t, tmpDir, remoteDir, name)
    }

    // Each clone creates issue with same ID but different content
    for name, dir := range cloneDirs {
        createIssue(t, dir, fmt.Sprintf("Issue from clone %s", name))
    }

    // Sync in specified order
    for _, name := range syncOrder {
        syncClone(t, cloneDirs[name], name)
    }

    // Final pull for convergence
    for name, dir := range cloneDirs {
        finalPull(t, dir, name)
    }

    // Verify all clones have all N issues
    expectedTitles := make(map[string]bool)
    for i := 0; i < numClones; i++ {
        name := string(rune('A' + i))
        expectedTitles[fmt.Sprintf("Issue from clone %s", name)] = true
    }

    for name, dir := range cloneDirs {
        titles := getTitles(t, dir)
        if !compareTitleSets(titles, expectedTitles) {
            t.Errorf("Clone %s missing issues: expected %v, got %v",
                name, expectedTitles, titles)
        }
    }

    t.Log("✓ All", numClones, "clones converged successfully")
}
```

### 3. Add performance benchmarks
Test convergence time and memory usage:

```go
func BenchmarkNWayCollision(b *testing.B) {
    for _, n := range []int{3, 5, 10, 20} {
        b.Run(fmt.Sprintf("N=%d", n), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                // Run N-way collision and measure time
                testNCloneCollisionBench(b, n)
            }
        })
    }
}
```

### 4. Add convergence time tests
Verify bounded convergence:

```go
func TestConvergenceTime(t *testing.T) {
    // Test that convergence happens within expected rounds
    // For N clones, should converge in at most N-1 sync rounds

    for n := 3; n <= 10; n++ {
        t.Run(fmt.Sprintf("N=%d", n), func(t *testing.T) {
            rounds := measureConvergenceRounds(t, n)
            maxExpected := n - 1
            if rounds > maxExpected {
                t.Errorf("Convergence took %d rounds, expected ≤ %d",
                    rounds, maxExpected)
            }
        })
    }
}
```

### 5. Add edge case tests
Test boundary conditions:
- All N clones have identical content (dedup works)
- N-1 clones have same content, 1 differs
- All N clones have unique content
- Mix of collisions and non-collisions

## Acceptance Criteria
- TestFiveCloneCollision passes with all sync orders
- All 5 clones converge to identical content
- Performance is acceptable (< 5 seconds for 5 clones)
- Convergence time is bounded (≤ N-1 rounds)
- Edge cases handled correctly
- Benchmarks show scalability to 10+ clones

## Files to Create/Modify
- beads_twoclone_test.go or beads_nway_test.go
- Add helper functions for N-clone setup

## Testing Strategy

### Test Matrix
| N Clones | Sync Orders | Expected Result |
|----------|-------------|-----------------|
| 3        | A→B→C       | Pass            |
| 3        | C→B→A       | Pass            |
| 5        | A→B→C→D→E   | Pass            |
| 5        | E→D→C→B→A   | Pass            |
| 5        | Random      | Pass            |
| 10       | Sequential  | Pass            |

### Performance Targets
- 3 clones: < 2 seconds
- 5 clones: < 5 seconds
- 10 clones: < 15 seconds

## Dependencies
- Requires bd-cbed9619.5, bd-cbed9619.4, bd-0dcea000, bd-4d7fca8a to be completed
- TestThreeCloneCollision must pass first

## Success Metrics
- All tests pass for N ∈ {3, 5, 10}
- Convergence time scales linearly (O(N))
- Memory usage reasonable (< 100MB for 10 clones)
- No data corruption or loss in any scenario


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


Fix TestTwoCloneCollision to compare content not timestamps

The test at beads_twoclone_test.go:204-207 currently compares full JSON output including timestamps, causing false negative failures.

Current behavior:
- Both clones converge to identical semantic content
- Clone A: test-2="Issue from clone A", test-1="Issue from clone B"
- Clone B: test-1="Issue from clone B", test-2="Issue from clone A"
- Titles match IDs correctly, no data corruption
- Only timestamps differ (expected and acceptable)

Fix needed:
- Replace exact JSON comparison with content-aware comparison
- Normalize or ignore timestamp fields when asserting convergence
- Test should PASS after this fix

This blocks completion of bd-71107098.


Add security tests for WriteFile permissions in doctor command

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

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

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

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

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

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


Document deletions manifest in AGENTS.md and README

Parent: bd-imj

## Task
Add documentation about the deletions manifest feature.

## Locations to Update

### AGENTS.md
- Explain that deletions.jsonl is tracked in git
- Document that `bd delete` records to the manifest
- Explain cross-clone propagation mechanism

### README.md
- Brief mention in .beads directory structure section
- Link to detailed docs if needed

### docs/deletions.md (new file)
- Full technical documentation
- Format specification
- Pruning policy
- Git history fallback
- Troubleshooting

## Acceptance Criteria
- AGENTS.md updated with deletion workflow
- README.md mentions deletions.jsonl purpose
- New docs/deletions.md with complete reference


Parent: bd-imj

Ensure deletions.jsonl is tracked in git (not ignored).

Update bd init and gitignore upgrade logic to:
1. NOT add deletions.jsonl to .gitignore
2. Ensure it is committed alongside beads.jsonl

The file must be in git for cross-clone propagation to work.

Acceptance criteria:
- bd init does not ignore deletions.jsonl
- Existing .gitignore files are not broken
- File appears in git status when modified


Consider backwards compatibility for ready() and list() return type change

PR #481 changed the return types of `ready()` and `list()` from `list[Issue]` to `list[IssueMinimal] | CompactedResult`. This is a breaking change for MCP clients.

## Impact Assessment
Breaking change affects:
- Any MCP client expecting `list[Issue]` from ready()
- Any MCP client expecting `list[Issue]` from list()
- Client code that accesses full Issue fields (description, design, acceptance_criteria, timestamps, dependencies, dependents)

## Current Behavior
- ready() returns `list[IssueMinimal] | CompactedResult`
- list() returns `list[IssueMinimal] | CompactedResult`
- show() still returns full `Issue` (good)

## Considerations
**Pros of current approach:**
- Forces clients to use show() for full details (good for context efficiency)
- Simple mental model (always use show for full data)
- Documentation warns about this

**Cons:**
- Clients expecting list[Issue] will break
- No graceful degradation option
- No migration period

## Potential Solutions
1. Add optional parameter `full_details=false` to ready/list (would increase payload)
2. Create separate tools: ready_minimal/list_minimal + ready_full/list_full
3. Accept breaking change and document upgrade path (current approach)
4. Version the MCP server and document migration guide

## Recommendation
Current approach (solution 3) is reasonable if:
- Changelog clearly documents the breaking change
- Migration guide provided to clients
- Error handling is graceful for clients expecting specific fields


Add tests for internal/importer package

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


Add mutation channel to internal/rpc/server.go

Add mutationChan chan MutationEvent to Server struct. Emit events on CreateIssue, UpdateIssue, DeleteIssue, AddComment. Non-blocking send with default case for full channel.


Centralize error handling patterns in storage layer

80+ instances of inconsistent error handling across sqlite.go with mix of %w, %v, and no wrapping.

Location: internal/storage/sqlite/sqlite.go (throughout)

Problem:
- Some use fmt.Errorf("op failed: %w", err) - correct wrapping
- Some use fmt.Errorf("op failed: %v", err) - loses error chain
- Some return err directly - no context
- Hard to debug production issues
- Can't distinguish error types

Solution: Create internal/storage/sqlite/errors.go:
- Define sentinel errors (ErrNotFound, ErrInvalidID, etc.)
- Create wrapDBError(op string, err error) helper
- Convert sql.ErrNoRows to ErrNotFound
- Always wrap with operation context

Impact: Lost error context; inconsistent messages; hard to debug

Effort: 5-7 hours


wish: a 'continue' or similar cmd/flag which means alter last issue

so many time I create an issue and then have another thought: 'oh, before I did X and it crashed there was ZZZ happening' or 'actually that is P4 not P2'. It would be nice if when `bd {cmd}` is used without a {title} or {id} it just adds or updates the most recently touched issue.


Add 'bd status' command for issue database overview

Implement a bd status command that provides a quick snapshot of the issue database state, similar to how git status shows working tree state.

Expected output: Show summary including counts by state (open, in-progress, blocked, closed), recent activity (last 7 days), and quick overview without needing multiple queries.

Example output showing issue counts, recent activity stats, and pointer to bd list for details.

Proposed options: --all (show all issues), --assigned (show issues assigned to current user), --json (JSON format output)

Use cases: Quick project health check, onboarding for new contributors, integration with shell prompts or CI/CD, daily standup reference


Batch test 3


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

## Problem

`checkGitHistoryForDeletions` assumes the repo structure:

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

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

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

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

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


GH#508: Orphan detection false positive when directory name contains dot

Directory names with dots trigger orphan issue detection because dot is also used for orphan ID format. bd rename-prefix fixes it. See: https://github.com/steveyegge/beads/issues/508


Platform tests: Linux, macOS, Windows

Test event-driven mode on all platforms. Verify inotify (Linux), FSEvents (macOS), ReadDirectoryChangesW (Windows). Test fallback behavior on each.


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.


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)


Add validation for duplicate external_ref in batch imports

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

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

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

Related: bd-1022


Review and document rarely-used commands

Document use cases or consider deprecation for infrequently-used commands.

**Commands to review:**
1. bd rename-prefix - How often is this used? Document use cases
2. bd detect-pollution - Consider integrating into bd validate
3. bd migrate-hash-ids - One-time migration, keep but document as legacy

**For each command:**
- Document typical use cases
- Add examples to help text
- Consider if it should be a subcommand instead
- Add deprecation warning if appropriate

**Not changing:**
- duplicates ✓ (useful for data quality)
- repair-deps ✓ (useful for fixing broken refs)
- restore ✓ (critical for compacted issues)
- compact ✓ (performance feature)

**Deliverable:**
- Updated help text
- Documentation in ADVANCED.md
- Deprecation plan if needed


Improve sync error messages

When bd sync fails, immediately surface the actual root cause (prefix mismatch, orphaned children, forced push detected, etc.) instead of requiring multiple retries to discover it. Make errors actionable with specific recovery steps.


Update documentation for bd prime and Claude integration

Update AGENTS.md, README.md, and QUICKSTART.md to document the new `bd prime` command, `bd setup claude` command, and tip system.


Update daemon commit logic for separate branch

Modify daemon to use worktree for commits when sync.branch configured.

Tasks:
- Update internal/daemon/server_export_import_auto.go
- Detect sync.branch configuration
- Ensure worktree exists before commit
- Sync JSONL to worktree
- Commit in worktree context
- Push to configured branch
- Fallback to current behavior if sync.branch not set
- Handle git errors (network, permissions, conflicts)

Estimated effort: 3-4 days


Audit Current Cache Usage

Understand exactly what code depends on the storage cache

Audit complete. See CACHE_AUDIT.md for full findings.

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


Add comprehensive tests for config.yaml issue-prefix migration

The GH #209 config.yaml migration lacks test coverage:

Missing tests:
- config.SetIssuePrefix() edge cases (empty file, comments, malformed YAML)
- config.GetIssuePrefix() with various config states
- MigrateConfigToYAML() automatic migration logic
- bd init writing to config.yaml instead of DB
- bd migrate DB→config.yaml migration path

Test scenarios needed:
1. SetIssuePrefix with empty config.yaml
2. SetIssuePrefix with existing config.yaml (preserves other settings)
3. SetIssuePrefix with commented issue-prefix line
4. SetIssuePrefix atomic write (temp file cleanup)
5. GetIssuePrefix fallback behavior
6. MigrateConfigToYAML when config.yaml missing prefix but DB has it
7. MigrateConfigToYAML when both missing (detect from issues)
8. MigrateConfigToYAML when config.yaml already has prefix (no-op)
9. Integration test: fresh bd init writes to config.yaml only
10. Integration test: upgrade from v0.21 DB migrates to config.yaml

Priority 1 because this is a user-facing migration affecting all users upgrading to v0.22.


Sync sanitize incorrectly removes newly created issues

## Problem

The sync sanitize process incorrectly identifies newly created issues as 'deleted issues resurrected by git merge' and removes them from the local JSONL file.

## Reproduction

1. Create a new issue: bd create --title='Test issue'
2. Run bd sync
3. Observe: New issue appears in sanitize removal list
4. Issue is removed from local JSONL but preserved on beads-sync branch

## Observed Behavior

During sync, the sanitize step outputs:
```
→ Sanitized JSONL: removed 738 deleted issue(s) that were resurrected by git merge
  - bd-08ea  (newly created issue!)
  - bd-tnsq  (newly created issue!)
  ...
```

The newly created issues get removed locally but remain on beads-sync branch.

## Expected Behavior

Newly created issues should NOT be removed by sanitize. The sanitize should only remove issues that:
1. Were previously deleted (have tombstones or are in deletions manifest)
2. Are being resurrected from old git history

## Root Cause Investigation

The sanitize logic likely compares the local DB snapshot against some reference and incorrectly classifies new issues as 'resurrected deleted issues'. Possible causes:
- Snapshot protection logic not accounting for new issues
- Deletion manifest containing stale entries
- Race condition between export and sanitize

## Impact

- New issues disappear from local JSONL after sync
- Issues remain on beads-sync but cause confusion
- Multi-agent workflows affected when agents can't see new issues locally
- Requires manual intervention to recover

## Files to Investigate

- cmd/bd/sync.go - sanitize logic
- cmd/bd/snapshot_manager.go - snapshot comparison
- Deletion manifest handling

## Acceptance Criteria

- [ ] Newly created issues survive sync without being sanitized
- [ ] Only truly deleted/resurrected issues are removed
- [ ] Add test case for this scenario


Implement core reset package (internal/reset)

Create the core reset logic in internal/reset/ package.

## Responsibilities
- ResetOptions struct with all flag options
- CountImpact() - count issues/tombstones that will be deleted
- ValidateState() - check .beads/ exists, check git dirty state
- ExecuteReset() - main reset logic (without CLI concerns)
- Integrate with daemon killall

## Interface Design
```go
type ResetOptions struct {
    Hard      bool   // Include git operations (git rm, commit)
    Backup    bool   // Create backup before reset
    DryRun    bool   // Preview only, don't execute
    SkipInit  bool   // Don't re-initialize after reset
}

type ResetResult struct {
    IssuesDeleted     int
    TombstonesDeleted int
    BackupPath        string  // if backup was created
    DaemonsKilled     int
}

type ImpactSummary struct {
    IssueCount      int
    OpenCount       int
    ClosedCount     int
    TombstoneCount  int
    HasUncommitted  bool  // git dirty state
}

func Reset(opts ResetOptions) (*ResetResult, error)
func CountImpact() (*ImpactSummary, error)
func ValidateState() error
```

## IMPORTANT: CLI vs Core Separation
- `Force` (skip confirmation) is NOT in ResetOptions - that's a CLI concern
- Core always executes when called; CLI decides whether to prompt first
- Keep CLI-agnostic: no prompts, no colored output, no user interaction
- Return errors for CLI to handle with user-friendly messages
- Unit testable in isolation

## Dependencies
- Uses daemon.KillAllDaemons() from internal/daemon/
- Calls bd init logic after reset (unless SkipInit)


Tombstone import creates duplicate entries when deletions.jsonl has entries already in JSONL

In the tombstone import logic (bd-dve), when a non-tombstone issue is in both JSONL and deletions.jsonl, we:
1. Skip the issue (line 149-155)
2. Create a tombstone from deletions.jsonl (lines 161-171)

This means if the JSONL contains issue 'bd-abc' (non-tombstone) and deletions.jsonl also has 'bd-abc', we skip the JSONL version but then add a tombstone version from deletions.jsonl. This is intentional but the skipped issue count may be confusing.

Consider: should we track converted-to-tombstone separately from skipped-deleted in the Result struct?


Write Agent Mail integration guide

Comprehensive guide for setting up and using Agent Mail with Beads.

Acceptance Criteria:
- Installation instructions
- Configuration (environment variables)
- Architecture diagram
- Benefits and tradeoffs
- When to use vs not use
- Troubleshooting section
- Migration from git-only mode

File: docs/AGENT_MAIL.md

Sections:
- Quick start
- How it works
- Integration points
- Graceful degradation
- Multi-machine deployment
- FAQ


Backend task


Add BEADS_DAEMON_MODE flag handling

Add environment variable BEADS_DAEMON_MODE (values: poll, events). Default to 'poll' for Phase 1. Wire into daemon startup to select runEventLoop vs runEventDrivenLoop.


Stealth mode global gitignore should use absolute project path

**GitHub Issue:** #538

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

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

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

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

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

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

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


bd doctor: per-fix confirmation mode

Add an interactive mode where users can approve/reject each fix individually rather than all-or-nothing. Currently --fix prompts once for all fixes. This would give users more control over which repairs to apply, especially useful when some fixes may have side effects.


No logging/debugging for tombstone resurrection events

Per the design document bd-zvg Open Question 1: Should resurrection log a warning? Recommendation was Yes. Currently, when an expired tombstone loses to a live issue (resurrection), there is no logging or debugging output. This makes it hard to understand why an issue reappeared. Recommendation: Add optional debug logging when resurrection occurs, e.g., Issue bd-abc resurrected (tombstone expired). Files: internal/merge/merge.go:359-366, 371-378, 400-405, 410-415


isLikelyHash rejects 3-char all-letter base36 hashes

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

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

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

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


Refactor daemon local-only sync functions to reduce duplication

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


Remove misleading placeholder 'bd merge' command from duplicates output

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

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

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

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

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

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

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


Implement bd stale command to find abandoned/forgotten issues

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

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

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

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

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


Create cmd/bd/daemon_watcher.go (~150 LOC)

Implement FileWatcher using fsnotify to watch JSONL file and git refs. Handle platform differences (inotify/FSEvents/ReadDirectoryChangesW). Include edge case handling for file rename, event storm, watcher failure.


Add bd alias command for manual alias control

Add command for users to manually view and reassign aliases.

## Command: bd alias

### Subcommands

#### 1. bd alias list
Show all alias mappings:
```bash
$ bd alias list
#1    → bd-af78e9a2  Fix authentication bug
#2    → bd-e5f6a7b8  Add logging to daemon
#42   → bd-1a2b3c4d  Investigate jujutsu integration
#100  → bd-9a8b7c6d  (reassigned after conflict)
```

#### 2. bd alias set <alias> <hash-id>
Manually assign alias to specific issue:
```bash
$ bd alias set 42 bd-1a2b3c4d
✓ Assigned alias #42 to bd-1a2b3c4d

$ bd alias set 1 bd-af78e9a2
✗ Error: Alias #1 already assigned to bd-e5f6a7b8
Use --force to override
```

#### 3. bd alias compact
Renumber all aliases to fill gaps:
```bash
$ bd alias compact [--dry-run]

Current aliases: #1, #2, #5, #7, #100, #101
After compacting: #1, #2, #3, #4, #5, #6

Renumbering:
  #5   → #3
  #7   → #4
  #100 → #5
  #101 → #6

Apply changes? [y/N]
```

#### 4. bd alias reset
Regenerate all aliases (sequential from 1):
```bash
$ bd alias reset [--sort-by=priority|created|id]

WARNING: This will reassign ALL aliases. Continue? [y/N]

Reassigning 150 issues by priority:
  bd-a1b2c3d4 → #1   (P0: Critical security bug)
  bd-e5f6a7b8 → #2   (P0: Data loss fix)
  bd-1a2b3c4d → #3   (P1: Jujutsu integration)
  ...
```

#### 5. bd alias find <hash-id>
Look up alias for hash ID:
```bash
$ bd alias find bd-af78e9a2
#1

$ bd alias find bd-nonexistent
✗ Error: Issue not found
```

## Use Cases

### 1. Keep Important Issues Low-Numbered
```bash
# After closing many P0 issues, compact to free low numbers
bd alias compact

# Or manually set
bd alias set 1 bd-<critical-bug-hash>
```

### 2. Consistent Aliases Across Clones
```bash
# After migration, coordinator assigns canonical aliases
bd alias reset --sort-by=id
git add .beads/aliases.jsonl
git commit -m "Canonical alias assignments"
git push

# Other clones pull and adopt
git pull
bd import  # Alias conflicts resolved automatically
```

### 3. Debug Alias Conflicts
```bash
# See which aliases were reassigned
bd alias list | grep "#100"
```

## Flags

### Global
- `--dry-run`: Preview changes without applying
- `--force`: Override existing alias assignments

### bd alias reset
- `--sort-by=priority`: Assign by priority (P0 first)
- `--sort-by=created`: Assign by creation time (oldest first)
- `--sort-by=id`: Assign by hash ID (lexicographic)

## Implementation

File: cmd/bd/alias.go
```go
func aliasListCmd() *cobra.Command {
    return &cobra.Command{
        Use:   "list",
        Short: "List all alias mappings",
        Run: func(cmd *cobra.Command, args []string) {
            aliases := storage.GetAllAliases()
            for _, a := range aliases {
                fmt.Printf("#%-4d → %s  %s\n",
                    a.Alias, a.IssueID, a.Title)
            }
        },
    }
}

func aliasSetCmd() *cobra.Command {
    return &cobra.Command{
        Use:   "set <alias> <hash-id>",
        Short: "Manually assign alias to issue",
        Args:  cobra.ExactArgs(2),
        Run: func(cmd *cobra.Command, args []string) {
            alias, _ := strconv.Atoi(args[0])
            hashID := args[1]

            force, _ := cmd.Flags().GetBool("force")
            if err := storage.SetAlias(alias, hashID, force); err != nil {
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
                os.Exit(1)
            }
            fmt.Printf("✓ Assigned alias #%d to %s\n", alias, hashID)
        },
    }
}
```

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

## Testing
- Test alias list output
- Test alias set with/without force
- Test alias compact removes gaps
- Test alias reset with different sort orders
- Test alias find lookup


Add bd init --contributor wizard

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


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


Imperator lacks mail hooks for automatic notification


Upgrade notification persists after running suggested commands

The upgrade notification kept showing even after running 'bd upgrade review' and 'bd doctor' as suggested.

Expected: Notification should be dismissed after running either/both commands
Actual: Notification persisted on every bd command

Investigation findings:
- metadata.json correctly shows last_bd_version=0.28.0
- The notification stopped appearing after a few more invocations
- May be related to daemon caching or a race condition
- The 'upgradeAcknowledged' flag in version_tracking.go:115 only persists per-session (in-memory)

Possible causes:
1. Daemon vs direct mode inconsistency - daemon may have stale version info
2. The version is checked/updated before daemon connection, but daemon has cached state
3. Race between trackBdVersion() and daemon startup

Status: May have resolved itself, but root cause unclear. Close if no longer reproducible.


Issue 1


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


Block direct status update to tombstone in UpdateIssue

Per bd-2m7 design: 'Should we support tombstone as valid input to bd update --status? Recommendation: No, tombstones only created via bd delete'

Currently nothing prevents 'bd update <id> --status=tombstone', which would create an invalid tombstone (missing deleted_at, deleted_by, etc.).

Location: internal/storage/sqlite/queries.go:500-664 UpdateIssue

Add validation to reject status=tombstone in updates.


Add tombstone doctor checks

Add bd doctor checks for: unmigrated deletions.jsonl, stale deletions.jsonl in Phase 3, tombstones expiring soon. Per design bd-dli.


Review PR #512: clarify bd ready docs

Review and merge PR #512 from aspiers. This PR clarifies what bd ready does after git pull in README.md. Simple 1-line change. URL: https://github.com/anthropics/beads/pull/512


Remove Cache-Related Tests

Delete or update tests that assume multi-repo caching


Merge timestamp tie-breaker should prefer local (left)

In mergeFieldByUpdatedAt, when timestamps are exactly equal, right wins. For consistency with IssueType (where local/left wins), equal timestamps should prefer left. Minor inconsistency.


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


Test parent issue


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


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


Daemon performAutoImport should update jsonl_content_hash after import

The daemon's performAutoImport function was not updating jsonl_content_hash after successful import, unlike the CLI import path. This could cause repeated imports if the file hash and DB hash diverge.

Fix: Added hash update after validatePostImport succeeds, using repoKey for multi-repo support.


bd validate - Comprehensive health check

Run all validation checks in one command.

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

Supports --fix-all for auto-repair.

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

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


fix: bd onboard hangs on Windows


Add fallback to polling on watcher failure

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


Configure database connection pool limits for daemon mode

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

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

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

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

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

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

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

Effort: 1 hour


Proposal workflow (propose/withdraw/accept)

Implement commands and state machine for moving issues between personal planning repos and canonical upstream repos, enabling contributors to propose work without polluting PRs.


Git history fallback missing timeout - could hang on large repos

## Problem

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

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

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

## Fix
Add context with timeout to git commands:

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

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


Consider central serialization package for JSON handling

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

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

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

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

Decision point: Evaluate if benefits outweigh refactoring cost

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


sync branch not match config

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

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


Add git operations for --hard reset

Implement git integration for hard reset mode.

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

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

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

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

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

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


Verify MCP Server Compatibility

Ensure MCP server works with cache-free daemon


Design migration path from deletions.jsonl to tombstones

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


Add error handling test for cmd.Help() in search command

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

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

In cmd/bd/search.go:39, the return value of cmd.Help() is not checked, flagged by errcheck linter.

Add test to verify:
- Error handling when cmd.Help() fails (e.g., output redirection fails)
- Proper error propagation to caller
- Command still exits gracefully on help error

This ensures the search command handles help errors properly and doesn't silently ignore failures.

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


Fix TestTwoCloneCollision timeout


Optimize test suite performance - cut runtime by 50%+

## Problem
Test suite takes ~20.8 seconds, with 95% of time spent in just 2 tests:
- TestHashIDs_MultiCloneConverge: 11.08s (53%)
- TestHashIDs_IdenticalContentDedup: 8.78s (42%)

Both tests in beads_hash_multiclone_test.go perform extensive Git operations (bare repos, multiple clones, sync rounds).

## Goal
Cut total test time by at least 50% (to ~10 seconds or less).

## Analysis
Tests already have some optimizations:
- --shared --depth=1 --no-tags for fast cloning
- Disabled hooks, gc, fsync
- Support -short flag

## Impact
- Faster development feedback loop
- Reduced CI costs and time
- Better developer experience


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


Test exclusive file reservations with two agents

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

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

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


Investigate jujutsu VCS as potential solution for conflict-free merging

## Context
Current N-way collision resolution struggles with Git line-based merge model. When 5+ clones create issues with same ID, Git merge conflicts require manual resolution, and our collision resolver can fail during convergence rounds.

## Research Question
Could jujutsu (jj) provide better conflict handling for JSONL files?

## Jujutsu Overview
- Next-gen VCS built on libgit2
- Designed to handle conflicts as first-class citizens
- Supports conflict-free replicated data types (CRDTs) in some scenarios
- Better handling of concurrent edits
- Can work with Git repos (compatible with existing infrastructure)

## Investigation Tasks
1. JSONL Merge Behavior - How does jj handle line-by-line JSONL conflicts?
2. Integration Feasibility - Can beads use jj as backend while maintaining Git compatibility?
3. Conflict Resolution Model - Does jj conflict model map to our collision resolution?
4. Operational Transform Support - Does jj implement operational transforms?

## Deliverables
1. Technical report on jj merge algorithm for JSONL
2. Proof-of-concept: 5-clone collision test using jj instead of Git
3. Performance comparison: Git vs jj for beads workload
4. Recommendation: Adopt, experiment further, or abandon

## References
- https://github.com/martinvonz/jj
- Related to bd-e6d71828, bd-7a2b58fc


Add --lock-timeout global flag

Add new global flag to control SQLite busy_timeout.

## Implementation
1. Add to cmd/bd/main.go:
   - `lockTimeout time.Duration` global variable
   - Register flag: `--lock-timeout=<duration>` (default 30s)

2. Add config support in internal/config/config.go:
   - `v.SetDefault("lock-timeout", "30s")`
   - Read from config.yaml if not set via flag

3. Pass timeout to sqlite.New() - see next task

## Acceptance Criteria
- `bd --lock-timeout=0 list` fails immediately if DB is locked
- `bd --lock-timeout=100ms list` waits max 100ms
- Config file setting works: `lock-timeout: 100ms`
- Default remains 30s for backward compatibility

Part of bd-olc1


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

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

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

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

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

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


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


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


Run bd doctor at end of bd init to verify setup

Run bd doctor diagnostics at end of bd init (after line 398 in init.go). If issues found, warn user immediately: '⚠ Setup incomplete. Run bd doctor --fix to complete setup.' Catches configuration problems before user encounters them in normal workflow.


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


Widespread double JSON encoding bug in daemon mode RPC calls

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

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

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

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

All commands now tested and working correctly with daemon mode.