Tier 5 (Future): JSONL archive rotation

Periodic rotation of issues.jsonl for long-running repos.

## Design

.beads/
├── issues.jsonl           # Current (hot)
├── archive/
│   ├── issues-2025-12.jsonl.gz  # Archived (cold)
│   └── ...
└── index.jsonl            # Merged index for queries

## Commands

bd archive rotate [flags]
  --older-than N    Archive issues closed > N days
  --compress        Gzip archives
  --dry-run         Preview

bd archive list           # Show archived periods
bd archive restore <period>  # Restore from archive

## Config

bd config set archive.enabled true
bd config set archive.rotate_days 90
bd config set archive.compress true
bd config set archive.path '.beads/archive'

## Considerations

- Archives can be gitignored (local only) or committed (shared)
- Query layer must check index, hydrate from archive
- Cold storage tiering (S3/GCS) for enterprise
- Merkle proofs preserved for audit

## Priority

This is post-1.0 work. Current focus is on squash (removes ephemeral).
Archive helps with long-term history but is less critical.


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


Merge3Way public API does not expose TTL parameter

The public Merge3Way() function in merge.go does not allow callers to configure the tombstone TTL. It hard-codes the default via merge3WayWithTTL(). While merge3WayWithTTL() exists, it is unexported (lowercase). This means the CLI and tests cannot configure TTL at merge time. Use cases: testing with different TTL values, per-repository TTL configuration, debugging with short TTL, supporting --ttl flag in bd merge command (mentioned in design doc bd-zvg). Recommendation: Export Merge3WayWithTTL (rename to uppercase). Files: internal/merge/merge.go:77, 292-298


bd doctor: detect issues referenced in commits but still open

Add a doctor check that finds 'orphaned' issues - ones referenced in git commit messages (e.g., 'fix bug (bd-xxx)') but still marked as open in beads.

**Detection logic:**
1. Get all open issue IDs from beads
2. Parse git log for issue ID references matching pattern \(prefix-[a-z0-9.]+\)
3. Report issues that appear in commits but are still open

**Output:**
⚠ Warning: N issues referenced in commits but still open
  bd-xxx: 'Issue title' (commit abc123)
  bd-yyy: 'Issue title' (commit def456)

  These may be implemented but not closed. Run 'bd show <id>' to check.

**Implementation:**
- Add check to doctor/checks.go
- Use git log parsing (already have git utilities)
- Match against configured issue_prefix


Run go install

go install ./cmd/bd


Update CHANGELOG.md

Write the [Unreleased] section with all changes for 0.39.0.

