Phase 2.3: TOON deletion tracking

Implement deletion tracking in TOON format.

## Overview
Phase 2.2 switched storage to TOON format. Phase 2.3 adds deletion tracking in TOON format for propagating deletions across clones.

## Required Work

### 2.3.1 Deletion Tracking (TOON Format)
- [ ] Implement deletions.toon file (tracking deleted issue records)
- [ ] Add DeleteTracker struct to record deleted issue IDs and metadata
- [ ] Update bdt delete command to record in deletions.toon
- [ ] Design deletion record format (ID, timestamp, reason, hash)
- [ ] Implement auto-prune of old deletion records (configurable TTL)

### 2.3.2 Sync Propagation
- [ ] Load deletions.toon during import
- [ ] Remove deleted issues from local database when imported from remote
- [ ] Handle edge cases (delete same issue in multiple clones)
- [ ] Deletion ordering and conflict resolution

### 2.3.3 Testing
- [ ] Unit tests for deletion tracking
- [ ] Integration tests for deletion propagation
- [ ] Multi-clone deletion scenarios
- [ ] TTL expiration tests

## Success Criteria
- deletions.toon stores deletion records in TOON format
- Deletions propagate across clones via git sync
- Old records auto-prune after TTL
- All 70+ tests still passing
- bdt delete command works seamlessly


Run bump-version.sh 0.30.7

Run ./scripts/bump-version.sh 0.30.7 to update version in all files


Beads commands operate on stale database without warning

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

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

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

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

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

## Implementation Complete

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

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

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

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

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


enhance 'bd status' to show recent activity

It would be nice to be able to quickly view the last N changes in the database, to see wha's recently been worked on. I'm imagining something like 'bd status activity'.


Implement or remove ExpectExit/ExpectStdout verification fields

The Verification struct in internal/types/workflow.go has ExpectExit and ExpectStdout fields that are never used by workflowVerifyCmd. Either implement the functionality or remove the dead fields.


Mail Commands: bd mail send/inbox/read/ack

Implement core mail commands in cmd/bd/mail.go

Commands:
- bd mail send <recipient> -s 'Subject' -m 'Body' [--urgent]
  - Creates issue with type=message, sender=identity, assignee=recipient
  - --urgent sets priority=0

- bd mail inbox [--from <sender>] [--priority <n>]
  - Lists open messages where assignee=my identity
  - Sorted by priority, then date

- bd mail read <id>
  - Shows full message content (subject, body, sender, timestamp)
  - Does NOT close (separate from ack)

- bd mail ack <id>
  - Marks message as read by closing it
  - Can ack multiple: bd mail ack <id1> <id2> ...

Requires: Identity configuration (bd-kwro.7)


Restart daemon with 0.33.2

Restart the bd daemon to pick up new version:

```bash
bd daemon --stop
bd daemon --start
bd daemon --health  # Verify Version: 0.33.2
```


Re: Thread Test 2

Great! Thread is working well.


Add composite index (status, priority) for common list queries

SearchIssues and GetReadyWork frequently filter by status and sort by priority. Currently uses two separate indexes.

**Common query pattern (queries.go:1646-1647):**
```sql
WHERE status = ?
ORDER BY priority ASC, created_at DESC
```

**Problem:** Index merge or full scan when both columns are used.

**Solution:** Add migration:
```sql
CREATE INDEX IF NOT EXISTS idx_issues_status_priority ON issues(status, priority);
```

**Expected impact:** Faster bd list, bd ready with filters. Particularly noticeable at 10K+ issues.


Monitor npm publish

Watch the npm publish action:
https://github.com/steveyegge/beads/actions/workflows/npm-publish.yml

Verify at: https://www.npmjs.com/package/@anthropics/claude-code-beads-plugin/v/0.30.4

Check:
```bash
npm view @anthropics/claude-code-beads-plugin@0.30.4 version
```

```verify
npm view @anthropics/claude-code-beads-plugin@0.30.4 version 2>/dev/null | grep -q '0.30.4'
```


Merge: bd-qioh

branch: polecat/Errata
target: main
source_issue: bd-qioh
rig: beads


Create and push git tag v{{version}}

Create the release tag and push it:

```bash
git tag v{{version}}
git push origin v{{version}}
```

This triggers the GoReleaser GitHub Action to build release binaries.


Update Homebrew formula

