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:
@@ -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"}]}
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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)**
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user