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.


deletions.jsonl gets corrupted with full issue objects instead of deletion records

## Bug Description

The deletions.jsonl file was found to contain full issue objects (like issues.jsonl) instead of deletion records.

### Expected Format (DeletionRecord)
```json
{"id":"bd-xxx","timestamp":"2025-...","actor":"user","reason":"deleted"}
```

### Actual Content Found
```json
{"id":"bd-03r","title":"Document deletions manifest...","description":"...","status":"closed",...}
```

## Impact
- bd sync sanitization step reads deletions.jsonl and removes any matching IDs from issues.jsonl
- With 60 full issue objects in deletions.jsonl, ALL 60 issues were incorrectly removed during sync
- This caused complete data loss of the issue database

## Root Cause (suspected)
Something wrote issues.jsonl content to deletions.jsonl. Possible causes:
- Export writing to wrong file
- File path confusion during sync
- Race condition between export and deletion tracking

## Related Issues
- bd-0b2: --no-git-history flag (just fixed)
- bd-4pv: export outputs only 1 issue after corruption
- bd-4t7: auto-import runs during --no-auto-import

## Reproduction
Unknown - discovered during bd sync session on 2025-11-26

## Fix
Need to investigate what code path could write issue objects to deletions.jsonl


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.


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

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

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

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

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

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

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

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


Git history fallback has incorrect logic for detecting deletions

## Problem

The `wasInGitHistory` function in `internal/importer/importer.go:891` returns true if the ID is found **anywhere** in git history. But finding an ID in history doesn't necessarily mean it was deleted - it could mean:

1. The issue was added (appears in a commit adding it)
2. The issue was modified (appears in commits updating it)
3. The issue was deleted (appears in a commit removing it)

The current logic incorrectly treats all three cases as 'deleted'.

## Correct Logic

`git log -S` with `--oneline` shows commits where the string was added OR removed. To detect deletion specifically:

1. ID appears in git history (was once in JSONL)
2. ID is NOT currently in JSONL

The second condition is already checked by the caller (`purgeDeletedIssues`), so technically the logic is correct in context. But the function name and doc comment are misleading.

## Fix Options

1. **Rename function** to `wasEverInJSONL` and update doc comment to clarify
2. **Add explicit check** for current JSONL state in the function itself

Option 1 is simpler and correct since caller already filters.


Sync should cleanup snapshot files after completion

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


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

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

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

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

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


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


Switch from deletions manifest to inline tombstones

Replace the current deletions.jsonl manifest with inline tombstone records in issues.jsonl.

## Problem Statement

The current deletions manifest approach has several issues:

1. **Wild poisoning** - A stale clone's deletions manifest can poison fresh databases when synced
2. **Two-level merge inconsistency** - Git content merge and beads snapshot merge use different bases
3. **SyncJSONLToWorktree overwrites** - Blindly copies local JSONL to worktree, losing remote issues
4. **3-day TTL too aggressive** - Deletions expire before dormant branches get merged

## Proposed Solution: Inline Tombstones

Instead of a separate deletions.jsonl file, embed deletion records directly in issues.jsonl:

```json
{"id":"beads-abc","status":"tombstone","title":"Original title","deleted_at":"2025-12-01T...","deleted_by":"user","expires_at":"2025-12-31T..."}
```

### Benefits

1. **Single source of truth** - No separate manifest to sync/merge
2. **Participates in normal 3-way merge** - Deletion conflicts resolved same as other fields
3. **Atomic with issue data** - Can't have orphaned deletions or missing tombstones
4. **Preserves metadata** - Can optionally keep title/type for audit trail

### Design Decisions Needed

1. **TTL duration** - 30 days default? Configurable via config.yaml?
2. **Tombstone content** - Minimal (just ID + timestamps) vs. full issue preservation?
3. **Migration path** - How to handle existing deletions.jsonl files?
4. **Status value** - Use "tombstone" or "deleted"? (tombstone clearer, deleted more intuitive)
5. **Merge semantics** - Does tombstone always win, or use updated_at like other fields?

### Migration Strategy

1. On import, convert deletions.jsonl entries to tombstones in JSONL
2. Deprecate but still read deletions.jsonl for backward compatibility
3. Stop writing to deletions.jsonl after N versions

## Related Issues

- GitHub #464: Beads deletes issues (fresh clone sync problem)
- bd-2e0: Add TTL to deletions manifest entries (current 3-day TTL)
- bd-53c: bd sync corrupts issues.jsonl in multi-clone environments


Document CLI + hooks as recommended approach over MCP

Update documentation to position CLI + bd prime hooks as the primary recommended approach over MCP server, explaining why minimizing context matters even with large context windows (compute cost, energy, environment, latency).


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 Result.ConvertedToTombstone counter for import statistics