After GoReleaser completes, the Homebrew tap should be auto-updated.

If manual update needed:
```bash
./scripts/update-homebrew.sh v0.30.4
```

Or manually update steveyegge/homebrew-beads with new SHA256.

Verify:
```bash
brew update
brew info beads
```


Tier 2: Basic bd mol squash command

Add bd mol squash command for basic molecule execution compression.

## Command

bd mol squash <molecule-id> [flags]
  --dry-run         Preview what would be squashed
  --keep-children   Don't delete ephemeral children after squash
  --json            JSON output

## Implementation

1. Find all ephemeral children of molecule (parent-child deps)
2. Concatenate child descriptions/notes into digest
3. Create digest issue in main repo with:
   - Title: 'Molecule Execution Summary: <original-title>'
   - digest_of: [list of squashed child IDs]
   - ephemeral: false (digest is permanent)
4. Delete ephemeral children (unless --keep-children)
5. Link digest to parent work item

## Schema Changes

Add to Issue struct:
- SquashedAt *time.Time
- SquashDigest string (ID of digest)
- DigestOf []string (IDs of squashed children)

## Acceptance Criteria

- bd mol squash <id> creates digest, removes children
- --dry-run shows preview
- Digest has proper metadata linking


Optimize GetReadyWork performance - 752ms on 10K database (target: <50ms)

# Performance Analysis (10K Issue Database)

Analyzed using CPU profiles from benchmark suite on Apple M2 Pro.

## Operation Performance

| Operation                        | Time    | Allocations | Memory |
|----------------------------------|---------|-------------|--------|
| bd ready (GetReadyWork)          | ~752ms  | 167,466     | 16MB   |
| bd list (SearchIssues no filter) | ~11.6ms | 89,214      | 5.8MB  |
| bd list (SearchIssues filtered)  | ~9.2ms  | 62,365      | 3.5MB  |
| bd create (CreateIssue)          | ~2.6ms  | 146         | 8.6KB  |
| bd update (UpdateIssue)          | ~0.32ms | 364         | 15KB   |
| bd close (UpdateIssue)           | ~0.32ms | 364         | 15KB   |

**Target: <50ms for all operations on 10K database**

**Current issue: GetReadyWork is 15x over target (752ms vs 50ms)**

## Root Cause

GetReadyWork (internal/storage/sqlite/ready.go:90-128) uses recursive CTE to propagate blocking:
- 65x slower than SearchIssues
- Recalculates entire blocked issue tree on every call
- Algorithm:
  1. Find directly blocked issues via 'blocks' dependencies
  2. Recursively propagate blockage to descendants (max depth: 50)
  3. Exclude all blocked issues from results

## CPU Profile Analysis

- Database syscalls (pthread_cond_signal, syscall6): ~75%
- SQLite engine overhead: inherent to recursive CTE
- Application code (query construction): <1%

**Bottleneck is the recursive CTE query execution, not application code.**

## Optimization Recommendations

### High Impact (Likely to achieve <50ms target)

1. **Cache blocked issue calculation**
   - Add `blocked_issues` table updated on dependency changes
   - Trade write complexity for read speed (ready called >> dependency changes)
   - Eliminates recursive CTE on every read

2. **Add/verify database indexes**
   ```sql
   CREATE INDEX IF NOT EXISTS idx_dependencies_blocked
     ON dependencies(issue_id, type, depends_on_id);
   CREATE INDEX IF NOT EXISTS idx_issues_status
     ON issues(status);
   ```

### Medium Impact

3. **Reduce allocations** (167K allocations for GetReadyWork)
   - Profile `scanIssues()` for object pooling opportunities
   - Reuse slice capacity for repeated calls

### Low Impact (Not recommended)
- Query optimization for CRUD operations (already <3ms)
- Connection pooling tuning (not showing in profiles)

## Verification

Run benchmarks to validate optimization:
```bash
make bench-quick
go tool pprof -http=:8080 internal/storage/sqlite/bench-cpu-*.prof
```

Profile files automatically generated in `internal/storage/sqlite/`.


Pinned field available in beads v0.37.0

Hey max,

Heads up on your mail overhaul work:

1. **Pinned field is available** - beads v0.37.0 (released by dave earlier) includes the pinned field on issues. You'll want to add this to BeadsMessage in types.go.