Format: Keep a Changelog (https://keepachangelog.com)

Sections:
- ### Added
- ### Changed
- ### Fixed
- ### Documentation

The bump script will stamp the date automatically.


Verify PyPI package

Confirm PyPI package published.

```bash
pip index versions beads-mcp 2>/dev/null | head -3
```

Or check: https://pypi.org/project/beads-mcp/

Should show 0.99.0-test.


Simplify wisp architecture: single DB with ephemeral flag

## Problem

The current wisp architecture uses a separate .beads-wisp/ directory with its own database. This creates unnecessary complexity:

- Separate directory structure and database initialization
- Parallel routing logic in both beads and gastown
- Merge queries from two stores for inbox/list operations
- All this for what is essentially a boolean flag

## Current State

Gastown mail code has to:
- resolveWispDir() vs resolveBeadsDir()
- Query both, merge results
- Route deletes to correct store

## Proposed Solution

Single database with an ephemeral boolean field on Issue. Behavior:
- bd create --ephemeral sets the flag
- JSONL export SKIPS ephemeral issues (they never sync)
- bd list shows all by default, --ephemeral / --persistent filters
- bd mol squash clears the flag (promotes to permanent, now exports)
- bd wisp gc deletes old ephemeral issues from the db
- No separate directory, no separate routing

## Migration

1. Update beads to support ephemeral flag in main db
2. Update bd wisp commands to work with flag instead of separate dir
3. Update gastown mail to use simple --ephemeral flag (remove all dual-routing)
4. Deprecate .beads-wisp/ directory pattern

## Acceptance Criteria

- Single database for all issues (ephemeral and persistent)
- ephemeral field on Issue type
- JSONL export skips ephemeral issues
- bd create --ephemeral works
- bd mol squash promotes ephemeral to persistent
- Gastown mail uses simple flag, no dual-routing


Kill running daemons

Stop all bd daemons before release:

```bash
pkill -f 'bd.*daemon' || true
sleep 1
pgrep -lf 'bd.*daemon'  # Should show nothing
```


bd list --parent: filter by parent issue

Add --parent flag to bd list to filter issues by parent.

Example:
```bash
bd list --parent=gt-h5n --status=open
```

Would show all open children of gt-h5n.

Useful for:
- Checking epic progress
- Finding swarmable work within an epic
- Molecule step listing


Push to main

Push the release commit to origin.

```bash
git push origin main
```

If rejected, someone else pushed. Pull, rebase, try again.


Wait for CI

Monitor GitHub Actions for release completion.

https://github.com/steveyegge/beads/actions

Expected time: 5-10 minutes

Watch for:
- Build artifacts (all platforms)
- Test suite pass
- npm publish
- PyPI publish


Refinery Patrol

Merge queue processor patrol loop with verification gates.


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

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

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

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


Add bd state and bd set-state helper commands

Convenience commands for the labels-as-state pattern documented in LABELS.md.

`bd state <role> <dimension>` - Query the current value of a state dimension
Example: bd state witness-alpha patrol → outputs: active

`bd set-state <role> <dimension>=<value> [--reason "..."]` - Atomically creates an event bead and updates the label
Example: bd set-state witness-alpha patrol=muted --reason "Investigating stuck polecat"

This would combine the manual steps:
1. bd create "<reason>" -t event --parent <role>
2. bd label remove <role> <dimension>:<old-value>
3. bd label add <role> <dimension>:<new-value>

Into a single atomic operation.

Discovered from: bd-31ae


doctor --fix should export when DB has more issues than JSONL

When 'bd doctor' detects a count mismatch (DB has more issues than JSONL), it currently recommends 'bd sync --import-only', which imports JSONL into DB. But JSONL is the source of truth, not the DB.

**Current behavior:**
- Doctor detects: DB has 355 issues, JSONL has 292
- Recommends: 'bd sync --import-only'
- User runs it: Returns '0 created, 0 updated' (no-op, because JSONL hasn't changed)
- User is stuck

**Root cause:**
The doctor fix is one-directional (JSONL→DB) when it should be bidirectional. If DB has MORE issues, they haven't been exported yet - the fix should be 'bd export' (DB→JSONL), not import.

**Desired fix:**
In fix.DBJSONLSync(), detect which has more data:
- If DB > JSONL: Run 'bd export' to sync JSONL (since DB is the working copy)
- If JSONL > DB: Run 'bd sync --import-only' to import (JSONL is source of truth)
- If equal but timestamps differ: Detect based on file mtime

This makes 'bd doctor --fix' actually fix the problem instead of being a no-op.


Fix --as flag: uses title instead of ID in mol bond

In bondProtoProto, the --as flag is documented as 'Custom ID for compound proto' but the implementation uses it as the title, not the issue ID.

**Current behavior (mol.go:637-638):**
```go
if customID != '' {
    compoundTitle = customID  // Used as title, not ID
}
```

**Options:**
1. Change flag description to say 'Custom title' (documentation fix)
2. Actually use it as a custom ID prefix or full ID (feature change)
3. Add separate --title flag and make --as actually set ID

Recommend option 1 for simplest fix - change 'Custom ID' to 'Custom title' in the flag description.


Merge: bd-g4b4

branch: polecat/Hooker
target: main
source_issue: bd-g4b4
rig: beads


bd cook: Compile formula to proto

Implement the 'bd cook' command that transforms formulas into protos:

Usage: bd cook <formula-file> [options]
       bd cook .beads/formulas/*.formula.yaml

Process:
1. Parse formula YAML (uses formula parser)
2. Resolve extends (formula inheritance)
3. Expand macros (expansion rules)
4. Apply aspects (cross-cutting concerns)
5. Generate flat proto bead

Output:
- Creates proto bead in .beads/ with mol-prefix
- Proto has all steps pre-expanded
- Proto is ready for pour/wisp instantiation

Example:
  bd cook .beads/formulas/mol-deacon-patrol.formula.yaml
  → Creates mol-deacon-patrol proto bead

Gas Town tracking: gt-8tmz.13


bd cook: waits_for field not preserved in cooked protos

The `waits_for` field in formula steps is not preserved in the cooked proto.

## Formula

```yaml
- id: aggregate
  title: Aggregate arm results
  needs: [survey-workers]
  waits_for: all-children
  description: |
    This is a **fanout gate**...
```

## Expected

The cooked proto step should have metadata indicating it's a gate:
- Label: `gate:all-children` or
- Field in issue metadata indicating wait behavior

## Actual

The `waits_for` field is silently ignored. Only the description mentions 'fanout gate'.

## Impact

The execution engine cannot determine which steps are gates that need to wait
for dynamically-bonded children. This breaks the Christmas Ornament pattern
where aggregate steps must wait for all polecat-arm wisps to complete.

## Related

bd-hr39: needs field not converted to step dependencies


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 {


Digest: Release v0.34.0 @ 2025-12-22 12:16

Released v0.34.0: wisp commands, chemistry UX, cross-project deps


test hook pin


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.


bd mol burn should work on mols, not just wisps

Currently `bd mol burn` only works on wisps (Ephemeral=true):

```
$ bd mol burn bd-mol-p7i
Error: molecule bd-mol-p7i is not a wisp (Ephemeral=false)
Hint: mol burn only works with wisp molecules
      Use 'bd delete' to remove non-wisp issues
```

The name 'burn' implies destruction - it should work on any molecule, not just wisps. When called on a regular mol, it should convert to `bd delete --cascade`.

**Proposed behavior:**
- If wisp: current behavior (delete without tombstones)
- If mol: `bd delete <id> --cascade` (delete with tombstones)

This makes the metaphor consistent: burn = destroy a molecule, regardless of phase.


Fix or remove TestFindJSONLPathDefault (issue #356)

Code health review found .test-skip permanently skips TestFindJSONLPathDefault.

The test references issue #356 about wrong JSONL filename expectations (issues.jsonl vs beads.jsonl).

Test file: internal/beads/beads_test.go

The underlying migration from beads.jsonl to issues.jsonl may be complete, so either:
1. Fix the test expectations
2. Remove the test if no longer needed
3. Document why it remains skipped


Preflight: Check git status

Ensure working tree is clean before starting release.

```bash
git status
```

If there are uncommitted changes, either:
- Commit them first
- Stash them: `git stash`
- Abort and resolve


Optimization: Tombstones still synced, adding overhead

Tombstoned (deleted) issues are still processed during sync, adding overhead.

## Evidence
```
Import complete: 0 created, 0 updated, 407 unchanged, 100 skipped
```
Those 100 skipped are tombstones - they're read from JSONL, parsed, then filtered out.

## Current State (Beads repo)
- 408 total issues
- 99 tombstones (24% of database)
- Every sync reads and skips these 99 entries

## Impact
- Sync time increases with tombstone count
- JSONL file size grows indefinitely
- Git history accumulates tombstone churn

## Proposed Solutions

### 1. JSONL Compaction (`bd compact`)
Periodically rewrite JSONL without tombstones:
```bash
bd compact  # Removes tombstones, rewrites issues.jsonl
```
Trade-off: Loses delete history, but that's in git anyway.

### 2. Tombstone TTL
Auto-remove tombstones older than N days during sync:
```go
if issue.Deleted && time.Since(issue.UpdatedAt) > 7*24*time.Hour {
    // Skip writing to new JSONL
}
```

### 3. Archive File
Move old closed issues to `issues-archive.jsonl`:
- Not synced regularly
- Available for historical queries
- Main JSONL stays small

### 4. Lazy Tombstone Handling
Don't write tombstones to JSONL at all - just remove the line:
- Simpler, but loses cross-clone delete propagation
- Would need different delete propagation mechanism

## Recommendation
Start with `bd compact` command - simple, explicit, user-controlled.

## Related
- gt-tnss: Analysis - Beads database size and hygiene strategy
- gt-ox67: Maintenance - Regular cleanup of closed MR/gate beads


Review changes since last release

Understand what's being released.

```bash
git log $(git describe --tags --abbrev=0)..HEAD --oneline
```

Categorize changes:
- Features (feat:)
- Fixes (fix:)
- Breaking changes
- Documentation


Test agent with fields


Code smell: 20+ global variables in main.go

main.go lines 63-103 have over 20 global variables managing command-line state, daemon connection, auto-flush, auto-import, and error handling.

**Globals include:**
dbPath, actor, store, jsonOutput, daemonStatus, daemonClient, noDaemon, rootCtx, rootCancel, autoFlushEnabled, flushMutex, storeMutex, storeActive, flushFailureCount, lastFlushError, flushManager, hookRunner, skipFinalFlush, autoImportEnabled, versionUpgradeDetected, previousVersion, upgradeAcknowledged

**Problem:**
- Global state makes testing difficult
- Race conditions possible between goroutines
- Hard to track which functions modify which globals

**Acceptance Criteria:**
- [ ] Create CommandContext struct to hold runtime state
- [ ] Pass context through command execution
- [ ] Reduce global count to essential minimum
- [ ] Tests pass


Import reports misleading '0 created, 0 updated' when actually importing all issues

When running 'bd import' on a fresh database (no existing issues), the command reports 'Import complete: 0 created, 0 updated' even though it successfully imported all issues from the JSONL file.

**Steps to reproduce:**
1. Delete .beads/beads.db
2. Run: bd import .beads/issues.jsonl
3. Observe output: 'Import complete: 0 created, 0 updated'
4. Run: bd list
5. Confirm: All issues are actually present in the database

**Expected behavior:**
Report the actual number of issues imported, e.g., 'Import complete: 523 created, 0 updated'

**Actual behavior:**
'Import complete: 0 created, 0 updated' (misleading - makes user think import failed)

**Impact:**
- Users think import failed when it succeeded
- Confusing during database sync operations (e.g., after git pull)
- Makes debugging harder (can't tell if import actually worked)

**Context:**
Discovered during VC session when syncing database after git pull. The misleading message caused confusion about whether the database was properly synced with the canonical JSONL file.


Daemon should be singleton and aggressively kill stale instances

Found 2 bd daemons running (PIDs 76868, 77515) during shutdown. The daemon should:

1. Be a singleton - only one instance per rig allowed
2. On startup, check for existing daemon and kill it before starting
3. Use a PID file or lock file to enforce this

Currently stale daemons can accumulate, causing confusion and resource waste.

**Investigation 2025-12-21:**

The singleton mechanism is already implemented and working correctly:

1. **daemon.lock** uses flock (exclusive non-blocking) to prevent duplicate daemons
2. **bd.sock.startlock** coordinates concurrent auto-starts via O_CREATE|O_EXCL
3. **Registry** tracks all daemons globally in ~/.beads/registry.json

Testing shows:
- Trying to start a second daemon gives: 'Error: daemon already running (PID X)'
- Multiple daemons for *different* rigs is expected/correct behavior

The original report ('Found 2 bd daemons running PIDs 76868, 77515') was likely:
1. Two daemons for different rigs (expected), OR
2. An edge case that's since been fixed

Consider closing as RESOLVED or clarifying the original scenario.