docs: update deletion docs for tombstones, add changelog entries

- Rewrote DELETIONS.md to document inline tombstones (replacing legacy
  deletions.jsonl approach)
- Added tombstone feature entries to CHANGELOG.md under Unreleased
- Fixed duplicate 0.29.0 header in CHANGELOG.md
- Ran bd migrate-tombstones on beads repo (dogfooding)
- Closed bd-vw8 epic (all 12 dependencies complete)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-13 07:27:55 -08:00
parent 5ec2038e5c
commit d80d82d693
3 changed files with 140 additions and 131 deletions

View File

@@ -309,7 +309,7 @@
{"id":"bd-d7e88238","title":"Rapid 3","description":"","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-29T19:11:57.459655-07:00","updated_at":"2025-10-30T17:12:58.189494-07:00"}
{"id":"bd-d9e0","title":"Extract validation functions to validators.go","description":"Move validatePriority, validateStatus, validateIssueType, validateTitle, validateEstimatedMinutes, validateFieldUpdate to validators.go","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T19:28:54.915909-07:00","updated_at":"2025-11-01T23:37:07.480417-07:00","closed_at":"2025-11-01T23:37:07.480417-07:00"}
{"id":"bd-dcd6f14b","title":"Batch test 4","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T15:29:02.053523-07:00","updated_at":"2025-10-31T12:00:43.182861-07:00","closed_at":"2025-10-31T12:00:43.182861-07:00"}
{"id":"bd-dd6f6d26","title":"Fix autoimport tests for content-hash collision scoring","description":"## Overview\nThree autoimport tests are failing after bd-cbed9619.4 because they expect behavior based on the old reference-counting collision resolution, but the system now uses deterministic content-hash scoring.\n\n## Failing Tests\n1. `TestAutoImportMultipleCollisionsRemapped` - expects local versions preserved\n2. `TestAutoImportAllCollisionsRemapped` - expects local versions preserved \n3. `TestAutoImportCollisionRemapMultipleFields` - expects specific collision resolution behavior\n\n## Root Cause\nThese tests were written when ScoreCollisions used reference counting to determine which version to keep. Now it uses content-hash comparison (introduced in commit 2e87329), which produces different but deterministic results.\n\n## Example\nOld behavior: Issue with more references would be kept\nNew behavior: Issue with lexicographically lower content hash is kept\n\n## Solution\nUpdate each test to:\n1. Verify the new content-hash based behavior is correct\n2. Check that the remapped issue (not necessarily local/remote) has the expected content\n3. Ensure dependencies are preserved on the correct remapped issue\n\n## Acceptance Criteria\n- All three autoimport tests pass\n- Tests verify content-hash determinism (same collision always resolves the same way)\n- Tests check dependency preservation on remapped issues\n- Test documentation explains content-hash scoring expectations\n\n## Files to Modify\n- `cmd/bd/autoimport_collision_test.go`\n\n## Testing\nRun: `go test ./cmd/bd -run \"TestAutoImport.*Collision\" -v`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-28T19:12:56.344193-07:00","updated_at":"2025-10-30T17:12:58.178703-07:00","closed_at":"2025-10-28T19:18:35.106895-07:00","dependencies":[{"issue_id":"bd-dd6f6d26","depends_on_id":"bd-cbed9619.4","type":"discovered-from","created_at":"2025-10-28T19:12:56.345276-07:00","created_by":"daemon"}]}
{"id":"bd-dd6f6d26","title":"Fix autoimport tests for content-hash collision scoring","description":"## Overview\nThree autoimport tests are failing after [deleted:bd-cbed9619.4] because they expect behavior based on the old reference-counting collision resolution, but the system now uses deterministic content-hash scoring.\n\n## Failing Tests\n1. `TestAutoImportMultipleCollisionsRemapped` - expects local versions preserved\n2. `TestAutoImportAllCollisionsRemapped` - expects local versions preserved \n3. `TestAutoImportCollisionRemapMultipleFields` - expects specific collision resolution behavior\n\n## Root Cause\nThese tests were written when ScoreCollisions used reference counting to determine which version to keep. Now it uses content-hash comparison (introduced in commit 2e87329), which produces different but deterministic results.\n\n## Example\nOld behavior: Issue with more references would be kept\nNew behavior: Issue with lexicographically lower content hash is kept\n\n## Solution\nUpdate each test to:\n1. Verify the new content-hash based behavior is correct\n2. Check that the remapped issue (not necessarily local/remote) has the expected content\n3. Ensure dependencies are preserved on the correct remapped issue\n\n## Acceptance Criteria\n- All three autoimport tests pass\n- Tests verify content-hash determinism (same collision always resolves the same way)\n- Tests check dependency preservation on remapped issues\n- Test documentation explains content-hash scoring expectations\n\n## Files to Modify\n- `cmd/bd/autoimport_collision_test.go`\n\n## Testing\nRun: `go test ./cmd/bd -run \"TestAutoImport.*Collision\" -v`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-28T19:12:56.344193-07:00","updated_at":"2025-12-13T07:27:37.180226-08:00","closed_at":"2025-10-28T19:18:35.106895-07:00","dependencies":[{"issue_id":"bd-dd6f6d26","depends_on_id":"bd-cbed9619.4","type":"discovered-from","created_at":"2025-10-28T19:12:56.345276-07:00","created_by":"daemon"}]}
{"id":"bd-de6","title":"Fix FindBeadsDir to prioritize main repo .beads for worktrees","description":"The FindBeadsDir function should prioritize finding .beads in the main repository root when accessed from a worktree, rather than finding worktree-local .beads directories. This ensures proper sharing of the database across all worktrees.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-07T16:48:36.883117467-07:00","updated_at":"2025-12-07T16:48:36.883117467-07:00"}
{"id":"bd-df11","title":"Add import metrics for external_ref matching statistics","description":"Add observability for external_ref matching behavior during imports to help debug and optimize import operations.\n\nMetrics to track:\n- Number of issues matched by external_ref\n- Number of issues matched by ID\n- Number of issues matched by content hash\n- Number of external_ref updates vs creates\n- Average import time with vs without external_ref\n\nOutput format:\n- Add to ImportResult struct\n- Include in import command output\n- Consider structured logging\n\nUse cases:\n- Debugging slow imports\n- Understanding match distribution\n- Optimizing import performance\n\nRelated: bd-1022","status":"open","priority":4,"issue_type":"chore","created_at":"2025-11-02T15:32:46.157899-08:00","updated_at":"2025-11-02T15:32:46.157899-08:00"}
{"id":"bd-df190564","title":"bd repair-deps - Orphaned dependency cleaner","description":"Find and fix orphaned dependency references.\n\nImplementation:\n- Scan all issues for dependencies pointing to non-existent issues\n- Report orphaned refs\n- Auto-fix with --fix flag\n- Interactive mode with --interactive\n\nFiles: cmd/bd/repair_deps.go (new)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-29T19:42:29.852745-07:00","updated_at":"2025-10-31T18:24:19.418221-07:00","closed_at":"2025-10-31T18:24:19.418221-07:00"}
@@ -373,6 +373,7 @@
{"id":"bd-fc2d","title":"Refactor sqlite.go (2298 lines)","description":"Break down internal/storage/sqlite/sqlite.go into smaller, more focused modules. The file is currently 2298 lines and should be split into logical components.","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-11-01T19:28:40.899111-07:00","updated_at":"2025-11-01T21:43:55.870724-07:00","closed_at":"2025-11-01T21:43:55.870724-07:00"}
{"id":"bd-fd56","title":"Wrap git operations in GitClient interface","description":"Create internal/daemonrunner/git.go with GitClient interface (HasUpstream, HasChanges, Commit, Push, Pull). Default implementation using os/exec. Use in Syncer and Run loop for testability.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T11:41:14.88734-07:00","updated_at":"2025-11-01T22:56:08.354697-07:00","closed_at":"2025-11-01T22:56:08.354697-07:00"}
{"id":"bd-fd8753d9","title":"Document bd edit command and verify MCP exclusion","description":"Follow-up from PR #152:\n1. Add \"bd edit\" to AGENTS.md with \"Humans only\" note\n2. Verify MCP server doesn't expose bd edit command\n3. Consider adding test for command registration","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-26T13:23:47.982295-07:00","updated_at":"2025-10-30T17:12:58.226229-07:00"}
{"id":"bd-fi05","title":"bd sync fails with orphaned issues and duplicate ID conflict","description":"After fixing the deleted_at TEXT column scanning bug (commit 18b1eb2), bd sync still fails with two issues:\n\n1. Orphan Detection Warning: 12 orphaned child issues whose parents no longer exist (bd-cb64c226.* and bd-cbed9619.*)\n\n2. Import Failure: UNIQUE constraint failed for bd-360 - this tombstone exists in both DB and JSONL\n\nError: \"Import failed: error creating depth-0 issues: bulk insert issues: failed to insert issue bd-360: sqlite3: constraint failed: UNIQUE constraint failed: issues.id\"\n\nFix options:\n- Delete orphaned child issues with bd delete\n- Resolve bd-360 duplicate (in deletions.jsonl vs tombstone in DB)\n- Reset sync branch: git branch -f beads-sync main \u0026\u0026 git push --force-with-lease origin beads-sync","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-13T07:14:33.831346-08:00","updated_at":"2025-12-13T07:14:41.268141-08:00"}
{"id":"bd-fl4","title":"Test sync branch setup","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T00:26:47.520507-08:00","updated_at":"2025-11-30T00:33:23.105998-08:00","closed_at":"2025-11-30T00:33:23.105998-08:00"}
{"id":"bd-fmc","title":"string(rune()) pattern in other test files still only works for single digits","description":"The bd-c5m fix only addressed worktree_divergence_test.go, but several other test files have the same issue:\n\n- cmd/bd/stale_test.go:218, 234\n- cmd/bd/export_integrity_integration_test.go:45-47, 279-280\n- internal/storage/sqlite/comments_test.go:146, 165\n- internal/storage/sqlite/blocked_cache_test.go:292\n- internal/storage/sqlite/compact_bench_test.go:117\n- internal/storage/sqlite/cycle_detection_test.go:418\n\nThese use string(rune('0'+i)) which produces wrong characters when i \u003e= 10.\nShould use strconv.Itoa(i) instead.","status":"closed","priority":4,"issue_type":"bug","created_at":"2025-12-02T22:29:12.355705-08:00","updated_at":"2025-12-02T22:39:51.844638-08:00","closed_at":"2025-12-02T22:39:51.844638-08:00"}
{"id":"bd-fmo","title":"Missing test for IsExpiredTombstone edge cases","description":"## Problem\n\nThe `IsExpiredTombstone()` function has several edge cases that aren't directly tested:\n\n1. **Invalid DeletedAt timestamp** - Returns false (safety), but not tested\n2. **ttl=0 fallback to DefaultTombstoneTTL** - Not tested directly\n3. **Tombstone with empty DeletedAt** - Returns false, tested implicitly but not explicitly\n4. **Clock skew grace period effect** - Not tested (would need time mocking)\n\n## Current Test Coverage\n\nThe existing tests use `time.Now()` which makes them non-deterministic:\n```go\nDeletedAt: time.Now().Add(-24 * time.Hour).Format(time.RFC3339)\n```\n\nThis works but doesn't test the expiration boundary precisely.\n\n## Recommendation\n\nAdd explicit unit tests for `IsExpiredTombstone()`:\n\n```go\nfunc TestIsExpiredTombstone(t *testing.T) {\n tests := []struct {\n name string\n status string\n deletedAt string\n ttl time.Duration\n expected bool\n }{\n {\"non-tombstone returns false\", \"open\", \"2024-01-01T00:00:00Z\", 0, false},\n {\"empty deleted_at returns false\", \"tombstone\", \"\", 0, false},\n {\"invalid timestamp returns false\", \"tombstone\", \"not-a-date\", 0, false},\n {\"within TTL returns false\", \"tombstone\", time.Now().Add(-1*time.Hour).Format(time.RFC3339), 24*time.Hour, false},\n {\"beyond TTL returns true\", \"tombstone\", time.Now().Add(-48*time.Hour).Format(time.RFC3339), 24*time.Hour, true},\n }\n // ...\n}\n```\n\n## Files\n- internal/merge/merge.go:255-290\n- internal/merge/merge_test.go (add TestIsExpiredTombstone)","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T16:36:09.106135-08:00","updated_at":"2025-12-07T02:16:16.881654-08:00","closed_at":"2025-12-07T02:16:16.881654-08:00"}
@@ -441,7 +442,7 @@
{"id":"bd-v0x","title":"Auto-detect issue prefix from existing JSONL in 'bd init'","description":"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`.\n\nCurrently you must specify `--prefix ef` manually. But the JSONL file already contains issues like `ef-1it`, `ef-1jp` etc., so the prefix is known.\n\n**Ideal UX**:\n```\n$ bd init\nDetected issue prefix 'ef' from existing JSONL (38 issues).\n✓ Database initialized...\n```\n\nThis would make fresh clone hydration a single command: `bd init` with no flags.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-27T20:21:21.049215-08:00","updated_at":"2025-11-30T10:50:04.275631-08:00","closed_at":"2025-11-28T21:57:11.164293-08:00"}
{"id":"bd-v29","title":"Deletions pruning doesn't include results in JSON output","description":"## Problem\n\nWhen `bd compact --json` runs with deletions pruning, the prune results are silently discarded:\n\n```go\n// Only report if there were deletions to prune\nif result.PrunedCount \u003e 0 {\n if jsonOutput {\n // JSON output will be included in the main response\n return // \u003c-- BUG: results are NOT included anywhere\n }\n ...\n}\n```\n\n## Location\n`cmd/bd/compact.go:925-929`\n\n## Impact\n- JSON consumers don't know deletions were pruned\n- No way to audit pruning via automation\n\n## Fix\nReturn prune results and include in JSON output structure:\n\n```json\n{\n \"success\": true,\n \"compacted\": {...},\n \"deletions_pruned\": {\n \"count\": 5,\n \"retention_days\": 7\n }\n}\n```","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-25T12:48:59.730979-08:00","updated_at":"2025-11-25T15:11:54.363653-08:00","closed_at":"2025-11-25T15:11:54.363653-08:00"}
{"id":"bd-vs9","title":"Fix unparam unused parameter in cmd/bd/doctor.go:541","description":"Linting issue: checkHooksQuick - path is unused (unparam) at cmd/bd/doctor.go:541:22. Error: func checkHooksQuick(path string) string {","status":"open","priority":0,"issue_type":"bug","created_at":"2025-12-07T15:35:17.02177046-07:00","updated_at":"2025-12-07T15:35:17.02177046-07:00"}
{"id":"bd-vw8","title":"Switch from deletions manifest to inline tombstones","description":"Replace the current deletions.jsonl manifest with inline tombstone records in issues.jsonl.\n\n## Problem Statement\n\nThe current deletions manifest approach has several issues:\n\n1. **Wild poisoning** - A stale clone's deletions manifest can poison fresh databases when synced\n2. **Two-level merge inconsistency** - Git content merge and beads snapshot merge use different bases\n3. **SyncJSONLToWorktree overwrites** - Blindly copies local JSONL to worktree, losing remote issues\n4. **3-day TTL too aggressive** - Deletions expire before dormant branches get merged\n\n## Proposed Solution: Inline Tombstones\n\nInstead of a separate deletions.jsonl file, embed deletion records directly in issues.jsonl:\n\n```json\n{\"id\":\"beads-abc\",\"status\":\"tombstone\",\"title\":\"Original title\",\"deleted_at\":\"2025-12-01T...\",\"deleted_by\":\"user\",\"expires_at\":\"2025-12-31T...\"}\n```\n\n### Benefits\n\n1. **Single source of truth** - No separate manifest to sync/merge\n2. **Participates in normal 3-way merge** - Deletion conflicts resolved same as other fields\n3. **Atomic with issue data** - Can't have orphaned deletions or missing tombstones\n4. **Preserves metadata** - Can optionally keep title/type for audit trail\n\n### Design Decisions Needed\n\n1. **TTL duration** - 30 days default? Configurable via config.yaml?\n2. **Tombstone content** - Minimal (just ID + timestamps) vs. full issue preservation?\n3. **Migration path** - How to handle existing deletions.jsonl files?\n4. **Status value** - Use \"tombstone\" or \"deleted\"? (tombstone clearer, deleted more intuitive)\n5. **Merge semantics** - Does tombstone always win, or use updated_at like other fields?\n\n### Migration Strategy\n\n1. On import, convert deletions.jsonl entries to tombstones in JSONL\n2. Deprecate but still read deletions.jsonl for backward compatibility\n3. Stop writing to deletions.jsonl after N versions\n\n## Related Issues\n\n- GitHub #464: Beads deletes issues (fresh clone sync problem)\n- bd-2e0: Add TTL to deletions manifest entries (current 3-day TTL)\n- bd-53c: bd sync corrupts issues.jsonl in multi-clone environments","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-05T13:42:24.384792-08:00","updated_at":"2025-12-05T13:42:24.384792-08:00","dependencies":[{"issue_id":"bd-vw8","depends_on_id":"bd-1r5","type":"blocks","created_at":"2025-12-05T14:57:31.111259-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-2m7","type":"blocks","created_at":"2025-12-05T14:57:33.57722-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-dli","type":"blocks","created_at":"2025-12-05T14:57:34.902784-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-zvg","type":"blocks","created_at":"2025-12-05T14:57:36.665817-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-fbj","type":"blocks","created_at":"2025-12-05T15:14:59.081452-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-olt","type":"blocks","created_at":"2025-12-05T15:14:59.118268-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-0ih","type":"blocks","created_at":"2025-12-05T15:14:59.155689-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-3b4","type":"blocks","created_at":"2025-12-05T15:14:59.192575-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-dve","type":"blocks","created_at":"2025-12-05T15:14:59.227233-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-8f9","type":"blocks","created_at":"2025-12-05T15:14:59.262816-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-s3v","type":"blocks","created_at":"2025-12-05T15:14:59.299481-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-okh","type":"blocks","created_at":"2025-12-05T15:14:59.337656-08:00","created_by":"daemon"}]}
{"id":"bd-vw8","title":"Switch from deletions manifest to inline tombstones","description":"Replace the current deletions.jsonl manifest with inline tombstone records in issues.jsonl.\n\n## Problem Statement\n\nThe current deletions manifest approach has several issues:\n\n1. **Wild poisoning** - A stale clone's deletions manifest can poison fresh databases when synced\n2. **Two-level merge inconsistency** - Git content merge and beads snapshot merge use different bases\n3. **SyncJSONLToWorktree overwrites** - Blindly copies local JSONL to worktree, losing remote issues\n4. **3-day TTL too aggressive** - Deletions expire before dormant branches get merged\n\n## Proposed Solution: Inline Tombstones\n\nInstead of a separate deletions.jsonl file, embed deletion records directly in issues.jsonl:\n\n```json\n{\"id\":\"beads-abc\",\"status\":\"tombstone\",\"title\":\"Original title\",\"deleted_at\":\"2025-12-01T...\",\"deleted_by\":\"user\",\"expires_at\":\"2025-12-31T...\"}\n```\n\n### Benefits\n\n1. **Single source of truth** - No separate manifest to sync/merge\n2. **Participates in normal 3-way merge** - Deletion conflicts resolved same as other fields\n3. **Atomic with issue data** - Can't have orphaned deletions or missing tombstones\n4. **Preserves metadata** - Can optionally keep title/type for audit trail\n\n### Design Decisions Needed\n\n1. **TTL duration** - 30 days default? Configurable via config.yaml?\n2. **Tombstone content** - Minimal (just ID + timestamps) vs. full issue preservation?\n3. **Migration path** - How to handle existing deletions.jsonl files?\n4. **Status value** - Use \"tombstone\" or \"deleted\"? (tombstone clearer, deleted more intuitive)\n5. **Merge semantics** - Does tombstone always win, or use updated_at like other fields?\n\n### Migration Strategy\n\n1. On import, convert deletions.jsonl entries to tombstones in JSONL\n2. Deprecate but still read deletions.jsonl for backward compatibility\n3. Stop writing to deletions.jsonl after N versions\n\n## Related Issues\n\n- GitHub #464: Beads deletes issues (fresh clone sync problem)\n- bd-2e0: Add TTL to deletions manifest entries (current 3-day TTL)\n- bd-53c: bd sync corrupts issues.jsonl in multi-clone environments","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-05T13:42:24.384792-08:00","updated_at":"2025-12-13T07:25:56.968329-08:00","closed_at":"2025-12-13T07:25:56.968329-08:00","dependencies":[{"issue_id":"bd-vw8","depends_on_id":"bd-1r5","type":"blocks","created_at":"2025-12-05T14:57:31.111259-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-2m7","type":"blocks","created_at":"2025-12-05T14:57:33.57722-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-dli","type":"blocks","created_at":"2025-12-05T14:57:34.902784-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-zvg","type":"blocks","created_at":"2025-12-05T14:57:36.665817-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-fbj","type":"blocks","created_at":"2025-12-05T15:14:59.081452-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-olt","type":"blocks","created_at":"2025-12-05T15:14:59.118268-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-0ih","type":"blocks","created_at":"2025-12-05T15:14:59.155689-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-3b4","type":"blocks","created_at":"2025-12-05T15:14:59.192575-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-dve","type":"blocks","created_at":"2025-12-05T15:14:59.227233-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-8f9","type":"blocks","created_at":"2025-12-05T15:14:59.262816-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-s3v","type":"blocks","created_at":"2025-12-05T15:14:59.299481-08:00","created_by":"daemon"},{"issue_id":"bd-vw8","depends_on_id":"bd-okh","type":"blocks","created_at":"2025-12-05T15:14:59.337656-08:00","created_by":"daemon"}]}
{"id":"bd-wcl","title":"Document CLI + hooks as recommended approach over MCP","description":"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).","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-12T00:15:25.923025-08:00","updated_at":"2025-11-26T20:13:52.891053-08:00","closed_at":"2025-11-26T18:06:51.020351-08:00"}
{"id":"bd-wmo","title":"PruneDeletions iterates map non-deterministically","description":"## Problem\n\n`PruneDeletions` iterates over `loadResult.Records` which is a map. Go maps iterate in random order, so:\n\n1. `result.PrunedIDs` order is non-deterministic\n2. `kept` slice order is non-deterministic → `WriteDeletions` output order varies\n\n## Location\n`internal/deletions/deletions.go:213`\n\n## Impact\n- Git diffs are noisy (file changes order on each prune)\n- Tests could be flaky if they depend on order\n- Harder to debug/audit\n\n## Fix\nSort by ID or timestamp before iterating:\n\n```go\n// Convert map to slice and sort\nvar records []DeletionRecord\nfor _, r := range loadResult.Records {\n records = append(records, r)\n}\nsort.Slice(records, func(i, j int) bool {\n return records[i].ID \u003c records[j].ID\n})\n```","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-11-25T12:49:11.290916-08:00","updated_at":"2025-11-25T15:15:21.903649-08:00","closed_at":"2025-11-25T15:15:21.903649-08:00"}
{"id":"bd-wucl","title":"Add Result.ConvertedToTombstone counter for import statistics","description":"The import Result struct has SkippedDeleted but doesn't distinguish between:\n1. Issues skipped because they're in deletions manifest (old behavior)\n2. Issues converted from deletions.jsonl to tombstones (new bd-dve behavior)\n\nAdd a new ConvertedToTombstone counter to track legacy deletions converted to tombstones during import. This provides better visibility into the migration process.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-07T01:40:34.097611-08:00","updated_at":"2025-12-07T02:18:09.466608-08:00","closed_at":"2025-12-07T02:18:09.466608-08:00","dependencies":[{"issue_id":"bd-wucl","depends_on_id":"bd-dve","type":"blocks","created_at":"2025-12-07T01:41:28.242971-08:00","created_by":"daemon"}]}