2. **Database migration** - Check if existing .beads databases need migration to support the pinned field. Run `bd doctor` to see if it flags anything.

3. **Sorting task** - Once you have the pinned field, gt-ngu1 (pinned beads first in mail inbox) needs implementing. Since messages now come from `bd list --type=message`, you'll need to either:
   - Sort in listBeads() after fetching, or
   - Ensure bd list returns pinned items first (may already do this?)

Check what version of bd you're building against.

-- Mayor


activity --follow: Silent error handling

In activity.go:175-179, when the daemon is down or errors occur during polling in --follow mode, errors are silently ignored:

```go
newEvents, err := fetchMutations(lastPoll)
if err != nil {
    // Daemon might be down, continue trying
    continue
}
```

This means:
- Users won't know if the daemon is unreachable
- Could appear frozen when actually failing
- No indication of lost events

Should at least show a warning after N consecutive failures, or show '...' indicator to show polling status.

Discovered during code review of bd-xo1o implementation.


Add external_projects to config schema

Add external_projects mapping to .beads/config.yaml:

```yaml
external_projects:
  beads: ../beads
  gastown: ../gastown
  other: /absolute/path/to/project
```

Used by bd ready and other commands to resolve external: references.

Part of cross-project dependency system.
See: gastown/docs/cross-project-deps.md


Fix unparam unused parameter in cmd/bd/doctor.go:541

