Add index on deleted_at for TTL queries

Per bd-2m7 design: 'Index for efficient tombstone filtering' was recommended. The current implementation does NOT add an index on deleted_at.

For TTL cleanup queries like 'SELECT * FROM issues WHERE deleted_at < datetime(now, -30 days)', this will require a full table scan.

Add to migration or new migration:
CREATE INDEX IF NOT EXISTS idx_issues_deleted_at ON issues(deleted_at) WHERE deleted_at IS NOT NULL


Add test coverage for internal/config package


CRITICAL: git-history-backfill purges entire database when JSONL reset

When a clone gets reset (git reset --hard origin/main), the git-history-backfill logic incorrectly adds ALL issues to the deletions manifest, then sync purges the entire database.\n\nFix adds safety guard: never delete more than 50% of issues via git-history-backfill. If threshold exceeded, abort with warning message.


Jira import script (jira2jsonl.py)

Create a Python script to import Jira issues into beads JSONL format.

**Pattern**: Follow examples/github-import/gh2jsonl.py

**Features needed**:
- Fetch issues via Jira REST API with JQL queries
- Pagination handling (100 issues per request)
- Map Jira fields to beads fields (see schema mapping in epic)
- Set external_ref to Jira issue URL for re-sync
- Support both Jira Cloud and Server/Data Center APIs
- Read config from bd config (jira.url, jira.project, jira.api_token)

**Config-driven mapping**:
- jira.status_map.* for status conversion
- jira.type_map.* for issue type conversion
- jira.priority_map.* for priority conversion

**Usage**:
```bash
python jira2jsonl.py --project PROJ | bd import
# or
python jira2jsonl.py --jql 'project=PROJ AND status\!=Done' | bd import
```

**Output**: JSONL with external_ref set to Jira issue URL


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.


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