The import Result struct has SkippedDeleted but doesn't distinguish between:
1. Issues skipped because they're in deletions manifest (old behavior)
2. Issues converted from deletions.jsonl to tombstones (new bd-dve behavior)

Add a new ConvertedToTombstone counter to track legacy deletions converted to tombstones during import. This provides better visibility into the migration process.


Add bd deleted command for audit trail

Parent: bd-imj

Add command to view deletion history.

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

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

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


Document sync.require_confirmation_on_mass_delete config option


Consolidate check-health DB opens into single connection

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


Refactor duplicate JSONL-from-git parsing code

Both readFirstIssueFromGit() in init.go and importFromGit() in autoimport.go have similar code patterns for:
1. Running git show <ref>:<path>
2. Scanning the output with bufio.Scanner
3. Parsing JSON lines

Could be refactored to share a helper like:
- readJSONLFromGit(gitRef, path string) ([]byte, error)
- Or a streaming version: streamJSONLFromGit(gitRef, path string) (io.Reader, error)

Files:
- cmd/bd/autoimport.go:225-256 (importFromGit)
- cmd/bd/init.go:1212-1243 (readFirstIssueFromGit)

Priority is low since code duplication is minimal and both functions work correctly.


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.


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.


troubleshoot GH#278 daemon exits every few secs


Add export test verifying tombstones are included in JSONL output

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

Add a test: TestExportIncludesTombstones


Add tests for tombstone edge cases

Missing test coverage for tombstone implementation:

1. Test tombstone validation (deleted_at requirement)
2. Test GetReadyWork excludes tombstones
3. Test SearchIssues behavior with tombstones
4. Integration test for creating/retrieving a tombstone via the delete command
5. Test that status=tombstone update is rejected


bd init resets metadata.json jsonl_export to beads.jsonl, ignoring existing issues.jsonl

When running 'bd init --prefix bd' in a repo that already has .beads/issues.jsonl, the init command overwrites metadata.json and sets jsonl_export back to 'beads.jsonl' instead of detecting and respecting the existing issues.jsonl file.

Steps to reproduce:
1. Have a repo with .beads/issues.jsonl (canonical) and metadata.json pointing to issues.jsonl
2. Delete beads.db and run 'bd init --prefix bd'
3. Check metadata.json - it now says jsonl_export: beads.jsonl

Expected: Init should detect existing issues.jsonl and use it.

Workaround: Manually edit metadata.json after init.


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


Add --parent flag to bd onboard output

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

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


Design tombstone merge semantics

Define how tombstones participate in 3-way merge. Key question: does tombstone always win over live issue (like current deletion-wins rule), or should we use updated_at comparison? Consider: tombstone with older timestamp vs live issue with newer edits. Recommendation: tombstone wins unless expired, matching current behavior but with longer TTL window.

# Design: Tombstone Merge Semantics

**Issue:** bd-zvg
**Author:** beads/refinery
**Status:** Draft
**Date:** 2025-12-05

## Overview

This document defines how tombstone records participate in the 3-way merge algorithm. The key question is how to resolve conflicts between tombstones and live issues.

## Current Behavior

The current 3-way merge in `internal/merge/merge.go` implements:

```go
// Lines 298-307: "RULE 2: deletion always wins over modification"
} else if inBase && inLeft && !inRight {
    // Deleted in right, maybe modified in left
    continue  // issue deleted
} else if inBase && !inLeft && inRight {
    // Deleted in left, maybe modified in right
    continue  // issue deleted
```

This is **implicit deletion** - an issue's absence means deletion. The new tombstone system makes deletion **explicit** with a `status: "tombstone"` record.

## Design Decision: Tombstone Wins Unless Expired

**Recommendation:** Preserve current "deletion wins" semantics, but with TTL expiration escape hatch.

### Rule 1: Non-Expired Tombstone Always Wins

When merging a tombstone against a live issue:

```
If tombstone.IsExpired(ttl):
    → Keep live issue (resurrection allowed)
Else:
    → Keep tombstone (issue remains deleted)
```

**Rationale:**
1. Preserves deterministic behavior users expect
2. Explicit deletion (tombstone) is a deliberate action
3. Prevents "zombie issues" that keep reappearing
4. TTL provides safety valve for stale clones

### Rule 2: Tombstone vs Tombstone - Latest Wins

When merging two tombstones for the same ID:

```go
func mergeTombstones(left, right *Issue) *Issue {
    // Both are tombstones - use later deleted_at
    if isTimeAfter(left.DeletedAt, right.DeletedAt) {
        return left
    }
    return right
}
```

**Rationale:**
- Most recent deletion is most authoritative
- Preserves audit trail (latest actor/reason)
- Matches `updated_at` conflict resolution pattern

### Rule 3: Closed Issue vs Tombstone

Special case: closed issue vs tombstone

```
If tombstone for ID X exists:
    → Tombstone wins (closed is not deleted)
```