Linting issue: checkHooksQuick - path is unused (unparam) at cmd/bd/doctor.go:541:22. Error: func checkHooksQuick(path string) string {


Implement bd mol current command

Show what molecule the agent should currently be working on. Referenced by gt-um6q, gt-lz13. Needed for molecule navigation workflow in templates.


Reduce global state in cmd/bd/main.go (25+ variables)

Code health review found main.go has 25+ global variables (lines 57-112):

- dbPath, actor, store, jsonOutput, daemonClient, noDaemon
- rootCtx, rootCancel, autoFlushEnabled
- isDirty (marked 'USED BY LEGACY CODE')
- needsFullExport (marked 'USED BY LEGACY CODE')
- flushTimer (marked 'DEPRECATED')
- flushMutex, storeMutex, storeActive
- flushFailureCount, lastFlushError, flushManager
- skipFinalFlush, autoImportEnabled
- versionUpgradeDetected, previousVersion, upgradeAcknowledged

Impact:
- Hard to test individual commands
- Race conditions possible
- State leakage between commands

Fix: Move toward dependency injection. Remove deprecated variables. Consider cmd/bd/internal package.

Investigation found flushTimer, isDirty, needsFullExport are actively used by both legacy autoflush.go and new flush_manager.go. Requires coordinated refactor to migrate all callers to FlushManager first. Estimated: significant effort.


Graph Link: replies_to for conversation threading

Implement replies_to link type for message threading.

New command:
- bd mail reply <id> -m 'Response' creates a message with replies_to set

Query support:
- bd show <id> --thread shows full conversation thread
- Thread traversal in storage layer

Storage:
- replies_to column in issues table
- Index for efficient thread queries

This enables Reddit-style nested threads where messages reply to other messages.


Work on beads-ldv: Fix bd graph crashes with nil pointer ...

Work on beads-ldv: Fix bd graph crashes with nil pointer dereference (GH#657). Fix nil pointer in computeDependencyCounts at graph.go:428. When done, submit MR (not PR) to integration branch for Refinery.


Add close_reason to JSONL export format documentation

PR #551 now persists close_reason to the database, but there's a question about whether this field should be exported to JSONL format.

## Current State
- close_reason is stored in issues.close_reason column
- close_reason is also stored in events table (audit trail)
- The JSONL export format may or may not include close_reason

## Questions
1. Should close_reason be exported to JSONL format?
2. If yes, where should it go (root level or nested in events)?
3. Should there be any special handling to avoid duplication?
4. How should close_reason be handled during JSONL import?

## Why This Matters
- JSONL is the git-friendly sync format
- Other beads instances import from JSONL
- close_reason is meaningful data that should be preserved across clones

## Suggested Action
- Check if close_reason is currently exported in JSONL
- If not, add it to the export schema
- Document the field in JSONL format spec
- Add tests for round-trip (export -> import -> verify close_reason)


Timer beads for scheduled agent callbacks

## Problem

Agents frequently need to wait for external events (CI completion, PR reviews, artifact builds) but have no good mechanism:
- `sleep N` blocks and is unreliable (often times out at 8+ minutes)
- Polling wastes context and is easy to forget
- No way to survive session restarts

## Proposal: Timer Beads

A new bead type or field that represents a scheduled callback:

### Creating timers
```bash
bd timer create --in 30s --callback "Check CI run 12345" --issue bd-xyz
bd timer create --at "2025-12-20T08:00:00" --callback "Morning standup"
bd timer create --in 5m --on-expire "tmux send-keys -t dave 'bd show bd-xyz'"
```

### Timer storage
- Store in beads (survives restarts)
- Fields: `expires_at`, `callback_description`, `on_expire_command`, `linked_issue`
- Status: pending, fired, cancelled

### Deacon integration
The Deacon daemon monitors timer beads:
1. Wakes on next timer expiry
2. Executes `on_expire` command (e.g., tmux send-keys to interrupt agent)
3. Marks timer as fired
4. Optionally updates linked issue

### Use cases
- CI monitoring: "ping me when build completes"
- PR reviews: "check back in 1 hour"
- Scheduled tasks: "remind me at EOD to sync"
- Blocking waits: agent registers callback instead of sleeping

## Acceptance criteria
- [ ] Timer bead type or field design
- [ ] `bd timer create/list/cancel` commands
- [ ] Deacon timer monitoring loop
- [ ] tmux integration for agent interrupts
- [ ] Survives daemon restarts


Add tests for validation functions

Validation functions like ParseIssueType have 0% coverage. These are critical for ensuring data quality and preventing invalid data from entering the system.


Add tests for delete operations

Core delete functionality including deleteViaDaemon, createTombstone, and deleteIssue functions have 0% coverage. These are critical for data integrity and need comprehensive test coverage.


Create git tag v{{version}}

Create the release tag:

```bash
git tag v{{version}}
```

Verify: `git tag | grep {{version}}`


Commit and push release

git add -A && git commit -m 'chore: bump version to 0.32.1' && git push


Digest: Version Bump: test-squash

## Molecule Execution Summary

**Molecule**: Version Bump: test-squash
**Steps**: 8

**Completed**: 0/8

---

### Steps

1. **[open]** Verify release artifacts
   Check GitHub releases page - binaries for darwin/linux/windows should be available

2. **[open]** Commit and push release
   git add -A && git commit && git push to trigger CI

3. **[open]** Update CHANGELOG.md with release notes
   Add meaningful release notes to CHANGELOG.md describing what changed in test-squash

4. **[open]** Wait for CI to pass
   Monitor GitHub Actions - all checks must pass before release artifacts are built

5. **[open]** Restart running daemons
   Kill and restart any running bd daemons to pick up new version: pkill -f 'bd daemon' && bd daemon --start

6. **[open]** Update local installation
   Run install script or brew upgrade to get new version locally: curl -fsSL .../install.sh | bash

7. **[open]** Run bump-version.sh test-squash
   Run ./scripts/bump-version.sh test-squash to update version in all files

8. **[open]** Update info.go versionChanges
   Add entry to versionChanges in cmd/bd/info.go with agent-actionable changes for test-squash


Merge: bd-ot0w

branch: polecat/dementus
target: main
source_issue: bd-ot0w
rig: beads


Auto-recovery mode (bd sync --auto-recover)

Add bd sync --auto-recover flag that: detects problematic sync state, backs up .beads/issues.db with timestamp, rebuilds DB from JSONL atomically, verifies consistency, reports what was fixed. Provides safety valve when sync integrity fails.


Commit release v0.33.2

Stage and commit the version bump:

```bash
git add cmd/bd/version.go cmd/bd/info.go CHANGELOG.md
git commit -m "release: v0.33.2"
```

Do NOT push yet - tag first.


getLocalSyncBranch silently ignores YAML parse errors

In autoimport.go:170-172, YAML parsing errors are silently ignored. If a user has malformed YAML in config.yaml, sync-branch will just silently be empty with no feedback.

Recommendation: Add debug logging since this function is only called during auto-import, and debugging silent failures is painful.

Add: debug.Logf("Warning: failed to parse config.yaml: %v", err)


Replace map[string]interface{} with typed JSON response structs

Many CLI commands use map[string]interface{} for JSON output which loses type safety and compile-time error detection.

Files with map[string]interface{}:
- cmd/bd/compact.go (10+ instances)
- cmd/bd/cleanup.go
- cmd/bd/daemons.go
- cmd/bd/daemon_lifecycle.go

Example fix:
```go
// Before
result := map[string]interface{}{
    "status": "ok",
    "count": 42,
}

// After
type CompactResponse struct {
    Status string `json:"status"`
    Count  int    `json:"count"`
}
result := CompactResponse{Status: "ok", Count: 42}
```

Benefits:
- Compile-time type checking
- IDE autocompletion
- Easier refactoring
- Self-documenting API


GH#405: Prefix parsing with hyphens treats first segment as prefix

Prefix me-py-toolkit gets parsed as just me- when detecting mismatches. Fix prefix parsing to handle multi-hyphen prefixes. See GitHub issue #405.


Split internal/storage/sqlite/queries.go (1586 lines)

Split internal/storage/sqlite/queries.go (1704 lines) into logical modules.

## Current State
queries.go is 1704 lines with mixed responsibilities:
- Issue CRUD operations
- Search/filter operations
- Delete operations (complex cascade logic)
- Helper functions (parsing, formatting)

## Proposed Split

### 1. queries.go (keep ~400 lines) - Core CRUD
```go
// Core issue operations
func (s *SQLiteStorage) CreateIssue(...)
func (s *SQLiteStorage) GetIssue(...)
func (s *SQLiteStorage) UpdateIssue(...)
func (s *SQLiteStorage) CloseIssue(...)
```

### 2. queries_search.go (~300 lines) - Search/Filter
```go
// Search and filtering
func (s *SQLiteStorage) SearchIssues(...)
func (s *SQLiteStorage) GetIssueByExternalRef(...)
func (s *SQLiteStorage) GetCloseReason(...)
func (s *SQLiteStorage) GetCloseReasonsForIssues(...)
```

### 3. queries_delete.go (~400 lines) - Delete Operations
```go
// Delete operations with cascade logic
func (s *SQLiteStorage) CreateTombstone(...)
func (s *SQLiteStorage) DeleteIssue(...)
func (s *SQLiteStorage) DeleteIssues(...)
func (s *SQLiteStorage) resolveDeleteSet(...)
func (s *SQLiteStorage) expandWithDependents(...)
func (s *SQLiteStorage) validateNoDependents(...)
func (s *SQLiteStorage) checkSingleIssueValidation(...)
func (s *SQLiteStorage) trackOrphanedIssues(...)
func (s *SQLiteStorage) collectOrphansForID(...)
func (s *SQLiteStorage) populateDeleteStats(...)
func (s *SQLiteStorage) executeDelete(...)
func (s *SQLiteStorage) findAllDependentsRecursive(...)
```

### 4. queries_helpers.go (~100 lines) - Utilities
```go
// Helper functions (already at top of file)
func parseNullableTimeString(...)
func parseJSONStringArray(...)
func formatJSONStringArray(...)
```

### 5. queries_rename.go (~100 lines) - ID/Prefix Operations
```go
// ID and prefix management
func (s *SQLiteStorage) UpdateIssueID(...)
func (s *SQLiteStorage) RenameDependencyPrefix(...)
func (s *SQLiteStorage) RenameCounterPrefix(...)
func (s *SQLiteStorage) ResetCounter(...)
```

## Implementation Steps

1. **Create new files** with package declaration:
   ```go
   // queries_delete.go
   package sqlite

   import (...)
   ```

2. **Move functions** - cut/paste, maintaining order within each file

3. **Update imports** - each file needs its own imports

4. **Run tests** after each file split:
   ```bash
   go test ./internal/storage/sqlite/...
   ```

5. **Run linter** to catch any issues:
   ```bash
   golangci-lint run ./internal/storage/sqlite/...
   ```

## File Organization
```
internal/storage/sqlite/
├── queries.go          # Core CRUD (~400 lines)
├── queries_search.go   # Search/filter (~300 lines)
├── queries_delete.go   # Delete cascade (~400 lines)
├── queries_helpers.go  # Utilities (~100 lines)
└── queries_rename.go   # ID operations (~100 lines)
```

## Success Criteria
- No file > 500 lines
- All tests pass
- No functionality changes
- Clear separation of concerns