View File

@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **Inline tombstones for soft-delete (bd-vw8)**
- Deleted issues now become tombstones with `status: "tombstone"` in `issues.jsonl`
- Full audit trail: `deleted_at`, `deleted_by`, `delete_reason`, `original_type`
- TTL-based expiration (default 30 days) with automatic pruning via `bd compact`
- Proper 3-way merge support: fresh tombstones win, expired tombstones allow resurrection
- Replaces the legacy `deletions.jsonl` manifest approach
- **`bd migrate-tombstones` command (bd-8f9)**
- Converts legacy `deletions.jsonl` entries to inline tombstones
- Archives old file as `deletions.jsonl.migrated`
- Use `--dry-run` to preview changes
### Fixed
- **`bd sync` race condition with daemon (bd-lw0x, bd-hxou)**
@@ -18,8 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.29.0] - 2025-12-03
## [0.29.0] - 2025-12-03
### Added
- **`--estimate` / `-e` flag for `bd create` and `bd update` (GH #443)**

View File

@@ -6,131 +6,116 @@ This document describes how bd tracks and propagates deletions across repository
When issues are deleted in one clone, those deletions need to propagate to other clones. Without this mechanism, deleted issues would "resurrect" when another clone's database is imported.
The **deletions manifest** (`.beads/deletions.jsonl`) is an append-only log that records every deletion. This file is committed to git and synced across all clones.
**Beads uses inline tombstones** - deleted issues are converted to a special `tombstone` status and remain in `issues.jsonl`. This provides:
## File Format
- Full audit trail (who, when, why)
- Atomic sync with issue data (no separate manifest to merge)
- TTL-based expiration (default 30 days)
- Proper 3-way merge conflict resolution
The deletions manifest is a JSON Lines file where each line is a deletion record:
## How Tombstones Work
```jsonl
{"id":"bd-abc","ts":"2025-01-15T10:00:00Z","by":"stevey","reason":"duplicate of bd-xyz"}
{"id":"bd-def","ts":"2025-01-15T10:05:00Z","by":"claude","reason":"cleanup"}
When you delete an issue:
1. The issue's status changes to `tombstone`
2. Deletion metadata is recorded (`deleted_at`, `deleted_by`, `delete_reason`)
3. The original issue type is preserved in `original_type`
4. All dependencies are removed (tombstones don't block anything)
5. The tombstone syncs via git like any other issue
### Tombstone Fields
| Field | Type | Description |
|-------|------|-------------|
| `status` | string | Always `"tombstone"` |
| `deleted_at` | ISO 8601 | When the issue was deleted |
| `deleted_by` | string | Actor who performed the deletion |
| `delete_reason` | string | Optional context (e.g., "duplicate", "cleanup") |
| `original_type` | string | Issue type before deletion (task, bug, etc.) |
### Example Tombstone in JSONL
```json
{"id":"bd-42","status":"tombstone","title":"Original title","deleted_at":"2025-01-15T10:00:00Z","deleted_by":"stevey","delete_reason":"duplicate of bd-xyz","original_type":"task"}
```
### Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Issue ID that was deleted |
| `ts` | string | Yes | ISO 8601 UTC timestamp |
| `by` | string | Yes | Actor who performed the deletion |
| `reason` | string | No | Optional context (e.g., "duplicate", "cleanup") |
## Commands
### Deleting Issues
```bash
bd delete bd-42 # Delete single issue
bd delete bd-42 bd-43 bd-44 # Delete multiple issues
bd cleanup -f # Delete all closed issues
bd delete bd-42 # Delete single issue (preview mode)
bd delete bd-42 --force # Actually delete
bd delete bd-42 bd-43 bd-44 -f # Delete multiple issues
bd delete bd-42 --cascade -f # Delete with all dependents
bd delete --from-file ids.txt -f # Delete from file (one ID per line)
bd delete bd-42 --dry-run # Preview what would be deleted
```
All deletions are automatically recorded to the manifest.
### Viewing Deletions
### Viewing Deleted Issues
```bash
bd deleted # Recent deletions (last 7 days)
bd deleted --since=30d # Deletions in last 30 days
bd deleted --all # All tracked deletions
bd deleted bd-xxx # Lookup specific issue
bd deleted --json # Machine-readable output
bd list --status=tombstone # List all tombstones
bd show bd-42 # View tombstone details (if you know the ID)
```
## Propagation Mechanism
## TTL and Expiration
### Export (Local Delete)
Tombstones expire after a configurable TTL (default: 30 days). This prevents unbounded growth while ensuring deletions propagate to all clones.
1. `bd delete` removes issue from SQLite
2. Deletion record appended to `deletions.jsonl`
3. `bd sync` commits and pushes the manifest
### How Expiration Works
### Import (Remote Delete)
1. Tombstones older than TTL + 1 hour grace period are eligible for pruning
2. `bd compact` removes expired tombstones from `issues.jsonl`
3. Git history fallback handles edge cases where pruned tombstones are needed
1. `bd sync` pulls updated manifest
2. Import checks each DB issue against manifest
3. If issue ID is in manifest, it's deleted from local DB
4. If issue ID is NOT in manifest and NOT in JSONL:
- Check git history (see fallback below)
- If found in history → deleted upstream, remove locally
- If not found → local unpushed work, keep it
## Git History Fallback
The manifest is pruned periodically to prevent unbounded growth. When a deletion record is pruned but the issue still exists in some clone's DB:
1. Import detects: "DB issue not in JSONL, not in manifest"
2. Falls back to git history search
3. Uses `git log -S` to check if issue ID was ever in JSONL
4. If found in history → it was deleted, remove from DB
5. **Backfill**: Re-append the deletion to manifest (self-healing)
This fallback ensures deletions propagate even after manifest pruning.
## Configuration
### Retention Period
By default, deletion records are kept for 7 days. Configure via:
```bash
bd config set deletions.retention_days 30
```
Or in `.beads/config.yaml`:
### Configuration
```yaml
deletions:
retention_days: 30
# .beads/config.yaml
tombstone:
ttl_days: 30 # Default: 30 days
```
### Auto-Compact Threshold
Auto-compaction during `bd sync` is opt-in:
Or via CLI:
```bash
bd config set deletions.auto_compact_threshold 100
bd config set tombstone.ttl_days 60
```
When the manifest exceeds this threshold, old records are pruned during sync. Set to 0 to disable (default).
### Manual Pruning
```bash
bd compact --retention 7 # Prune records older than 7 days
bd compact --retention 0 # Prune all records (use git fallback)
bd compact # Prune expired tombstones (and other compaction)
```
## Size Estimates
- Each record: ~80 bytes
- 7-day retention with 100 deletions/day: ~56KB
- Git compressed: ~10KB
The manifest stays small even with heavy deletion activity.
## Conflict Resolution
When multiple clones delete issues simultaneously:
When the same issue is modified in one clone and deleted in another:
1. Both append their deletion records
2. Git merges (append-only = no conflicts)
3. Result: duplicate entries for same ID (different timestamps)
4. `LoadDeletions` deduplicates by ID (keeps any entry)
5. Result: deletion propagates correctly
1. Both changes sync via git
2. 3-way merge detects the conflict
3. Resolution rules:
- If tombstone is expired → live issue wins (resurrection)
- If tombstone is fresh → tombstone wins (deletion propagates)
- `updated_at` timestamps break ties
Duplicate records are harmless and cleaned up during pruning.
This ensures deletions propagate reliably while handling clock skew and delayed syncs.
## Migration from Legacy Format
Prior to v0.30, beads used a separate `deletions.jsonl` manifest. To migrate:
```bash
bd migrate-tombstones # Convert deletions.jsonl to inline tombstones
bd migrate-tombstones --dry-run # Preview changes first
```
The migration:
1. Reads existing deletions from `deletions.jsonl`
2. Creates tombstone entries in `issues.jsonl`
3. Archives the old file as `deletions.jsonl.migrated`
After migration, run `bd sync` to propagate tombstones to other clones.
## Troubleshooting
@@ -139,70 +124,81 @@ Duplicate records are harmless and cleaned up during pruning.
If a deleted issue reappears after sync:
```bash
# Check if in manifest
bd deleted bd-xxx
# Check if it's a tombstone
bd list --status=tombstone | grep bd-xxx
# Force re-import
# Check tombstone details
bd show bd-xxx
# Force re-import from JSONL
bd import --force
# If still appearing, check git history
git log -S '"id":"bd-xxx"' -- .beads/beads.jsonl
```
### Manifest Not Being Committed
Ensure deletions.jsonl is tracked:
If the issue keeps reappearing, the tombstone may have expired. Re-delete it:
```bash
git add .beads/deletions.jsonl
bd delete bd-xxx --force
bd sync
```
And NOT in .gitignore.
### Tombstones Not Syncing
### Large Manifest
If the manifest is growing too large:
Ensure tombstones are being exported:
```bash
# Check size
wc -l .beads/deletions.jsonl
# Check if tombstone is in JSONL
grep '"id":"bd-xxx"' .beads/issues.jsonl
# Manual prune
bd compact --retention 7
# Force export
bd export --force
bd sync
```
# Enable auto-compact
bd config set deletions.auto_compact_threshold 100
### Too Many Tombstones
If you have many old tombstones:
```bash
# Check tombstone count
bd list --status=tombstone | wc -l
# Prune expired tombstones
bd compact
```
## Design Rationale
### Why JSONL?
### Why Inline Tombstones?
- Append-only: natural for deletion logs
- Human-readable: easy to audit
- Git-friendly: line-based diffs
- No merge conflicts: append = trivial merge
The previous `deletions.jsonl` manifest had issues:
### Why Not Delete from JSONL?
- **Wild poisoning**: Stale clone's manifest could delete issues incorrectly
- **Merge inconsistency**: Separate file meant separate merge logic
- **Two sources of truth**: Issue data and deletion data could diverge
Removing lines from `beads.jsonl` would work but:
- Loses audit trail (who deleted what when)
- Harder to merge (line deletions can conflict)
- Can't distinguish "deleted" from "never existed"
Inline tombstones solve these by:
### Why Time-Based Pruning?
- Single source of truth (`issues.jsonl`)
- Same merge semantics as regular issues
- Atomic with issue data
- Full audit trail preserved
- Bounds manifest size
### Why TTL-Based Expiration?
- Bounds storage growth (tombstones eventually pruned)
- Git history fallback handles edge cases
- 7-day default handles most sync scenarios
- 30-day default handles typical sync scenarios
- Configurable for teams with longer sync cycles
### Why Git Fallback?
### Why 1-Hour Grace Period?
- Handles pruned records gracefully
- Self-healing via backfill
- Works with shallow clones (partial fallback)
- No data loss from aggressive pruning
Clock skew between machines can cause issues:
- Machine A deletes issue at 10:00 (its clock)
- Machine B's clock is 30 minutes ahead
- Without grace period, B might see tombstone as expired immediately
The 1-hour grace period ensures tombstones propagate even with minor clock drift.
## Related