**Rationale:**
- Closed issues are still visible in history
- Tombstones are for permanent removal
- User explicitly chose "delete" over "close"

## Merge Algorithm Update

The 3-way merge algorithm needs updates to handle tombstones explicitly:

```go
func merge3WayWithTombstones(base, left, right []Issue, ttl time.Duration) ([]Issue, []string) {
    // ... existing setup ...

    for key := range allKeys {
        baseIssue, inBase := baseMap[key]
        leftIssue, inLeft := leftMap[key]
        rightIssue, inRight := rightMap[key]

        leftTombstone := inLeft && IsTombstone(leftIssue)
        rightTombstone := inRight && IsTombstone(rightIssue)

        // NEW CASE: Tombstone vs Live Issue
        if leftTombstone && !rightTombstone && inRight {
            // Left deleted, right has live issue
            if leftIssue.IsExpired(ttl) {
                result = append(result, rightIssue)  // Resurrection allowed
            } else {
                result = append(result, leftIssue)   // Tombstone wins
            }
            continue
        }

        if rightTombstone && !leftTombstone && inLeft {
            // Right deleted, left has live issue
            if rightIssue.IsExpired(ttl) {
                result = append(result, leftIssue)   // Resurrection allowed
            } else {
                result = append(result, rightIssue)  // Tombstone wins
            }
            continue
        }

        // NEW CASE: Both are tombstones
        if leftTombstone && rightTombstone {
            // Merge tombstones - latest wins
            merged := mergeTombstones(leftIssue, rightIssue)
            result = append(result, merged)
            continue
        }

        // EXISTING CASE: Both are live issues
        // ... existing merge logic ...
    }
}
```

## Edge Cases

### 1. Issue Deleted Then Re-Created

**Scenario:**
1. Clone A deletes issue `bd-abc` (creates tombstone)
2. Clone B (unaware of deletion) closes issue `bd-abc`
3. Clone C creates new issue that happens to get ID `bd-abc` (unlikely with UUIDs)

**Resolution:** Tombstone wins. The ID is "claimed" by the tombstone until expiration.

### 2. Concurrent Deletion

**Scenario:**
1. Clone A deletes issue `bd-abc` at T1
2. Clone B deletes same issue `bd-abc` at T2
3. Both create tombstones with different metadata

**Resolution:** Merge tombstones, use max(T1, T2) as deleted_at.

### 3. Tombstone in Base Only

**Scenario:**
1. Base has tombstone for `bd-abc`
2. Neither left nor right have the tombstone (both pruned/compacted)

**Resolution:** Issue is truly gone. No record in output.

### 4. Fast-Forward Merge

**Scenario:**
- Base empty, left has issues A,B,C
- Right has issues A,B,D and tombstone for C

**Resolution:** Output has A, B, D, and tombstone for C.

## Integration Points

### With TTL Design (bd-1r5)

```go
func (t *Tombstone) IsExpired(ttl time.Duration) bool {
    expiresAt := t.DeletedAt.Add(ttl).Add(clockSkewGrace)
    return time.Now().After(expiresAt)
}
```

The merge algorithm must accept TTL as a parameter.

### With Storage Format (bd-2m7)

Tombstone detection:

```go
func IsTombstone(issue *Issue) bool {
    return issue.Status == "tombstone"
}
```

### With Migration (bd-dli)

During migration, entries from `deletions.jsonl` become tombstones that participate in merges normally.

## Summary of Merge Rules

| Left | Right | Condition | Result |
|------|-------|-----------|--------|
| Live | Tombstone | Not expired | Tombstone |
| Live | Tombstone | Expired | Live |
| Tombstone | Live | Not expired | Tombstone |
| Tombstone | Live | Expired | Live |
| Tombstone | Tombstone | - | Later deleted_at wins |
| Live | Live | - | Existing 3-way merge |
| Absent | Tombstone | - | Tombstone |
| Tombstone | Absent | - | Tombstone |
| Absent | Live | - | Live (new issue) |

## Implementation Notes

### Performance Consideration

The merge algorithm now needs TTL configuration. Options:
1. Pass TTL as parameter (recommended)
2. Read from config file during merge (slower, file I/O)
3. Embed TTL in tombstone record (per-bd-1r5 expires_at field)

**Recommendation:** Pass TTL as parameter, with optional `expires_at` field for future optimization.

### Backward Compatibility

- Old merges without tombstone awareness will treat `status: "tombstone"` as a regular status
- Won't break, but tombstone might not "win" against live issue
- Migration should happen before enabling sync-branch features

## Open Questions

1. **Should resurrection log a warning?**
   - Recommendation: Yes, `"Issue bd-abc resurrected (tombstone expired)"`

2. **Should we track resurrection count?**
   - Recommendation: No for v1, adds complexity

3. **Should `bd merge` command accept --ttl flag?**
   - Recommendation: Yes, for testing/debugging